@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.
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/src/timers.ts ADDED
@@ -0,0 +1,150 @@
1
+ // ── T1 — durable timer storage ────────────────────────────────────────────────
2
+ //
3
+ // A timer is the durable half of `ctx.sleep`/`ctx.sleepUntil` (and of
4
+ // `suspend({ timeout })` when a timer service is wired): the execution suspends
5
+ // with `wakeAt` recorded in its EXECUTION_SUSPENDED condition, and a timer row
6
+ // makes the wake FINDABLE — a poller can enumerate due timers without scanning
7
+ // every execution's log. Timer storage lives in the same store family as events
8
+ // (T1.3), so one backup captures the full kernel state.
9
+ //
10
+ // Firing is at-least-once: the service resumes the execution, then completes the
11
+ // timer. A crash in between re-delivers, and the service's suspended-on-this-
12
+ // wake guard makes the duplicate a no-op (idempotent wakes).
13
+
14
+ export interface TimerRecord {
15
+ executionId: string;
16
+ /** Absolute wake time (epoch ms) — matches the logged `condition.wakeAt`. */
17
+ wakeAt: number;
18
+ /**
19
+ * ADR 0118 D4 — marks a RE-DRIVE row: a backed-off retry of a resume that hit `contended`
20
+ * (`attempt` counts the tries so far), not the wake of a timed suspend. `TimerService.fire` does
21
+ * NOT judge it by `suspendedOn.wakeAt` (which would retire it as obsolete); it hands it to the
22
+ * registered re-drivers. Optional and additive: a plain wake carries none.
23
+ *
24
+ * `schedule` UPSERTS it: re-scheduling an existing `(executionId, wakeAt)` never clears a flag
25
+ * already set (a redrive row is never downgraded to a plain wake), and a record that carries one
26
+ * sets it on a row that had none. A store must keep a `redrive` row distinguishable from a plain
27
+ * one through `due`.
28
+ */
29
+ redrive?: { attempt: number };
30
+ /**
31
+ * ADR 0130 D10 - marks a REPLAY-REFUSAL row: a node that could not open a sealed recorded answer
32
+ * refused to run a `running` execution and left this row so a node that CAN picks it up. `head` is
33
+ * the execution's log head at the refusal; the timer poll re-drives only while the log still sits
34
+ * there. `wakeAt` is an ordinary instant (when the poll should look next), never an encoded stamp.
35
+ * Optional and additive: a plain wake carries none. A node that predates the field reads the row
36
+ * as a plain wake it can never match to a park and retires it as obsolete (fail safe: never re-driven).
37
+ *
38
+ * `holds` (optional, absent = 0) counts how many times a poller has HELD the row (rescheduled it
39
+ * to a later instant because it could not act yet). It is part of the row's own data so the next
40
+ * instant is a DETERMINISTIC function of the row (`wakeAt` + back-off(holds + 1), no node-local
41
+ * jitter): N pollers that read the same row all write the SAME successor key, so concurrent
42
+ * holders converge on one row instead of multiplying it.
43
+ *
44
+ * `schedule` UPSERTS it like `redrive`: re-scheduling an existing `(executionId, wakeAt)` never
45
+ * clears a stamp already set, and a record that carries one sets it. On a same-key upsert the
46
+ * stored stamp keeps the MAX `head` and the MAX `holds` of the two (never last-writer-wins: two
47
+ * nodes writing in the same millisecond must not move a head backwards). A store must keep a
48
+ * `refused` row distinguishable from a plain one through `due`.
49
+ */
50
+ refused?: { head: number; holds?: number };
51
+ }
52
+
53
+ export interface TimerStore {
54
+ /** Idempotent per (executionId, wakeAt): re-scheduling the same wake is a no-op. */
55
+ schedule(timer: TimerRecord): Promise<void>;
56
+ /**
57
+ * Timers with `wakeAt <= now`, ordered soonest-first.
58
+ *
59
+ * `options.redrive === false` (ADR 0118 D4) EXCLUDES re-drive rows BEFORE the limit applies: a
60
+ * node with no re-driver registered would otherwise see a batch full of rows it cannot judge and
61
+ * starve every plain wake behind them. A store that ignores the option is still correct (the
62
+ * service skips such rows itself); it only keeps that starvation window.
63
+ *
64
+ * `options.inbox === false` (ADR 0110) likewise EXCLUDES the inbox wake row (a NON-re-drive row at `wakeAt === -1`,
65
+ * `INBOX_WAKE_AT`) before the limit: a node with the inbox flag off cannot run the notify path
66
+ * and must leave those rows for a node that can, but they sort first (instant -1), so a batch of
67
+ * them would starve every real timer behind it. The sentinel is -1, an instant no timed
68
+ * park uses, so a flag-off node still fires every real timer (including `ctx.sleep(0)` at clock 0).
69
+ *
70
+ * `options.refused === false` (ADR 0130 D10) EXCLUDES replay-refusal rows BEFORE the limit: a node
71
+ * with no abandonment evidence configured (or no inbox) cannot act on them, and must not fetch
72
+ * - let alone read the execution of - one per row. A store that ignores the option is still
73
+ * correct (the service skips such rows itself); it only keeps that starvation window.
74
+ */
75
+ due(
76
+ now: number,
77
+ limit?: number,
78
+ options?: { redrive?: boolean; inbox?: boolean; refused?: boolean }
79
+ ): Promise<TimerRecord[]>;
80
+ /** Remove a fired (or obsolete) timer. Unknown timers are a no-op. */
81
+ complete(executionId: string, wakeAt: number): Promise<void>;
82
+ close(): Promise<void>;
83
+ }
84
+
85
+ /** Reference in-memory {@link TimerStore} (tests, ephemeral apps). */
86
+ /** Same-key upsert of a refusal stamp: the MAX head and the MAX holds (absent holds stays absent). */
87
+ function mergeRefused(
88
+ prior: TimerRecord["refused"],
89
+ next: TimerRecord["refused"]
90
+ ): TimerRecord["refused"] {
91
+ if (next === undefined) return prior;
92
+ if (prior === undefined) return next;
93
+ const holds =
94
+ prior.holds === undefined && next.holds === undefined
95
+ ? undefined
96
+ : Math.max(prior.holds ?? 0, next.holds ?? 0);
97
+ return { head: Math.max(prior.head, next.head), ...(holds !== undefined && { holds }) };
98
+ }
99
+
100
+ export class InMemoryTimerStore implements TimerStore {
101
+ private readonly timers = new Map<string, TimerRecord>();
102
+
103
+ private key(executionId: string, wakeAt: number): string {
104
+ return `${executionId}@${wakeAt}`;
105
+ }
106
+
107
+ async schedule(timer: TimerRecord): Promise<void> {
108
+ const key = this.key(timer.executionId, timer.wakeAt);
109
+ // Upsert (ADR 0118 D4): a flagged row is never downgraded by a plain schedule.
110
+ const redrive = timer.redrive ?? this.timers.get(key)?.redrive;
111
+ const prior = this.timers.get(key)?.refused;
112
+ const refused = mergeRefused(prior, timer.refused);
113
+ this.timers.set(key, {
114
+ executionId: timer.executionId,
115
+ wakeAt: timer.wakeAt,
116
+ ...(redrive !== undefined && { redrive: { ...redrive } }),
117
+ ...(refused !== undefined && { refused: { ...refused } }),
118
+ });
119
+ }
120
+
121
+ async due(
122
+ now: number,
123
+ limit = 100,
124
+ options?: { redrive?: boolean; inbox?: boolean; refused?: boolean }
125
+ ): Promise<TimerRecord[]> {
126
+ return [...this.timers.values()]
127
+ .filter(
128
+ (t) =>
129
+ t.wakeAt <= now &&
130
+ (options?.redrive !== false || t.redrive === undefined) &&
131
+ (options?.refused !== false || t.refused === undefined) &&
132
+ (options?.inbox !== false || t.wakeAt !== -1 || t.redrive !== undefined)
133
+ )
134
+ .sort((a, b) => a.wakeAt - b.wakeAt)
135
+ .slice(0, limit)
136
+ .map((t) => ({
137
+ ...t,
138
+ ...(t.redrive !== undefined && { redrive: { ...t.redrive } }),
139
+ ...(t.refused !== undefined && { refused: { ...t.refused } }),
140
+ }));
141
+ }
142
+
143
+ async complete(executionId: string, wakeAt: number): Promise<void> {
144
+ this.timers.delete(this.key(executionId, wakeAt));
145
+ }
146
+
147
+ async close(): Promise<void> {
148
+ this.timers.clear();
149
+ }
150
+ }