"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.testClockWith = exports.testClock = exports.sleeps = exports.sleep = exports.setTime = exports.save = exports.live = exports.defaultTestClock = exports.currentTimeMillis = exports.adjustWith = exports.adjust = exports.TestClockImpl = exports.TestClock = void 0;
var Chunk = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/data/Chunk"));
var Context = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/data/Context"));
var Duration = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/data/Duration"));
var Equal = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/data/Equal"));
var _Function = /*#__PURE__*/require("@effect/data/Function");
var HashMap = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/data/HashMap"));
var number = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/data/Number"));
var Option = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/data/Option"));
var Order = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/data/Order"));
var FiberStatus = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/io/Fiber/Status"));
var clock = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/io/internal/clock"));
var core = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/io/internal/core"));
var defaultServices = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/io/internal/defaultServices"));
var effect = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/io/internal/effect"));
var circular = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/io/internal/effect/circular"));
var fiberRuntime = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/io/internal/fiberRuntime"));
var layer = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/io/internal/layer"));
var ref = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/io/internal/ref"));
var synchronized = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/io/internal/synchronizedRef"));
var Annotations = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/io/internal/testing/annotations"));
var Live = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/io/internal/testing/live"));
var Data = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/io/internal/testing/testClock/data"));
var SuspendedWarningData = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/io/internal/testing/testClock/suspendedWarningData"));
var WarningData = /*#__PURE__*/_interopRequireWildcard( /*#__PURE__*/require("@effect/io/internal/testing/testClock/warningData"));
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
var _a;
/** @internal */
const TestClock = /*#__PURE__*/Context.Tag( /*#__PURE__*/Symbol.for("@effect/test/TestClock"));
/**
 * The warning message that will be displayed if a test is using time but is
 * not advancing the `TestClock`.
 *
 * @internal
 */
exports.TestClock = TestClock;
const warning = "Warning: A test is using time, but is not advancing " + "the test clock, which may result in the test hanging. Use TestClock.adjust to " + "manually advance the time.";
/**
 * The warning message that will be displayed if a test is advancing the clock
 * but a fiber is still running.
 *
 * @internal
 */
