@ap3x/cadence 0.1.1 → 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 CHANGED
@@ -1,24 +1,23 @@
1
1
  # @ap3x/cadence
2
2
 
3
- A standalone durable scheduler: cron, interval, and one-shot jobs with exactly-once firing semantics and pluggable storage.
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
- ## Install
7
+ ## Run from source
8
8
 
9
- ```bash
10
- npm install @ap3x/cadence
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
- - **Exactly-once slot advance** — claiming a due occurrence and advancing its slot happen in one
17
- critical section, so a given occurrence is never fired twice by the running engine. A crash
18
- *between* the two releases the claim, and the occurrence fires again on restart — the design
19
- prefers re-firing over silently dropping a scheduled run.
20
- - **Durable across restarts** — state reloads from the store; in-flight attempts abandoned by a
21
- crashed process are reclaimed and routed through the retry ladder.
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.
22
21
  - **Overlap + misfire policies** — choose what happens when an occurrence comes due while the
23
22
  previous attempt is still running (`allow`, `queue`, `skip_if_running`), and what to do with
24
23
  occurrences missed while the engine was down.
@@ -32,8 +31,22 @@ The bundled stores hold their claim in process memory and rely on JavaScript's r
32
31
  as the critical section. **Run one engine process per store directory** — multi-process exclusion
33
32
  is out of scope for the in-memory and file stores.
34
33
 
35
- The `Store` interface is designed for it: a backend that can claim and advance in one transaction
36
- (a Postgres row lock, say) extends the same guarantee across processes. No such adapter ships here.
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.
37
50
 
38
51
  Depends on no other `@ap3x` package.
39
52
 
@@ -0,0 +1,4 @@
1
+ import type { Attempt } from "./types";
2
+ /** Keep updates bound to the same durable execution identity. */
3
+ export declare function assertAttemptIdentity(a: Attempt, patch: Partial<Attempt>): void;
4
+ //# sourceMappingURL=attempt-identity.d.ts.map
@@ -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"}
@@ -16,7 +16,7 @@ export interface EngineOptions {
16
16
  retryBaseMs?: number;
17
17
  /** Retry backoff ladder cap. Default 60s. */
18
18
  retryCapMs?: number;
19
- /** Id source for schedules and attempts. Default: per-engine counter. */
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,yEAAyE;IACzE,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;CA+Q5B"}
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 { ClaimedOccurrence, Store } from "./store";
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":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,iBAAiB,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACxD,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;IAI1C,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;IAItC,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjE,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"}
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"}
@@ -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
- updateAttempt(id: string, patch: Partial<Attempt>): Promise<void>;
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". */
@@ -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;CACtB;AAED,MAAM,WAAW,KAAK;IACpB,cAAc,CAAC,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAAC;IACvD,aAAa,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;IACrC,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAExD;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;IAEjE;;;;;;OAMG;IACH,WAAW,CACT,EAAE,EAAE,MAAM,EACV,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,eAAe,EAAE,MAAM,EACvB,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjB,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAElE,2DAA2D;IAC3D,cAAc,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAC7D,0EAA0E;IAC1E,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvD,sFAAsF;IACtF,cAAc,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAC/D,oEAAoE;IACpE,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IACpE,uDAAuD;IACvD,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;CACzD;AAED,uEAAuE;AACvE,qBAAa,UAAW,SAAQ,KAAK;IACnC,QAAQ,CAAC,IAAI,EAAE,WAAW,GAAG,UAAU,GAAG,QAAQ,GAAG,SAAS,CAAC;gBAEnD,IAAI,EAAE,WAAW,GAAG,UAAU,GAAG,QAAQ,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM;CAKnF"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/core/store.ts"],"names":[],"mappings":"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"}
@@ -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;
@@ -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;;;;;;;;;;;EAWxB,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"}
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"}
@@ -1 +1 @@
1
- {"version":3,"file":"file-store.d.ts","sourceRoot":"","sources":["../src/file-store.ts"],"names":[],"mappings":"AAuCA,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;;gBASzB,GAAG,EAAE,MAAM;IAyDjB,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;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;IAKtC,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"}
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 ?? defaultIdFactory();
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 claimed = await this.#store.claimDue(now, this.#batchLimit);
390
- for (const c of claimed) await this.#dispatch(c, now);
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(a.id, {
400
- status: "pending",
401
- retryAt: this.#retryAt(s, a.occurrenceAt, now, a.attempt),
402
- error: "orphaned: worker crashed or stalled"
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(a.id, {
406
- status: "dead_letter",
407
- finishedAt: now.toISOString(),
408
- error: "orphaned: worker crashed or stalled"
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
- const attemptNo = a.attempt + 1;
427
- await this.#store.updateAttempt(a.id, {
428
- status: "running",
429
- attempt: attemptNo,
430
- startedAt: now.toISOString()
431
- });
432
- this.#track(this.#execute(s, a.id, a.occurrenceAt, attemptNo));
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.#track(this.#fire(s, toFire));
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(attemptId, {
544
- status: "succeeded",
545
- finishedAt: now.toISOString(),
546
- ...outcome.result !== void 0 ? { outcome: outcome.result } : {}
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(attemptId, {
550
- status: "pending",
551
- retryAt: this.#retryAt(s, occurrenceAt, now, attemptNo),
552
- ...outcome.error !== void 0 ? { error: outcome.error } : {}
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(attemptId, {
556
- status: "dead_letter",
557
- finishedAt: now.toISOString(),
558
- ...outcome.error !== void 0 ? { error: outcome.error } : {}
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) Object.assign(a, patch);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@ap3x/cadence",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Durable scheduler — cron, interval, and one-shot jobs with exactly-once slot advance",
5
5
  "repository": {
6
6
  "type": "git",