@loomwork/event-runtime 0.0.0-stage → 0.1.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.
Files changed (153) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +8 -0
  3. package/README.md +95 -2
  4. package/dist/BranchRegistry.d.ts +140 -0
  5. package/dist/BranchRegistry.d.ts.map +1 -0
  6. package/dist/BranchRegistry.js +66 -0
  7. package/dist/BranchRegistry.js.map +1 -0
  8. package/dist/CoalescingEventStore.d.ts +97 -0
  9. package/dist/CoalescingEventStore.d.ts.map +1 -0
  10. package/dist/CoalescingEventStore.js +180 -0
  11. package/dist/CoalescingEventStore.js.map +1 -0
  12. package/dist/InMemoryArchiveStore.d.ts +19 -0
  13. package/dist/InMemoryArchiveStore.d.ts.map +1 -0
  14. package/dist/InMemoryArchiveStore.js +49 -0
  15. package/dist/InMemoryArchiveStore.js.map +1 -0
  16. package/dist/InMemoryAsyncEventStore.d.ts +111 -0
  17. package/dist/InMemoryAsyncEventStore.d.ts.map +1 -0
  18. package/dist/InMemoryAsyncEventStore.js +374 -0
  19. package/dist/InMemoryAsyncEventStore.js.map +1 -0
  20. package/dist/InMemoryBranchRegistry.d.ts +31 -0
  21. package/dist/InMemoryBranchRegistry.d.ts.map +1 -0
  22. package/dist/InMemoryBranchRegistry.js +167 -0
  23. package/dist/InMemoryBranchRegistry.js.map +1 -0
  24. package/dist/ProfilingEventStore.d.ts +75 -0
  25. package/dist/ProfilingEventStore.d.ts.map +1 -0
  26. package/dist/ProfilingEventStore.js +133 -0
  27. package/dist/ProfilingEventStore.js.map +1 -0
  28. package/dist/ShardPlan.d.ts +87 -0
  29. package/dist/ShardPlan.d.ts.map +1 -0
  30. package/dist/ShardPlan.js +193 -0
  31. package/dist/ShardPlan.js.map +1 -0
  32. package/dist/ShardedEventStore.d.ts +149 -0
  33. package/dist/ShardedEventStore.d.ts.map +1 -0
  34. package/dist/ShardedEventStore.js +468 -0
  35. package/dist/ShardedEventStore.js.map +1 -0
  36. package/dist/SubscriberRegistry.d.ts +59 -0
  37. package/dist/SubscriberRegistry.d.ts.map +1 -0
  38. package/dist/SubscriberRegistry.js +113 -0
  39. package/dist/SubscriberRegistry.js.map +1 -0
  40. package/dist/TenantScopedArchiveStore.d.ts +36 -0
  41. package/dist/TenantScopedArchiveStore.d.ts.map +1 -0
  42. package/dist/TenantScopedArchiveStore.js +96 -0
  43. package/dist/TenantScopedArchiveStore.js.map +1 -0
  44. package/dist/TenantScopedEventStore.d.ts +59 -0
  45. package/dist/TenantScopedEventStore.d.ts.map +1 -0
  46. package/dist/TenantScopedEventStore.js +227 -0
  47. package/dist/TenantScopedEventStore.js.map +1 -0
  48. package/dist/archive.d.ts +68 -0
  49. package/dist/archive.d.ts.map +1 -0
  50. package/dist/archive.js +207 -0
  51. package/dist/archive.js.map +1 -0
  52. package/dist/archiveSpi.d.ts +165 -0
  53. package/dist/archiveSpi.d.ts.map +1 -0
  54. package/dist/archiveSpi.js +184 -0
  55. package/dist/archiveSpi.js.map +1 -0
  56. package/dist/creationMarker.d.ts +13 -0
  57. package/dist/creationMarker.d.ts.map +1 -0
  58. package/dist/creationMarker.js +17 -0
  59. package/dist/creationMarker.js.map +1 -0
  60. package/dist/decoratorPlan.typecheck.d.ts +2 -0
  61. package/dist/decoratorPlan.typecheck.d.ts.map +1 -0
  62. package/dist/decoratorPlan.typecheck.js +41 -0
  63. package/dist/decoratorPlan.typecheck.js.map +1 -0
  64. package/dist/eventKindClass.d.ts +40 -0
  65. package/dist/eventKindClass.d.ts.map +1 -0
  66. package/dist/eventKindClass.js +99 -0
  67. package/dist/eventKindClass.js.map +1 -0
  68. package/dist/executionIdOrder.d.ts +21 -0
  69. package/dist/executionIdOrder.d.ts.map +1 -0
  70. package/dist/executionIdOrder.js +43 -0
  71. package/dist/executionIdOrder.js.map +1 -0
  72. package/dist/fencing.d.ts +21 -0
  73. package/dist/fencing.d.ts.map +1 -0
  74. package/dist/fencing.js +36 -0
  75. package/dist/fencing.js.map +1 -0
  76. package/dist/index.d.ts +39 -0
  77. package/dist/index.d.ts.map +1 -0
  78. package/dist/index.js +126 -0
  79. package/dist/index.js.map +1 -0
  80. package/dist/installOptional.d.ts +52 -0
  81. package/dist/installOptional.d.ts.map +1 -0
  82. package/dist/installOptional.js +112 -0
  83. package/dist/installOptional.js.map +1 -0
  84. package/dist/keyUnavailable.d.ts +67 -0
  85. package/dist/keyUnavailable.d.ts.map +1 -0
  86. package/dist/keyUnavailable.js +87 -0
  87. package/dist/keyUnavailable.js.map +1 -0
  88. package/dist/leases.d.ts +177 -0
  89. package/dist/leases.d.ts.map +1 -0
  90. package/dist/leases.js +142 -0
  91. package/dist/leases.js.map +1 -0
  92. package/dist/payloadShredded.d.ts +30 -0
  93. package/dist/payloadShredded.d.ts.map +1 -0
  94. package/dist/payloadShredded.js +67 -0
  95. package/dist/payloadShredded.js.map +1 -0
  96. package/dist/scaleDecorators.bench.d.ts +2 -0
  97. package/dist/scaleDecorators.bench.d.ts.map +1 -0
  98. package/dist/scaleDecorators.bench.js +93 -0
  99. package/dist/scaleDecorators.bench.js.map +1 -0
  100. package/dist/sequenceRange.bench.d.ts +2 -0
  101. package/dist/sequenceRange.bench.d.ts.map +1 -0
  102. package/dist/sequenceRange.bench.js +46 -0
  103. package/dist/sequenceRange.bench.js.map +1 -0
  104. package/dist/sequenceRange.d.ts +10 -0
  105. package/dist/sequenceRange.d.ts.map +1 -0
  106. package/dist/sequenceRange.js +35 -0
  107. package/dist/sequenceRange.js.map +1 -0
  108. package/dist/timers.d.ts +87 -0
  109. package/dist/timers.d.ts.map +1 -0
  110. package/dist/timers.js +68 -0
  111. package/dist/timers.js.map +1 -0
  112. package/dist/types.d.ts +634 -0
  113. package/dist/types.d.ts.map +1 -0
  114. package/dist/types.js +429 -0
  115. package/dist/types.js.map +1 -0
  116. package/dist/writeConflicts.d.ts +46 -0
  117. package/dist/writeConflicts.d.ts.map +1 -0
  118. package/dist/writeConflicts.js +63 -0
  119. package/dist/writeConflicts.js.map +1 -0
  120. package/memory.d.ts +4 -0
  121. package/memory.js +13 -0
  122. package/package.json +67 -3
  123. package/sequence.d.ts +1 -0
  124. package/sequence.js +5 -0
  125. package/src/BranchRegistry.ts +161 -0
  126. package/src/CoalescingEventStore.ts +214 -0
  127. package/src/InMemoryArchiveStore.ts +69 -0
  128. package/src/InMemoryAsyncEventStore.ts +425 -0
  129. package/src/InMemoryBranchRegistry.ts +178 -0
  130. package/src/ProfilingEventStore.ts +153 -0
  131. package/src/ShardPlan.ts +235 -0
  132. package/src/ShardedEventStore.ts +557 -0
  133. package/src/SubscriberRegistry.ts +115 -0
  134. package/src/TenantScopedArchiveStore.ts +114 -0
  135. package/src/TenantScopedEventStore.ts +266 -0
  136. package/src/archive.ts +248 -0
  137. package/src/archiveSpi.ts +308 -0
  138. package/src/creationMarker.ts +15 -0
  139. package/src/decoratorPlan.typecheck.ts +52 -0
  140. package/src/eventKindClass.ts +127 -0
  141. package/src/executionIdOrder.ts +38 -0
  142. package/src/fencing.ts +31 -0
  143. package/src/index.ts +151 -0
  144. package/src/installOptional.ts +136 -0
  145. package/src/keyUnavailable.ts +98 -0
  146. package/src/leases.ts +319 -0
  147. package/src/payloadShredded.ts +63 -0
  148. package/src/scaleDecorators.bench.ts +107 -0
  149. package/src/sequenceRange.bench.ts +51 -0
  150. package/src/sequenceRange.ts +35 -0
  151. package/src/timers.ts +150 -0
  152. package/src/types.ts +857 -0
  153. package/src/writeConflicts.ts +56 -0
