@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.
- 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
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
|
+
}
|