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 +91 -7
- package/build/index.mjs +91 -7
- package/package.json +1 -1
- package/types.d.ts +60 -6
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`
|
|
64000
|
-
*
|
|
64001
|
-
*
|
|
64002
|
-
*
|
|
64003
|
-
*
|
|
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
|
-
|
|
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.
|
|
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`
|
|
63980
|
-
*
|
|
63981
|
-
*
|
|
63982
|
-
*
|
|
63983
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
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`
|
|
26447
|
-
*
|
|
26448
|
-
*
|
|
26449
|
-
*
|
|
26450
|
-
*
|
|
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.
|
|
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).
|