backtest-kit 11.0.0 → 11.2.0

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.
package/build/index.cjs CHANGED
@@ -63859,6 +63859,34 @@ class CronUtils {
63859
63859
  * on successful settle.
63860
63860
  */
63861
63861
  this._firedOnce = new Set();
63862
+ /**
63863
+ * Last interval boundary already fired per periodic slot.
63864
+ *
63865
+ * Key shape (no `alignedMs` segment — one entry per logical slot, not per
63866
+ * boundary; always carries the generation suffix `:g${generation}`, and the
63867
+ * `:${symbol}` scope only in fan-out mode):
63868
+ * - Periodic global: `${name}${genSuffix}`.
63869
+ * - Periodic fan-out: `${name}:${symbol}${genSuffix}`.
63870
+ *
63871
+ * Value is the aligned-boundary epoch ms (`alignedMs`) most recently opened
63872
+ * for that slot. `_tick` fires a periodic entry whenever the incoming tick's
63873
+ * aligned boundary is **strictly greater** than the stored value, instead of
63874
+ * requiring the tick to land *exactly* on the boundary. This fixes the
63875
+ * dropped-boundary bug: when virtual time jumps over a boundary (e.g. a
63876
+ * `5m`-driven loop skipping from 00:14 to 00:29 never lands on the `15m`
63877
+ * 00:15 boundary), the old `ts === alignedMs` check silently lost the tick.
63878
+ * With the watermark, the next tick whose `alignedMs` advanced past the
63879
+ * stored value fires once for the newest crossed boundary (catch-up
63880
+ * collapses multiple skipped boundaries into a single invocation at the
63881
+ * latest one).
63882
+ *
63883
+ * Written synchronously in `_tick` at slot-open time (before the `await`),
63884
+ * so a still-in-flight handler does not let a later tick re-open the same
63885
+ * (or an already-passed) boundary. Fire-once entries never touch this map —
63886
+ * they use `_firedOnce`. Pruned by `_clearBoundaryFor` on
63887
+ * `register`/`unregister` and wiped by `dispose`.
63888
+ */
63889
+ this._lastBoundary = new Map();
63862
63890
  /**
63863
63891
  * Register a periodic cron entry.
63864
63892
  *
@@ -63904,6 +63932,7 @@ class CronUtils {
63904
63932
  }
63905
63933
  }
63906
63934
  this._clearFiredOnceFor(entry.name);
63935
+ this._clearBoundaryFor(entry.name);
63907
63936
  const generation = ++this._generationCounter;
63908
63937
  this._entries.set(entry.name, { entry, generation });
63909
63938
  return () => this.unregister(entry.name);
@@ -63920,6 +63949,7 @@ class CronUtils {
63920
63949
  LOGGER_SERVICE$1.info(CRON_METHOD_NAME_UNREGISTER, { name });
63921
63950
  this._entries.delete(name);
63922
63951
  this._clearFiredOnceFor(name);
63952
+ this._clearBoundaryFor(name);
63923
63953
  };
63924
63954
  /**
63925
63955
  * Clear fire-once marks so that fire-once entries can fire again.
@@ -63996,11 +64026,24 @@ class CronUtils {
63996
64026
  * - Slot key: `${name}:once` (+ scope) (+ gen).
63997
64027
  * - `aligned` = the 1-minute-aligned `when` from step 0.
63998
64028
  * 5. **Periodic** (`entry.interval` set):
63999
- * - Align `when` further to the entry's interval via {@link alignToInterval}.
64000
- * - If `ts !== alignedMs`, the tick is mid-interval skip.
64001
- * (This is the "remainder === 0" boundary check from the spec;
64002
- * since `ts` is already on the 1-minute boundary, the check is exact
64003
- * for `1m` and consistent for higher intervals.)
64029
+ * - Align `when` to the entry's interval via {@link alignToInterval} to
64030
+ * get `alignedMs`, the boundary this tick belongs to.
64031
+ * - Compare against the slot's watermark in `_lastBoundary` (keyed by
64032
+ * `${name}` + scope + gen, without the `alignedMs` segment). If a
64033
+ * watermark exists and `alignedMs <= lastBoundary`, this boundary was
64034
+ * already fired — skip.
64035
+ * - This **watermark** check replaces the old exact `ts === alignedMs`
64036
+ * match. The exact match required virtual time to land *precisely* on
64037
+ * the boundary; when a tick jumped clean over a boundary (e.g. a `5m`
64038
+ * loop going 00:14 → 00:29 never touching the `15m` 00:15 boundary)
64039
+ * the boundary was silently lost. With the watermark, the first tick
64040
+ * whose `alignedMs` advanced past the stored value fires once, at the
64041
+ * newest crossed boundary (catch-up collapses several skipped
64042
+ * boundaries into a single invocation at the latest one).
64043
+ * - The watermark is advanced to `alignedMs` synchronously when the slot
64044
+ * is opened (before the `await`), so a concurrent tick on the same or
64045
+ * an already-passed boundary cannot open a duplicate slot while the
64046
+ * handler is still in flight.
64004
64047
  * - Slot key: `${name}:${alignedMs}` (+ scope) (+ gen).
64005
64048
  * 6. Singleshot per slot key: look up the slot in `_inFlight`. If a promise
64006
64049
  * already exists, `await` the same promise. Otherwise invoke
@@ -64049,6 +64092,9 @@ class CronUtils {
64049
64092
  let alignedMs;
64050
64093
  let slotKey;
64051
64094
  let firedKey;
64095
+ // Periodic-only watermark key (no `alignedMs` segment); null for
64096
+ // fire-once entries, which coordinate via `_firedOnce` instead.
64097
+ let boundaryKey;
64052
64098
  if (entry.interval === undefined) {
64053
64099
  const onceKey = `${entry.name}${scope}${genSuffix}`;
64054
64100
  if (this._firedOnce.has(onceKey)) {
@@ -64058,11 +64104,18 @@ class CronUtils {
64058
64104
  alignedMs = ts;
64059
64105
  slotKey = `${entry.name}:once${scope}${genSuffix}`;
64060
64106
  firedKey = onceKey;
64107
+ boundaryKey = null;
64061
64108
  }
64062
64109
  else {
64063
64110
  aligned = alignToInterval(when, entry.interval);
64064
64111
  alignedMs = aligned.getTime();
64065
- if (ts !== alignedMs) {
64112
+ boundaryKey = `${entry.name}${scope}${genSuffix}`;
64113
+ const lastBoundary = this._lastBoundary.get(boundaryKey);
64114
+ // Fire when the tick's aligned boundary has advanced past the last one
64115
+ // we fired for this slot. Using `>` instead of the old `ts === alignedMs`
64116
+ // means a virtual-time jump that skips clean over a boundary still
64117
+ // fires once, at the newest crossed boundary, rather than dropping it.
64118
+ if (lastBoundary !== undefined && alignedMs <= lastBoundary) {
64066
64119
  continue;
64067
64120
  }
64068
64121
  slotKey = `${entry.name}:${alignedMs}${scope}${genSuffix}`;
@@ -64070,6 +64123,13 @@ class CronUtils {
64070
64123
  }
64071
64124
  let pending = this._inFlight.get(slotKey);
64072
64125
  if (!pending) {
64126
+ // Advance the watermark synchronously at slot-open time, before the
64127
+ // await below. Otherwise a later tick on the same (or an already
64128
+ // crossed) boundary, arriving while this handler is still in flight,
64129
+ // would see the stale watermark and open a duplicate slot.
64130
+ if (boundaryKey !== null) {
64131
+ this._lastBoundary.set(boundaryKey, alignedMs);
64132
+ }
64073
64133
  pending = this._runEntry(entry, symbol, aligned, alignedMs, slotKey, firedKey, backtest);
64074
64134
  this._inFlight.set(slotKey, pending);
64075
64135
  }
@@ -64161,7 +64221,10 @@ class CronUtils {
64161
64221
  * 3. Wipes `_firedOnce` — all fire-once marks are dropped, so any future
64162
64222
  * re-registration of the same `name` fires again on the next matching
64163
64223
  * tick.
64164
- * 4. Does **not** touch `_inFlight` — in-flight handlers continue to
64224
+ * 4. Wipes `_lastBoundary` — all periodic watermarks are dropped, so a
64225
+ * re-registered periodic entry starts firing from its next crossed
64226
+ * boundary again.
64227
+ * 5. Does **not** touch `_inFlight` — in-flight handlers continue to
64165
64228
  * settle in the background and clear their own slots via `.finally()`.
64166
64229
  * Their final `_firedOnce.add(firedKey)` writes carry old-generation
64167
64230
  * keys and are harmless (lookup uses the post-dispose generation).
@@ -64180,8 +64243,29 @@ class CronUtils {
64180
64243
  this.disable();
64181
64244
  this._entries.clear();
64182
64245
  this._firedOnce.clear();
64246
+ this._lastBoundary.clear();
64183
64247
  };
64184
64248
  }
64249
+ /**
64250
+ * Garbage-collect every `_lastBoundary` key that belongs to the entry `name`
64251
+ * (any generation, global or fan-out).
64252
+ *
64253
+ * Called from `register`/`unregister` alongside `_clearFiredOnceFor`. Like
64254
+ * that helper this is memory hygiene, not correctness — the generation suffix
64255
+ * already isolates re-registrations, so a stale watermark from an old
64256
+ * generation can never gate a new entry.
64257
+ */
64258
+ _clearBoundaryFor(name) {
64259
+ if (!name) {
64260
+ return;
64261
+ }
64262
+ const prefix = `${name}:`;
64263
+ for (const key of this._lastBoundary.keys()) {
64264
+ if (key === name || key.startsWith(prefix)) {
64265
+ this._lastBoundary.delete(key);
64266
+ }
64267
+ }
64268
+ }
64185
64269
  /**
64186
64270
  * Garbage-collect every `_firedOnce` key that belongs to the entry `name`
64187
64271
  * (any generation, global or fan-out).
package/build/index.mjs CHANGED
@@ -63839,6 +63839,34 @@ class CronUtils {
63839
63839
  * on successful settle.
63840
63840
  */
63841
63841
  this._firedOnce = new Set();
63842
+ /**
63843
+ * Last interval boundary already fired per periodic slot.
63844
+ *
63845
+ * Key shape (no `alignedMs` segment — one entry per logical slot, not per
63846
+ * boundary; always carries the generation suffix `:g${generation}`, and the
63847
+ * `:${symbol}` scope only in fan-out mode):
63848
+ * - Periodic global: `${name}${genSuffix}`.
63849
+ * - Periodic fan-out: `${name}:${symbol}${genSuffix}`.
63850
+ *
63851
+ * Value is the aligned-boundary epoch ms (`alignedMs`) most recently opened
63852
+ * for that slot. `_tick` fires a periodic entry whenever the incoming tick's
63853
+ * aligned boundary is **strictly greater** than the stored value, instead of
63854
+ * requiring the tick to land *exactly* on the boundary. This fixes the
63855
+ * dropped-boundary bug: when virtual time jumps over a boundary (e.g. a
63856
+ * `5m`-driven loop skipping from 00:14 to 00:29 never lands on the `15m`
63857
+ * 00:15 boundary), the old `ts === alignedMs` check silently lost the tick.
63858
+ * With the watermark, the next tick whose `alignedMs` advanced past the
63859
+ * stored value fires once for the newest crossed boundary (catch-up
63860
+ * collapses multiple skipped boundaries into a single invocation at the
63861
+ * latest one).
63862
+ *
63863
+ * Written synchronously in `_tick` at slot-open time (before the `await`),
63864
+ * so a still-in-flight handler does not let a later tick re-open the same
63865
+ * (or an already-passed) boundary. Fire-once entries never touch this map —
63866
+ * they use `_firedOnce`. Pruned by `_clearBoundaryFor` on
63867
+ * `register`/`unregister` and wiped by `dispose`.
63868
+ */
63869
+ this._lastBoundary = new Map();
63842
63870
  /**
63843
63871
  * Register a periodic cron entry.
63844
63872
  *
@@ -63884,6 +63912,7 @@ class CronUtils {
63884
63912
  }
63885
63913
  }
63886
63914
  this._clearFiredOnceFor(entry.name);
63915
+ this._clearBoundaryFor(entry.name);
63887
63916
  const generation = ++this._generationCounter;
63888
63917
  this._entries.set(entry.name, { entry, generation });
63889
63918
  return () => this.unregister(entry.name);
@@ -63900,6 +63929,7 @@ class CronUtils {
63900
63929
  LOGGER_SERVICE$1.info(CRON_METHOD_NAME_UNREGISTER, { name });
63901
63930
  this._entries.delete(name);
63902
63931
  this._clearFiredOnceFor(name);
63932
+ this._clearBoundaryFor(name);
63903
63933
  };
63904
63934
  /**
63905
63935
  * Clear fire-once marks so that fire-once entries can fire again.
@@ -63976,11 +64006,24 @@ class CronUtils {
63976
64006
  * - Slot key: `${name}:once` (+ scope) (+ gen).
63977
64007
  * - `aligned` = the 1-minute-aligned `when` from step 0.
63978
64008
  * 5. **Periodic** (`entry.interval` set):
63979
- * - Align `when` further to the entry's interval via {@link alignToInterval}.
63980
- * - If `ts !== alignedMs`, the tick is mid-interval skip.
63981
- * (This is the "remainder === 0" boundary check from the spec;
63982
- * since `ts` is already on the 1-minute boundary, the check is exact
63983
- * for `1m` and consistent for higher intervals.)
64009
+ * - Align `when` to the entry's interval via {@link alignToInterval} to
64010
+ * get `alignedMs`, the boundary this tick belongs to.
64011
+ * - Compare against the slot's watermark in `_lastBoundary` (keyed by
64012
+ * `${name}` + scope + gen, without the `alignedMs` segment). If a
64013
+ * watermark exists and `alignedMs <= lastBoundary`, this boundary was
64014
+ * already fired — skip.
64015
+ * - This **watermark** check replaces the old exact `ts === alignedMs`
64016
+ * match. The exact match required virtual time to land *precisely* on
64017
+ * the boundary; when a tick jumped clean over a boundary (e.g. a `5m`
64018
+ * loop going 00:14 → 00:29 never touching the `15m` 00:15 boundary)
64019
+ * the boundary was silently lost. With the watermark, the first tick
64020
+ * whose `alignedMs` advanced past the stored value fires once, at the
64021
+ * newest crossed boundary (catch-up collapses several skipped
64022
+ * boundaries into a single invocation at the latest one).
64023
+ * - The watermark is advanced to `alignedMs` synchronously when the slot
64024
+ * is opened (before the `await`), so a concurrent tick on the same or
64025
+ * an already-passed boundary cannot open a duplicate slot while the
64026
+ * handler is still in flight.
63984
64027
  * - Slot key: `${name}:${alignedMs}` (+ scope) (+ gen).
63985
64028
  * 6. Singleshot per slot key: look up the slot in `_inFlight`. If a promise
63986
64029
  * already exists, `await` the same promise. Otherwise invoke
@@ -64029,6 +64072,9 @@ class CronUtils {
64029
64072
  let alignedMs;
64030
64073
  let slotKey;
64031
64074
  let firedKey;
64075
+ // Periodic-only watermark key (no `alignedMs` segment); null for
64076
+ // fire-once entries, which coordinate via `_firedOnce` instead.
64077
+ let boundaryKey;
64032
64078
  if (entry.interval === undefined) {
64033
64079
  const onceKey = `${entry.name}${scope}${genSuffix}`;
64034
64080
  if (this._firedOnce.has(onceKey)) {
@@ -64038,11 +64084,18 @@ class CronUtils {
64038
64084
  alignedMs = ts;
64039
64085
  slotKey = `${entry.name}:once${scope}${genSuffix}`;
64040
64086
  firedKey = onceKey;
64087
+ boundaryKey = null;
64041
64088
  }
64042
64089
  else {
64043
64090
  aligned = alignToInterval(when, entry.interval);
64044
64091
  alignedMs = aligned.getTime();
64045
- if (ts !== alignedMs) {
64092
+ boundaryKey = `${entry.name}${scope}${genSuffix}`;
64093
+ const lastBoundary = this._lastBoundary.get(boundaryKey);
64094
+ // Fire when the tick's aligned boundary has advanced past the last one
64095
+ // we fired for this slot. Using `>` instead of the old `ts === alignedMs`
64096
+ // means a virtual-time jump that skips clean over a boundary still
64097
+ // fires once, at the newest crossed boundary, rather than dropping it.
64098
+ if (lastBoundary !== undefined && alignedMs <= lastBoundary) {
64046
64099
  continue;
64047
64100
  }
64048
64101
  slotKey = `${entry.name}:${alignedMs}${scope}${genSuffix}`;
@@ -64050,6 +64103,13 @@ class CronUtils {
64050
64103
  }
64051
64104
  let pending = this._inFlight.get(slotKey);
64052
64105
  if (!pending) {
64106
+ // Advance the watermark synchronously at slot-open time, before the
64107
+ // await below. Otherwise a later tick on the same (or an already
64108
+ // crossed) boundary, arriving while this handler is still in flight,
64109
+ // would see the stale watermark and open a duplicate slot.
64110
+ if (boundaryKey !== null) {
64111
+ this._lastBoundary.set(boundaryKey, alignedMs);
64112
+ }
64053
64113
  pending = this._runEntry(entry, symbol, aligned, alignedMs, slotKey, firedKey, backtest);
64054
64114
  this._inFlight.set(slotKey, pending);
64055
64115
  }
@@ -64141,7 +64201,10 @@ class CronUtils {
64141
64201
  * 3. Wipes `_firedOnce` — all fire-once marks are dropped, so any future
64142
64202
  * re-registration of the same `name` fires again on the next matching
64143
64203
  * tick.
64144
- * 4. Does **not** touch `_inFlight` — in-flight handlers continue to
64204
+ * 4. Wipes `_lastBoundary` — all periodic watermarks are dropped, so a
64205
+ * re-registered periodic entry starts firing from its next crossed
64206
+ * boundary again.
64207
+ * 5. Does **not** touch `_inFlight` — in-flight handlers continue to
64145
64208
  * settle in the background and clear their own slots via `.finally()`.
64146
64209
  * Their final `_firedOnce.add(firedKey)` writes carry old-generation
64147
64210
  * keys and are harmless (lookup uses the post-dispose generation).
@@ -64160,8 +64223,29 @@ class CronUtils {
64160
64223
  this.disable();
64161
64224
  this._entries.clear();
64162
64225
  this._firedOnce.clear();
64226
+ this._lastBoundary.clear();
64163
64227
  };
64164
64228
  }
64229
+ /**
64230
+ * Garbage-collect every `_lastBoundary` key that belongs to the entry `name`
64231
+ * (any generation, global or fan-out).
64232
+ *
64233
+ * Called from `register`/`unregister` alongside `_clearFiredOnceFor`. Like
64234
+ * that helper this is memory hygiene, not correctness — the generation suffix
64235
+ * already isolates re-registrations, so a stale watermark from an old
64236
+ * generation can never gate a new entry.
64237
+ */
64238
+ _clearBoundaryFor(name) {
64239
+ if (!name) {
64240
+ return;
64241
+ }
64242
+ const prefix = `${name}:`;
64243
+ for (const key of this._lastBoundary.keys()) {
64244
+ if (key === name || key.startsWith(prefix)) {
64245
+ this._lastBoundary.delete(key);
64246
+ }
64247
+ }
64248
+ }
64165
64249
  /**
64166
64250
  * Garbage-collect every `_firedOnce` key that belongs to the entry `name`
64167
64251
  * (any generation, global or fan-out).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "backtest-kit",
3
- "version": "11.0.0",
3
+ "version": "11.2.0",
4
4
  "description": "A TypeScript library for trading system backtest",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",
package/types.d.ts CHANGED
@@ -26322,6 +26322,44 @@ declare class CronUtils {
26322
26322
  * on successful settle.
26323
26323
  */
26324
26324
  private readonly _firedOnce;
26325
+ /**
26326
+ * Last interval boundary already fired per periodic slot.
26327
+ *
26328
+ * Key shape (no `alignedMs` segment — one entry per logical slot, not per
26329
+ * boundary; always carries the generation suffix `:g${generation}`, and the
26330
+ * `:${symbol}` scope only in fan-out mode):
26331
+ * - Periodic global: `${name}${genSuffix}`.
26332
+ * - Periodic fan-out: `${name}:${symbol}${genSuffix}`.
26333
+ *
26334
+ * Value is the aligned-boundary epoch ms (`alignedMs`) most recently opened
26335
+ * for that slot. `_tick` fires a periodic entry whenever the incoming tick's
26336
+ * aligned boundary is **strictly greater** than the stored value, instead of
26337
+ * requiring the tick to land *exactly* on the boundary. This fixes the
26338
+ * dropped-boundary bug: when virtual time jumps over a boundary (e.g. a
26339
+ * `5m`-driven loop skipping from 00:14 to 00:29 never lands on the `15m`
26340
+ * 00:15 boundary), the old `ts === alignedMs` check silently lost the tick.
26341
+ * With the watermark, the next tick whose `alignedMs` advanced past the
26342
+ * stored value fires once for the newest crossed boundary (catch-up
26343
+ * collapses multiple skipped boundaries into a single invocation at the
26344
+ * latest one).
26345
+ *
26346
+ * Written synchronously in `_tick` at slot-open time (before the `await`),
26347
+ * so a still-in-flight handler does not let a later tick re-open the same
26348
+ * (or an already-passed) boundary. Fire-once entries never touch this map —
26349
+ * they use `_firedOnce`. Pruned by `_clearBoundaryFor` on
26350
+ * `register`/`unregister` and wiped by `dispose`.
26351
+ */
26352
+ private readonly _lastBoundary;
26353
+ /**
26354
+ * Garbage-collect every `_lastBoundary` key that belongs to the entry `name`
26355
+ * (any generation, global or fan-out).
26356
+ *
26357
+ * Called from `register`/`unregister` alongside `_clearFiredOnceFor`. Like
26358
+ * that helper this is memory hygiene, not correctness — the generation suffix
26359
+ * already isolates re-registrations, so a stale watermark from an old
26360
+ * generation can never gate a new entry.
26361
+ */
26362
+ private _clearBoundaryFor;
26325
26363
  /**
26326
26364
  * Garbage-collect every `_firedOnce` key that belongs to the entry `name`
26327
26365
  * (any generation, global or fan-out).
@@ -26443,11 +26481,24 @@ declare class CronUtils {
26443
26481
  * - Slot key: `${name}:once` (+ scope) (+ gen).
26444
26482
  * - `aligned` = the 1-minute-aligned `when` from step 0.
26445
26483
  * 5. **Periodic** (`entry.interval` set):
26446
- * - Align `when` further to the entry's interval via {@link alignToInterval}.
26447
- * - If `ts !== alignedMs`, the tick is mid-interval skip.
26448
- * (This is the "remainder === 0" boundary check from the spec;
26449
- * since `ts` is already on the 1-minute boundary, the check is exact
26450
- * for `1m` and consistent for higher intervals.)
26484
+ * - Align `when` to the entry's interval via {@link alignToInterval} to
26485
+ * get `alignedMs`, the boundary this tick belongs to.
26486
+ * - Compare against the slot's watermark in `_lastBoundary` (keyed by
26487
+ * `${name}` + scope + gen, without the `alignedMs` segment). If a
26488
+ * watermark exists and `alignedMs <= lastBoundary`, this boundary was
26489
+ * already fired — skip.
26490
+ * - This **watermark** check replaces the old exact `ts === alignedMs`
26491
+ * match. The exact match required virtual time to land *precisely* on
26492
+ * the boundary; when a tick jumped clean over a boundary (e.g. a `5m`
26493
+ * loop going 00:14 → 00:29 never touching the `15m` 00:15 boundary)
26494
+ * the boundary was silently lost. With the watermark, the first tick
26495
+ * whose `alignedMs` advanced past the stored value fires once, at the
26496
+ * newest crossed boundary (catch-up collapses several skipped
26497
+ * boundaries into a single invocation at the latest one).
26498
+ * - The watermark is advanced to `alignedMs` synchronously when the slot
26499
+ * is opened (before the `await`), so a concurrent tick on the same or
26500
+ * an already-passed boundary cannot open a duplicate slot while the
26501
+ * handler is still in flight.
26451
26502
  * - Slot key: `${name}:${alignedMs}` (+ scope) (+ gen).
26452
26503
  * 6. Singleshot per slot key: look up the slot in `_inFlight`. If a promise
26453
26504
  * already exists, `await` the same promise. Otherwise invoke
@@ -26541,7 +26592,10 @@ declare class CronUtils {
26541
26592
  * 3. Wipes `_firedOnce` — all fire-once marks are dropped, so any future
26542
26593
  * re-registration of the same `name` fires again on the next matching
26543
26594
  * tick.
26544
- * 4. Does **not** touch `_inFlight` — in-flight handlers continue to
26595
+ * 4. Wipes `_lastBoundary` — all periodic watermarks are dropped, so a
26596
+ * re-registered periodic entry starts firing from its next crossed
26597
+ * boundary again.
26598
+ * 5. Does **not** touch `_inFlight` — in-flight handlers continue to
26545
26599
  * settle in the background and clear their own slots via `.finally()`.
26546
26600
  * Their final `_firedOnce.add(firedKey)` writes carry old-generation
26547
26601
  * keys and are harmless (lookup uses the post-dispose generation).