static class TimedCache<A> {
  long timeout;
  volatile A value; // volatile for peek()
  O function;
  long set;
  Lock lock = lock();
  bool keepValueWhileCalculating; // special mode to make peek() always return something
  
  // stats
  int stores, fails, hits;
  
  *(double timeoutSeconds, IF0<A> function) {
    this(function, timeoutSeconds);
  }
  
  *(double timeoutSeconds, O function) {
    this(function, timeoutSeconds);
  }
  
  *(O function, double timeoutSeconds) {
    this(timeoutSeconds);
    this.function = function;
  }

  // 0 = no timeout  
  *(double timeoutSeconds) {
    timeout = toMS(timeoutSeconds);
  }
  
  A set(A a) {
    lock lock;
    ++stores;
    value = a;
    set = now();
    ret a;
  }
  
  bool has() {
    lock lock;
    clean();
    if (set != 0) {
      ++hits;
      true;
    }
    ++fails;
    false;
  }
  
  A get() {
    lock lock;
    if (function != null) ret get(function);
    clean();
    if (set != 0) ++hits; else ++fails;
    ret value;
  }
  
  A get(O makerFunction) {
    lock lock;
    if (keepValueWhileCalculating) {
      if (value == null || shouldClean())
        set((A) callF(makerFunction));
      ret value;
    } else {
      ret this.has() ? getNoClean() : set((A) callF(makerFunction));
    }
  }
  
  A getNoClean() {
    lock lock; // lock to wait for potentially running calculation
    ret value;
  }
  
  // clear if timed out
  void clean() {
    lock lock;
    if (shouldClean()) clear();
  }
  
  bool shouldClean() {
    ret timeout > 0 && now() > set+timeout;
  }
  
  // clear now
  void clear() {
    lock lock;
    set = 0;
    value = null;
  }
  
  S stats() {
    ret "Stores: " + stores + ", hits: " + hits + ", fails: " + fails;
  }
  
  A peek() {
    ret value;
  }
}