aifsmjs 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,17 +14,20 @@ function after(ms, fn, opts) {
14
14
  const { st, ct } = resolveTimers(opts);
15
15
  let fired = false;
16
16
  let cancelled = false;
17
- const handle = st(() => {
18
- fired = true;
19
- if (cancelled) return;
20
- fn();
21
- }, ms);
17
+ const timer = {};
22
18
  const cancel = () => {
23
19
  if (fired || cancelled) return;
24
20
  cancelled = true;
25
- ct(handle);
21
+ if (timer.handle !== void 0) ct(timer.handle);
22
+ if (opts?.signal) opts.signal.removeEventListener("abort", cancel);
26
23
  };
27
- if (opts?.signal) {
24
+ timer.handle = st(() => {
25
+ fired = true;
26
+ if (cancelled) return;
27
+ if (opts?.signal) opts.signal.removeEventListener("abort", cancel);
28
+ fn();
29
+ }, ms);
30
+ if (opts?.signal && !fired) {
28
31
  opts.signal.addEventListener("abort", cancel, { once: true });
29
32
  }
30
33
  return Object.freeze({ cancel });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/timer/scheduler.ts"],"names":[],"mappings":";;;AAuBA,IAAM,IAAA,GAAoB,MAAA,CAAO,MAAA,CAAO,EAAE,QAAQ,MAAM;AAAC,CAAA,EAAG,CAAA;AAE5D,SAAS,cAAc,IAAA,EAGrB;AACA,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,MAAM,UAAA,KAAe,CAAC,IAAI,EAAA,KAAO,UAAA,CAAW,UAAA,CAAW,EAAA,EAAI,EAAE,CAAA,CAAA;AAAA,IACjE,IAAI,IAAA,EAAM,YAAA,KAAiB,CAAC,CAAA,KAAM,UAAA,CAAW,aAAa,CAAW,CAAA;AAAA,GACvE;AACF;AAUO,SAAS,KAAA,CAAM,EAAA,EAAY,EAAA,EAAgB,IAAA,EAAkC;AAClF,EAAA,IAAI,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,OAAO,IAAA;AAElC,EAAA,MAAM,EAAE,EAAA,EAAI,EAAA,EAAG,GAAI,cAAc,IAAI,CAAA;AACrC,EAAA,IAAI,KAAA,GAAQ,KAAA;AACZ,EAAA,IAAI,SAAA,GAAY,KAAA;AAEhB,EAAA,MAAM,MAAA,GAAS,GAAG,MAAM;AACtB,IAAA,KAAA,GAAQ,IAAA;AAER,IAAA,IAAI,SAAA,EAAW;AACf,IAAA,EAAA,EAAG;AAAA,EACL,GAAG,EAAE,CAAA;AAEL,EAAA,MAAM,SAAS,MAAM;AACnB,IAAA,IAAI,SAAS,SAAA,EAAW;AACxB,IAAA,SAAA,GAAY,IAAA;AACZ,IAAA,EAAA,CAAG,MAAM,CAAA;AAAA,EACX,CAAA;AAEA,EAAA,IAAI,MAAM,MAAA,EAAQ;AAChB,IAAA,IAAA,CAAK,OAAO,gBAAA,CAAiB,OAAA,EAAS,QAAQ,EAAE,IAAA,EAAM,MAAM,CAAA;AAAA,EAC9D;AAEA,EAAA,OAAO,MAAA,CAAO,MAAA,CAAO,EAAE,MAAA,EAAQ,CAAA;AACjC;AAgBO,SAAS,gBAAgB,QAAA,EAAoC;AAClE,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAiB;AAErC,EAAA,MAAM,KAAA,GAAmB;AAAA,IACvB,KAAA,CAAM,EAAA,EAAI,EAAA,EAAI,IAAA,EAAM;AAClB,MAAA,MAAM,MAAA,GAAuB,EAAE,GAAG,QAAA,EAAU,GAAG,IAAA,EAAK;AAGpD,MAAA,MAAM,OAA8B,EAAC;AACrC,MAAA,MAAM,UAAU,MAAM;AACpB,QAAA,IAAI,IAAA,CAAK,GAAA,EAAK,OAAA,CAAQ,MAAA,CAAO,KAAK,GAAG,CAAA;AACrC,QAAA,EAAA,EAAG;AAAA,MACL,CAAA;AACA,MAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,EAAA,EAAI,OAAA,EAAS,MAAM,CAAA;AACvC,MAAA,MAAM,MAAA,GAAsB,OAAO,MAAA,CAAO;AAAA,QACxC,MAAA,GAAS;AACP,UAAA,KAAA,CAAM,MAAA,EAAO;AACb,UAAA,IAAI,IAAA,CAAK,GAAA,EAAK,OAAA,CAAQ,MAAA,CAAO,KAAK,GAAG,CAAA;AAAA,QACvC;AAAA,OACD,CAAA;AACD,MAAA,IAAA,CAAK,GAAA,GAAM,MAAA;AACX,MAAA,OAAA,CAAQ,IAAI,MAAM,CAAA;AAClB,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAAA,IACA,SAAA,GAAY;AACV,MAAA,KAAA,MAAW,CAAA,IAAK,OAAA,EAAS,CAAA,CAAE,MAAA,EAAO;AAClC,MAAA,OAAA,CAAQ,KAAA,EAAM;AAAA,IAChB,CAAA;AAAA,IACA,IAAI,IAAA,GAAO;AACT,MAAA,OAAO,OAAA,CAAQ,IAAA;AAAA,IACjB;AAAA,GACF;AACA,EAAA,OAAO,KAAA;AACT","file":"index.cjs","sourcesContent":["export type AfterHandle = Readonly<{\n cancel(): void;\n}>;\n\nexport type SetTimeoutFn = (fn: () => void, ms: number) => unknown;\nexport type ClearTimeoutFn = (handle: unknown) => void;\n\nexport type AfterOptions = Readonly<{\n /**\n * If supplied and aborted, the callback never runs and any pending timer is\n * cleared. Aborting after fire is a no-op.\n */\n signal?: AbortSignal;\n /**\n * Override `setTimeout` (testing, SSR, custom loops). Defaults to globalThis.\n */\n setTimeout?: SetTimeoutFn;\n /**\n * Override `clearTimeout`. Must match the `setTimeout` you injected.\n */\n clearTimeout?: ClearTimeoutFn;\n}>;\n\nconst NOOP: AfterHandle = Object.freeze({ cancel: () => {} });\n\nfunction resolveTimers(opts: AfterOptions | undefined): {\n st: SetTimeoutFn;\n ct: ClearTimeoutFn;\n} {\n return {\n st: opts?.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms)),\n ct: opts?.clearTimeout ?? ((h) => globalThis.clearTimeout(h as number)),\n };\n}\n\n/**\n * Schedule `fn` to run after `ms` milliseconds. Returns a handle whose\n * `cancel()` clears the pending timer. Optional `signal` aborts the timer when\n * triggered. Aborting after the callback fires is a no-op.\n *\n * Per Node guidance, the abort listener is registered with `{ once: true }` to\n * avoid leaking listeners when the same signal is reused across many timers.\n */\nexport function after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle {\n if (opts?.signal?.aborted) return NOOP;\n\n const { st, ct } = resolveTimers(opts);\n let fired = false;\n let cancelled = false;\n\n const handle = st(() => {\n fired = true;\n /* v8 ignore next — defensive race guard: cancel() sets cancelled=true and clears the timer, but if a custom setTimeout fires after clear, this short-circuits fn(). */\n if (cancelled) return;\n fn();\n }, ms);\n\n const cancel = () => {\n if (fired || cancelled) return;\n cancelled = true;\n ct(handle);\n };\n\n if (opts?.signal) {\n opts.signal.addEventListener(\"abort\", cancel, { once: true });\n }\n\n return Object.freeze({ cancel });\n}\n\nexport type Scheduler = Readonly<{\n after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle;\n cancelAll(): void;\n readonly size: number;\n}>;\n\n/**\n * Build a scheduler that tracks every pending `after()` so they can be\n * cancelled together (e.g. on machine destroy). Each `after` returns a handle\n * whose `cancel()` also removes it from the tracking set.\n *\n * `defaults` are merged into every call — typically you inject `setTimeout` /\n * `clearTimeout` once at construction.\n */\nexport function createScheduler(defaults?: AfterOptions): Scheduler {\n const pending = new Set<AfterHandle>();\n\n const sched: Scheduler = {\n after(ms, fn, opts) {\n const merged: AfterOptions = { ...defaults, ...opts };\n // Forward-reference slot so `wrapped` can find the tracked handle\n // before it is constructed below.\n const slot: { ref?: AfterHandle } = {};\n const wrapped = () => {\n if (slot.ref) pending.delete(slot.ref);\n fn();\n };\n const inner = after(ms, wrapped, merged);\n const handle: AfterHandle = Object.freeze({\n cancel() {\n inner.cancel();\n if (slot.ref) pending.delete(slot.ref);\n },\n });\n slot.ref = handle;\n pending.add(handle);\n return handle;\n },\n cancelAll() {\n for (const h of pending) h.cancel();\n pending.clear();\n },\n get size() {\n return pending.size;\n },\n };\n return sched;\n}\n"]}
1
+ {"version":3,"sources":["../../src/timer/scheduler.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["export type AfterHandle = Readonly<{\n cancel(): void;\n}>;\n\nexport type SetTimeoutFn = (fn: () => void, ms: number) => unknown;\nexport type ClearTimeoutFn = (handle: unknown) => void;\n\nexport type AfterOptions = Readonly<{\n /**\n * If supplied and aborted, the callback never runs and any pending timer is\n * cleared. Aborting after fire is a no-op.\n */\n signal?: AbortSignal;\n /**\n * Override `setTimeout` (testing, SSR, custom loops). Defaults to globalThis.\n */\n setTimeout?: SetTimeoutFn;\n /**\n * Override `clearTimeout`. Must match the `setTimeout` you injected.\n */\n clearTimeout?: ClearTimeoutFn;\n}>;\n\nconst NOOP: AfterHandle = Object.freeze({ cancel: () => {} });\n\nfunction resolveTimers(opts: AfterOptions | undefined): {\n st: SetTimeoutFn;\n ct: ClearTimeoutFn;\n} {\n return {\n st: opts?.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms)),\n ct: opts?.clearTimeout ?? ((h) => globalThis.clearTimeout(h as number)),\n };\n}\n\n/**\n * Schedule `fn` to run after `ms` milliseconds. Returns a handle whose\n * `cancel()` clears the pending timer. Optional `signal` aborts the timer when\n * triggered. Aborting after the callback fires is a no-op.\n *\n * The abort listener is registered with `{ once: true }` as a baseline, but\n * `{ once: true }` alone does NOT prevent listener accumulation when the same\n * signal is reused across many timers: it only removes the listener when the\n * signal aborts, not when the timer fires normally or `cancel()` is called.\n * We therefore explicitly call `signal.removeEventListener(\"abort\", cancel)`\n * inside the fire callback and at the end of `cancel()` so that a shared,\n * long-lived signal never accumulates dead listeners across timer reuse.\n */\nexport function after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle {\n if (opts?.signal?.aborted) return NOOP;\n\n const { st, ct } = resolveTimers(opts);\n let fired = false;\n let cancelled = false;\n // `cancel` and the timer handle reference each other. A const cell holds the\n // handle so `cancel` can be defined BEFORE `st(...)` runs (letting a custom\n // `st` that fires its callback synchronously reference `cancel` without\n // hitting the temporal-dead-zone) while still being able to clear the handle\n // assigned afterwards. A synchronous fire sets `fired=true`, so cancel() never\n // reads the still-unset handle in that path.\n const timer: { handle?: ReturnType<typeof st> } = {};\n\n const cancel = () => {\n if (fired || cancelled) return;\n cancelled = true;\n if (timer.handle !== undefined) ct(timer.handle);\n // Detach the abort listener so a reused signal does not accumulate dead\n // closures after this timer is cancelled.\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", cancel);\n };\n\n timer.handle = st(() => {\n fired = true;\n /* v8 ignore next — defensive race guard: cancel() sets cancelled=true and clears the timer, but if a custom setTimeout fires after clear, this short-circuits fn(). */\n if (cancelled) return;\n // Detach the abort listener now that the timer has fired — the listener\n // will never be invoked and must not accumulate on a reused signal.\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", cancel);\n fn();\n }, ms);\n\n // Attach only if the timer has not already fired synchronously (a custom `st`\n // may fire inline); otherwise the listener would be registered AFTER the fire\n // path's removal ran and would then leak until the signal aborts.\n if (opts?.signal && !fired) {\n opts.signal.addEventListener(\"abort\", cancel, { once: true });\n }\n\n return Object.freeze({ cancel });\n}\n\nexport type Scheduler = Readonly<{\n after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle;\n cancelAll(): void;\n readonly size: number;\n}>;\n\n/**\n * Build a scheduler that tracks every pending `after()` so they can be\n * cancelled together (e.g. on machine destroy). Each `after` returns a handle\n * whose `cancel()` also removes it from the tracking set.\n *\n * `defaults` are merged into every call — typically you inject `setTimeout` /\n * `clearTimeout` once at construction.\n */\nexport function createScheduler(defaults?: AfterOptions): Scheduler {\n const pending = new Set<AfterHandle>();\n\n const sched: Scheduler = {\n after(ms, fn, opts) {\n const merged: AfterOptions = { ...defaults, ...opts };\n // Forward-reference slot so `wrapped` can find the tracked handle\n // before it is constructed below.\n const slot: { ref?: AfterHandle } = {};\n const wrapped = () => {\n if (slot.ref) pending.delete(slot.ref);\n fn();\n };\n const inner = after(ms, wrapped, merged);\n const handle: AfterHandle = Object.freeze({\n cancel() {\n inner.cancel();\n if (slot.ref) pending.delete(slot.ref);\n },\n });\n slot.ref = handle;\n pending.add(handle);\n return handle;\n },\n cancelAll() {\n for (const h of pending) h.cancel();\n pending.clear();\n },\n get size() {\n return pending.size;\n },\n };\n return sched;\n}\n"]}
@@ -23,8 +23,13 @@ type AfterOptions = Readonly<{
23
23
  * `cancel()` clears the pending timer. Optional `signal` aborts the timer when
24
24
  * triggered. Aborting after the callback fires is a no-op.
25
25
  *
26
- * Per Node guidance, the abort listener is registered with `{ once: true }` to
27
- * avoid leaking listeners when the same signal is reused across many timers.
26
+ * The abort listener is registered with `{ once: true }` as a baseline, but
27
+ * `{ once: true }` alone does NOT prevent listener accumulation when the same
28
+ * signal is reused across many timers: it only removes the listener when the
29
+ * signal aborts, not when the timer fires normally or `cancel()` is called.
30
+ * We therefore explicitly call `signal.removeEventListener("abort", cancel)`
31
+ * inside the fire callback and at the end of `cancel()` so that a shared,
32
+ * long-lived signal never accumulates dead listeners across timer reuse.
28
33
  */
29
34
  declare function after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle;
30
35
  type Scheduler = Readonly<{
@@ -23,8 +23,13 @@ type AfterOptions = Readonly<{
23
23
  * `cancel()` clears the pending timer. Optional `signal` aborts the timer when
24
24
  * triggered. Aborting after the callback fires is a no-op.
25
25
  *
26
- * Per Node guidance, the abort listener is registered with `{ once: true }` to
27
- * avoid leaking listeners when the same signal is reused across many timers.
26
+ * The abort listener is registered with `{ once: true }` as a baseline, but
27
+ * `{ once: true }` alone does NOT prevent listener accumulation when the same
28
+ * signal is reused across many timers: it only removes the listener when the
29
+ * signal aborts, not when the timer fires normally or `cancel()` is called.
30
+ * We therefore explicitly call `signal.removeEventListener("abort", cancel)`
31
+ * inside the fire callback and at the end of `cancel()` so that a shared,
32
+ * long-lived signal never accumulates dead listeners across timer reuse.
28
33
  */
29
34
  declare function after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle;
30
35
  type Scheduler = Readonly<{
@@ -12,17 +12,20 @@ function after(ms, fn, opts) {
12
12
  const { st, ct } = resolveTimers(opts);
13
13
  let fired = false;
14
14
  let cancelled = false;
15
- const handle = st(() => {
16
- fired = true;
17
- if (cancelled) return;
18
- fn();
19
- }, ms);
15
+ const timer = {};
20
16
  const cancel = () => {
21
17
  if (fired || cancelled) return;
22
18
  cancelled = true;
23
- ct(handle);
19
+ if (timer.handle !== void 0) ct(timer.handle);
20
+ if (opts?.signal) opts.signal.removeEventListener("abort", cancel);
24
21
  };
25
- if (opts?.signal) {
22
+ timer.handle = st(() => {
23
+ fired = true;
24
+ if (cancelled) return;
25
+ if (opts?.signal) opts.signal.removeEventListener("abort", cancel);
26
+ fn();
27
+ }, ms);
28
+ if (opts?.signal && !fired) {
26
29
  opts.signal.addEventListener("abort", cancel, { once: true });
27
30
  }
28
31
  return Object.freeze({ cancel });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/timer/scheduler.ts"],"names":[],"mappings":";AAuBA,IAAM,IAAA,GAAoB,MAAA,CAAO,MAAA,CAAO,EAAE,QAAQ,MAAM;AAAC,CAAA,EAAG,CAAA;AAE5D,SAAS,cAAc,IAAA,EAGrB;AACA,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,MAAM,UAAA,KAAe,CAAC,IAAI,EAAA,KAAO,UAAA,CAAW,UAAA,CAAW,EAAA,EAAI,EAAE,CAAA,CAAA;AAAA,IACjE,IAAI,IAAA,EAAM,YAAA,KAAiB,CAAC,CAAA,KAAM,UAAA,CAAW,aAAa,CAAW,CAAA;AAAA,GACvE;AACF;AAUO,SAAS,KAAA,CAAM,EAAA,EAAY,EAAA,EAAgB,IAAA,EAAkC;AAClF,EAAA,IAAI,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,OAAO,IAAA;AAElC,EAAA,MAAM,EAAE,EAAA,EAAI,EAAA,EAAG,GAAI,cAAc,IAAI,CAAA;AACrC,EAAA,IAAI,KAAA,GAAQ,KAAA;AACZ,EAAA,IAAI,SAAA,GAAY,KAAA;AAEhB,EAAA,MAAM,MAAA,GAAS,GAAG,MAAM;AACtB,IAAA,KAAA,GAAQ,IAAA;AAER,IAAA,IAAI,SAAA,EAAW;AACf,IAAA,EAAA,EAAG;AAAA,EACL,GAAG,EAAE,CAAA;AAEL,EAAA,MAAM,SAAS,MAAM;AACnB,IAAA,IAAI,SAAS,SAAA,EAAW;AACxB,IAAA,SAAA,GAAY,IAAA;AACZ,IAAA,EAAA,CAAG,MAAM,CAAA;AAAA,EACX,CAAA;AAEA,EAAA,IAAI,MAAM,MAAA,EAAQ;AAChB,IAAA,IAAA,CAAK,OAAO,gBAAA,CAAiB,OAAA,EAAS,QAAQ,EAAE,IAAA,EAAM,MAAM,CAAA;AAAA,EAC9D;AAEA,EAAA,OAAO,MAAA,CAAO,MAAA,CAAO,EAAE,MAAA,EAAQ,CAAA;AACjC;AAgBO,SAAS,gBAAgB,QAAA,EAAoC;AAClE,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAiB;AAErC,EAAA,MAAM,KAAA,GAAmB;AAAA,IACvB,KAAA,CAAM,EAAA,EAAI,EAAA,EAAI,IAAA,EAAM;AAClB,MAAA,MAAM,MAAA,GAAuB,EAAE,GAAG,QAAA,EAAU,GAAG,IAAA,EAAK;AAGpD,MAAA,MAAM,OAA8B,EAAC;AACrC,MAAA,MAAM,UAAU,MAAM;AACpB,QAAA,IAAI,IAAA,CAAK,GAAA,EAAK,OAAA,CAAQ,MAAA,CAAO,KAAK,GAAG,CAAA;AACrC,QAAA,EAAA,EAAG;AAAA,MACL,CAAA;AACA,MAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,EAAA,EAAI,OAAA,EAAS,MAAM,CAAA;AACvC,MAAA,MAAM,MAAA,GAAsB,OAAO,MAAA,CAAO;AAAA,QACxC,MAAA,GAAS;AACP,UAAA,KAAA,CAAM,MAAA,EAAO;AACb,UAAA,IAAI,IAAA,CAAK,GAAA,EAAK,OAAA,CAAQ,MAAA,CAAO,KAAK,GAAG,CAAA;AAAA,QACvC;AAAA,OACD,CAAA;AACD,MAAA,IAAA,CAAK,GAAA,GAAM,MAAA;AACX,MAAA,OAAA,CAAQ,IAAI,MAAM,CAAA;AAClB,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAAA,IACA,SAAA,GAAY;AACV,MAAA,KAAA,MAAW,CAAA,IAAK,OAAA,EAAS,CAAA,CAAE,MAAA,EAAO;AAClC,MAAA,OAAA,CAAQ,KAAA,EAAM;AAAA,IAChB,CAAA;AAAA,IACA,IAAI,IAAA,GAAO;AACT,MAAA,OAAO,OAAA,CAAQ,IAAA;AAAA,IACjB;AAAA,GACF;AACA,EAAA,OAAO,KAAA;AACT","file":"index.js","sourcesContent":["export type AfterHandle = Readonly<{\n cancel(): void;\n}>;\n\nexport type SetTimeoutFn = (fn: () => void, ms: number) => unknown;\nexport type ClearTimeoutFn = (handle: unknown) => void;\n\nexport type AfterOptions = Readonly<{\n /**\n * If supplied and aborted, the callback never runs and any pending timer is\n * cleared. Aborting after fire is a no-op.\n */\n signal?: AbortSignal;\n /**\n * Override `setTimeout` (testing, SSR, custom loops). Defaults to globalThis.\n */\n setTimeout?: SetTimeoutFn;\n /**\n * Override `clearTimeout`. Must match the `setTimeout` you injected.\n */\n clearTimeout?: ClearTimeoutFn;\n}>;\n\nconst NOOP: AfterHandle = Object.freeze({ cancel: () => {} });\n\nfunction resolveTimers(opts: AfterOptions | undefined): {\n st: SetTimeoutFn;\n ct: ClearTimeoutFn;\n} {\n return {\n st: opts?.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms)),\n ct: opts?.clearTimeout ?? ((h) => globalThis.clearTimeout(h as number)),\n };\n}\n\n/**\n * Schedule `fn` to run after `ms` milliseconds. Returns a handle whose\n * `cancel()` clears the pending timer. Optional `signal` aborts the timer when\n * triggered. Aborting after the callback fires is a no-op.\n *\n * Per Node guidance, the abort listener is registered with `{ once: true }` to\n * avoid leaking listeners when the same signal is reused across many timers.\n */\nexport function after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle {\n if (opts?.signal?.aborted) return NOOP;\n\n const { st, ct } = resolveTimers(opts);\n let fired = false;\n let cancelled = false;\n\n const handle = st(() => {\n fired = true;\n /* v8 ignore next — defensive race guard: cancel() sets cancelled=true and clears the timer, but if a custom setTimeout fires after clear, this short-circuits fn(). */\n if (cancelled) return;\n fn();\n }, ms);\n\n const cancel = () => {\n if (fired || cancelled) return;\n cancelled = true;\n ct(handle);\n };\n\n if (opts?.signal) {\n opts.signal.addEventListener(\"abort\", cancel, { once: true });\n }\n\n return Object.freeze({ cancel });\n}\n\nexport type Scheduler = Readonly<{\n after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle;\n cancelAll(): void;\n readonly size: number;\n}>;\n\n/**\n * Build a scheduler that tracks every pending `after()` so they can be\n * cancelled together (e.g. on machine destroy). Each `after` returns a handle\n * whose `cancel()` also removes it from the tracking set.\n *\n * `defaults` are merged into every call — typically you inject `setTimeout` /\n * `clearTimeout` once at construction.\n */\nexport function createScheduler(defaults?: AfterOptions): Scheduler {\n const pending = new Set<AfterHandle>();\n\n const sched: Scheduler = {\n after(ms, fn, opts) {\n const merged: AfterOptions = { ...defaults, ...opts };\n // Forward-reference slot so `wrapped` can find the tracked handle\n // before it is constructed below.\n const slot: { ref?: AfterHandle } = {};\n const wrapped = () => {\n if (slot.ref) pending.delete(slot.ref);\n fn();\n };\n const inner = after(ms, wrapped, merged);\n const handle: AfterHandle = Object.freeze({\n cancel() {\n inner.cancel();\n if (slot.ref) pending.delete(slot.ref);\n },\n });\n slot.ref = handle;\n pending.add(handle);\n return handle;\n },\n cancelAll() {\n for (const h of pending) h.cancel();\n pending.clear();\n },\n get size() {\n return pending.size;\n },\n };\n return sched;\n}\n"]}
1
+ {"version":3,"sources":["../../src/timer/scheduler.ts"],"names":[],"mappings":";AAuBA,IAAM,IAAA,GAAoB,MAAA,CAAO,MAAA,CAAO,EAAE,QAAQ,MAAM;AAAC,CAAA,EAAG,CAAA;AAE5D,SAAS,cAAc,IAAA,EAGrB;AACA,EAAA,OAAO;AAAA,IACL,EAAA,EAAI,MAAM,UAAA,KAAe,CAAC,IAAI,EAAA,KAAO,UAAA,CAAW,UAAA,CAAW,EAAA,EAAI,EAAE,CAAA,CAAA;AAAA,IACjE,IAAI,IAAA,EAAM,YAAA,KAAiB,CAAC,CAAA,KAAM,UAAA,CAAW,aAAa,CAAW,CAAA;AAAA,GACvE;AACF;AAeO,SAAS,KAAA,CAAM,EAAA,EAAY,EAAA,EAAgB,IAAA,EAAkC;AAClF,EAAA,IAAI,IAAA,EAAM,MAAA,EAAQ,OAAA,EAAS,OAAO,IAAA;AAElC,EAAA,MAAM,EAAE,EAAA,EAAI,EAAA,EAAG,GAAI,cAAc,IAAI,CAAA;AACrC,EAAA,IAAI,KAAA,GAAQ,KAAA;AACZ,EAAA,IAAI,SAAA,GAAY,KAAA;AAOhB,EAAA,MAAM,QAA4C,EAAC;AAEnD,EAAA,MAAM,SAAS,MAAM;AACnB,IAAA,IAAI,SAAS,SAAA,EAAW;AACxB,IAAA,SAAA,GAAY,IAAA;AACZ,IAAA,IAAI,KAAA,CAAM,MAAA,KAAW,MAAA,EAAW,EAAA,CAAG,MAAM,MAAM,CAAA;AAG/C,IAAA,IAAI,MAAM,MAAA,EAAQ,IAAA,CAAK,MAAA,CAAO,mBAAA,CAAoB,SAAS,MAAM,CAAA;AAAA,EACnE,CAAA;AAEA,EAAA,KAAA,CAAM,MAAA,GAAS,GAAG,MAAM;AACtB,IAAA,KAAA,GAAQ,IAAA;AAER,IAAA,IAAI,SAAA,EAAW;AAGf,IAAA,IAAI,MAAM,MAAA,EAAQ,IAAA,CAAK,MAAA,CAAO,mBAAA,CAAoB,SAAS,MAAM,CAAA;AACjE,IAAA,EAAA,EAAG;AAAA,EACL,GAAG,EAAE,CAAA;AAKL,EAAA,IAAI,IAAA,EAAM,MAAA,IAAU,CAAC,KAAA,EAAO;AAC1B,IAAA,IAAA,CAAK,OAAO,gBAAA,CAAiB,OAAA,EAAS,QAAQ,EAAE,IAAA,EAAM,MAAM,CAAA;AAAA,EAC9D;AAEA,EAAA,OAAO,MAAA,CAAO,MAAA,CAAO,EAAE,MAAA,EAAQ,CAAA;AACjC;AAgBO,SAAS,gBAAgB,QAAA,EAAoC;AAClE,EAAA,MAAM,OAAA,uBAAc,GAAA,EAAiB;AAErC,EAAA,MAAM,KAAA,GAAmB;AAAA,IACvB,KAAA,CAAM,EAAA,EAAI,EAAA,EAAI,IAAA,EAAM;AAClB,MAAA,MAAM,MAAA,GAAuB,EAAE,GAAG,QAAA,EAAU,GAAG,IAAA,EAAK;AAGpD,MAAA,MAAM,OAA8B,EAAC;AACrC,MAAA,MAAM,UAAU,MAAM;AACpB,QAAA,IAAI,IAAA,CAAK,GAAA,EAAK,OAAA,CAAQ,MAAA,CAAO,KAAK,GAAG,CAAA;AACrC,QAAA,EAAA,EAAG;AAAA,MACL,CAAA;AACA,MAAA,MAAM,KAAA,GAAQ,KAAA,CAAM,EAAA,EAAI,OAAA,EAAS,MAAM,CAAA;AACvC,MAAA,MAAM,MAAA,GAAsB,OAAO,MAAA,CAAO;AAAA,QACxC,MAAA,GAAS;AACP,UAAA,KAAA,CAAM,MAAA,EAAO;AACb,UAAA,IAAI,IAAA,CAAK,GAAA,EAAK,OAAA,CAAQ,MAAA,CAAO,KAAK,GAAG,CAAA;AAAA,QACvC;AAAA,OACD,CAAA;AACD,MAAA,IAAA,CAAK,GAAA,GAAM,MAAA;AACX,MAAA,OAAA,CAAQ,IAAI,MAAM,CAAA;AAClB,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAAA,IACA,SAAA,GAAY;AACV,MAAA,KAAA,MAAW,CAAA,IAAK,OAAA,EAAS,CAAA,CAAE,MAAA,EAAO;AAClC,MAAA,OAAA,CAAQ,KAAA,EAAM;AAAA,IAChB,CAAA;AAAA,IACA,IAAI,IAAA,GAAO;AACT,MAAA,OAAO,OAAA,CAAQ,IAAA;AAAA,IACjB;AAAA,GACF;AACA,EAAA,OAAO,KAAA;AACT","file":"index.js","sourcesContent":["export type AfterHandle = Readonly<{\n cancel(): void;\n}>;\n\nexport type SetTimeoutFn = (fn: () => void, ms: number) => unknown;\nexport type ClearTimeoutFn = (handle: unknown) => void;\n\nexport type AfterOptions = Readonly<{\n /**\n * If supplied and aborted, the callback never runs and any pending timer is\n * cleared. Aborting after fire is a no-op.\n */\n signal?: AbortSignal;\n /**\n * Override `setTimeout` (testing, SSR, custom loops). Defaults to globalThis.\n */\n setTimeout?: SetTimeoutFn;\n /**\n * Override `clearTimeout`. Must match the `setTimeout` you injected.\n */\n clearTimeout?: ClearTimeoutFn;\n}>;\n\nconst NOOP: AfterHandle = Object.freeze({ cancel: () => {} });\n\nfunction resolveTimers(opts: AfterOptions | undefined): {\n st: SetTimeoutFn;\n ct: ClearTimeoutFn;\n} {\n return {\n st: opts?.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms)),\n ct: opts?.clearTimeout ?? ((h) => globalThis.clearTimeout(h as number)),\n };\n}\n\n/**\n * Schedule `fn` to run after `ms` milliseconds. Returns a handle whose\n * `cancel()` clears the pending timer. Optional `signal` aborts the timer when\n * triggered. Aborting after the callback fires is a no-op.\n *\n * The abort listener is registered with `{ once: true }` as a baseline, but\n * `{ once: true }` alone does NOT prevent listener accumulation when the same\n * signal is reused across many timers: it only removes the listener when the\n * signal aborts, not when the timer fires normally or `cancel()` is called.\n * We therefore explicitly call `signal.removeEventListener(\"abort\", cancel)`\n * inside the fire callback and at the end of `cancel()` so that a shared,\n * long-lived signal never accumulates dead listeners across timer reuse.\n */\nexport function after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle {\n if (opts?.signal?.aborted) return NOOP;\n\n const { st, ct } = resolveTimers(opts);\n let fired = false;\n let cancelled = false;\n // `cancel` and the timer handle reference each other. A const cell holds the\n // handle so `cancel` can be defined BEFORE `st(...)` runs (letting a custom\n // `st` that fires its callback synchronously reference `cancel` without\n // hitting the temporal-dead-zone) while still being able to clear the handle\n // assigned afterwards. A synchronous fire sets `fired=true`, so cancel() never\n // reads the still-unset handle in that path.\n const timer: { handle?: ReturnType<typeof st> } = {};\n\n const cancel = () => {\n if (fired || cancelled) return;\n cancelled = true;\n if (timer.handle !== undefined) ct(timer.handle);\n // Detach the abort listener so a reused signal does not accumulate dead\n // closures after this timer is cancelled.\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", cancel);\n };\n\n timer.handle = st(() => {\n fired = true;\n /* v8 ignore next — defensive race guard: cancel() sets cancelled=true and clears the timer, but if a custom setTimeout fires after clear, this short-circuits fn(). */\n if (cancelled) return;\n // Detach the abort listener now that the timer has fired — the listener\n // will never be invoked and must not accumulate on a reused signal.\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", cancel);\n fn();\n }, ms);\n\n // Attach only if the timer has not already fired synchronously (a custom `st`\n // may fire inline); otherwise the listener would be registered AFTER the fire\n // path's removal ran and would then leak until the signal aborts.\n if (opts?.signal && !fired) {\n opts.signal.addEventListener(\"abort\", cancel, { once: true });\n }\n\n return Object.freeze({ cancel });\n}\n\nexport type Scheduler = Readonly<{\n after(ms: number, fn: () => void, opts?: AfterOptions): AfterHandle;\n cancelAll(): void;\n readonly size: number;\n}>;\n\n/**\n * Build a scheduler that tracks every pending `after()` so they can be\n * cancelled together (e.g. on machine destroy). Each `after` returns a handle\n * whose `cancel()` also removes it from the tracking set.\n *\n * `defaults` are merged into every call — typically you inject `setTimeout` /\n * `clearTimeout` once at construction.\n */\nexport function createScheduler(defaults?: AfterOptions): Scheduler {\n const pending = new Set<AfterHandle>();\n\n const sched: Scheduler = {\n after(ms, fn, opts) {\n const merged: AfterOptions = { ...defaults, ...opts };\n // Forward-reference slot so `wrapped` can find the tracked handle\n // before it is constructed below.\n const slot: { ref?: AfterHandle } = {};\n const wrapped = () => {\n if (slot.ref) pending.delete(slot.ref);\n fn();\n };\n const inner = after(ms, wrapped, merged);\n const handle: AfterHandle = Object.freeze({\n cancel() {\n inner.cancel();\n if (slot.ref) pending.delete(slot.ref);\n },\n });\n slot.ref = handle;\n pending.add(handle);\n return handle;\n },\n cancelAll() {\n for (const h of pending) h.cancel();\n pending.clear();\n },\n get size() {\n return pending.size;\n },\n };\n return sched;\n}\n"]}
package/llms-full.txt CHANGED
@@ -543,6 +543,35 @@ All notable changes to this project will be documented in this file.
543
543
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
544
544
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
545
545
 
546
+ ## [Unreleased]
547
+
548
+ ## [0.5.1] - 2026-06-02
549
+
550
+ ### Fixed
551
+
552
+ - **Memory: `after()` and `createScheduler().after()` accumulated dead abort
553
+ listeners on a reused `AbortSignal`.** `{ once: true }` only removes the
554
+ listener when the signal fires — not when the timer fires normally or
555
+ `cancel()` is called. Scheduling many timers on one long-lived signal
556
+ therefore accumulated dead `"abort"` closures. The fix explicitly calls
557
+ `signal.removeEventListener("abort", cancel)` inside the fire callback (so
558
+ a fired timer detaches immediately) and at the end of `cancel()` (so a
559
+ cancelled timer also detaches). Same class of leak as the [0.1.2] and
560
+ [0.3.1] abort-listener fixes; the timer subpath was the remaining gap.
561
+ Four regression tests added to `test/timer/scheduler.test.ts` covering
562
+ both `after()` and `createScheduler().after()`, fire and cancel paths.
563
+ (`src/timer/scheduler.ts`)
564
+
565
+ ### Changed
566
+
567
+ - **`fast-check` peer dependency range extended to `^3.20.0 || ^4.0.0`.**
568
+ The ai\*js family standard is fast-check v4.8; consumers who have already
569
+ upgraded to v4 no longer need to suppress a peer warning. The devDependency
570
+ is pinned to `^4.8.0` so CI runs against v4. Consumers who remain on v3
571
+ are fully unaffected — the `||` range keeps v3 satisfied. The `aifsmjs/pbt`
572
+ subpath is the only entry point that imports fast-check; the core and all
573
+ other subpaths are tree-shake-free of it.
574
+
546
575
  ## [0.4.1] — 2026-05-29
547
576
 
548
577
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aifsmjs",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Small, strict FSM library for deterministic, replayable state machines in any TypeScript/JS app — multi-step forms, checkout funnels, auth flows, tutorials, scene flow. Pure step() lifecycle, opt-in effects, inspect, replay, and a fast-check property-based testing adapter. Browser / Node / Bun / Deno / WebView / Worker friendly.",
5
5
  "keywords": [
6
6
  "fsm",
@@ -97,7 +97,7 @@
97
97
  "prepublishOnly": "pnpm typecheck && pnpm lint && pnpm coverage && pnpm build && pnpm verify:exports && pnpm verify:llms && pnpm check:size"
98
98
  },
99
99
  "peerDependencies": {
100
- "fast-check": "^3.20.0"
100
+ "fast-check": "^3.20.0 || ^4.0.0"
101
101
  },
102
102
  "peerDependenciesMeta": {
103
103
  "fast-check": {
@@ -108,7 +108,7 @@
108
108
  "@biomejs/biome": "^1.9.0",
109
109
  "@types/node": "^22.0.0",
110
110
  "@vitest/coverage-v8": "^4.1.7",
111
- "fast-check": "^3.20.0",
111
+ "fast-check": "^4.8.0",
112
112
  "tsup": "^8.3.0",
113
113
  "tsx": "^4.22.3",
114
114
  "typescript": "^5.6.0",