const suspendedWarning = "Warning: A test is advancing the test clock, " + "but a fiber is not suspending, which may result in the test hanging. Use " + "TestAspect.diagnose to identity the fiber that is not suspending.";
/** @internal */
class TestClockImpl {
  constructor(clockState, live, annotations, warningState, suspendedWarningState) {
    this.clockState = clockState;
    this.live = live;
    this.annotations = annotations;
    this.warningState = warningState;
    this.suspendedWarningState = suspendedWarningState;
    this[_a] = clock.ClockTypeId;
    /**
     * Returns the current clock time in milliseconds.
     */
    this.currentTimeMillis = core.map(ref.get(this.clockState), data => data.instant);
    /**
     * Returns the current clock time in nanoseconds.
     */
    this.currentTimeNanos = core.map(ref.get(this.clockState), data => BigInt(data.instant * 1000000));
  }
  /**
   * Unsafely returns the current time in milliseconds.
   */
  unsafeCurrentTimeMillis() {
    return ref.unsafeGet(this.clockState).instant;
  }
  /**
   * Unsafely returns the current time in nanoseconds.
   */
  unsafeCurrentTimeNanos() {
    return BigInt(ref.unsafeGet(this.clockState).instant * 1000000);
  }
  /**
   * Saves the `TestClock`'s current state in an effect which, when run, will
   * restore the `TestClock` state to the saved state.
   */
  save() {
    return core.map(ref.get(this.clockState), data => ref.set(this.clockState, data));
  }
  /**
   * Sets the current clock time to the specified instant. Any effects that
   * were scheduled to occur on or before the new time will be run in order.
   */
  setTime(instant) {
    return core.zipRight(this.warningDone(), this.run(() => instant));
  }
  /**
   * Semantically blocks the current fiber until the clock time is equal to or
   * greater than the specified duration. Once the clock time is adjusted to
   * on or after the duration, the fiber will automatically be resumed.
   */
  sleep(durationInput) {
    const duration = Duration.decode(durationInput);
    return core.flatMap(core.deferredMake(), deferred => core.flatMap(shouldAwait => shouldAwait ? core.zipRight(core.deferredAwait(deferred))(this.warningStart()) : core.asUnit(core.deferredSucceed(deferred, void 0)))(ref.modify(this.clockState, data => {
      const end = data.instant + Duration.toMillis(duration);
      if (end > data.instant) {
        return [true, Data.make(data.instant, Chunk.prepend([end, deferred])(data.sleeps))];
      }
      return [false, data];
    })));
  }
  /**
   * Returns a list of the times at which all queued effects are scheduled to
   * resume.
   */
  sleeps() {
    return core.map(ref.get(this.clockState), data => Chunk.map(data.sleeps, _ => _[0]));
  }
  /**
   * Increments the current clock time by the specified duration. Any effects
   * that were scheduled to occur on or before the new time will be run in
   * order.
   */
  adjust(durationInput) {
    const duration = Duration.decode(durationInput);
    return core.zipRight(this.warningDone(), this.run(n => n + Duration.toMillis(duration)));
  }
  /**
   * Increments the current clock time by the specified duration. Any effects
   * that were scheduled to occur on or before the new time will be run in
   * order.
   */
  adjustWith(durationInput) {
    const duration = Duration.decode(durationInput);
    return effect => fiberRuntime.zipLeftOptions(effect, this.adjust(duration), {
      concurrent: true
    });
  }
  /**
   * Returns a set of all fibers in this test.
   */
  supervisedFibers() {
    return this.annotations.supervisedFibers();
  }
  /**
   * Captures a "snapshot" of the identifier and status of all fibers in this
   * test other than the current fiber. Fails with the `Unit` value if any of
   * these fibers are not done or suspended. Note that because we cannot
   * synchronize on the status of multiple fibers at the same time this
   * snapshot may not be fully consistent.
   */
  freeze() {
    return core.flatMap(this.supervisedFibers(), fibers => effect.reduce(HashMap.empty(), (map, fiber) => core.flatMap(status => {
      if (FiberStatus.isDone(status)) {
        return core.succeed(HashMap.set(map, fiber.id(), status));
      }
      if (FiberStatus.isSuspended(status)) {
        return core.succeed(HashMap.set(map, fiber.id(), status));
      }
      return core.fail(void 0);
    })(fiber.status()))(fibers));
  }
  /**
   * Forks a fiber that will display a warning message if a test is using time
   * but is not advancing the `TestClock`.
   */
  warningStart() {
    return synchronized.updateSomeEffect(this.warningState, data => WarningData.isStart(data) ? Option.some(core.map(fiber => WarningData.pending(fiber))(fiberRuntime.fork(core.interruptible(this.live.provide(effect.delay(Duration.seconds(5))(effect.log(warning, "Warning"))))))) : Option.none());
  }
  /**
   * Cancels the warning message that is displayed if a test is using time but
   * is not advancing the `TestClock`.
   */
  warningDone() {
    return synchronized.updateSomeEffect(this.warningState, warningData => {
      if (WarningData.isStart(warningData)) {
        return Option.some(core.succeed(WarningData.done));
      }
      if (WarningData.isPending(warningData)) {
        return Option.some(core.as(WarningData.done)(core.interruptFiber(warningData.fiber)));
      }
      return Option.none();
    });
  }
  /**
   * Returns whether all descendants of this fiber are done or suspended.
   */
  suspended() {
    return core.flatMap(([first, last]) => Equal.equals(first, last) ? core.succeed(first) : core.fail(void 0))(core.zip(this.live.provide(core.zipRight(this.freeze())(effect.sleep(Duration.millis(5)))))(this.freeze()));
  }
  /**
   * Polls until all descendants of this fiber are done or suspended.
   */
  awaitSuspended() {
    return core.zipRight(this.suspendedWarningDone())(core.zipRight(effect.eventually(effect.filterOrFail(_Function.identity, _Function.constVoid)(core.zipWith(core.zipRight(this.suspended())(this.live.provide(effect.sleep(Duration.millis(10)))), Equal.equals)(this.suspended()))))(this.suspendedWarningStart()));
  }
  /**
   * Forks a fiber that will display a warning message if a test is advancing
   * the `TestClock` but a fiber is not suspending.
   */
  suspendedWarningStart() {
    return synchronized.updateSomeEffect(this.suspendedWarningState, suspendedWarningData => {
      if (SuspendedWarningData.isStart(suspendedWarningData)) {
        return Option.some(core.map(fiber => SuspendedWarningData.pending(fiber))(fiberRuntime.fork(core.interruptible(this.live.provide(effect.delay(Duration.seconds(5))(core.zipRight(ref.set(this.suspendedWarningState, SuspendedWarningData.done))(effect.log(suspendedWarning, "Warning"))))))));
      }
      return Option.none();
    });
  }
  /**
   * Cancels the warning message that is displayed if a test is advancing the
   * `TestClock` but a fiber is not suspending.
   */
  suspendedWarningDone() {
    return synchronized.updateSomeEffect(this.suspendedWarningState, suspendedWarningData => {
      if (SuspendedWarningData.isPending(suspendedWarningData)) {
        return Option.some(core.as(SuspendedWarningData.start)(core.interruptFiber(suspendedWarningData.fiber)));
      }
      return Option.none();
    });
  }
  /**
   * Runs all effects scheduled to occur on or before the specified instant,
   * which may depend on the current time, in order.
   */
  run(f) {
    return core.zipRight(core.flatMap(option => {
      switch (option._tag) {
        case "None":
          {
            return core.unit;
          }
        case "Some":
          {
            const [end, deferred] = option.value;
            return core.zipRight(this.run(() => end))(core.zipRight(core.yieldNow())(core.deferredSucceed(deferred, void 0)));
          }
      }
    })(ref.modify(this.clockState, data => {
      const end = f(data.instant);
      const sorted = Chunk.sort(Order.mapInput(_ => _[0])(number.Order))(data.sleeps);
      if (Chunk.isNonEmpty(sorted)) {
        const [instant, deferred] = Chunk.headNonEmpty(sorted);
        if (instant <= end) {
          return [Option.some([end, deferred]), Data.make(instant, Chunk.tailNonEmpty(sorted))];
        }
      }
      return [Option.none(), Data.make(end, data.sleeps)];
    })))(this.awaitSuspended());
  }
}
exports.TestClockImpl = TestClockImpl;
_a = clock.ClockTypeId;
/** @internal */
const live = data => layer.scoped(TestClock, effect.gen(function* ($) {
  const live = yield* $(Live.Live);
  const annotations = yield* $(Annotations.Annotations);
  const clockState = yield* $(core.sync(() => ref.unsafeMake(data)));
  const warningState = yield* $(circular.makeSynchronized(WarningData.start));
  const suspendedWarningState = yield* $(circular.makeSynchronized(SuspendedWarningData.start));
  const testClock = new TestClockImpl(clockState, live, annotations, warningState, suspendedWarningState);
  yield* $(fiberRuntime.withClockScoped(testClock));
  yield* $(fiberRuntime.addFinalizer(() => core.zipRight(testClock.warningDone(), testClock.suspendedWarningDone())));
  return testClock;
}));
/** @internal */
exports.live = live;
const defaultTestClock = /*#__PURE__*/live( /*#__PURE__*/Data.make( /*#__PURE__*/new Date(0).getTime(), /*#__PURE__*/Chunk.empty()));
/**
 * Accesses a `TestClock` instance in the context and increments the time
 * by the specified duration, running any actions scheduled for on or before
 * the new time in order.
 *
 * @internal
 */
