/ contract / src / tools / manualTimer.js
manualTimer.js
  1  import { Far } from '@endo/marshal';
  2  import { bindAllMethods } from '@agoric/internal';
  3  import { buildManualTimer as build } from '@agoric/swingset-vat/tools/manual-timer.js';
  4  import { TimeMath } from '@agoric/time';
  5  import { eventLoopIteration } from './eventLoopIteration.js';
  6  
  7  const { Fail } = assert;
  8  
  9  // we wrap SwingSet's buildManualTimer to accomodate the needs of
 10  // zoe's tests
 11  
 12  /**
 13   * @typedef {{
 14   *  timeStep?: import('@agoric/time/src/types').RelativeTime | bigint,
 15   *  eventLoopIteration?: () => Promise<void>,
 16   * }} ZoeManualTimerOptions
 17   */
 18  
 19  const nolog = (..._args) => {};
 20  
 21  const defaultOptions = { timeStep: 1n, eventLoopIteration };
 22  
 23  /**
 24   * A fake TimerService, for unit tests that do not use a real
 25   * kernel. You can make time pass by calling `advanceTo(when)`, or one
 26   * `timeStep` at a time by calling `tick()`.
 27   *
 28   * `advanceTo()` merely schedules a wakeup: the actual
 29   * handlers (in the code under test) are invoked several turns
 30   * later. Some zoe/etc tests want to poll for the consequences of
 31   * those invocations. The best approach is to get an appropriate
 32   * Promise from your code-under-test, wait for it to fire, and then
 33   * poll. But some libraries do not offer this convenience, especially
 34   * when they use internal "fire and forget" actions.
 35   *
 36   * To support those tests, the manual timer accepts a
 37   * `eventLoopIteration` option. If provided, each call to `tick()`
 38   * will wait for all triggered activity to complete before
 39   * returning. That doesn't mean the `wake()` handler's result promise
 40   * has fired; it just means there are no settled Promises still trying
 41   * to execute their callbacks.
 42   *
 43   * The following will wait for all such Promise activity to finish
 44   * before returning from `tick()`:
 45   *
 46   *  eventLoopIteration = () => new Promise(setImmediate);
 47   *  mt = buildManualTimer(log, startTime, { eventLoopIteration })
 48   *
 49   * `tickN(count)` calls `tick()` multiple times, awaiting each one
 50   *
 51   * The first argument is called to log 'tick' events, which might help
 52   * with "golden transcript" -style tests to distinguish tick
 53   * boundaries
 54   *
 55   * @param {(...args: any[]) => void} [log]
 56   * @param {import('@agoric/time/src/types').Timestamp | bigint} [startValue=0n]
 57   * @param {ZoeManualTimerOptions} [options]
 58   * @returns {ManualTimer}
 59   */
 60  
 61  const buildManualTimer = (
 62    log = nolog,
 63    startValue = 0n,
 64    options = defaultOptions
 65  ) => {
 66    const { timeStep, eventLoopIteration, ...buildOptions } = options;
 67    const optSuffix = msg => (msg ? `: ${msg}` : '');
 68    const callbacks = {
 69      advanceTo: (newTime, msg) => log(`@@ tick:${newTime}${optSuffix(msg)} @@`),
 70      setWakeup: (now, when) =>
 71        log(`@@ schedule task for:${when}, currently: ${now} @@`)
 72      // wake: now => log(`@@ run task at:${now} @@`),
 73    };
 74  
 75    // neither of these could possibly be a record, because the caller
 76    // doesn't have our brand yet, but this makes the types maximally
 77    // tolerant
 78    startValue = TimeMath.absValue(startValue);
 79    const timeStepValue = TimeMath.relValue(timeStep);
 80    assert.typeof(startValue, 'bigint');
 81    assert.typeof(timeStepValue, 'bigint');
 82  
 83    const timerService = build({
 84      startTime: startValue,
 85      ...buildOptions,
 86      callbacks
 87    });
 88    const toRT = rt =>
 89      TimeMath.coerceRelativeTimeRecord(rt, timerService.getTimerBrand());
 90  
 91    const tick = msg => {
 92      const oldTime = timerService.getCurrentTimestamp();
 93      const newTime = TimeMath.addAbsRel(oldTime, toRT(timeStepValue));
 94      timerService.advanceTo(TimeMath.absValue(newTime), msg);
 95      console.log(eventLoopIteration);
 96      // that schedules wakeups, but they don't fire until a later turn
 97      return eventLoopIteration && eventLoopIteration();
 98    };
 99  
100    const tickN = async (nTimes, msg) => {
101      nTimes >= 1 || Fail`invariant nTimes >= 1`;
102      for (let i = 0; i < nTimes; i += 1) {
103        // eslint-disable-next-line no-await-in-loop
104        await tick(msg);
105      }
106    };
107  
108    const setWakeup = (when, handler, cancelToken) => {
109      return timerService.setWakeup(when, handler, cancelToken);
110    };
111  
112    return Far('ManualTimer', {
113      ...bindAllMethods(timerService),
114      tick,
115      tickN,
116      setWakeup
117    });
118  };
119  harden(buildManualTimer);
120  
121  export default buildManualTimer;