package/dist/leases.js ADDED
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ // ── Durable ownership of a running execution (#380) ───────────────────────────
3
+ //
4
+ // A `running` execution owned by a healthy node is byte-identical, in the log, to one
5
+ // orphaned by a dead one: the difference is a fact about the cluster, not about the
6
+ // execution, so no fold can see it. That is why `recoverAbandoned` demands a predicate — and
7
+ // why a double dispatch today is merely SAFE rather than avoided. Fencing (K9.4) rejects the
8
+ // loser's write; both runs still executed, and both still paid for their effects.
9
+ //
10
+ // A lease is the missing fact: ownership with an expiry, written where both nodes can see it.
11
+ //
12
+ // Deliberately shaped like `TimerStore`: a small store with a polling service above it, no
13
+ // coordination protocol, and nothing required of a deployment that does not want it. Unwired,
14
+ // every path behaves byte-identically (G2).
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.InMemoryLeaseStore = exports.LEASE_STORE_SPI_VERSION = void 0;
17
+ exports.assertTtl = assertTtl;
18
+ exports.assertSteal = assertSteal;
19
+ exports.assertNoBatchSteal = assertNoBatchSteal;
20
+ /**
21
+ * SPI version for the {@link LeaseStore} contract. Pinned by `runLeaseStoreConformance`.
22
+ *
23
+ * 2.0.0 (L28): `expired`'s `now` became optional and MUST default to the store's stamping clock.
24
+ * MAJOR, not additive: a 1.x store written `expired(now: number)` still type-checks against the
25
+ * new signature but, handed `undefined`, returns `[]` — recovery silently recovers nothing.
26
+ *
27
+ * 2.1.0 (L29): optional `storeNow()` — the store's own clock. Additive: a store without it behaves
28
+ * exactly as before, and a caller with no `storeNow()` falls back to `Date.now()`.
29
+ *
30
+ * 2.2.0 (ADR 0130 D11, heartbeat leases): optional `scope` (who can see this store's leases:
31
+ * "process" | "host" | "cluster"; absent = unknown, treated as NOT cluster-visible), optional `acquireMany` / `renewMany` (returns the HELD
32
+ * set) / `releaseMany`, and the `steal` option on `acquire`. Additive: a store without them is
33
+ * driven through the single-id methods by the caller (correct, just slower). A store that claims
34
+ * 2.2.0 implements all three batch methods and the steal gate; one that claims 2.1.0 is not asked.
35
+ */
36
+ exports.LEASE_STORE_SPI_VERSION = "2.2.0";
37
+ /**
38
+ * Map-backed {@link LeaseStore} — the browser-safe / test reference implementation beside the
39
+ * durable backends. Behaviour is pinned by `runLeaseStoreConformance`.
40
+ */
41
+ class InMemoryLeaseStore {
42
+ scope = "process";
43
+ leases = new Map();
44
+ now;
45
+ constructor(options = {}) {
46
+ this.now = options.now ?? (() => Date.now());
47
+ }
48
+ async acquire(executionId, owner, ttlMs, now = this.now(), options) {
49
+ assertTtl(ttlMs);
50
+ const steal = assertSteal(options);
51
+ const held = this.leases.get(executionId);
52
+ // Someone else's, and still alive (unless the caller proved it may take it).
53
+ if (held !== undefined && held.owner !== owner && held.expiresAt > now && !steal)
54
+ return null;
55
+ const lease = { executionId, owner, expiresAt: now + ttlMs };
56
+ this.leases.set(executionId, lease);
57
+ return { ...lease };
58
+ }
59
+ async acquireMany(executionIds, owner, ttlMs, now = this.now(), options) {
60
+ assertTtl(ttlMs);
61
+ assertNoBatchSteal(options);
62
+ const out = [];
63
+ for (const id of new Set(executionIds)) {
64
+ const lease = await this.acquire(id, owner, ttlMs, now);
65
+ if (lease !== null)
66
+ out.push(lease);
67
+ }
68
+ return out;
69
+ }
70
+ async renewMany(executionIds, owner, ttlMs, now = this.now()) {
71
+ assertTtl(ttlMs);
72
+ const held = [];
73
+ for (const id of new Set(executionIds))
74
+ if (await this.renew(id, owner, ttlMs, now))
75
+ held.push(id);
76
+ return held;
77
+ }
78
+ async releaseMany(executionIds, owner) {
79
+ for (const id of new Set(executionIds))
80
+ await this.release(id, owner);
81
+ }
82
+ async renew(executionId, owner, ttlMs, now = this.now()) {
83
+ assertTtl(ttlMs);
84
+ const held = this.leases.get(executionId);
85
+ if (held === undefined || held.owner !== owner)
86
+ return false;
87
+ held.expiresAt = now + ttlMs;
88
+ return true;
89
+ }
90
+ async expired(now = this.now(), limit = 100) {
91
+ return [...this.leases.values()]
92
+ .filter((l) => l.expiresAt <= now)
93
+ .sort((a, b) => a.expiresAt - b.expiresAt)
94
+ .slice(0, limit)
95
+ .map((l) => ({ ...l }));
96
+ }
97
+ async get(executionId) {
98
+ const held = this.leases.get(executionId);
99
+ return held !== undefined ? { ...held } : null;
100
+ }
101
+ /** The store's own (injectable) clock: the one `expiresAt` is stamped and judged on (SPI 2.1.0). */
102
+ async storeNow() {
103
+ return this.now();
104
+ }
105
+ async release(executionId, owner) {
106
+ const held = this.leases.get(executionId);
107
+ if (held?.owner === owner)
108
+ this.leases.delete(executionId);
109
+ }
110
+ async close() {
111
+ this.leases.clear();
112
+ }
113
+ }
114
+ exports.InMemoryLeaseStore = InMemoryLeaseStore;
115
+ /** A zero or negative TTL would make a lease expired on arrival — refuse it at every backend. */
116
+ function assertTtl(ttlMs) {
117
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
118
+ throw new RangeError(`lease ttlMs must be a positive number of milliseconds, got ${ttlMs}`);
119
+ }
120
+ }
121
+ /**
122
+ * The `steal` gate every backend applies BEFORE touching storage: `true` only for a real
123
+ * {@link StealProof}; `undefined` options or no `steal` is `false`; anything else THROWS
124
+ * `TypeError` (a flag is never a proof).
125
+ */
126
+ function assertSteal(options) {
127
+ const steal = options?.steal;
128
+ if (steal === undefined)
129
+ return false;
130
+ const fence = steal?.fence;
131
+ if (typeof fence !== "number" || !Number.isSafeInteger(fence) || fence < 1) {
132
+ throw new TypeError("lease steal requires a conditional-fence proof: { steal: { fence: <positive integer> } }");
133
+ }
134
+ return true;
135
+ }
136
+ /** `acquireMany` takes no `steal`: a fence belongs to one execution. THROWS `TypeError`, changing nothing. */
137
+ function assertNoBatchSteal(options) {
138
+ if (options?.steal !== undefined) {
139
+ throw new TypeError("acquireMany does not accept `steal`: a fence belongs to one execution; steal through acquire()");
140
+ }
141
+ }
142
+ //# sourceMappingURL=leases.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"leases.js","sourceRoot":"","sources":["../src/leases.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The realm-safe brand of the erasure outcome (ADR 0039, amendment 2026-10-17 (c)). A crypto-shredded
3
+ * payload is recognised by THIS symbol, never by the error's `name` alone: any store decorator, third
4
+ * party store or app code can throw an `Error` that merely NAMES itself `PayloadShreddedError`, and on
5
+ * the authorization path "proven erasure" means "unknown owner", which `unknownOwner: "allow"` admits.
6
+ *
7
+ * `Symbol.for` (the global registry) makes the brand survive duplicated package copies (dual CJS/ESM
8
+ * builds, two installs), where `instanceof` does not. A store that cannot extend the crypto plugin's
9
+ * class sets the brand on its own error with {@link brandPayloadShredded}.
10
+ */
11
+ export declare const PAYLOAD_SHREDDED_BRAND: unique symbol;
12
+ /**
13
+ * Mark `err` as the erasure outcome (non-enumerable, so serialization and logging are unchanged) and
14
+ * return it. Idempotent. The error's `name` and message are untouched.
15
+ *
16
+ * Brand BEFORE freezing: a frozen or otherwise non-extensible object cannot take the brand, and
17
+ * throwing here would replace the erasure with a TypeError. For such an object this returns a branded
18
+ * WRAPPER instead (a new `Error` with the same name, message, own fields and stack, `cause` = the
19
+ * original), so the erasure is still recognised.
20
+ */
21
+ export declare function brandPayloadShredded<T extends object>(err: T): T;
22
+ /**
23
+ * True when `err` carries the {@link PAYLOAD_SHREDDED_BRAND} as an OWN property (an inherited brand,
24
+ * e.g. from a shared prototype, does not count). The ONE definition every consumer uses.
25
+ * A plain object or `Error` that only has `name === "PayloadShreddedError"` is NOT shredded, and a
26
+ * value that crossed a process boundary (JSON) has lost the brand: such a boundary must re-establish
27
+ * trust explicitly (e.g. by checking the log's own `PAYLOAD_SHREDDED` tombstone).
28
+ */
29
+ export declare function isPayloadShredded(err: unknown): boolean;
30
+ //# sourceMappingURL=payloadShredded.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payloadShredded.d.ts","sourceRoot":"","sources":["../src/payloadShredded.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,EAAE,OAAO,MAAoD,CAAC;AAEjG;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAyBhE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAOvD"}
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PAYLOAD_SHREDDED_BRAND = void 0;
4
+ exports.brandPayloadShredded = brandPayloadShredded;
5
+ exports.isPayloadShredded = isPayloadShredded;
6
+ /**
7
+ * The realm-safe brand of the erasure outcome (ADR 0039, amendment 2026-10-17 (c)). A crypto-shredded
8
+ * payload is recognised by THIS symbol, never by the error's `name` alone: any store decorator, third
9
+ * party store or app code can throw an `Error` that merely NAMES itself `PayloadShreddedError`, and on
10
+ * the authorization path "proven erasure" means "unknown owner", which `unknownOwner: "allow"` admits.
11
+ *
12
+ * `Symbol.for` (the global registry) makes the brand survive duplicated package copies (dual CJS/ESM
13
+ * builds, two installs), where `instanceof` does not. A store that cannot extend the crypto plugin's
14
+ * class sets the brand on its own error with {@link brandPayloadShredded}.
15
+ */
16
+ exports.PAYLOAD_SHREDDED_BRAND = Symbol.for("loom.payloadShredded");
17
+ /**
18
+ * Mark `err` as the erasure outcome (non-enumerable, so serialization and logging are unchanged) and
19
+ * return it. Idempotent. The error's `name` and message are untouched.
20
+ *
21
+ * Brand BEFORE freezing: a frozen or otherwise non-extensible object cannot take the brand, and
22
+ * throwing here would replace the erasure with a TypeError. For such an object this returns a branded
23
+ * WRAPPER instead (a new `Error` with the same name, message, own fields and stack, `cause` = the
24
+ * original), so the erasure is still recognised.
25
+ */
26
+ function brandPayloadShredded(err) {
27
+ if (!Object.isExtensible(err)) {
28
+ const src = err;
29
+ const wrapper = new Error(typeof src.message === "string" ? src.message : "", { cause: err });
30
+ for (const key of Object.getOwnPropertyNames(src)) {
31
+ if (key === "cause")
32
+ continue;
33
+ const d = Object.getOwnPropertyDescriptor(src, key);
34
+ if (d !== undefined)
35
+ Object.defineProperty(wrapper, key, { ...d, configurable: true });
36
+ }
37
+ if (!Object.prototype.hasOwnProperty.call(src, "name") && typeof src.name === "string") {
38
+ Object.defineProperty(wrapper, "name", {
39
+ value: src.name,
40
+ configurable: true,
41
+ writable: true,
42
+ });
43
+ }
44
+ return brandPayloadShredded(wrapper);
45
+ }
46
+ Object.defineProperty(err, exports.PAYLOAD_SHREDDED_BRAND, {
47
+ value: true,
48
+ enumerable: false,
49
+ configurable: true,
50
+ writable: false,
51
+ });
52
+ return err;
53
+ }
54
+ /**
55
+ * True when `err` carries the {@link PAYLOAD_SHREDDED_BRAND} as an OWN property (an inherited brand,
56
+ * e.g. from a shared prototype, does not count). The ONE definition every consumer uses.
57
+ * A plain object or `Error` that only has `name === "PayloadShreddedError"` is NOT shredded, and a
58
+ * value that crossed a process boundary (JSON) has lost the brand: such a boundary must re-establish
59
+ * trust explicitly (e.g. by checking the log's own `PAYLOAD_SHREDDED` tombstone).
60
+ */
61
+ function isPayloadShredded(err) {
62
+ return (typeof err === "object" &&
63
+ err !== null &&
64
+ Object.prototype.hasOwnProperty.call(err, exports.PAYLOAD_SHREDDED_BRAND) &&
65
+ err[exports.PAYLOAD_SHREDDED_BRAND] === true);
66
+ }
67
+ //# sourceMappingURL=payloadShredded.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payloadShredded.js","sourceRoot":"","sources":["../src/payloadShredded.ts"],"names":[],"mappings":";;;AAqBA,oDAyBC;AASD,8CAOC;AA9DD;;;;;;;;;GASG;AACU,QAAA,sBAAsB,GAAkB,MAAM,CAAC,GAAG,CAAC,sBAAsB,CAAU,CAAC;AAEjG;;;;;;;;GAQG;AACH,SAAgB,oBAAoB,CAAmB,GAAM;IAC3D,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC;QAC9B,MAAM,GAAG,GAAG,GAAyC,CAAC;QACtD,MAAM,OAAO,GAAG,IAAI,KAAK,CAAC,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;QAC9F,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,mBAAmB,CAAC,GAAG,CAAC,EAAE,CAAC;YAClD,IAAI,GAAG,KAAK,OAAO;gBAAE,SAAS;YAC9B,MAAM,CAAC,GAAG,MAAM,CAAC,wBAAwB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YACpD,IAAI,CAAC,KAAK,SAAS;gBAAE,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,GAAG,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;QACzF,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACvF,MAAM,CAAC,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE;gBACrC,KAAK,EAAE,GAAG,CAAC,IAAI;gBACf,YAAY,EAAE,IAAI;gBAClB,QAAQ,EAAE,IAAI;aACf,CAAC,CAAC;QACL,CAAC;QACD,OAAO,oBAAoB,CAAC,OAAO,CAAiB,CAAC;IACvD,CAAC;IACD,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,8BAAsB,EAAE;QACjD,KAAK,EAAE,IAAI;QACX,UAAU,EAAE,KAAK;QACjB,YAAY,EAAE,IAAI;QAClB,QAAQ,EAAE,KAAK;KAChB,CAAC,CAAC;IACH,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,iBAAiB,CAAC,GAAY;IAC5C,OAAO,CACL,OAAO,GAAG,KAAK,QAAQ;QACvB,GAAG,KAAK,IAAI;QACZ,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,GAAG,EAAE,8BAAsB,CAAC;QAChE,GAA+B,CAAC,8BAAsB,CAAC,KAAK,IAAI,CAClE,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=scaleDecorators.bench.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaleDecorators.bench.d.ts","sourceRoot":"","sources":["../src/scaleDecorators.bench.ts"],"names":[],"mappings":""}
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const CoalescingEventStore_1 = require("./CoalescingEventStore");
5
+ const InMemoryAsyncEventStore_1 = require("./InMemoryAsyncEventStore");
6
+ const ShardedEventStore_1 = require("./ShardedEventStore");
7
+ // ── What the scale-out decorators cost and buy (ADR 0074) ────────────────────
8
+ //
9
+ // `scaleDecorators.test.ts` already proves round-trips are SAVED — it counts them. What a
10
+ // count cannot say is what that is worth, because the answer depends entirely on what one
11
+ // round-trip costs: over a free in-memory store, coalescing is pure overhead, and over a
12
+ // network backend it is the difference between 18k and 78k events/sec (`docs/benchmarks.md`,
13
+ // measured against real Postgres).
14
+ //
15
+ // So this bench prices a round-trip EXPLICITLY, spinning rather than sleeping — a timer
16
+ // would put scheduler granularity into a measurement whose whole point is the number of
17
+ // calls. `ROUND_TRIP_US` is the one assumption; every result below is a function of it, and
18
+ // a reader who thinks their backend is cheaper or dearer can move it and re-run.
19
+ //
20
+ // The second half is the honest counterweight: what the decorators cost when there is
21
+ // nothing to save. A decorator that is free on a free backend is a decorator that can be
22
+ // left in place.
23
+ const ROUND_TRIP_US = 20;
24
+ const evt = (executionId, sequence) => ({
25
+ id: `${executionId}-${sequence}`,
26
+ workflowId: "wf",
27
+ executionId,
28
+ eventType: "STEP_COMPLETED",
29
+ sequence,
30
+ timestamp: 1000 + sequence,
31
+ payload: { sequence },
32
+ });
33
+ /** Burn `us` microseconds of CPU. Deterministic; no timer, no scheduler. */
34
+ function spin(us) {
35
+ const until = performance.now() + us / 1000;
36
+ while (performance.now() < until) {
37
+ /* deliberately empty */
38
+ }
39
+ }
40
+ /**
41
+ * A backend that charges `ROUND_TRIP_US` per CALL, not per envelope — which is what a
42
+ * network backend does, and the entire reason batching exists. It keeps nothing: the
43
+ * measurement is the round-trips, not the storage.
44
+ */
45
+ function pricedStore() {
46
+ return {
47
+ append: async () => {
48
+ spin(ROUND_TRIP_US);
49
+ },
50
+ appendBatch: async () => {
51
+ spin(ROUND_TRIP_US);
52
+ },
53
+ read: async () => [],
54
+ readFrom: async () => [],
55
+ subscribe: async () => () => { },
56
+ close: async () => { },
57
+ };
58
+ }
59
+ /** One envelope from each of N distinct executions, appended concurrently — the real shape:
60
+ * N journals, each holding a single pending envelope, all flushing into one turn. */
61
+ const N = 512;
62
+ const wave = Array.from({ length: N }, (_, i) => evt(`x${i}`, 0));
63
+ async function appendAll(store) {
64
+ await Promise.all(wave.map((e) => store.append(e)));
65
+ }
66
+ (0, vitest_1.describe)(`${N} cross-execution appends, one round-trip priced at ${ROUND_TRIP_US}µs`, () => {
67
+ (0, vitest_1.bench)("per-event append (no decorator)", async () => {
68
+ await appendAll(pricedStore());
69
+ });
70
+ (0, vitest_1.bench)("CoalescingEventStore (default maxBatch: 16)", async () => {
71
+ await appendAll(new CoalescingEventStore_1.CoalescingEventStore(pricedStore(), { maxBatch: 16 }));
72
+ });
73
+ (0, vitest_1.bench)("CoalescingEventStore (maxBatch: 64)", async () => {
74
+ await appendAll(new CoalescingEventStore_1.CoalescingEventStore(pricedStore(), { maxBatch: 64 }));
75
+ });
76
+ });
77
+ (0, vitest_1.describe)(`${N} cross-execution appends against a FREE backend (what the decorators cost)`, () => {
78
+ // The counterweight. Over an in-memory store there are no round-trips worth saving, so
79
+ // these numbers are the price of leaving a decorator wired where it buys nothing.
80
+ (0, vitest_1.bench)("InMemoryAsyncEventStore (no decorator)", async () => {
81
+ await appendAll(new InMemoryAsyncEventStore_1.InMemoryAsyncEventStore());
82
+ });
83
+ (0, vitest_1.bench)("CoalescingEventStore over in-memory", async () => {
84
+ await appendAll(new CoalescingEventStore_1.CoalescingEventStore(new InMemoryAsyncEventStore_1.InMemoryAsyncEventStore(), { maxBatch: 16 }));
85
+ });
86
+ (0, vitest_1.bench)("ShardedEventStore over 4 in-memory shards", async () => {
87
+ // Routing only — hash the id, pick a shard, delegate. What sharding actually buys is
88
+ // parallelism across backends, which a single-process bench cannot show and this
89
+ // deliberately does not claim.
90
+ await appendAll(new ShardedEventStore_1.ShardedEventStore(Array.from({ length: 4 }, () => new InMemoryAsyncEventStore_1.InMemoryAsyncEventStore())));
91
+ });
92
+ });
93
+ //# sourceMappingURL=scaleDecorators.bench.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaleDecorators.bench.js","sourceRoot":"","sources":["../src/scaleDecorators.bench.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=sequenceRange.bench.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sequenceRange.bench.d.ts","sourceRoot":"","sources":["../src/sequenceRange.bench.ts"],"names":[],"mappings":""}
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const InMemoryAsyncEventStore_1 = require("./InMemoryAsyncEventStore");
5
+ const sequenceRange_1 = require("./sequenceRange");
6
+ // The fork/materialize hot path: extract a bounded [from..to] window from a long,
7
+ // sequence-sorted log. The baselines are the previous O(n) shapes (a `.filter`, and
8
+ // read()+sort()+filter) so the binary-search win is visible on a large history.
9
+ function ev(sequence) {
10
+ return {
11
+ id: `e-${sequence}`,
12
+ workflowId: "wf",
13
+ executionId: "x",
14
+ eventType: "STEP_STARTED",
15
+ sequence,
16
+ timestamp: sequence,
17
+ payload: {},
18
+ };
19
+ }
20
+ const N = 100_000;
21
+ const events = Array.from({ length: N }, (_, i) => ev(i));
22
+ // Fork early in the history — the pathological case for a full scan.
23
+ const FROM = 100;
24
+ const TO = 200;
25
+ (0, vitest_1.describe)("bounded range over a 100k-event log", () => {
26
+ (0, vitest_1.bench)("filter baseline (previous shape)", () => {
27
+ events.filter((e) => e.sequence >= FROM && e.sequence <= TO);
28
+ });
29
+ (0, vitest_1.bench)("sliceBySequence (binary search)", () => {
30
+ (0, sequenceRange_1.sliceBySequence)(events, FROM, TO);
31
+ });
32
+ });
33
+ (0, vitest_1.describe)("InMemoryAsyncEventStore range read (100k events)", () => {
34
+ const store = new InMemoryAsyncEventStore_1.InMemoryAsyncEventStore();
35
+ // Prime the store once outside the measured loop.
36
+ for (let i = 0; i < N; i++)
37
+ void store.append(ev(i));
38
+ (0, vitest_1.bench)("read()+sort()+filter baseline (previous shape)", async () => {
39
+ const all = [...(await store.read("x"))].sort((a, b) => a.sequence - b.sequence);
40
+ all.filter((e) => e.sequence >= FROM && e.sequence <= TO);
41
+ });
42
+ (0, vitest_1.bench)("readRange (binary search, no sort)", async () => {
43
+ await store.readRange("x", FROM, TO);
44
+ });
45
+ });
46
+ //# sourceMappingURL=sequenceRange.bench.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sequenceRange.bench.js","sourceRoot":"","sources":["../src/sequenceRange.bench.ts"],"names":[],"mappings":";;AACA,mCAAyC;AACzC,uEAAoE;AACpE,mDAAkD;AAElD,kFAAkF;AAClF,oFAAoF;AACpF,gFAAgF;AAEhF,SAAS,EAAE,CAAC,QAAgB;IAC1B,OAAO;QACL,EAAE,EAAE,KAAK,QAAQ,EAAE;QACnB,UAAU,EAAE,IAAI;QAChB,WAAW,EAAE,GAAG;QAChB,SAAS,EAAE,cAAc;QACzB,QAAQ;QACR,SAAS,EAAE,QAAQ;QACnB,OAAO,EAAE,EAAE;KACZ,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,GAAG,OAAO,CAAC;AAClB,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AAC1D,qEAAqE;AACrE,MAAM,IAAI,GAAG,GAAG,CAAC;AACjB,MAAM,EAAE,GAAG,GAAG,CAAC;AAEf,IAAA,iBAAQ,EAAC,qCAAqC,EAAE,GAAG,EAAE;IACnD,IAAA,cAAK,EAAC,kCAAkC,EAAE,GAAG,EAAE;QAC7C,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,IAAI,IAAI,IAAI,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;IAC/D,CAAC,CAAC,CAAC;IAEH,IAAA,cAAK,EAAC,iCAAiC,EAAE,GAAG,EAAE;QAC5C,IAAA,+BAAe,EAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAA,iBAAQ,EAAC,kDAAkD,EAAE,GAAG,EAAE;IAChE,MAAM,KAAK,GAAG,IAAI,iDAAuB,EAAE,CAAC;IAC5C,kDAAkD;IAClD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,KAAK,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAErD,IAAA,cAAK,EAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;QACjE,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAC;QACjF,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,IAAI,IAAI,IAAI,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;IAC5D,CAAC,CAAC,CAAC;IAEH,IAAA,cAAK,EAAC,oCAAoC,EAAE,KAAK,IAAI,EAAE;QACrD,MAAM,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { EventEnvelope } from "@loomwork/core";
2
+ /** First index whose event.sequence >= `seq` (i.e. events.length if none). */
3
+ export declare function lowerBoundBySequence(events: readonly EventEnvelope[], seq: number): number;
4
+ /**
5
+ * Events with `fromSequence <= sequence <= toSequence`, assuming `events` is
6
+ * sorted ascending by sequence. O(log n) to locate the bounds + O(k) to copy the
7
+ * k in-range events — no full scan.
8
+ */
9
+ export declare function sliceBySequence(events: readonly EventEnvelope[], fromSequence: number, toSequence: number): EventEnvelope[];
10
+ //# sourceMappingURL=sequenceRange.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sequenceRange.d.ts","sourceRoot":"","sources":["../src/sequenceRange.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAQpD,8EAA8E;AAC9E,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAS1F;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,SAAS,aAAa,EAAE,EAChC,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,GACjB,aAAa,EAAE,CAKjB"}
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.lowerBoundBySequence = lowerBoundBySequence;
4
+ exports.sliceBySequence = sliceBySequence;
5
+ // Binary-search helpers over an event array that is sorted ascending by
6
+ // `sequence` — the invariant every EventStore preserves (the per-execution
7
+ // sequencer assigns strictly increasing sequences, and `copyPrefix` copies in
8
+ // order). They turn range reads on an in-memory log from O(n) filters into
9
+ // O(log n + k) slices, and back the workflow-runtime fork/materialize path.
10
+ /** First index whose event.sequence >= `seq` (i.e. events.length if none). */
11
+ function lowerBoundBySequence(events, seq) {
12
+ let lo = 0;
13
+ let hi = events.length;
14
+ while (lo < hi) {
15
+ const mid = (lo + hi) >> 1;
16
+ if (events[mid].sequence < seq)
17
+ lo = mid + 1;
18
+ else
19
+ hi = mid;
20
+ }
21
+ return lo;
22
+ }
23
+ /**
24
+ * Events with `fromSequence <= sequence <= toSequence`, assuming `events` is
25
+ * sorted ascending by sequence. O(log n) to locate the bounds + O(k) to copy the
26
+ * k in-range events — no full scan.
27
+ */
28
+ function sliceBySequence(events, fromSequence, toSequence) {
29
+ if (toSequence < fromSequence)
30
+ return [];
31
+ const start = lowerBoundBySequence(events, fromSequence);
32
+ const end = lowerBoundBySequence(events, toSequence + 1);
33
+ return events.slice(start, end);
34
+ }
35
+ //# sourceMappingURL=sequenceRange.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sequenceRange.js","sourceRoot":"","sources":["../src/sequenceRange.ts"],"names":[],"mappings":";;AASA,oDASC;AAOD,0CASC;AAhCD,wEAAwE;AACxE,2EAA2E;AAC3E,8EAA8E;AAC9E,2EAA2E;AAC3E,4EAA4E;AAE5E,8EAA8E;AAC9E,SAAgB,oBAAoB,CAAC,MAAgC,EAAE,GAAW;IAChF,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,IAAI,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC;IACvB,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC;QACf,MAAM,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAK,MAAM,CAAC,GAAG,CAAmB,CAAC,QAAQ,GAAG,GAAG;YAAE,EAAE,GAAG,GAAG,GAAG,CAAC,CAAC;;YAC3D,EAAE,GAAG,GAAG,CAAC;IAChB,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED;;;;GAIG;AACH,SAAgB,eAAe,CAC7B,MAAgC,EAChC,YAAoB,EACpB,UAAkB;IAElB,IAAI,UAAU,GAAG,YAAY;QAAE,OAAO,EAAE,CAAC;IACzC,MAAM,KAAK,GAAG,oBAAoB,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACzD,MAAM,GAAG,GAAG,oBAAoB,CAAC,MAAM,EAAE,UAAU,GAAG,CAAC,CAAC,CAAC;IACzD,OAAO,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAClC,CAAC"}
@@ -0,0 +1,87 @@
1
+ export interface TimerRecord {
2
+ executionId: string;
3
+ /** Absolute wake time (epoch ms) — matches the logged `condition.wakeAt`. */
4
+ wakeAt: number;
5
+ /**
6
+ * ADR 0118 D4 — marks a RE-DRIVE row: a backed-off retry of a resume that hit `contended`
7
+ * (`attempt` counts the tries so far), not the wake of a timed suspend. `TimerService.fire` does
8
+ * NOT judge it by `suspendedOn.wakeAt` (which would retire it as obsolete); it hands it to the
9
+ * registered re-drivers. Optional and additive: a plain wake carries none.
10
+ *
11
+ * `schedule` UPSERTS it: re-scheduling an existing `(executionId, wakeAt)` never clears a flag
12
+ * already set (a redrive row is never downgraded to a plain wake), and a record that carries one
13
+ * sets it on a row that had none. A store must keep a `redrive` row distinguishable from a plain
14
+ * one through `due`.
15
+ */
16
+ redrive?: {
17
+ attempt: number;
18
+ };
19
+ /**
20
+ * ADR 0130 D10 - marks a REPLAY-REFUSAL row: a node that could not open a sealed recorded answer
21
+ * refused to run a `running` execution and left this row so a node that CAN picks it up. `head` is
22
+ * the execution's log head at the refusal; the timer poll re-drives only while the log still sits
23
+ * there. `wakeAt` is an ordinary instant (when the poll should look next), never an encoded stamp.
24
+ * Optional and additive: a plain wake carries none. A node that predates the field reads the row
25
+ * as a plain wake it can never match to a park and retires it as obsolete (fail safe: never re-driven).
26
+ *
27
+ * `holds` (optional, absent = 0) counts how many times a poller has HELD the row (rescheduled it
28
+ * to a later instant because it could not act yet). It is part of the row's own data so the next
29
+ * instant is a DETERMINISTIC function of the row (`wakeAt` + back-off(holds + 1), no node-local
30
+ * jitter): N pollers that read the same row all write the SAME successor key, so concurrent
31
+ * holders converge on one row instead of multiplying it.
32
+ *
33
+ * `schedule` UPSERTS it like `redrive`: re-scheduling an existing `(executionId, wakeAt)` never
34
+ * clears a stamp already set, and a record that carries one sets it. On a same-key upsert the
35
+ * stored stamp keeps the MAX `head` and the MAX `holds` of the two (never last-writer-wins: two
36
+ * nodes writing in the same millisecond must not move a head backwards). A store must keep a
37
+ * `refused` row distinguishable from a plain one through `due`.
38
+ */
39
+ refused?: {
40
+ head: number;
41
+ holds?: number;
42
+ };
43
+ }
44
+ export interface TimerStore {
45
+ /** Idempotent per (executionId, wakeAt): re-scheduling the same wake is a no-op. */
46
+ schedule(timer: TimerRecord): Promise<void>;
47
+ /**
48
+ * Timers with `wakeAt <= now`, ordered soonest-first.
49
+ *
50
+ * `options.redrive === false` (ADR 0118 D4) EXCLUDES re-drive rows BEFORE the limit applies: a
51
+ * node with no re-driver registered would otherwise see a batch full of rows it cannot judge and
52
+ * starve every plain wake behind them. A store that ignores the option is still correct (the
53
+ * service skips such rows itself); it only keeps that starvation window.
54
+ *
55
+ * `options.inbox === false` (ADR 0110) likewise EXCLUDES the inbox wake row (a NON-re-drive row at `wakeAt === -1`,
56
+ * `INBOX_WAKE_AT`) before the limit: a node with the inbox flag off cannot run the notify path
57
+ * and must leave those rows for a node that can, but they sort first (instant -1), so a batch of
58
+ * them would starve every real timer behind it. The sentinel is -1, an instant no timed
59
+ * park uses, so a flag-off node still fires every real timer (including `ctx.sleep(0)` at clock 0).
60
+ *
61
+ * `options.refused === false` (ADR 0130 D10) EXCLUDES replay-refusal rows BEFORE the limit: a node
62
+ * with no abandonment evidence configured (or no inbox) cannot act on them, and must not fetch
63
+ * - let alone read the execution of - one per row. A store that ignores the option is still
64
+ * correct (the service skips such rows itself); it only keeps that starvation window.
65
+ */
66
+ due(now: number, limit?: number, options?: {
67
+ redrive?: boolean;
68
+ inbox?: boolean;
69
+ refused?: boolean;
70
+ }): Promise<TimerRecord[]>;
71
+ /** Remove a fired (or obsolete) timer. Unknown timers are a no-op. */
72
+ complete(executionId: string, wakeAt: number): Promise<void>;
73
+ close(): Promise<void>;
74
+ }
75
+ export declare class InMemoryTimerStore implements TimerStore {
76
+ private readonly timers;
77
+ private key;
78
+ schedule(timer: TimerRecord): Promise<void>;
79
+ due(now: number, limit?: number, options?: {
80
+ redrive?: boolean;
81
+ inbox?: boolean;
82
+ refused?: boolean;
83
+ }): Promise<TimerRecord[]>;
84
+ complete(executionId: string, wakeAt: number): Promise<void>;
85
+ close(): Promise<void>;
86
+ }
87
+ //# sourceMappingURL=timers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timers.d.ts","sourceRoot":"","sources":["../src/timers.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;;OAUG;IACH,OAAO,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5C;AAED,MAAM,WAAW,UAAU;IACzB,oFAAoF;IACpF,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C;;;;;;;;;;;;;;;;;;OAkBG;IACH,GAAG,CACD,GAAG,EAAE,MAAM,EACX,KAAK,CAAC,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAClE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC1B,sEAAsE;IACtE,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAiBD,qBAAa,kBAAmB,YAAW,UAAU;IACnD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkC;IAEzD,OAAO,CAAC,GAAG;IAIL,QAAQ,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAc3C,GAAG,CACP,GAAG,EAAE,MAAM,EACX,KAAK,SAAM,EACX,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAClE,OAAO,CAAC,WAAW,EAAE,CAAC;IAkBnB,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}