@loomwork/event-runtime 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +95 -2
- package/dist/BranchRegistry.d.ts +140 -0
- package/dist/BranchRegistry.d.ts.map +1 -0
- package/dist/BranchRegistry.js +66 -0
- package/dist/BranchRegistry.js.map +1 -0
- package/dist/CoalescingEventStore.d.ts +97 -0
- package/dist/CoalescingEventStore.d.ts.map +1 -0
- package/dist/CoalescingEventStore.js +180 -0
- package/dist/CoalescingEventStore.js.map +1 -0
- package/dist/InMemoryArchiveStore.d.ts +19 -0
- package/dist/InMemoryArchiveStore.d.ts.map +1 -0
- package/dist/InMemoryArchiveStore.js +49 -0
- package/dist/InMemoryArchiveStore.js.map +1 -0
- package/dist/InMemoryAsyncEventStore.d.ts +111 -0
- package/dist/InMemoryAsyncEventStore.d.ts.map +1 -0
- package/dist/InMemoryAsyncEventStore.js +374 -0
- package/dist/InMemoryAsyncEventStore.js.map +1 -0
- package/dist/InMemoryBranchRegistry.d.ts +31 -0
- package/dist/InMemoryBranchRegistry.d.ts.map +1 -0
- package/dist/InMemoryBranchRegistry.js +167 -0
- package/dist/InMemoryBranchRegistry.js.map +1 -0
- package/dist/ProfilingEventStore.d.ts +75 -0
- package/dist/ProfilingEventStore.d.ts.map +1 -0
- package/dist/ProfilingEventStore.js +133 -0
- package/dist/ProfilingEventStore.js.map +1 -0
- package/dist/ShardPlan.d.ts +87 -0
- package/dist/ShardPlan.d.ts.map +1 -0
- package/dist/ShardPlan.js +193 -0
- package/dist/ShardPlan.js.map +1 -0
- package/dist/ShardedEventStore.d.ts +149 -0
- package/dist/ShardedEventStore.d.ts.map +1 -0
- package/dist/ShardedEventStore.js +468 -0
- package/dist/ShardedEventStore.js.map +1 -0
- package/dist/SubscriberRegistry.d.ts +59 -0
- package/dist/SubscriberRegistry.d.ts.map +1 -0
- package/dist/SubscriberRegistry.js +113 -0
- package/dist/SubscriberRegistry.js.map +1 -0
- package/dist/TenantScopedArchiveStore.d.ts +36 -0
- package/dist/TenantScopedArchiveStore.d.ts.map +1 -0
- package/dist/TenantScopedArchiveStore.js +96 -0
- package/dist/TenantScopedArchiveStore.js.map +1 -0
- package/dist/TenantScopedEventStore.d.ts +59 -0
- package/dist/TenantScopedEventStore.d.ts.map +1 -0
- package/dist/TenantScopedEventStore.js +227 -0
- package/dist/TenantScopedEventStore.js.map +1 -0
- package/dist/archive.d.ts +68 -0
- package/dist/archive.d.ts.map +1 -0
- package/dist/archive.js +207 -0
- package/dist/archive.js.map +1 -0
- package/dist/archiveSpi.d.ts +165 -0
- package/dist/archiveSpi.d.ts.map +1 -0
- package/dist/archiveSpi.js +184 -0
- package/dist/archiveSpi.js.map +1 -0
- package/dist/creationMarker.d.ts +13 -0
- package/dist/creationMarker.d.ts.map +1 -0
- package/dist/creationMarker.js +17 -0
- package/dist/creationMarker.js.map +1 -0
- package/dist/decoratorPlan.typecheck.d.ts +2 -0
- package/dist/decoratorPlan.typecheck.d.ts.map +1 -0
- package/dist/decoratorPlan.typecheck.js +41 -0
- package/dist/decoratorPlan.typecheck.js.map +1 -0
- package/dist/eventKindClass.d.ts +40 -0
- package/dist/eventKindClass.d.ts.map +1 -0
- package/dist/eventKindClass.js +99 -0
- package/dist/eventKindClass.js.map +1 -0
- package/dist/executionIdOrder.d.ts +21 -0
- package/dist/executionIdOrder.d.ts.map +1 -0
- package/dist/executionIdOrder.js +43 -0
- package/dist/executionIdOrder.js.map +1 -0
- package/dist/fencing.d.ts +21 -0
- package/dist/fencing.d.ts.map +1 -0
- package/dist/fencing.js +36 -0
- package/dist/fencing.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +126 -0
- package/dist/index.js.map +1 -0
- package/dist/installOptional.d.ts +52 -0
- package/dist/installOptional.d.ts.map +1 -0
- package/dist/installOptional.js +112 -0
- package/dist/installOptional.js.map +1 -0
- package/dist/keyUnavailable.d.ts +67 -0
- package/dist/keyUnavailable.d.ts.map +1 -0
- package/dist/keyUnavailable.js +87 -0
- package/dist/keyUnavailable.js.map +1 -0
- package/dist/leases.d.ts +177 -0
- package/dist/leases.d.ts.map +1 -0
- package/dist/leases.js +142 -0
- package/dist/leases.js.map +1 -0
- package/dist/payloadShredded.d.ts +30 -0
- package/dist/payloadShredded.d.ts.map +1 -0
- package/dist/payloadShredded.js +67 -0
- package/dist/payloadShredded.js.map +1 -0
- package/dist/scaleDecorators.bench.d.ts +2 -0
- package/dist/scaleDecorators.bench.d.ts.map +1 -0
- package/dist/scaleDecorators.bench.js +93 -0
- package/dist/scaleDecorators.bench.js.map +1 -0
- package/dist/sequenceRange.bench.d.ts +2 -0
- package/dist/sequenceRange.bench.d.ts.map +1 -0
- package/dist/sequenceRange.bench.js +46 -0
- package/dist/sequenceRange.bench.js.map +1 -0
- package/dist/sequenceRange.d.ts +10 -0
- package/dist/sequenceRange.d.ts.map +1 -0
- package/dist/sequenceRange.js +35 -0
- package/dist/sequenceRange.js.map +1 -0
- package/dist/timers.d.ts +87 -0
- package/dist/timers.d.ts.map +1 -0
- package/dist/timers.js +68 -0
- package/dist/timers.js.map +1 -0
- package/dist/types.d.ts +634 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +429 -0
- package/dist/types.js.map +1 -0
- package/dist/writeConflicts.d.ts +46 -0
- package/dist/writeConflicts.d.ts.map +1 -0
- package/dist/writeConflicts.js +63 -0
- package/dist/writeConflicts.js.map +1 -0
- package/memory.d.ts +4 -0
- package/memory.js +13 -0
- package/package.json +67 -3
- package/sequence.d.ts +1 -0
- package/sequence.js +5 -0
- package/src/BranchRegistry.ts +161 -0
- package/src/CoalescingEventStore.ts +214 -0
- package/src/InMemoryArchiveStore.ts +69 -0
- package/src/InMemoryAsyncEventStore.ts +425 -0
- package/src/InMemoryBranchRegistry.ts +178 -0
- package/src/ProfilingEventStore.ts +153 -0
- package/src/ShardPlan.ts +235 -0
- package/src/ShardedEventStore.ts +557 -0
- package/src/SubscriberRegistry.ts +115 -0
- package/src/TenantScopedArchiveStore.ts +114 -0
- package/src/TenantScopedEventStore.ts +266 -0
- package/src/archive.ts +248 -0
- package/src/archiveSpi.ts +308 -0
- package/src/creationMarker.ts +15 -0
- package/src/decoratorPlan.typecheck.ts +52 -0
- package/src/eventKindClass.ts +127 -0
- package/src/executionIdOrder.ts +38 -0
- package/src/fencing.ts +31 -0
- package/src/index.ts +151 -0
- package/src/installOptional.ts +136 -0
- package/src/keyUnavailable.ts +98 -0
- package/src/leases.ts +319 -0
- package/src/payloadShredded.ts +63 -0
- package/src/scaleDecorators.bench.ts +107 -0
- package/src/sequenceRange.bench.ts +51 -0
- package/src/sequenceRange.ts +35 -0
- package/src/timers.ts +150 -0
- package/src/types.ts +857 -0
- package/src/writeConflicts.ts +56 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a store decorator handles one OPTIONAL SPI method of the store it wraps (ADR 0074
|
|
3
|
+
* Amendment, L1):
|
|
4
|
+
*
|
|
5
|
+
* - `"forward"` — call the inner store's method verbatim (bound to the inner store);
|
|
6
|
+
* - a function — the decorator's own implementation (it transforms ids, payloads, …);
|
|
7
|
+
* - `{ refuse: reason }` — deliberately NOT offered, with the reason recorded. The method is
|
|
8
|
+
* left uninstalled, so callers see exactly what they see for a store without it.
|
|
9
|
+
*
|
|
10
|
+
* What the compiler enforces: every key in `M` is REQUIRED (`-?` strips the `?` a mapped type
|
|
11
|
+
* over `S` would otherwise inherit from the optional SPI methods). So a decorator whose plan is
|
|
12
|
+
* `satisfies DecoratorPlan<EventStore, OptionalEventStoreMethod>` fails to compile if it omits
|
|
13
|
+
* any optional method — including one added to the SPI later, since `OptionalEventStoreMethod`
|
|
14
|
+
* is derived from the interface. Pinned by `decoratorPlan.typecheck.ts`, which `pnpm typecheck`
|
|
15
|
+
* checks. What it cannot enforce: that a decorator uses a plan at all — that is the decorator
|
|
16
|
+
* conformance kit's job, and the `packages/spec` fitness test makes every decorator run it.
|
|
17
|
+
*/
|
|
18
|
+
export type DecoratorPlan<S, M extends keyof S> = {
|
|
19
|
+
readonly [K in M]-?: "forward" | NonNullable<S[K]> | {
|
|
20
|
+
readonly refuse: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Install each optional method of `plan` on `target`, **iff the inner store has it** — so a
|
|
25
|
+
* capability probe (`if (store.copyPrefix)`) answers exactly as it would against `inner`,
|
|
26
|
+
* and a decorator never advertises a capability its backend lacks. O(|M|) once, at
|
|
27
|
+
* construction; zero cost per call (a forward is a bound method, not a trampoline).
|
|
28
|
+
*
|
|
29
|
+
* A function entry is bound to `bindTo` when given (a decorator whose implementation uses
|
|
30
|
+
* `this`), else installed as-is. `{ refuse }` entries are recorded for {@link refusalsOf}.
|
|
31
|
+
*
|
|
32
|
+
* Also carries the inner store's structural encryption marker (`keys`) onto `target` when
|
|
33
|
+
* `target` does not define its own — see {@link ENCRYPTION_MARKER}.
|
|
34
|
+
*/
|
|
35
|
+
export declare function installOptional<S extends object, M extends keyof S>(target: S, inner: S, plan: DecoratorPlan<S, M>, bindTo?: object): void;
|
|
36
|
+
/** The store `target` was built over (recorded by {@link forwardStoreMarkers}), or `undefined`. */
|
|
37
|
+
export declare function decoratedInnerOf(target: object): object | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Carry the STRUCTURAL markers other layers read by shape from `inner` up to the decorator `target`
|
|
40
|
+
* (unless `target` defines its own): the encryption marker `keys`, `sealsWholeState` (ADR 0130 D8) and
|
|
41
|
+
* the tenant marker `tenantScope` (ADR 0138 H3), the snapshot codec marker `snapshotCodec` (ADR 0141), and record `target` as wrapping `inner`. EVERY store
|
|
42
|
+
* decorator (event, snapshot, archive) must call this - {@link installOptional} does - or a guard that
|
|
43
|
+
* reads a marker on the outermost store sees a plain, unscoped one. The decorator conformance kit
|
|
44
|
+
* asserts it for each decorator in the repo.
|
|
45
|
+
*/
|
|
46
|
+
export declare function forwardStoreMarkers(target: object, inner: object): void;
|
|
47
|
+
/**
|
|
48
|
+
* The optional methods a decorator deliberately refused through {@link installOptional},
|
|
49
|
+
* with their reasons. Empty for a store that refused nothing (or did not use the helper).
|
|
50
|
+
*/
|
|
51
|
+
export declare function refusalsOf(store: object): ReadonlyMap<string, string>;
|
|
52
|
+
//# sourceMappingURL=installOptional.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"installOptional.d.ts","sourceRoot":"","sources":["../src/installOptional.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,CAAC,IAAI;IAChD,QAAQ,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,SAAS,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG;QAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KAAE;CACjF,CAAC;AAiBF;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,EACjE,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,CAAC,EACR,IAAI,EAAE,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,EACzB,MAAM,CAAC,EAAE,MAAM,GACd,IAAI,CAsBN;AAgBD,mGAAmG;AACnG,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEnE;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAuBvE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAErE"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.installOptional = installOptional;
|
|
4
|
+
exports.decoratedInnerOf = decoratedInnerOf;
|
|
5
|
+
exports.forwardStoreMarkers = forwardStoreMarkers;
|
|
6
|
+
exports.refusalsOf = refusalsOf;
|
|
7
|
+
// Refusals, keyed by the decorated object. A WeakMap so a discarded store takes its entry
|
|
8
|
+
// with it; read back by the decorator conformance kit through `refusalsOf`.
|
|
9
|
+
const refusals = new WeakMap();
|
|
10
|
+
/**
|
|
11
|
+
* The encryption-boundary marker a store exposes structurally (`EncryptingEventStore.keys`,
|
|
12
|
+
* `EncryptingSnapshotStore.keys`). `loom doctor` and `LogLifecycle`'s compaction/archive
|
|
13
|
+
* guard read it by shape, so a decorator stacked ABOVE an encrypting store must carry it up —
|
|
14
|
+
* otherwise `Coalescing(Encrypting(pg))` looks like a plain store and compaction would write
|
|
15
|
+
* the decrypted fold to a plaintext snapshot store (ADR 0022 Amendment).
|
|
16
|
+
*/
|
|
17
|
+
const ENCRYPTION_MARKER = "keys";
|
|
18
|
+
/** `EncryptingSnapshotStore.sealsWholeState` (ADR 0130 D8), carried up the same way. */
|
|
19
|
+
const WHOLE_STATE_MARKER = "sealsWholeState";
|
|
20
|
+
/**
|
|
21
|
+
* Install each optional method of `plan` on `target`, **iff the inner store has it** — so a
|
|
22
|
+
* capability probe (`if (store.copyPrefix)`) answers exactly as it would against `inner`,
|
|
23
|
+
* and a decorator never advertises a capability its backend lacks. O(|M|) once, at
|
|
24
|
+
* construction; zero cost per call (a forward is a bound method, not a trampoline).
|
|
25
|
+
*
|
|
26
|
+
* A function entry is bound to `bindTo` when given (a decorator whose implementation uses
|
|
27
|
+
* `this`), else installed as-is. `{ refuse }` entries are recorded for {@link refusalsOf}.
|
|
28
|
+
*
|
|
29
|
+
* Also carries the inner store's structural encryption marker (`keys`) onto `target` when
|
|
30
|
+
* `target` does not define its own — see {@link ENCRYPTION_MARKER}.
|
|
31
|
+
*/
|
|
32
|
+
function installOptional(target, inner, plan, bindTo) {
|
|
33
|
+
const writable = target;
|
|
34
|
+
const source = inner;
|
|
35
|
+
let refused;
|
|
36
|
+
for (const method of Object.keys(plan)) {
|
|
37
|
+
const entry = plan[method];
|
|
38
|
+
if (typeof entry === "object" && entry !== null) {
|
|
39
|
+
refused ??= new Map();
|
|
40
|
+
refused.set(String(method), entry.refuse);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const innerMethod = source[method];
|
|
44
|
+
if (typeof innerMethod !== "function")
|
|
45
|
+
continue; // the backend lacks it: so do we
|
|
46
|
+
if (entry === "forward") {
|
|
47
|
+
writable[method] = innerMethod.bind(inner);
|
|
48
|
+
}
|
|
49
|
+
else if (typeof entry === "function") {
|
|
50
|
+
writable[method] =
|
|
51
|
+
bindTo === undefined ? entry : entry.bind(bindTo);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (refused !== undefined)
|
|
55
|
+
refusals.set(target, refused);
|
|
56
|
+
forwardStoreMarkers(target, inner);
|
|
57
|
+
}
|
|
58
|
+
/** `TenantScopedEventStore.tenantScope` (and the archive/snapshot twins), carried up the same way (ADR 0138 H3). */
|
|
59
|
+
const TENANT_MARKER = "tenantScope";
|
|
60
|
+
/**
|
|
61
|
+
* ADR 0141 - the snapshot codec a store WRITES (`"none" | "zstd"`), carried up through every snapshot
|
|
62
|
+
* decorator so the composition root (`defineConfig`) reads what the outermost store really does and can
|
|
63
|
+
* refuse a mismatch, instead of the opt-in being silently dropped by a wrapper.
|
|
64
|
+
*/
|
|
65
|
+
const CODEC_MARKER = "snapshotCodec";
|
|
66
|
+
// Who a decorator wraps, keyed by the decorator: lets a guard walk a stack whose layers hide their
|
|
67
|
+
// inner store (a closure-built decorator exposes no `inner`). A WeakMap, so it never keeps a store alive.
|
|
68
|
+
const wrapped = new WeakMap();
|
|
69
|
+
/** The store `target` was built over (recorded by {@link forwardStoreMarkers}), or `undefined`. */
|
|
70
|
+
function decoratedInnerOf(target) {
|
|
71
|
+
return wrapped.get(target);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Carry the STRUCTURAL markers other layers read by shape from `inner` up to the decorator `target`
|
|
75
|
+
* (unless `target` defines its own): the encryption marker `keys`, `sealsWholeState` (ADR 0130 D8) and
|
|
76
|
+
* the tenant marker `tenantScope` (ADR 0138 H3), the snapshot codec marker `snapshotCodec` (ADR 0141), and record `target` as wrapping `inner`. EVERY store
|
|
77
|
+
* decorator (event, snapshot, archive) must call this - {@link installOptional} does - or a guard that
|
|
78
|
+
* reads a marker on the outermost store sees a plain, unscoped one. The decorator conformance kit
|
|
79
|
+
* asserts it for each decorator in the repo.
|
|
80
|
+
*/
|
|
81
|
+
function forwardStoreMarkers(target, inner) {
|
|
82
|
+
const writable = target;
|
|
83
|
+
const source = inner;
|
|
84
|
+
wrapped.set(target, inner);
|
|
85
|
+
const marker = source[ENCRYPTION_MARKER];
|
|
86
|
+
if (typeof marker === "object" && marker !== null && writable[ENCRYPTION_MARKER] === undefined) {
|
|
87
|
+
writable[ENCRYPTION_MARKER] = marker;
|
|
88
|
+
}
|
|
89
|
+
// ADR 0130 D8 - a snapshot store that seals its WHOLE state says so structurally; a decorator
|
|
90
|
+
// above it must not hide that (the doctor / kernel would read a sealed store as plaintext).
|
|
91
|
+
if (source[WHOLE_STATE_MARKER] === true && writable[WHOLE_STATE_MARKER] === undefined) {
|
|
92
|
+
writable[WHOLE_STATE_MARKER] = true;
|
|
93
|
+
}
|
|
94
|
+
// ADR 0138 H3 - a decorator above a tenant-scoped store is still scoped to that tenant.
|
|
95
|
+
const tenant = source[TENANT_MARKER];
|
|
96
|
+
if (typeof tenant === "string" && writable[TENANT_MARKER] === undefined) {
|
|
97
|
+
writable[TENANT_MARKER] = tenant;
|
|
98
|
+
}
|
|
99
|
+
// ADR 0141 - a decorator above a codec-writing snapshot store still writes that codec.
|
|
100
|
+
const codec = source[CODEC_MARKER];
|
|
101
|
+
if ((codec === "none" || codec === "zstd") && writable[CODEC_MARKER] === undefined) {
|
|
102
|
+
writable[CODEC_MARKER] = codec;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The optional methods a decorator deliberately refused through {@link installOptional},
|
|
107
|
+
* with their reasons. Empty for a store that refused nothing (or did not use the helper).
|
|
108
|
+
*/
|
|
109
|
+
function refusalsOf(store) {
|
|
110
|
+
return refusals.get(store) ?? new Map();
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=installOptional.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"installOptional.js","sourceRoot":"","sources":["../src/installOptional.ts"],"names":[],"mappings":";;AAgDA,0CA2BC;AAiBD,4CAEC;AAUD,kDAuBC;AAMD,gCAEC;AAlHD,0FAA0F;AAC1F,4EAA4E;AAC5E,MAAM,QAAQ,GAAG,IAAI,OAAO,EAAuC,CAAC;AAEpE;;;;;;GAMG;AACH,MAAM,iBAAiB,GAAG,MAAM,CAAC;AACjC,wFAAwF;AACxF,MAAM,kBAAkB,GAAG,iBAAiB,CAAC;AAE7C;;;;;;;;;;;GAWG;AACH,SAAgB,eAAe,CAC7B,MAAS,EACT,KAAQ,EACR,IAAyB,EACzB,MAAe;IAEf,MAAM,QAAQ,GAAG,MAAsC,CAAC;IACxD,MAAM,MAAM,GAAG,KAAqC,CAAC;IACrD,IAAI,OAAwC,CAAC;IAC7C,KAAK,MAAM,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAQ,EAAE,CAAC;QAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAY,CAAC;QACtC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YAChD,OAAO,KAAK,IAAI,GAAG,EAAE,CAAC;YACtB,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,EAAG,KAA4B,CAAC,MAAM,CAAC,CAAC;YAClE,SAAS;QACX,CAAC;QACD,MAAM,WAAW,GAAG,MAAM,CAAC,MAAqB,CAAC,CAAC;QAClD,IAAI,OAAO,WAAW,KAAK,UAAU;YAAE,SAAS,CAAC,iCAAiC;QAClF,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,QAAQ,CAAC,MAAqB,CAAC,GAAI,WAA4C,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9F,CAAC;aAAM,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE,CAAC;YACvC,QAAQ,CAAC,MAAqB,CAAC;gBAC7B,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAE,KAAsC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACxF,CAAC;IACH,CAAC;IACD,IAAI,OAAO,KAAK,SAAS;QAAE,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzD,mBAAmB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AACrC,CAAC;AAED,oHAAoH;AACpH,MAAM,aAAa,GAAG,aAAa,CAAC;AAEpC;;;;GAIG;AACH,MAAM,YAAY,GAAG,eAAe,CAAC;AAErC,mGAAmG;AACnG,0GAA0G;AAC1G,MAAM,OAAO,GAAG,IAAI,OAAO,EAAkB,CAAC;AAE9C,mGAAmG;AACnG,SAAgB,gBAAgB,CAAC,MAAc;IAC7C,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AAC7B,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,mBAAmB,CAAC,MAAc,EAAE,KAAa;IAC/D,MAAM,QAAQ,GAAG,MAAsC,CAAC;IACxD,MAAM,MAAM,GAAG,KAAqC,CAAC;IACrD,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC3B,MAAM,MAAM,GAAG,MAAM,CAAC,iBAAiB,CAAC,CAAC;IACzC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,QAAQ,CAAC,iBAAiB,CAAC,KAAK,SAAS,EAAE,CAAC;QAC/F,QAAQ,CAAC,iBAAiB,CAAC,GAAG,MAAM,CAAC;IACvC,CAAC;IACD,8FAA8F;IAC9F,4FAA4F;IAC5F,IAAI,MAAM,CAAC,kBAAkB,CAAC,KAAK,IAAI,IAAI,QAAQ,CAAC,kBAAkB,CAAC,KAAK,SAAS,EAAE,CAAC;QACtF,QAAQ,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAAC;IACtC,CAAC;IACD,wFAAwF;IACxF,MAAM,MAAM,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC;IACrC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,QAAQ,CAAC,aAAa,CAAC,KAAK,SAAS,EAAE,CAAC;QACxE,QAAQ,CAAC,aAAa,CAAC,GAAG,MAAM,CAAC;IACnC,CAAC;IACD,uFAAuF;IACvF,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;IACnC,IAAI,CAAC,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,MAAM,CAAC,IAAI,QAAQ,CAAC,YAAY,CAAC,KAAK,SAAS,EAAE,CAAC;QACnF,QAAQ,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC;IACjC,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAgB,UAAU,CAAC,KAAa;IACtC,OAAO,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,GAAG,EAAE,CAAC;AAC1C,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { EventEnvelope } from "@loomwork/core";
|
|
2
|
+
/** The stable `code` of {@link KeyUnavailableError}. */
|
|
3
|
+
export declare const KEY_UNAVAILABLE_CODE: "KEY_UNAVAILABLE";
|
|
4
|
+
/**
|
|
5
|
+
* ADR 0130 residuals D7 - a sealing store holds ciphertext for a subject whose key THIS node's key
|
|
6
|
+
* store does not have, and the log carries NO `PAYLOAD_SHREDDED` tombstone for it: the node is
|
|
7
|
+
* misconfigured (a different or empty key store), not erased. It is deliberately not
|
|
8
|
+
* `PayloadShreddedError`, which stays exclusively the erasure outcome. Lives beside the store error
|
|
9
|
+
* contract so the kernel can recognise it without depending on the crypto plugin.
|
|
10
|
+
*
|
|
11
|
+
* Carries the subject id and stream id, never ciphertext or key material. The kernel translates it
|
|
12
|
+
* to a denial-shaped `SealedStreamUnreadableError` (operator-only detail `key-unavailable`).
|
|
13
|
+
*/
|
|
14
|
+
export declare class KeyUnavailableError extends Error {
|
|
15
|
+
readonly subjectId: string;
|
|
16
|
+
readonly streamId?: string | undefined;
|
|
17
|
+
/** The stream's workflow id (a plaintext column), so the kernel can word its refusal. */
|
|
18
|
+
readonly workflowId?: string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* The stream with each sealed field replaced by a placeholder (its status and tip are
|
|
21
|
+
* plaintext). Lets the erasure lifecycle (`EraseCoordinator`, `exportSubject`) judge a stream
|
|
22
|
+
* in the ShredIncomplete window (key destroyed, tombstone not yet written). Never ciphertext.
|
|
23
|
+
*/
|
|
24
|
+
readonly redacted?: readonly EventEnvelope[] | undefined;
|
|
25
|
+
readonly code: "KEY_UNAVAILABLE";
|
|
26
|
+
constructor(subjectId: string, streamId?: string | undefined,
|
|
27
|
+
/** The stream's workflow id (a plaintext column), so the kernel can word its refusal. */
|
|
28
|
+
workflowId?: string | undefined,
|
|
29
|
+
/**
|
|
30
|
+
* The stream with each sealed field replaced by a placeholder (its status and tip are
|
|
31
|
+
* plaintext). Lets the erasure lifecycle (`EraseCoordinator`, `exportSubject`) judge a stream
|
|
32
|
+
* in the ShredIncomplete window (key destroyed, tombstone not yet written). Never ciphertext.
|
|
33
|
+
*/
|
|
34
|
+
redacted?: readonly EventEnvelope[] | undefined);
|
|
35
|
+
}
|
|
36
|
+
/** True for a {@link KeyUnavailableError}, by its stable `code` (survives duplicate package copies). */
|
|
37
|
+
export declare function isKeyUnavailable(e: unknown): e is KeyUnavailableError;
|
|
38
|
+
/**
|
|
39
|
+
* ADR 0130 residuals D7 (round 4) - does this stream carry the DURABLE ERASE INTENT of `subjectId`?
|
|
40
|
+
* `shredSubject` appends an `ERASURE_PENDING` (an existing, ignorable, pure-audit kernel kind) naming
|
|
41
|
+
* the key's subject to each affected execution's log BEFORE it destroys the key. It is the only
|
|
42
|
+
* evidence that distinguishes the ShredIncomplete window (key destroyed, tombstone not yet written:
|
|
43
|
+
* intent present) from a node that simply never had the key (no intent). `subjectId` omitted accepts
|
|
44
|
+
* any intent.
|
|
45
|
+
*/
|
|
46
|
+
export declare function recordsErasureIntent(events: readonly Pick<EventEnvelope, "eventType" | "payload">[], subjectId?: string): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* ADR 0130 D7 (round 5) - proof that the KEY STORE destroyed `subjectId`'s key (`KeyStore.wasDestroyed`,
|
|
49
|
+
* or the operator's explicit assertion `() => true`). The log proves intent, never destruction.
|
|
50
|
+
*/
|
|
51
|
+
export type DestructionProof = (subjectId: string) => boolean | Promise<boolean>;
|
|
52
|
+
/**
|
|
53
|
+
* The retry window, decided: a {@link KeyUnavailableError} carrying a redacted view is the genuine
|
|
54
|
+
* ShredIncomplete window ONLY when that view records this subject's erase intent AND `destroyed`
|
|
55
|
+
* proves the key store destroyed the key (a dangling intent whose key still lives elsewhere is not
|
|
56
|
+
* the window). No proof, or a false one, fails closed. Returns the redacted view in that case,
|
|
57
|
+
* `undefined` otherwise (the caller rethrows).
|
|
58
|
+
*/
|
|
59
|
+
export declare function erasureWindowView(err: unknown, destroyed?: DestructionProof): Promise<readonly EventEnvelope[] | undefined>;
|
|
60
|
+
/**
|
|
61
|
+
* The ONE gate of the retry window: `err` is a {@link KeyUnavailableError} AND `events` (the
|
|
62
|
+
* stream's log, redacted or raw) records this subject's erase intent AND `destroyed` proves the key
|
|
63
|
+
* store destroyed the key. Every read in the window (event log, sealed checkpoint) uses it, so
|
|
64
|
+
* they cannot drift. No proof, or a false one, fails closed.
|
|
65
|
+
*/
|
|
66
|
+
export declare function isErasureWindow(err: unknown, events: readonly Pick<EventEnvelope, "eventType" | "payload">[], destroyed?: DestructionProof): Promise<boolean>;
|
|
67
|
+
//# sourceMappingURL=keyUnavailable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyUnavailable.d.ts","sourceRoot":"","sources":["../src/keyUnavailable.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,wDAAwD;AACxD,eAAO,MAAM,oBAAoB,EAAG,iBAA0B,CAAC;AAE/D;;;;;;;;;GASG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;IAG1C,QAAQ,CAAC,SAAS,EAAE,MAAM;IAC1B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM;IAC1B,yFAAyF;IACzF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE;IAX9C,QAAQ,CAAC,IAAI,oBAAwB;gBAE1B,SAAS,EAAE,MAAM,EACjB,QAAQ,CAAC,EAAE,MAAM,YAAA;IAC1B,yFAAyF;IAChF,UAAU,CAAC,EAAE,MAAM,YAAA;IAC5B;;;;OAIG;IACM,QAAQ,CAAC,EAAE,SAAS,aAAa,EAAE,YAAA;CAQ/C;AAED,wGAAwG;AACxG,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,mBAAmB,CAErE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,IAAI,CAAC,aAAa,EAAE,WAAW,GAAG,SAAS,CAAC,EAAE,EAC/D,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAOT;AAED;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEjF;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,OAAO,EACZ,SAAS,CAAC,EAAE,gBAAgB,GAC3B,OAAO,CAAC,SAAS,aAAa,EAAE,GAAG,SAAS,CAAC,CAG/C;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,SAAS,IAAI,CAAC,aAAa,EAAE,WAAW,GAAG,SAAS,CAAC,EAAE,EAC/D,SAAS,CAAC,EAAE,gBAAgB,GAC3B,OAAO,CAAC,OAAO,CAAC,CAIlB"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KeyUnavailableError = exports.KEY_UNAVAILABLE_CODE = void 0;
|
|
4
|
+
exports.isKeyUnavailable = isKeyUnavailable;
|
|
5
|
+
exports.recordsErasureIntent = recordsErasureIntent;
|
|
6
|
+
exports.erasureWindowView = erasureWindowView;
|
|
7
|
+
exports.isErasureWindow = isErasureWindow;
|
|
8
|
+
/** The stable `code` of {@link KeyUnavailableError}. */
|
|
9
|
+
exports.KEY_UNAVAILABLE_CODE = "KEY_UNAVAILABLE";
|
|
10
|
+
/**
|
|
11
|
+
* ADR 0130 residuals D7 - a sealing store holds ciphertext for a subject whose key THIS node's key
|
|
12
|
+
* store does not have, and the log carries NO `PAYLOAD_SHREDDED` tombstone for it: the node is
|
|
13
|
+
* misconfigured (a different or empty key store), not erased. It is deliberately not
|
|
14
|
+
* `PayloadShreddedError`, which stays exclusively the erasure outcome. Lives beside the store error
|
|
15
|
+
* contract so the kernel can recognise it without depending on the crypto plugin.
|
|
16
|
+
*
|
|
17
|
+
* Carries the subject id and stream id, never ciphertext or key material. The kernel translates it
|
|
18
|
+
* to a denial-shaped `SealedStreamUnreadableError` (operator-only detail `key-unavailable`).
|
|
19
|
+
*/
|
|
20
|
+
class KeyUnavailableError extends Error {
|
|
21
|
+
subjectId;
|
|
22
|
+
streamId;
|
|
23
|
+
workflowId;
|
|
24
|
+
redacted;
|
|
25
|
+
code = exports.KEY_UNAVAILABLE_CODE;
|
|
26
|
+
constructor(subjectId, streamId,
|
|
27
|
+
/** The stream's workflow id (a plaintext column), so the kernel can word its refusal. */
|
|
28
|
+
workflowId,
|
|
29
|
+
/**
|
|
30
|
+
* The stream with each sealed field replaced by a placeholder (its status and tip are
|
|
31
|
+
* plaintext). Lets the erasure lifecycle (`EraseCoordinator`, `exportSubject`) judge a stream
|
|
32
|
+
* in the ShredIncomplete window (key destroyed, tombstone not yet written). Never ciphertext.
|
|
33
|
+
*/
|
|
34
|
+
redacted) {
|
|
35
|
+
super(`the encryption key for subject "${subjectId}"${streamId ? ` (stream "${streamId}")` : ""} is not available to this node and the log records no erasure (no PAYLOAD_SHREDDED): this node's key store lacks the key. It is not a crypto-shred.`);
|
|
36
|
+
this.subjectId = subjectId;
|
|
37
|
+
this.streamId = streamId;
|
|
38
|
+
this.workflowId = workflowId;
|
|
39
|
+
this.redacted = redacted;
|
|
40
|
+
this.name = "KeyUnavailableError";
|
|
41
|
+
Object.setPrototypeOf(this, KeyUnavailableError.prototype);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.KeyUnavailableError = KeyUnavailableError;
|
|
45
|
+
/** True for a {@link KeyUnavailableError}, by its stable `code` (survives duplicate package copies). */
|
|
46
|
+
function isKeyUnavailable(e) {
|
|
47
|
+
return e?.code === exports.KEY_UNAVAILABLE_CODE;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* ADR 0130 residuals D7 (round 4) - does this stream carry the DURABLE ERASE INTENT of `subjectId`?
|
|
51
|
+
* `shredSubject` appends an `ERASURE_PENDING` (an existing, ignorable, pure-audit kernel kind) naming
|
|
52
|
+
* the key's subject to each affected execution's log BEFORE it destroys the key. It is the only
|
|
53
|
+
* evidence that distinguishes the ShredIncomplete window (key destroyed, tombstone not yet written:
|
|
54
|
+
* intent present) from a node that simply never had the key (no intent). `subjectId` omitted accepts
|
|
55
|
+
* any intent.
|
|
56
|
+
*/
|
|
57
|
+
function recordsErasureIntent(events, subjectId) {
|
|
58
|
+
return events.some((e) => e.eventType === "ERASURE_PENDING" &&
|
|
59
|
+
(subjectId === undefined ||
|
|
60
|
+
e.payload?.subjectId === subjectId));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The retry window, decided: a {@link KeyUnavailableError} carrying a redacted view is the genuine
|
|
64
|
+
* ShredIncomplete window ONLY when that view records this subject's erase intent AND `destroyed`
|
|
65
|
+
* proves the key store destroyed the key (a dangling intent whose key still lives elsewhere is not
|
|
66
|
+
* the window). No proof, or a false one, fails closed. Returns the redacted view in that case,
|
|
67
|
+
* `undefined` otherwise (the caller rethrows).
|
|
68
|
+
*/
|
|
69
|
+
async function erasureWindowView(err, destroyed) {
|
|
70
|
+
if (!isKeyUnavailable(err) || !err.redacted)
|
|
71
|
+
return undefined;
|
|
72
|
+
return (await isErasureWindow(err, err.redacted, destroyed)) ? err.redacted : undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The ONE gate of the retry window: `err` is a {@link KeyUnavailableError} AND `events` (the
|
|
76
|
+
* stream's log, redacted or raw) records this subject's erase intent AND `destroyed` proves the key
|
|
77
|
+
* store destroyed the key. Every read in the window (event log, sealed checkpoint) uses it, so
|
|
78
|
+
* they cannot drift. No proof, or a false one, fails closed.
|
|
79
|
+
*/
|
|
80
|
+
async function isErasureWindow(err, events, destroyed) {
|
|
81
|
+
if (!isKeyUnavailable(err))
|
|
82
|
+
return false;
|
|
83
|
+
if (!recordsErasureIntent(events, err.subjectId))
|
|
84
|
+
return false;
|
|
85
|
+
return destroyed !== undefined && (await destroyed(err.subjectId)) === true;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=keyUnavailable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyUnavailable.js","sourceRoot":"","sources":["../src/keyUnavailable.ts"],"names":[],"mappings":";;;AAsCA,4CAEC;AAUD,oDAUC;AAeD,8CAMC;AAQD,0CAQC;AA/FD,wDAAwD;AAC3C,QAAA,oBAAoB,GAAG,iBAA0B,CAAC;AAE/D;;;;;;;;;GASG;AACH,MAAa,mBAAoB,SAAQ,KAAK;IAGjC;IACA;IAEA;IAMA;IAXF,IAAI,GAAG,4BAAoB,CAAC;IACrC,YACW,SAAiB,EACjB,QAAiB;IAC1B,yFAAyF;IAChF,UAAmB;IAC5B;;;;OAIG;IACM,QAAmC;QAE5C,KAAK,CACH,mCAAmC,SAAS,IAAI,QAAQ,CAAC,CAAC,CAAC,aAAa,QAAQ,IAAI,CAAC,CAAC,CAAC,EAAE,qJAAqJ,CAC/O,CAAC;QAbO,cAAS,GAAT,SAAS,CAAQ;QACjB,aAAQ,GAAR,QAAQ,CAAS;QAEjB,eAAU,GAAV,UAAU,CAAS;QAMnB,aAAQ,GAAR,QAAQ,CAA2B;QAK5C,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;QAClC,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,mBAAmB,CAAC,SAAS,CAAC,CAAC;IAC7D,CAAC;CACF;AApBD,kDAoBC;AAED,wGAAwG;AACxG,SAAgB,gBAAgB,CAAC,CAAU;IACzC,OAAQ,CAA+B,EAAE,IAAI,KAAK,4BAAoB,CAAC;AACzE,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,oBAAoB,CAClC,MAA+D,EAC/D,SAAkB;IAElB,OAAO,MAAM,CAAC,IAAI,CAChB,CAAC,CAAC,EAAE,EAAE,CACJ,CAAC,CAAC,SAAS,KAAK,iBAAiB;QACjC,CAAC,SAAS,KAAK,SAAS;YACrB,CAAC,CAAC,OAA0C,EAAE,SAAS,KAAK,SAAS,CAAC,CAC5E,CAAC;AACJ,CAAC;AAQD;;;;;;GAMG;AACI,KAAK,UAAU,iBAAiB,CACrC,GAAY,EACZ,SAA4B;IAE5B,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC9D,OAAO,CAAC,MAAM,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;AAC1F,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,eAAe,CACnC,GAAY,EACZ,MAA+D,EAC/D,SAA4B;IAE5B,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IACzC,IAAI,CAAC,oBAAoB,CAAC,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/D,OAAO,SAAS,KAAK,SAAS,IAAI,CAAC,MAAM,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,KAAK,IAAI,CAAC;AAC9E,CAAC"}
|
package/dist/leases.d.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPI version for the {@link LeaseStore} contract. Pinned by `runLeaseStoreConformance`.
|
|
3
|
+
*
|
|
4
|
+
* 2.0.0 (L28): `expired`'s `now` became optional and MUST default to the store's stamping clock.
|
|
5
|
+
* MAJOR, not additive: a 1.x store written `expired(now: number)` still type-checks against the
|
|
6
|
+
* new signature but, handed `undefined`, returns `[]` — recovery silently recovers nothing.
|
|
7
|
+
*
|
|
8
|
+
* 2.1.0 (L29): optional `storeNow()` — the store's own clock. Additive: a store without it behaves
|
|
9
|
+
* exactly as before, and a caller with no `storeNow()` falls back to `Date.now()`.
|
|
10
|
+
*
|
|
11
|
+
* 2.2.0 (ADR 0130 D11, heartbeat leases): optional `scope` (who can see this store's leases:
|
|
12
|
+
* "process" | "host" | "cluster"; absent = unknown, treated as NOT cluster-visible), optional `acquireMany` / `renewMany` (returns the HELD
|
|
13
|
+
* set) / `releaseMany`, and the `steal` option on `acquire`. Additive: a store without them is
|
|
14
|
+
* driven through the single-id methods by the caller (correct, just slower). A store that claims
|
|
15
|
+
* 2.2.0 implements all three batch methods and the steal gate; one that claims 2.1.0 is not asked.
|
|
16
|
+
*/
|
|
17
|
+
export declare const LEASE_STORE_SPI_VERSION = "2.2.0";
|
|
18
|
+
export interface Lease {
|
|
19
|
+
executionId: string;
|
|
20
|
+
/** Who holds it. A node id, a pod name, a process uuid — the deployment's vocabulary. */
|
|
21
|
+
owner: string;
|
|
22
|
+
/**
|
|
23
|
+
* Absolute expiry (epoch ms), on the store's clock (the one `expired()` defaults to), not
|
|
24
|
+
* necessarily the caller's. Past this, the lease is collectable by anyone.
|
|
25
|
+
*/
|
|
26
|
+
expiresAt: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The proof a caller must hold to `steal` a lease: the fence it won with a CONDITIONAL fence
|
|
30
|
+
* acquisition (`acquireFenceIf`, i.e. proof the execution is parked). The store cannot verify it
|
|
31
|
+
* against the event log (the lease store is deliberately a separate, dumb SPI); it can only refuse
|
|
32
|
+
* a steal that does not even carry one, so a flag cannot be passed by accident.
|
|
33
|
+
*
|
|
34
|
+
* The proof is SHAPE-ONLY. Callers MUST pass a fence actually won via `acquireFenceIf` /
|
|
35
|
+
* `acquireFence` for THIS execution. A bogus steal is a liveness attack, never log corruption:
|
|
36
|
+
* the live owner's next `renew` fails (`LeaseLostError`), but the fence is the write guard
|
|
37
|
+
* (ADR 0130 D11 section 4), so the stale owner's appends are still rejected. An explicit `now`
|
|
38
|
+
* bypasses the expiry gate on every backend (pre-existing): production callers must never pass
|
|
39
|
+
* `now`, and on Postgres should omit it so the DATABASE clock rules.
|
|
40
|
+
*/
|
|
41
|
+
export interface StealProof {
|
|
42
|
+
/** The fence value the caller won: a positive safe integer (0 is the inert park token, ADR 0118). */
|
|
43
|
+
fence: number;
|
|
44
|
+
}
|
|
45
|
+
/** Options for {@link LeaseStore.acquire} / {@link LeaseStore.acquireMany} (SPI 2.2.0). */
|
|
46
|
+
export interface AcquireOptions {
|
|
47
|
+
/**
|
|
48
|
+
* Take the lease even though ANOTHER owner holds it unexpired (a slow release left a stale row
|
|
49
|
+
* behind a driver that has already proved the execution is parked). Valid ONLY with a
|
|
50
|
+
* {@link StealProof}; anything else (`true`, `{}`, a negative or fractional fence) THROWS
|
|
51
|
+
* `TypeError` and changes nothing. After a release (no row), a steal is just an acquire. The
|
|
52
|
+
* displaced owner's next `renew`/`renewMany` reports it lost (`false` / absent).
|
|
53
|
+
*
|
|
54
|
+
* Single-id `acquire` ONLY: a fence belongs to one execution, so `acquireMany` REJECTS `steal`
|
|
55
|
+
* with `TypeError` (nothing changes).
|
|
56
|
+
*/
|
|
57
|
+
steal?: StealProof;
|
|
58
|
+
}
|
|
59
|
+
export interface LeaseStore {
|
|
60
|
+
/**
|
|
61
|
+
* OPTIONAL (SPI 2.2.0): who can observe this store's leases. `"process"`: one process (in-memory);
|
|
62
|
+
* `"host"`: processes on one machine (a local file); `"cluster"`: every node (a shared server).
|
|
63
|
+
* Absent means unknown and is treated as NOT cluster-visible. Heartbeat evidence ("a lease
|
|
64
|
+
* expired, so the driver is gone") holds only if every node reads the same table.
|
|
65
|
+
*/
|
|
66
|
+
readonly scope?: "process" | "host" | "cluster";
|
|
67
|
+
/**
|
|
68
|
+
* Take ownership, or report that someone else has it.
|
|
69
|
+
*
|
|
70
|
+
* Returns the lease on success and `null` when another owner holds an unexpired one — that
|
|
71
|
+
* single `null` is the mutual exclusion "only one node should run this execution" needs.
|
|
72
|
+
* Re-acquiring a lease you already hold is a renewal, not a conflict: a node that restarts
|
|
73
|
+
* its own work must not be locked out by its own previous claim.
|
|
74
|
+
*
|
|
75
|
+
* An EXPIRED lease is acquirable by anyone, which is the whole point — the previous owner
|
|
76
|
+
* stopped saying it was alive.
|
|
77
|
+
*/
|
|
78
|
+
acquire(executionId: string, owner: string, ttlMs: number, now?: number, options?: AcquireOptions): Promise<Lease | null>;
|
|
79
|
+
/**
|
|
80
|
+
* OPTIONAL (SPI 2.2.0): `acquire` for many executions in ONE round trip. Returns the leases
|
|
81
|
+
* granted; an id another owner holds unexpired is simply absent (the batch form of `null`).
|
|
82
|
+
* Duplicate ids count once. Per id the semantics are exactly {@link acquire}'s, and `now`
|
|
83
|
+
* applies to the whole batch. `options.steal` THROWS `TypeError` (steals go through `acquire`). Empty input returns `[]` without touching the store.
|
|
84
|
+
*/
|
|
85
|
+
acquireMany?(executionIds: readonly string[], owner: string, ttlMs: number, now?: number, options?: AcquireOptions): Promise<Lease[]>;
|
|
86
|
+
/**
|
|
87
|
+
* OPTIONAL (SPI 2.2.0): `renew` for many executions in one round trip. Returns the HELD set:
|
|
88
|
+
* the ids whose lease this `owner` still holds after the call (their expiry extended). Every
|
|
89
|
+
* other id, released, taken over, or never held, is absent, and nothing about it changes: a
|
|
90
|
+
* renew NEVER resurrects. The returned set is what a keeper diffs against its own to find the
|
|
91
|
+
* lost ids, so a store must not echo its input.
|
|
92
|
+
*/
|
|
93
|
+
renewMany?(executionIds: readonly string[], owner: string, ttlMs: number, now?: number): Promise<string[]>;
|
|
94
|
+
/** OPTIONAL (SPI 2.2.0): `release` for many executions in one round trip; other owners' are untouched. */
|
|
95
|
+
releaseMany?(executionIds: readonly string[], owner: string): Promise<void>;
|
|
96
|
+
/**
|
|
97
|
+
* Extend a lease you hold. `false` means you no longer hold it — someone else took over
|
|
98
|
+
* after it expired — and a caller that keeps working on `false` is the split-brain this
|
|
99
|
+
* exists to surface. Never resurrects: renewing an expired lease that another owner has
|
|
100
|
+
* since taken fails rather than stealing it back.
|
|
101
|
+
*/
|
|
102
|
+
renew(executionId: string, owner: string, ttlMs: number, now?: number): Promise<boolean>;
|
|
103
|
+
/**
|
|
104
|
+
* Leases past their expiry, soonest-expired first. The evidence an execution was orphaned.
|
|
105
|
+
*
|
|
106
|
+
* `now` is OPTIONAL (SPI 2.0.0) and an implementation MUST default it to the store's OWN
|
|
107
|
+
* clock — the same one `acquire`/`renew` stamp `expiresAt` with. Expiry is only meaningful
|
|
108
|
+
* judged on the clock that stamped it: a caller passing wall-clock `Date.now()` to a store
|
|
109
|
+
* running on an injected/simulated clock sees every live lease as expired (a
|
|
110
|
+
* double-execution hazard). Callers with no reason to override the clock
|
|
111
|
+
* (`recoverAbandoned`) pass `undefined`. An implementation that still declares
|
|
112
|
+
* `expired(now: number)` compiles, but given `undefined` its comparison is false (`<=
|
|
113
|
+
* undefined`, SQL `<= NULL`) and it returns `[]`: recovery silently recovers nothing.
|
|
114
|
+
*
|
|
115
|
+
* The SQL stores' "own clock" is one clock for every node: SQLite is single-host, so the
|
|
116
|
+
* process clock is its one clock; Postgres stamps and judges on DATABASE time (L29), so node
|
|
117
|
+
* skew cannot make a live lease look expired or let a fast node steal one.
|
|
118
|
+
*/
|
|
119
|
+
expired(now?: number, limit?: number): Promise<Lease[]>;
|
|
120
|
+
/**
|
|
121
|
+
* OPTIONAL read: the lease on an execution, or `null` if nothing holds one.
|
|
122
|
+
*
|
|
123
|
+
* `expired()` answers "who stopped saying they were alive", which cannot distinguish an
|
|
124
|
+
* execution someone is actively renewing from one that never had a lease at all. That
|
|
125
|
+
* distinction is the whole question for `orphanCensus` (#408): a stuck execution with NO
|
|
126
|
+
* lease is a path nothing is watching, and counting those is how "should the runtime lease
|
|
127
|
+
* every run" stops being a guess.
|
|
128
|
+
*/
|
|
129
|
+
get?(executionId: string): Promise<Lease | null>;
|
|
130
|
+
/**
|
|
131
|
+
* OPTIONAL (SPI 2.1.0): the store's OWN clock, in epoch ms, the one `expiresAt` is stamped and
|
|
132
|
+
* `expired()` judged on. A reader that must compare `expiresAt` to "now" outside the store
|
|
133
|
+
* (`orphanCensus`) asks this rather than assuming its host's `Date.now()`, which for a shared
|
|
134
|
+
* database (Postgres stamps on DB time) is a different clock. Absent means the store uses the
|
|
135
|
+
* caller-visible process clock.
|
|
136
|
+
*/
|
|
137
|
+
storeNow?(): Promise<number>;
|
|
138
|
+
/** Give it up. Releasing a lease held by someone else is a no-op, never a steal. */
|
|
139
|
+
release(executionId: string, owner: string): Promise<void>;
|
|
140
|
+
close(): Promise<void>;
|
|
141
|
+
}
|
|
142
|
+
export interface InMemoryLeaseStoreOptions {
|
|
143
|
+
/** Injectable clock, as everywhere else in the runtime. Default: `Date.now`. */
|
|
144
|
+
now?: () => number;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Map-backed {@link LeaseStore} — the browser-safe / test reference implementation beside the
|
|
148
|
+
* durable backends. Behaviour is pinned by `runLeaseStoreConformance`.
|
|
149
|
+
*/
|
|
150
|
+
export declare class InMemoryLeaseStore implements LeaseStore {
|
|
151
|
+
readonly scope: "process";
|
|
152
|
+
private readonly leases;
|
|
153
|
+
private readonly now;
|
|
154
|
+
constructor(options?: InMemoryLeaseStoreOptions);
|
|
155
|
+
acquire(executionId: string, owner: string, ttlMs: number, now?: number, options?: AcquireOptions): Promise<Lease | null>;
|
|
156
|
+
acquireMany(executionIds: readonly string[], owner: string, ttlMs: number, now?: number, options?: AcquireOptions): Promise<Lease[]>;
|
|
157
|
+
renewMany(executionIds: readonly string[], owner: string, ttlMs: number, now?: number): Promise<string[]>;
|
|
158
|
+
releaseMany(executionIds: readonly string[], owner: string): Promise<void>;
|
|
159
|
+
renew(executionId: string, owner: string, ttlMs: number, now?: number): Promise<boolean>;
|
|
160
|
+
expired(now?: number, limit?: number): Promise<Lease[]>;
|
|
161
|
+
get(executionId: string): Promise<Lease | null>;
|
|
162
|
+
/** The store's own (injectable) clock: the one `expiresAt` is stamped and judged on (SPI 2.1.0). */
|
|
163
|
+
storeNow(): Promise<number>;
|
|
164
|
+
release(executionId: string, owner: string): Promise<void>;
|
|
165
|
+
close(): Promise<void>;
|
|
166
|
+
}
|
|
167
|
+
/** A zero or negative TTL would make a lease expired on arrival — refuse it at every backend. */
|
|
168
|
+
export declare function assertTtl(ttlMs: number): void;
|
|
169
|
+
/**
|
|
170
|
+
* The `steal` gate every backend applies BEFORE touching storage: `true` only for a real
|
|
171
|
+
* {@link StealProof}; `undefined` options or no `steal` is `false`; anything else THROWS
|
|
172
|
+
* `TypeError` (a flag is never a proof).
|
|
173
|
+
*/
|
|
174
|
+
export declare function assertSteal(options: AcquireOptions | undefined): boolean;
|
|
175
|
+
/** `acquireMany` takes no `steal`: a fence belongs to one execution. THROWS `TypeError`, changing nothing. */
|
|
176
|
+
export declare function assertNoBatchSteal(options: AcquireOptions | undefined): void;
|
|
177
|
+
//# sourceMappingURL=leases.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"leases.d.ts","sourceRoot":"","sources":["../src/leases.ts"],"names":[],"mappings":"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"}
|