exports.defaultTestClock = defaultTestClock;
const adjust = durationInput => {
  const duration = Duration.decode(durationInput);
  return testClockWith(testClock => testClock.adjust(duration));
};
/** @internal */
exports.adjust = adjust;
const adjustWith = /*#__PURE__*/(0, _Function.dual)(2, (effect, durationInput) => {
  const duration = Duration.decode(durationInput);
  return testClockWith(testClock => testClock.adjustWith(duration)(effect));
});
/**
 * Accesses a `TestClock` instance in the context and saves the clock
 * state in an effect which, when run, will restore the `TestClock` to the
 * saved state.
 *
 * @internal
 */
exports.adjustWith = adjustWith;
const save = () => testClockWith(testClock => testClock.save());
/**
 * Accesses a `TestClock` instance in the context and sets the clock time
 * to the specified `Instant`, running any actions scheduled for on or before
 * the new time in order.
 *
 * @internal
 */
exports.save = save;
const setTime = instant => testClockWith(testClock => testClock.setTime(instant));
/**
 * Semantically blocks the current fiber until the clock time is equal to or
 * greater than the specified duration. Once the clock time is adjusted to
 * on or after the duration, the fiber will automatically be resumed.
 *
 * @internal
 */
exports.setTime = setTime;
const sleep = durationInput => {
  const duration = Duration.decode(durationInput);
  return testClockWith(testClock => testClock.sleep(duration));
};
/**
 * Accesses a `TestClock` instance in the context and returns a list of
 * times that effects are scheduled to run.
 *
 * @internal
 */
exports.sleep = sleep;
const sleeps = () => testClockWith(testClock => testClock.sleeps());
/**
 * Retrieves the `TestClock` service for this test.
 *
 * @internal
 */
exports.sleeps = sleeps;
const testClock = () => testClockWith(core.succeed);
/**
 * Retrieves the `TestClock` service for this test and uses it to run the
 * specified workflow.
 *
 * @internal
 */
exports.testClock = testClock;
const testClockWith = f => core.fiberRefGetWith(defaultServices.currentServices, services => f(Context.get(clock.clockTag)(services)));
/**
 * Accesses the current time of a `TestClock` instance in the context in
 * milliseconds.
 *
 * @internal
 */
exports.testClockWith = testClockWith;
const currentTimeMillis = /*#__PURE__*/testClockWith(testClock => testClock.currentTimeMillis);
exports.currentTimeMillis = currentTimeMillis;
//# sourceMappingURL=testClock.js.map