@ap3x/cadence 0.1.0 → 0.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/README.md +38 -7
- package/dist/core/attempt-identity.d.ts +4 -0
- package/dist/core/attempt-identity.d.ts.map +1 -0
- package/dist/core/engine.d.ts +1 -1
- package/dist/core/engine.d.ts.map +1 -1
- package/dist/core/memory-store.d.ts +1 -1
- package/dist/core/memory-store.d.ts.map +1 -1
- package/dist/core/store.d.ts +10 -2
- package/dist/core/store.d.ts.map +1 -1
- package/dist/core/types.d.ts +4 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/file-store.d.ts.map +1 -1
- package/dist/index.js +153 -55
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,22 +1,53 @@
|
|
|
1
1
|
# @ap3x/cadence
|
|
2
2
|
|
|
3
|
-
A standalone
|
|
3
|
+
A standalone scheduler for cron, interval and one-shot jobs, with pluggable storage and retry policies. The bundled file store persists schedules and attempts for a single engine process; it does not guarantee exactly-once external effects.
|
|
4
4
|
|
|
5
5
|
Part of [AP3X](https://github.com/AP3X-Dev/AP3X) — a TypeScript multi-agent framework.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Run from source
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
Follow the repository's [source quick start](../../README.md#quick-start-from-source) to install
|
|
10
|
+
locked dependencies and build all workspaces, including cadence. That local install/build/help
|
|
11
|
+
path has been exercised; published-artifact installation qualification remains open.
|
|
12
12
|
|
|
13
13
|
## What's inside
|
|
14
14
|
|
|
15
15
|
- **Schedules** — cron expressions, fixed intervals, and one-shot timers.
|
|
16
|
-
- **
|
|
17
|
-
|
|
16
|
+
- **Occurrence claims** — bundled stores use in-memory claim markers to exclude competing
|
|
17
|
+
claims within one process. Schedule advancement is distinct from attempt persistence and execution.
|
|
18
|
+
- **File-backed state and retries** — the file store reloads persisted schedules and attempts;
|
|
19
|
+
stale running attempts are reclaimed and routed through the retry policy. The memory store
|
|
20
|
+
does not survive process restart.
|
|
21
|
+
- **Overlap + misfire policies** — choose what happens when an occurrence comes due while the
|
|
22
|
+
previous attempt is still running (`allow`, `queue`, `skip_if_running`), and what to do with
|
|
23
|
+
occurrences missed while the engine was down.
|
|
24
|
+
- **Pluggable stores** — in-memory and file-backed stores ship with the package, behind a store
|
|
25
|
+
interface with a conformance suite.
|
|
18
26
|
- **Injectable clock** — no wall-clock reads in core, so schedules are testable deterministically.
|
|
19
27
|
|
|
28
|
+
## Concurrency scope
|
|
29
|
+
|
|
30
|
+
The bundled stores hold their claim in process memory and rely on JavaScript's run-to-completion
|
|
31
|
+
as the critical section. **Run one engine process per store directory** — multi-process exclusion
|
|
32
|
+
is out of scope for the in-memory and file stores.
|
|
33
|
+
|
|
34
|
+
The `Store` interface specifies claim/advance consistency requirements for alternative adapters.
|
|
35
|
+
No PostgreSQL adapter ships here, and satisfying those methods alone would not make an external
|
|
36
|
+
action part of the same transaction.
|
|
37
|
+
|
|
38
|
+
## Crash and effect semantics
|
|
39
|
+
|
|
40
|
+
A crash after claiming but before persisted slot advancement releases the transient claim, so
|
|
41
|
+
the occurrence may be claimed again after restart. The engine advances a slot before opening
|
|
42
|
+
its execution attempt: a crash in that window can leave an advanced schedule without an attempt
|
|
43
|
+
record for recovery. Persisted running attempts can be retried, including when an external effect
|
|
44
|
+
completed but its successful result was not recorded. Those retries may repeat the effect.
|
|
45
|
+
|
|
46
|
+
Consumers must handle effect idempotency and reconciliation separately. Current stores and
|
|
47
|
+
engine behavior do not establish loss-free delivery across every crash point, multi-process
|
|
48
|
+
recovery, or exactly-once remote actions. See the [durability architecture](../../docs/architecture/durability.md)
|
|
49
|
+
for the current boundary and proposed qualification work.
|
|
50
|
+
|
|
20
51
|
Depends on no other `@ap3x` package.
|
|
21
52
|
|
|
22
53
|
## Documentation
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attempt-identity.d.ts","sourceRoot":"","sources":["../../src/core/attempt-identity.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC,iEAAiE;AACjE,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAM/E"}
|
package/dist/core/engine.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface EngineOptions {
|
|
|
16
16
|
retryBaseMs?: number;
|
|
17
17
|
/** Retry backoff ladder cap. Default 60s. */
|
|
18
18
|
retryCapMs?: number;
|
|
19
|
-
/**
|
|
19
|
+
/** Restart-safe id source for schedules and attempts. Default: randomUUID. */
|
|
20
20
|
idFactory?: () => string;
|
|
21
21
|
}
|
|
22
22
|
export declare class Engine {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/core/engine.ts"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAoB,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE7D,OAAO,KAAK,EAAqB,KAAK,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,WAAW,EAAqB,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE3F,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC;IACb,yCAAyC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/core/engine.ts"],"names":[],"mappings":"AA+CA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAoB,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE7D,OAAO,KAAK,EAAqB,KAAK,EAAE,MAAM,SAAS,CAAC;AACxD,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,WAAW,EAAqB,KAAK,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE3F,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,KAAK,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC;IACb,yCAAyC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mFAAmF;IACnF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,MAAM,CAAC;CAC1B;AAED,qBAAa,MAAM;;gBAmBL,IAAI,EAAE,aAAa;IAc/B;;;OAGG;IACG,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC;IA6BrD,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIpC,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvD,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IAI9C,IAAI,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;IAI3B,+EAA+E;IAC/E,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAIhE,iFAAiF;IACjF,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAMvD,gFAAgF;IAChF,KAAK,IAAI,IAAI;IAOb;;;OAGG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CA2V5B"}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* multi-process store (e.g. Postgres) would enforce it with a row lock.
|
|
14
14
|
*/
|
|
15
15
|
import type { Clock } from "./clock";
|
|
16
|
-
import type
|
|
16
|
+
import { type ClaimedOccurrence, type Store } from "./store";
|
|
17
17
|
import type { Attempt, Schedule } from "./types";
|
|
18
18
|
export declare class InMemoryStore implements Store {
|
|
19
19
|
#private;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/core/memory-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/core/memory-store.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,KAAK,EAAc,MAAM,SAAS,CAAC;AACzE,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEjD,qBAAa,aAAc,YAAW,KAAK;;gBAQ7B,KAAK,EAAE,KAAK;IAIlB,cAAc,CAAC,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAK1C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IAKtD,aAAa,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;IAIpC,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAK5C,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAKvD,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAchE,WAAW,CACf,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,eAAe,EAAE,MAAM,EACvB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC;IAUV,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAOtC,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAQjE,cAAc,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAa5D,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAItD,cAAc,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAY9D,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IASnE,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;CAK9D"}
|
package/dist/core/store.d.ts
CHANGED
|
@@ -8,8 +8,14 @@ import type { Attempt, Schedule } from "./types";
|
|
|
8
8
|
export interface ClaimedOccurrence {
|
|
9
9
|
schedule: Schedule;
|
|
10
10
|
occurrenceAt: string;
|
|
11
|
+
claimToken?: string;
|
|
11
12
|
}
|
|
12
13
|
export interface Store {
|
|
14
|
+
transaction?<T>(call: () => Promise<T>): Promise<T>;
|
|
15
|
+
afterCommit?(effect: () => void | Promise<void>): void;
|
|
16
|
+
claimAttempt?(id: string, now: Date): Promise<Attempt | undefined>;
|
|
17
|
+
renewAttempt?(id: string, claimToken: string): Promise<void>;
|
|
18
|
+
/** Create only; an existing id raises StoreError(conflict). */
|
|
13
19
|
createSchedule(s: Schedule): Promise<void>;
|
|
14
20
|
getSchedule(id: string): Promise<Schedule | undefined>;
|
|
15
21
|
listSchedules(): Promise<Schedule[]>;
|
|
@@ -30,9 +36,11 @@ export interface Store {
|
|
|
30
36
|
* what guarantees each occurrence fires exactly once, even under concurrent
|
|
31
37
|
* or crashed workers.
|
|
32
38
|
*/
|
|
33
|
-
advanceSlot(id: string, nextRunAt: string | null, lastTriggeredAt: string, occurrenceCount: number): Promise<void>;
|
|
39
|
+
advanceSlot(id: string, nextRunAt: string | null, lastTriggeredAt: string, occurrenceCount: number, claimToken?: string): Promise<void>;
|
|
40
|
+
/** Create only; an existing id raises StoreError(conflict). */
|
|
34
41
|
openAttempt(a: Attempt): Promise<void>;
|
|
35
|
-
|
|
42
|
+
/** Identity (id, scheduleId, occurrenceAt) is immutable. */
|
|
43
|
+
updateAttempt(id: string, patch: Partial<Attempt>, claimToken?: string): Promise<void>;
|
|
36
44
|
/** Attempts with status "pending" and `retryAt <= now`. */
|
|
37
45
|
listDueRetries(now: Date, limit: number): Promise<Attempt[]>;
|
|
38
46
|
/** Whether the schedule has any attempt currently in status "running". */
|
package/dist/core/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/core/store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEjD,iFAAiF;AACjF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,QAAQ,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/core/store.ts"],"names":[],"mappings":"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"}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -56,6 +56,8 @@ export declare const ScheduleSchema: import("@sinclair/typebox").TObject<{
|
|
|
56
56
|
metadata: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnknown>>;
|
|
57
57
|
}>;
|
|
58
58
|
export interface Attempt {
|
|
59
|
+
claimToken?: string;
|
|
60
|
+
leaseUntil?: string;
|
|
59
61
|
id: string;
|
|
60
62
|
scheduleId: string;
|
|
61
63
|
occurrenceAt: string;
|
|
@@ -68,6 +70,8 @@ export interface Attempt {
|
|
|
68
70
|
outcome?: unknown;
|
|
69
71
|
}
|
|
70
72
|
export declare const AttemptSchema: import("@sinclair/typebox").TObject<{
|
|
73
|
+
claimToken: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
74
|
+
leaseUntil: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
|
|
71
75
|
id: import("@sinclair/typebox").TString;
|
|
72
76
|
scheduleId: import("@sinclair/typebox").TString;
|
|
73
77
|
occurrenceAt: import("@sinclair/typebox").TString;
|
package/dist/core/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAcA,4EAA4E;AAC5E,eAAO,MAAM,YAAY;;;;CAIf,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,OAAO,YAAY,CAAC,CAAC;AAE5E,gFAAgF;AAChF,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,iBAAiB,GAAG,OAAO,CAAC;AAElE,oEAAoE;AACpE,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAEzD,qDAAqD;AACrD,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,aAAa,CAAC;AAmC3F,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;EAqBzB,CAAC;AAMH,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,eAAO,MAAM,aAAa
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAcA,4EAA4E;AAC5E,eAAO,MAAM,YAAY;;;;CAIf,CAAC;AACX,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,OAAO,YAAY,CAAC,CAAC;AAE5E,gFAAgF;AAChF,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,iBAAiB,GAAG,OAAO,CAAC;AAElE,oEAAoE;AACpE,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAEzD,qDAAqD;AACrD,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,aAAa,CAAC;AAmC3F,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,EAAE,aAAa,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;EAqBzB,CAAC;AAMH,MAAM,WAAW,OAAO;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,aAAa,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,eAAO,MAAM,aAAa;;;;;;;;;;;;;EAaxB,CAAC;AAMH,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;EAgB5B,CAAC"}
|
package/dist/file-store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/file-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/file-store.ts"],"names":[],"mappings":"AAwCA,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,KAAK,EAAc,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,KAAK,OAAO,EAAiB,KAAK,QAAQ,EAAkB,MAAM,cAAc,CAAC;AAK1F,qBAAa,SAAU,YAAW,KAAK;;gBAUzB,GAAG,EAAE,MAAM;IAmEjB,cAAc,CAAC,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAM1C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IAKtD,aAAa,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC;IAIpC,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAO5C,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAQvD,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAchE,WAAW,CACf,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,eAAe,EAAE,MAAM,EACvB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC;IAWV,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAQtC,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IASjE,cAAc,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAa5D,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAItD,cAAc,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAY9D,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IASnE,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;CAK9D"}
|
package/dist/index.js
CHANGED
|
@@ -83,6 +83,7 @@ var TestClock = class {
|
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
// src/core/engine.ts
|
|
86
|
+
import { randomUUID } from "crypto";
|
|
86
87
|
import { Value } from "@sinclair/typebox/value";
|
|
87
88
|
|
|
88
89
|
// src/core/next-run.ts
|
|
@@ -245,6 +246,8 @@ var ScheduleSchema = Type.Object({
|
|
|
245
246
|
metadata: Type.Optional(MetadataSchema)
|
|
246
247
|
});
|
|
247
248
|
var AttemptSchema = Type.Object({
|
|
249
|
+
claimToken: Type.Optional(Type.String()),
|
|
250
|
+
leaseUntil: Type.Optional(Type.String()),
|
|
248
251
|
id: Type.String(),
|
|
249
252
|
scheduleId: Type.String(),
|
|
250
253
|
occurrenceAt: Type.String(),
|
|
@@ -301,7 +304,7 @@ var Engine = class {
|
|
|
301
304
|
this.#orphanGraceMs = opts.orphanGraceMs ?? 3e5;
|
|
302
305
|
this.#retryBaseMs = opts.retryBaseMs ?? 1e3;
|
|
303
306
|
this.#retryCapMs = opts.retryCapMs ?? 6e4;
|
|
304
|
-
this.#idFactory = opts.idFactory ??
|
|
307
|
+
this.#idFactory = opts.idFactory ?? randomUUID;
|
|
305
308
|
}
|
|
306
309
|
// -- registration & passthroughs ----------------------------------------
|
|
307
310
|
/**
|
|
@@ -386,8 +389,11 @@ var Engine = class {
|
|
|
386
389
|
const now = this.#clock.now();
|
|
387
390
|
await this.#recoverOrphans(now);
|
|
388
391
|
await this.#runDueRetries(now);
|
|
389
|
-
const
|
|
390
|
-
|
|
392
|
+
const claimAndPlan = async () => {
|
|
393
|
+
const claimed = await this.#store.claimDue(now, this.#batchLimit);
|
|
394
|
+
for (const c of claimed) await this.#dispatch(c, now);
|
|
395
|
+
};
|
|
396
|
+
await (this.#store.transaction?.(claimAndPlan) ?? claimAndPlan());
|
|
391
397
|
}
|
|
392
398
|
/** Crashed-worker recovery: route stale "running" attempts to the retry ladder. */
|
|
393
399
|
async #recoverOrphans(now) {
|
|
@@ -395,18 +401,26 @@ var Engine = class {
|
|
|
395
401
|
for (const a of orphans) {
|
|
396
402
|
if (this.#inflightAttemptIds.has(a.id)) continue;
|
|
397
403
|
const s = await this.#store.getSchedule(a.scheduleId);
|
|
398
|
-
if (s !== void 0 && a.attempt < s.maxAttempts) {
|
|
399
|
-
await this.#store.updateAttempt(
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
+
if (!a.error?.startsWith("uncertain:") && s !== void 0 && a.attempt < s.maxAttempts) {
|
|
405
|
+
await this.#store.updateAttempt(
|
|
406
|
+
a.id,
|
|
407
|
+
{
|
|
408
|
+
status: "pending",
|
|
409
|
+
retryAt: this.#retryAt(s, a.occurrenceAt, now, a.attempt),
|
|
410
|
+
error: a.error ?? "orphaned: worker crashed or stalled"
|
|
411
|
+
},
|
|
412
|
+
a.claimToken
|
|
413
|
+
);
|
|
404
414
|
} else {
|
|
405
|
-
await this.#store.updateAttempt(
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
415
|
+
await this.#store.updateAttempt(
|
|
416
|
+
a.id,
|
|
417
|
+
{
|
|
418
|
+
status: "dead_letter",
|
|
419
|
+
finishedAt: now.toISOString(),
|
|
420
|
+
error: a.error ?? "orphaned: worker crashed or stalled"
|
|
421
|
+
},
|
|
422
|
+
a.claimToken
|
|
423
|
+
);
|
|
410
424
|
}
|
|
411
425
|
}
|
|
412
426
|
}
|
|
@@ -423,13 +437,21 @@ var Engine = class {
|
|
|
423
437
|
});
|
|
424
438
|
continue;
|
|
425
439
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
440
|
+
if (this.#store.claimAttempt) {
|
|
441
|
+
const owned = await this.#store.claimAttempt(a.id, now);
|
|
442
|
+
if (owned)
|
|
443
|
+
this.#track(
|
|
444
|
+
this.#execute(s, owned.id, owned.occurrenceAt, owned.attempt, owned.claimToken)
|
|
445
|
+
);
|
|
446
|
+
} else {
|
|
447
|
+
const attemptNo = a.attempt + 1;
|
|
448
|
+
await this.#store.updateAttempt(a.id, {
|
|
449
|
+
status: "running",
|
|
450
|
+
attempt: attemptNo,
|
|
451
|
+
startedAt: now.toISOString()
|
|
452
|
+
});
|
|
453
|
+
this.#track(this.#execute(s, a.id, a.occurrenceAt, attemptNo));
|
|
454
|
+
}
|
|
433
455
|
}
|
|
434
456
|
}
|
|
435
457
|
/** Apply misfire + overlap policies to one claimed occurrence, advance its slot. */
|
|
@@ -460,7 +482,8 @@ var Engine = class {
|
|
|
460
482
|
s.id,
|
|
461
483
|
c.occurrenceAt,
|
|
462
484
|
s.lastTriggeredAt ?? s.createdAt,
|
|
463
|
-
s.occurrenceCount
|
|
485
|
+
s.occurrenceCount,
|
|
486
|
+
c.claimToken
|
|
464
487
|
);
|
|
465
488
|
return;
|
|
466
489
|
}
|
|
@@ -477,7 +500,7 @@ var Engine = class {
|
|
|
477
500
|
}
|
|
478
501
|
const nextRunAt = this.#boundedNext(s, anchor, countAfter);
|
|
479
502
|
const lastTriggeredAt = firedCount > 0 ? now.toISOString() : s.lastTriggeredAt ?? s.createdAt;
|
|
480
|
-
await this.#store.advanceSlot(s.id, nextRunAt, lastTriggeredAt, countAfter);
|
|
503
|
+
await this.#store.advanceSlot(s.id, nextRunAt, lastTriggeredAt, countAfter, c.claimToken);
|
|
481
504
|
if (skipped) {
|
|
482
505
|
await this.#store.openAttempt({
|
|
483
506
|
id: this.#idFactory(),
|
|
@@ -491,9 +514,36 @@ var Engine = class {
|
|
|
491
514
|
return;
|
|
492
515
|
}
|
|
493
516
|
if (toFire.length === 0) return;
|
|
494
|
-
this.#
|
|
517
|
+
if (this.#store.transaction && this.#store.claimAttempt) {
|
|
518
|
+
const planned = [];
|
|
519
|
+
for (const occurrenceAt of toFire) {
|
|
520
|
+
const id = this.#idFactory();
|
|
521
|
+
const delay = s.jitterMs ? applyJitter(0, s.jitterMs, hashSeed(`${s.id}@${occurrenceAt}`)) : 0;
|
|
522
|
+
const readyAt = now.getTime() + delay;
|
|
523
|
+
planned.push({ id, readyAt });
|
|
524
|
+
await this.#store.openAttempt({
|
|
525
|
+
id,
|
|
526
|
+
scheduleId: s.id,
|
|
527
|
+
occurrenceAt,
|
|
528
|
+
attempt: 0,
|
|
529
|
+
status: "pending",
|
|
530
|
+
retryAt: new Date(readyAt).toISOString()
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
this.#store.afterCommit?.(() => {
|
|
534
|
+
this.#track(this.#firePrepared(s, planned));
|
|
535
|
+
});
|
|
536
|
+
} else this.#track(this.#fire(s, toFire));
|
|
495
537
|
}
|
|
496
538
|
// -- execution -----------------------------------------------------------
|
|
539
|
+
async #firePrepared(s, ids) {
|
|
540
|
+
for (const { id, readyAt } of ids) {
|
|
541
|
+
const delay = readyAt - this.#clock.now().getTime();
|
|
542
|
+
if (delay > 0) await this.#clock.sleep(delay, this.#loopCtl?.signal);
|
|
543
|
+
const owned = await this.#store.claimAttempt?.(id, this.#clock.now());
|
|
544
|
+
if (owned) await this.#execute(s, id, owned.occurrenceAt, owned.attempt, owned.claimToken);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
497
547
|
/** Fire the given occurrences of one schedule, in order, honoring jitter. */
|
|
498
548
|
async #fire(s, occurrences) {
|
|
499
549
|
for (const occurrenceAt of occurrences) {
|
|
@@ -515,10 +565,17 @@ var Engine = class {
|
|
|
515
565
|
}
|
|
516
566
|
}
|
|
517
567
|
/** One attempt: executor call under a timeout signal, then the retry ladder. */
|
|
518
|
-
async #execute(s, attemptId, occurrenceAt, attemptNo) {
|
|
568
|
+
async #execute(s, attemptId, occurrenceAt, attemptNo, claimToken) {
|
|
519
569
|
this.#inflightAttemptIds.add(attemptId);
|
|
520
570
|
const ac = new AbortController();
|
|
521
571
|
const timeoutCtl = new AbortController();
|
|
572
|
+
const renewal = claimToken && this.#store.renewAttempt ? (async () => {
|
|
573
|
+
while (!timeoutCtl.signal.aborted) {
|
|
574
|
+
await this.#clock.sleep(1e4, timeoutCtl.signal);
|
|
575
|
+
if (!timeoutCtl.signal.aborted)
|
|
576
|
+
await this.#store.renewAttempt?.(attemptId, claimToken);
|
|
577
|
+
}
|
|
578
|
+
})().catch(() => ac.abort()) : Promise.resolve();
|
|
522
579
|
if (s.maxRuntimeMs !== void 0 && s.maxRuntimeMs > 0) {
|
|
523
580
|
void this.#clock.sleep(s.maxRuntimeMs, timeoutCtl.signal).then(() => {
|
|
524
581
|
if (!timeoutCtl.signal.aborted) ac.abort();
|
|
@@ -536,27 +593,40 @@ var Engine = class {
|
|
|
536
593
|
outcome = { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
537
594
|
} finally {
|
|
538
595
|
timeoutCtl.abort();
|
|
596
|
+
await renewal;
|
|
539
597
|
this.#inflightAttemptIds.delete(attemptId);
|
|
540
598
|
}
|
|
541
599
|
const now = this.#clock.now();
|
|
542
600
|
if (outcome.ok) {
|
|
543
|
-
await this.#store.updateAttempt(
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
601
|
+
await this.#store.updateAttempt(
|
|
602
|
+
attemptId,
|
|
603
|
+
{
|
|
604
|
+
status: "succeeded",
|
|
605
|
+
finishedAt: now.toISOString(),
|
|
606
|
+
...outcome.result !== void 0 ? { outcome: outcome.result } : {}
|
|
607
|
+
},
|
|
608
|
+
claimToken
|
|
609
|
+
);
|
|
548
610
|
} else if (attemptNo < s.maxAttempts) {
|
|
549
|
-
await this.#store.updateAttempt(
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
611
|
+
await this.#store.updateAttempt(
|
|
612
|
+
attemptId,
|
|
613
|
+
{
|
|
614
|
+
status: "pending",
|
|
615
|
+
retryAt: this.#retryAt(s, occurrenceAt, now, attemptNo),
|
|
616
|
+
...outcome.error !== void 0 ? { error: outcome.error } : {}
|
|
617
|
+
},
|
|
618
|
+
claimToken
|
|
619
|
+
);
|
|
554
620
|
} else {
|
|
555
|
-
await this.#store.updateAttempt(
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
621
|
+
await this.#store.updateAttempt(
|
|
622
|
+
attemptId,
|
|
623
|
+
{
|
|
624
|
+
status: "dead_letter",
|
|
625
|
+
finishedAt: now.toISOString(),
|
|
626
|
+
...outcome.error !== void 0 ? { error: outcome.error } : {}
|
|
627
|
+
},
|
|
628
|
+
claimToken
|
|
629
|
+
);
|
|
560
630
|
}
|
|
561
631
|
}
|
|
562
632
|
// -- helpers ---------------------------------------------------------------
|
|
@@ -592,10 +662,6 @@ var Engine = class {
|
|
|
592
662
|
void wrapped.finally(() => this.#inflight.delete(wrapped));
|
|
593
663
|
}
|
|
594
664
|
};
|
|
595
|
-
function defaultIdFactory() {
|
|
596
|
-
let n = 0;
|
|
597
|
-
return () => `cad_${++n}`;
|
|
598
|
-
}
|
|
599
665
|
function hashSeed(input) {
|
|
600
666
|
let h = 5381;
|
|
601
667
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -604,6 +670,25 @@ function hashSeed(input) {
|
|
|
604
670
|
return h;
|
|
605
671
|
}
|
|
606
672
|
|
|
673
|
+
// src/core/store.ts
|
|
674
|
+
var StoreError = class extends Error {
|
|
675
|
+
code;
|
|
676
|
+
constructor(code, message) {
|
|
677
|
+
super(message);
|
|
678
|
+
this.name = "StoreError";
|
|
679
|
+
this.code = code;
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// src/core/attempt-identity.ts
|
|
684
|
+
function assertAttemptIdentity(a, patch) {
|
|
685
|
+
for (const key of ["id", "scheduleId", "occurrenceAt"]) {
|
|
686
|
+
if (Object.hasOwn(patch, key) && patch[key] !== a[key]) {
|
|
687
|
+
throw new StoreError("conflict", `attempt ${key} is immutable`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
607
692
|
// src/core/memory-store.ts
|
|
608
693
|
var InMemoryStore = class {
|
|
609
694
|
#clock;
|
|
@@ -616,6 +701,7 @@ var InMemoryStore = class {
|
|
|
616
701
|
this.#clock = clock;
|
|
617
702
|
}
|
|
618
703
|
async createSchedule(s) {
|
|
704
|
+
if (this.#schedules.has(s.id)) throw new StoreError("conflict", "duplicate schedule id");
|
|
619
705
|
this.#schedules.set(s.id, { ...s });
|
|
620
706
|
}
|
|
621
707
|
async getSchedule(id) {
|
|
@@ -656,11 +742,17 @@ var InMemoryStore = class {
|
|
|
656
742
|
this.#claimed.delete(id);
|
|
657
743
|
}
|
|
658
744
|
async openAttempt(a) {
|
|
745
|
+
if (this.#attempts.some((existing) => existing.id === a.id)) {
|
|
746
|
+
throw new StoreError("conflict", "duplicate attempt id");
|
|
747
|
+
}
|
|
659
748
|
this.#attempts.push({ ...a });
|
|
660
749
|
}
|
|
661
750
|
async updateAttempt(id, patch) {
|
|
662
751
|
const a = this.#attempts.find((x) => x.id === id);
|
|
663
|
-
if (a)
|
|
752
|
+
if (a) {
|
|
753
|
+
assertAttemptIdentity(a, patch);
|
|
754
|
+
Object.assign(a, patch);
|
|
755
|
+
}
|
|
664
756
|
}
|
|
665
757
|
async listDueRetries(now, limit) {
|
|
666
758
|
const nowMs = now.getTime();
|
|
@@ -686,16 +778,6 @@ var InMemoryStore = class {
|
|
|
686
778
|
}
|
|
687
779
|
};
|
|
688
780
|
|
|
689
|
-
// src/core/store.ts
|
|
690
|
-
var StoreError = class extends Error {
|
|
691
|
-
code;
|
|
692
|
-
constructor(code, message) {
|
|
693
|
-
super(message);
|
|
694
|
-
this.name = "StoreError";
|
|
695
|
-
this.code = code;
|
|
696
|
-
}
|
|
697
|
-
};
|
|
698
|
-
|
|
699
781
|
// src/file-store.ts
|
|
700
782
|
import {
|
|
701
783
|
closeSync,
|
|
@@ -720,6 +802,7 @@ var FileStore = class {
|
|
|
720
802
|
#attempts = [];
|
|
721
803
|
/** Schedule ids whose due slot is claimed pending `advanceSlot`. Transient by design. */
|
|
722
804
|
#claimed = /* @__PURE__ */ new Set();
|
|
805
|
+
#persistedJson;
|
|
723
806
|
constructor(dir) {
|
|
724
807
|
mkdirSync(dir, { recursive: true });
|
|
725
808
|
this.#statePath = join(dir, STATE_FILE);
|
|
@@ -744,14 +827,22 @@ var FileStore = class {
|
|
|
744
827
|
if (!Value2.Check(ScheduleSchema, s)) {
|
|
745
828
|
throw new StoreError("corrupt", `${this.#statePath}: invalid schedule record`);
|
|
746
829
|
}
|
|
830
|
+
if (this.#schedules.has(s.id)) throw new StoreError("corrupt", "duplicate schedule id");
|
|
747
831
|
this.#schedules.set(s.id, s);
|
|
748
832
|
}
|
|
749
833
|
for (const a of state.attempts) {
|
|
750
834
|
if (!Value2.Check(AttemptSchema, a)) {
|
|
751
835
|
throw new StoreError("corrupt", `${this.#statePath}: invalid attempt record`);
|
|
752
836
|
}
|
|
837
|
+
if (this.#attempts.some((existing) => existing.id === a.id)) {
|
|
838
|
+
throw new StoreError("corrupt", "duplicate attempt id");
|
|
839
|
+
}
|
|
753
840
|
this.#attempts.push(a);
|
|
754
841
|
}
|
|
842
|
+
this.#persistedJson = JSON.stringify({
|
|
843
|
+
schedules: [...this.#schedules.values()],
|
|
844
|
+
attempts: this.#attempts
|
|
845
|
+
});
|
|
755
846
|
}
|
|
756
847
|
/** Atomically rewrite the full state: temp file + fsync + rename. */
|
|
757
848
|
#persist() {
|
|
@@ -759,6 +850,7 @@ var FileStore = class {
|
|
|
759
850
|
schedules: [...this.#schedules.values()],
|
|
760
851
|
attempts: this.#attempts
|
|
761
852
|
});
|
|
853
|
+
if (json === this.#persistedJson) return;
|
|
762
854
|
const fd = openSync(this.#tempPath, "w");
|
|
763
855
|
try {
|
|
764
856
|
writeSync(fd, json);
|
|
@@ -767,8 +859,10 @@ var FileStore = class {
|
|
|
767
859
|
closeSync(fd);
|
|
768
860
|
}
|
|
769
861
|
renameSync(this.#tempPath, this.#statePath);
|
|
862
|
+
this.#persistedJson = json;
|
|
770
863
|
}
|
|
771
864
|
async createSchedule(s) {
|
|
865
|
+
if (this.#schedules.has(s.id)) throw new StoreError("conflict", "duplicate schedule id");
|
|
772
866
|
this.#schedules.set(s.id, { ...s });
|
|
773
867
|
this.#persist();
|
|
774
868
|
}
|
|
@@ -816,12 +910,16 @@ var FileStore = class {
|
|
|
816
910
|
this.#claimed.delete(id);
|
|
817
911
|
}
|
|
818
912
|
async openAttempt(a) {
|
|
913
|
+
if (this.#attempts.some((existing) => existing.id === a.id)) {
|
|
914
|
+
throw new StoreError("conflict", "duplicate attempt id");
|
|
915
|
+
}
|
|
819
916
|
this.#attempts.push({ ...a });
|
|
820
917
|
this.#persist();
|
|
821
918
|
}
|
|
822
919
|
async updateAttempt(id, patch) {
|
|
823
920
|
const a = this.#attempts.find((x) => x.id === id);
|
|
824
921
|
if (a) {
|
|
922
|
+
assertAttemptIdentity(a, patch);
|
|
825
923
|
Object.assign(a, patch);
|
|
826
924
|
this.#persist();
|
|
827
925
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ap3x/cadence",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Durable scheduler — cron, interval, and one-shot jobs with exactly-once
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Durable scheduler — cron, interval, and one-shot jobs with exactly-once slot advance",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/AP3X-Dev/AP3X.git",
|