@opinionated-machine/sse-fallback 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 +21 -0
- package/README.md +365 -0
- package/dist/binding.d.ts +82 -0
- package/dist/binding.d.ts.map +1 -0
- package/dist/binding.js +181 -0
- package/dist/binding.js.map +1 -0
- package/dist/bindingTypes.d.ts +327 -0
- package/dist/bindingTypes.d.ts.map +1 -0
- package/dist/bindingTypes.js +43 -0
- package/dist/bindingTypes.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/pollGate.d.ts +58 -0
- package/dist/pollGate.d.ts.map +1 -0
- package/dist/pollGate.js +64 -0
- package/dist/pollGate.js.map +1 -0
- package/dist/reconciler.d.ts +245 -0
- package/dist/reconciler.d.ts.map +1 -0
- package/dist/reconciler.js +568 -0
- package/dist/reconciler.js.map +1 -0
- package/dist/scheduler.d.ts +19 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +51 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/subscription.d.ts +164 -0
- package/dist/subscription.d.ts.map +1 -0
- package/dist/subscription.js +878 -0
- package/dist/subscription.js.map +1 -0
- package/dist/transport.d.ts +177 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +168 -0
- package/dist/transport.js.map +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
export class Reconciler {
|
|
2
|
+
config;
|
|
3
|
+
snapshotToEvents;
|
|
4
|
+
terminalSet;
|
|
5
|
+
highWatermark = null;
|
|
6
|
+
hydrationBuffer = null;
|
|
7
|
+
hydrationBufferLimit;
|
|
8
|
+
stateValue;
|
|
9
|
+
stateInitialized = false;
|
|
10
|
+
stateSuspended = false;
|
|
11
|
+
/**
|
|
12
|
+
* Events delivered while the state layer is gap-suspended, in arrival
|
|
13
|
+
* order, so the repair snapshot can re-apply the ones it does not cover.
|
|
14
|
+
*/
|
|
15
|
+
stateReplayBuffer = [];
|
|
16
|
+
/**
|
|
17
|
+
* The replay buffer overflowed and was dropped. A below-watermark snapshot
|
|
18
|
+
* can no longer repair state without losing the events it does not cover,
|
|
19
|
+
* so the suspension holds until a snapshot reaches the watermark.
|
|
20
|
+
*/
|
|
21
|
+
stateReplayTruncated = false;
|
|
22
|
+
terminated = false;
|
|
23
|
+
onInvalidVersion;
|
|
24
|
+
constructor(config, options) {
|
|
25
|
+
this.config = config;
|
|
26
|
+
this.onInvalidVersion = options.onInvalidVersion;
|
|
27
|
+
const shorthandEvent = config.snapshotEvent;
|
|
28
|
+
this.snapshotToEvents =
|
|
29
|
+
config.snapshotToEvents ??
|
|
30
|
+
(shorthandEvent !== undefined
|
|
31
|
+
? (snapshot) => [{ event: shorthandEvent, data: snapshot }]
|
|
32
|
+
: () => []);
|
|
33
|
+
this.terminalSet = new Set(config.terminalEvents ?? []);
|
|
34
|
+
this.hydrationBufferLimit = options.hydrationBufferLimit;
|
|
35
|
+
}
|
|
36
|
+
get isTerminated() {
|
|
37
|
+
return this.terminated;
|
|
38
|
+
}
|
|
39
|
+
get isHydrating() {
|
|
40
|
+
return this.hydrationBuffer !== null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether the state layer is suspended after a detected gap. `apply` stays
|
|
44
|
+
* disabled until a snapshot repairs state, so `getState()` is known-stale
|
|
45
|
+
* while this is true.
|
|
46
|
+
*/
|
|
47
|
+
get isStateSuspended() {
|
|
48
|
+
return this.stateSuspended;
|
|
49
|
+
}
|
|
50
|
+
getState() {
|
|
51
|
+
return this.stateValue;
|
|
52
|
+
}
|
|
53
|
+
/** Start buffering live events until the next snapshot arrives. */
|
|
54
|
+
beginHydration() {
|
|
55
|
+
if (this.terminated)
|
|
56
|
+
return;
|
|
57
|
+
this.hydrationBuffer = [];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Give up on subscribe-first hydration and resume direct delivery.
|
|
61
|
+
*
|
|
62
|
+
* The buffered events are FLUSHED, not discarded: hydration never completed,
|
|
63
|
+
* so no snapshot subsumes them and dropping them would lose exactly the
|
|
64
|
+
* events the buffer existed to protect. They pass through the version gate
|
|
65
|
+
* in arrival order, so a later snapshot still wins on version.
|
|
66
|
+
*/
|
|
67
|
+
abandonHydration() {
|
|
68
|
+
const outcome = {
|
|
69
|
+
deliveries: [],
|
|
70
|
+
stale: false,
|
|
71
|
+
advanced: false,
|
|
72
|
+
hydrationCompleted: false,
|
|
73
|
+
terminated: false,
|
|
74
|
+
stateSuspended: this.stateSuspended,
|
|
75
|
+
stateRepaired: false,
|
|
76
|
+
};
|
|
77
|
+
if (this.hydrationBuffer === null)
|
|
78
|
+
return outcome;
|
|
79
|
+
this.finishHydration(null, outcome);
|
|
80
|
+
return outcome;
|
|
81
|
+
}
|
|
82
|
+
handleEvent(incoming) {
|
|
83
|
+
const outcome = {
|
|
84
|
+
deliveries: [],
|
|
85
|
+
duplicate: false,
|
|
86
|
+
buffered: false,
|
|
87
|
+
bufferOverflow: false,
|
|
88
|
+
terminated: false,
|
|
89
|
+
stateSuspended: this.stateSuspended,
|
|
90
|
+
};
|
|
91
|
+
if (this.terminated)
|
|
92
|
+
return outcome;
|
|
93
|
+
if (this.hydrationBuffer !== null) {
|
|
94
|
+
if (this.hydrationBuffer.length >= this.hydrationBufferLimit) {
|
|
95
|
+
// Drop the buffer; the caller refetches the snapshot, which subsumes
|
|
96
|
+
// everything dropped (or is older than still-flowing live events,
|
|
97
|
+
// which the version gate handles either way).
|
|
98
|
+
this.hydrationBuffer = [];
|
|
99
|
+
outcome.bufferOverflow = true;
|
|
100
|
+
return outcome;
|
|
101
|
+
}
|
|
102
|
+
this.hydrationBuffer.push(incoming);
|
|
103
|
+
outcome.buffered = true;
|
|
104
|
+
return outcome;
|
|
105
|
+
}
|
|
106
|
+
this.gateAndDeliver(incoming, 'sse', outcome);
|
|
107
|
+
outcome.stateSuspended = this.stateSuspended;
|
|
108
|
+
return outcome;
|
|
109
|
+
}
|
|
110
|
+
handleSnapshot(snapshot) {
|
|
111
|
+
const outcome = {
|
|
112
|
+
deliveries: [],
|
|
113
|
+
stale: false,
|
|
114
|
+
advanced: false,
|
|
115
|
+
hydrationCompleted: false,
|
|
116
|
+
terminated: false,
|
|
117
|
+
stateSuspended: this.stateSuspended,
|
|
118
|
+
stateRepaired: false,
|
|
119
|
+
};
|
|
120
|
+
if (this.terminated)
|
|
121
|
+
return outcome;
|
|
122
|
+
const versioned = this.config.version !== 'none';
|
|
123
|
+
// A version this reconciler cannot order (a body missing the field, NaN,
|
|
124
|
+
// an empty string) is treated as no version at all rather than stored:
|
|
125
|
+
// an unorderable watermark compares as "not less than" against every
|
|
126
|
+
// later item, which would drop the whole stream as stale duplicates.
|
|
127
|
+
const snapshotVersion = versioned
|
|
128
|
+
? (this.normalizeVersion(this.config.version.ofSnapshot(snapshot), 'snapshot') ?? null)
|
|
129
|
+
: null;
|
|
130
|
+
// An epoch change re-scopes ordering, so counters on either side are not
|
|
131
|
+
// comparable and "below the watermark" means nothing: a snapshot from the
|
|
132
|
+
// new epoch is the resync, not a stale arrival. Without this a writer
|
|
133
|
+
// migrated to a lower epoch (the documented `createEventIdSequence` ->
|
|
134
|
+
// `createRedisEventIdSequence` move) would have every snapshot dropped.
|
|
135
|
+
const epochChanged = snapshotVersion !== null &&
|
|
136
|
+
this.highWatermark !== null &&
|
|
137
|
+
this.isEpochChange(this.highWatermark, snapshotVersion);
|
|
138
|
+
const watermarkComparison = !epochChanged && snapshotVersion !== null && this.highWatermark !== null
|
|
139
|
+
? this.compare(snapshotVersion, this.highWatermark)
|
|
140
|
+
: undefined;
|
|
141
|
+
if (snapshotVersion !== null && watermarkComparison !== undefined && watermarkComparison <= 0) {
|
|
142
|
+
return this.handleStaleSnapshot(snapshot, snapshotVersion, watermarkComparison, outcome);
|
|
143
|
+
}
|
|
144
|
+
// Deliver the snapshot: state layer first (replacement semantics), then
|
|
145
|
+
// synthesized events. Synthetic events are NOT applied to state — the
|
|
146
|
+
// snapshot already subsumes them.
|
|
147
|
+
if (this.config.state) {
|
|
148
|
+
this.stateValue = this.config.state.init(snapshot);
|
|
149
|
+
this.stateInitialized = true;
|
|
150
|
+
// This snapshot is at or above the watermark, so it covers every event
|
|
151
|
+
// delivered during a suspension: there is nothing left to replay.
|
|
152
|
+
this.stateSuspended = false;
|
|
153
|
+
this.stateReplayBuffer = [];
|
|
154
|
+
this.stateReplayTruncated = false;
|
|
155
|
+
outcome.state = { value: this.stateValue };
|
|
156
|
+
}
|
|
157
|
+
const synthetic = this.snapshotToEvents(snapshot);
|
|
158
|
+
for (const event of synthetic) {
|
|
159
|
+
const delivered = {
|
|
160
|
+
...event,
|
|
161
|
+
origin: 'poll',
|
|
162
|
+
};
|
|
163
|
+
outcome.deliveries.push(delivered);
|
|
164
|
+
if (this.terminalSet.has(event.event)) {
|
|
165
|
+
this.terminated = true;
|
|
166
|
+
outcome.terminated = true;
|
|
167
|
+
// Nothing is delivered after the terminal event — `finishHydration`
|
|
168
|
+
// and `handleEvent` apply the same rule.
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// An empty synthetic list still advances the watermark: the poll proved
|
|
173
|
+
// the state at snapshotVersion, so later stale arrivals must be droppable.
|
|
174
|
+
if (versioned && snapshotVersion !== null) {
|
|
175
|
+
this.highWatermark = snapshotVersion;
|
|
176
|
+
}
|
|
177
|
+
outcome.advanced = true;
|
|
178
|
+
this.finishHydration(snapshotVersion, outcome);
|
|
179
|
+
outcome.stateSuspended = this.stateSuspended;
|
|
180
|
+
return outcome;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The stale-poll race: everything this snapshot describes has already been
|
|
184
|
+
* delivered (or superseded) through the stream.
|
|
185
|
+
*
|
|
186
|
+
* One exception: a gap-suspended state layer is repaired by ANY snapshot,
|
|
187
|
+
* not only one whose version matches the watermark exactly. Live events keep
|
|
188
|
+
* advancing the watermark after a gap, so the repair snapshot this branch
|
|
189
|
+
* was polled for usually arrives strictly below it; requiring equality left
|
|
190
|
+
* `apply` disabled forever and froze `getState()` at its pre-gap value while
|
|
191
|
+
* events kept flowing. The events the snapshot does not cover are replayed
|
|
192
|
+
* onto it, so nothing delivered during the suspension is dropped from state.
|
|
193
|
+
* The watermark is NOT rewound, so nothing is re-delivered.
|
|
194
|
+
*
|
|
195
|
+
* With a dropped replay buffer that is impossible, so the suspension holds
|
|
196
|
+
* until a snapshot reaches the watermark, which covers everything delivered
|
|
197
|
+
* during it and needs no replay.
|
|
198
|
+
*/
|
|
199
|
+
handleStaleSnapshot(snapshot, snapshotVersion, watermarkComparison, outcome) {
|
|
200
|
+
outcome.stale = true;
|
|
201
|
+
const canRepair = !this.stateReplayTruncated || watermarkComparison === 0;
|
|
202
|
+
if (this.stateSuspended && this.config.state && canRepair) {
|
|
203
|
+
this.repairState(snapshot, snapshotVersion, outcome);
|
|
204
|
+
}
|
|
205
|
+
this.finishHydration(snapshotVersion, outcome);
|
|
206
|
+
outcome.stateSuspended = this.stateSuspended;
|
|
207
|
+
return outcome;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Re-initialize a gap-suspended state layer from a snapshot below the
|
|
211
|
+
* watermark, then re-apply the buffered events the snapshot does not cover.
|
|
212
|
+
*
|
|
213
|
+
* The repair snapshot is usually older than the watermark, because live
|
|
214
|
+
* events keep arriving while the repair poll is in flight. Without the
|
|
215
|
+
* replay those events would be missing from state permanently: delivered to
|
|
216
|
+
* listeners, skipped by `apply` while suspended, then overwritten by an
|
|
217
|
+
* `init` that predates them. The next event would apply to a state that
|
|
218
|
+
* never saw them.
|
|
219
|
+
*
|
|
220
|
+
* The cut is by arrival order, anchored on the last buffered event the
|
|
221
|
+
* snapshot demonstrably covers. Events after it are replayed even when they
|
|
222
|
+
* carry no orderable version, because arrival order is the only ordering
|
|
223
|
+
* they have.
|
|
224
|
+
*/
|
|
225
|
+
repairState(snapshot, snapshotVersion, outcome) {
|
|
226
|
+
const state = this.config.state;
|
|
227
|
+
if (!state)
|
|
228
|
+
return;
|
|
229
|
+
this.stateValue = state.init(snapshot);
|
|
230
|
+
this.stateInitialized = true;
|
|
231
|
+
this.stateSuspended = false;
|
|
232
|
+
const buffered = this.stateReplayBuffer;
|
|
233
|
+
this.stateReplayBuffer = [];
|
|
234
|
+
this.stateReplayTruncated = false;
|
|
235
|
+
let covered = -1;
|
|
236
|
+
for (const [index, entry] of buffered.entries()) {
|
|
237
|
+
if (entry.version !== undefined && this.compare(entry.version, snapshotVersion) <= 0) {
|
|
238
|
+
covered = index;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
for (const entry of buffered.slice(covered + 1)) {
|
|
242
|
+
this.stateValue = state.apply(this.stateValue, entry.delivered);
|
|
243
|
+
}
|
|
244
|
+
outcome.state = { value: this.stateValue };
|
|
245
|
+
outcome.stateRepaired = true;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Keep a delivered event for the repair snapshot to replay. Overflow drops
|
|
249
|
+
* the whole buffer rather than half of it: replaying a partial buffer would
|
|
250
|
+
* apply deltas across a hole, which is exactly what the suspension exists
|
|
251
|
+
* to prevent.
|
|
252
|
+
*/
|
|
253
|
+
recordSuspendedEvent(delivered, version) {
|
|
254
|
+
if (this.stateReplayTruncated)
|
|
255
|
+
return;
|
|
256
|
+
if (this.stateReplayBuffer.length >= this.hydrationBufferLimit) {
|
|
257
|
+
this.stateReplayBuffer = [];
|
|
258
|
+
this.stateReplayTruncated = true;
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.stateReplayBuffer.push({ delivered, version });
|
|
262
|
+
}
|
|
263
|
+
finishHydration(snapshotVersion, outcome) {
|
|
264
|
+
if (this.hydrationBuffer === null)
|
|
265
|
+
return;
|
|
266
|
+
const buffered = this.hydrationBuffer;
|
|
267
|
+
this.hydrationBuffer = null;
|
|
268
|
+
outcome.hydrationCompleted = true;
|
|
269
|
+
// Flush buffered live events newer than the snapshot, in arrival order.
|
|
270
|
+
// Everything at or below the snapshot version is subsumed by it.
|
|
271
|
+
for (const incoming of buffered) {
|
|
272
|
+
if (this.terminated)
|
|
273
|
+
break;
|
|
274
|
+
const eventOutcome = {
|
|
275
|
+
deliveries: [],
|
|
276
|
+
duplicate: false,
|
|
277
|
+
buffered: false,
|
|
278
|
+
bufferOverflow: false,
|
|
279
|
+
terminated: false,
|
|
280
|
+
stateSuspended: this.stateSuspended,
|
|
281
|
+
};
|
|
282
|
+
this.gateAndDeliver(incoming, 'sse', eventOutcome);
|
|
283
|
+
outcome.deliveries.push(...eventOutcome.deliveries);
|
|
284
|
+
if (eventOutcome.state)
|
|
285
|
+
outcome.state = eventOutcome.state;
|
|
286
|
+
// The first hole is enough: one repair poll covers every later one, and
|
|
287
|
+
// reporting the earliest keeps `from` at the last version actually
|
|
288
|
+
// delivered before the loss.
|
|
289
|
+
if (eventOutcome.gap && !outcome.gap)
|
|
290
|
+
outcome.gap = eventOutcome.gap;
|
|
291
|
+
if (eventOutcome.terminated)
|
|
292
|
+
outcome.terminated = true;
|
|
293
|
+
}
|
|
294
|
+
outcome.stateSuspended = this.stateSuspended;
|
|
295
|
+
// snapshotVersion is already the watermark, so gateAndDeliver dropped
|
|
296
|
+
// anything the snapshot subsumed. (Parameter kept for readability.)
|
|
297
|
+
void snapshotVersion;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Record a detected gap on the outcome and suspend the state layer, so the
|
|
301
|
+
* caller polls for a repair snapshot instead of applying deltas across a
|
|
302
|
+
* hole.
|
|
303
|
+
*/
|
|
304
|
+
registerGap(gap, outcome) {
|
|
305
|
+
outcome.gap = gap;
|
|
306
|
+
if (this.config.state && (this.config.state.reinitOnGap ?? true)) {
|
|
307
|
+
// Deltas must not be applied across a gap; the repair snapshot
|
|
308
|
+
// re-initializes state.
|
|
309
|
+
this.stateSuspended = true;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: the version gate is the correctness core — splitting it would scatter the invariants it guards
|
|
313
|
+
gateAndDeliver(incoming, origin, outcome) {
|
|
314
|
+
const versioned = this.config.version !== 'none';
|
|
315
|
+
let version;
|
|
316
|
+
if (versioned) {
|
|
317
|
+
version = this.extractEventVersion(incoming);
|
|
318
|
+
if (version !== undefined && this.highWatermark !== null) {
|
|
319
|
+
const gap = this.detectGap(this.highWatermark, version);
|
|
320
|
+
// An epoch change is resolved BEFORE the duplicate gate, because the
|
|
321
|
+
// two epochs are not comparable: a lower epoch is not an older event,
|
|
322
|
+
// it is a different ordering scope. Ranking it as a duplicate is how
|
|
323
|
+
// a writer that moved to a lower epoch (the documented
|
|
324
|
+
// `createEventIdSequence` -> `createRedisEventIdSequence` migration,
|
|
325
|
+
// whose DEFAULT_EPOCH is '0') wedged the subscription for good —
|
|
326
|
+
// every event sorted below a watermark it could never reach.
|
|
327
|
+
// Adopting the new epoch and reporting the gap turns that into what
|
|
328
|
+
// the docs promise: a resync poll. Two writers with different epochs
|
|
329
|
+
// broadcasting into one room (explicitly unsupported) now alternate
|
|
330
|
+
// gaps instead of silently dropping one writer's events; the repair
|
|
331
|
+
// polls are rate-limited by the deadman backoff and the poll gate.
|
|
332
|
+
if (gap?.reason === 'epoch-change' && this.ordersEpochs) {
|
|
333
|
+
this.registerGap(gap, outcome);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
const comparison = this.compare(version, this.highWatermark);
|
|
337
|
+
if (comparison <= 0) {
|
|
338
|
+
outcome.duplicate = true;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (gap)
|
|
342
|
+
this.registerGap(gap, outcome);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const delivered = {
|
|
347
|
+
event: incoming.event,
|
|
348
|
+
data: incoming.data,
|
|
349
|
+
...(incoming.id !== undefined ? { id: incoming.id } : {}),
|
|
350
|
+
origin,
|
|
351
|
+
};
|
|
352
|
+
outcome.deliveries.push(delivered);
|
|
353
|
+
if (this.config.state && this.stateInitialized && !this.stateSuspended) {
|
|
354
|
+
this.stateValue = this.config.state.apply(this.stateValue, delivered);
|
|
355
|
+
outcome.state = { value: this.stateValue };
|
|
356
|
+
}
|
|
357
|
+
else if (this.config.state && this.stateSuspended) {
|
|
358
|
+
this.recordSuspendedEvent(delivered, version);
|
|
359
|
+
}
|
|
360
|
+
if (versioned && version !== undefined) {
|
|
361
|
+
this.highWatermark = version;
|
|
362
|
+
}
|
|
363
|
+
if (this.terminalSet.has(incoming.event)) {
|
|
364
|
+
this.terminated = true;
|
|
365
|
+
outcome.terminated = true;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
extractEventVersion(incoming) {
|
|
369
|
+
const versionConfig = this.config.version;
|
|
370
|
+
if (versionConfig === 'none')
|
|
371
|
+
return undefined;
|
|
372
|
+
if (versionConfig.ofEvent) {
|
|
373
|
+
const extracted = versionConfig.ofEvent({
|
|
374
|
+
event: incoming.event,
|
|
375
|
+
data: incoming.data,
|
|
376
|
+
...(incoming.id !== undefined ? { id: incoming.id } : {}),
|
|
377
|
+
origin: 'sse',
|
|
378
|
+
});
|
|
379
|
+
// `undefined` is a documented answer here ("this event carries no
|
|
380
|
+
// version"), so it is not reported; anything else unorderable is.
|
|
381
|
+
return extracted === undefined ? undefined : this.normalizeVersion(extracted, 'event');
|
|
382
|
+
}
|
|
383
|
+
// Default: the SSE id, when it is a shape this package can order —
|
|
384
|
+
// a bare integer, or a `createEventIdSequence()` id.
|
|
385
|
+
if (incoming.id === undefined || incoming.id === '')
|
|
386
|
+
return undefined;
|
|
387
|
+
return parseDefaultVersion(incoming.id);
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* A version the gate can actually order, or `undefined`.
|
|
391
|
+
*
|
|
392
|
+
* Nothing in the type system stops an extractor from handing back
|
|
393
|
+
* `undefined` (a snapshot body whose version field is absent), `NaN`, an
|
|
394
|
+
* empty string or an object, and storing one as the watermark is the worst
|
|
395
|
+
* outcome the gate has: `defaultCompareVersions` falls through to a
|
|
396
|
+
* lexicographic comparison against `'undefined'`/`'NaN'`, every later item
|
|
397
|
+
* ranks at or below it, and the subscription drops everything as a
|
|
398
|
+
* duplicate — silently, forever. Dropping the version instead costs
|
|
399
|
+
* deduplication (at-least-once delivery) and keeps the stream flowing.
|
|
400
|
+
*/
|
|
401
|
+
normalizeVersion(value, source) {
|
|
402
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
403
|
+
return value;
|
|
404
|
+
if (typeof value === 'string' && value !== '')
|
|
405
|
+
return value;
|
|
406
|
+
this.onInvalidVersion?.({ source, value });
|
|
407
|
+
return undefined;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Whether the epoch bypass below applies. A binding that declares
|
|
411
|
+
* `version.compare` owns ordering end to end, including across epochs, so
|
|
412
|
+
* its verdict is never overridden here — the bypass exists to correct the
|
|
413
|
+
* DEFAULT comparator, which ranks by epoch and therefore reads a lowered
|
|
414
|
+
* epoch as an older version.
|
|
415
|
+
*/
|
|
416
|
+
get ordersEpochs() {
|
|
417
|
+
const versionConfig = this.config.version;
|
|
418
|
+
return versionConfig !== 'none' && versionConfig.compare === undefined;
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Whether two versions belong to different ordering scopes under the
|
|
422
|
+
* default comparator — see {@link ordersEpochs}.
|
|
423
|
+
*/
|
|
424
|
+
isEpochChange(a, b) {
|
|
425
|
+
return this.ordersEpochs && isEpochChange(a, b);
|
|
426
|
+
}
|
|
427
|
+
compare(a, b) {
|
|
428
|
+
const versionConfig = this.config.version;
|
|
429
|
+
if (versionConfig !== 'none' && versionConfig.compare) {
|
|
430
|
+
return versionConfig.compare(a, b);
|
|
431
|
+
}
|
|
432
|
+
return defaultCompareVersions(a, b);
|
|
433
|
+
}
|
|
434
|
+
detectGap(watermark, next) {
|
|
435
|
+
const versionConfig = this.config.version;
|
|
436
|
+
if (versionConfig === 'none')
|
|
437
|
+
return undefined;
|
|
438
|
+
const watermarkCounter = toCounter(watermark);
|
|
439
|
+
const nextCounter = toCounter(next);
|
|
440
|
+
if (watermarkCounter === undefined || nextCounter === undefined)
|
|
441
|
+
return undefined;
|
|
442
|
+
// Counters restart with a new epoch, so a cross-epoch step measures
|
|
443
|
+
// nothing — which makes it MORE of a resynchronization point, not less.
|
|
444
|
+
// Reporting it repairs delta state from a snapshot; staying silent would
|
|
445
|
+
// apply deltas across the restart and let a busy stream keep the deadman
|
|
446
|
+
// moving, so the repair never happens.
|
|
447
|
+
if (watermarkCounter.epoch !== nextCounter.epoch) {
|
|
448
|
+
return { from: watermark, to: next, reason: 'epoch-change' };
|
|
449
|
+
}
|
|
450
|
+
// A skipped counter is only measurable when the versions are dense.
|
|
451
|
+
if (!versionConfig.dense)
|
|
452
|
+
return undefined;
|
|
453
|
+
return nextCounter.counter > watermarkCounter.counter + 1n
|
|
454
|
+
? { from: watermark, to: next, reason: 'sequence' }
|
|
455
|
+
: undefined;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const INTEGER_STRING_PATTERN = /^-?\d+$/;
|
|
459
|
+
/**
|
|
460
|
+
* The numeric value of a version, or `undefined` when comparing it as a
|
|
461
|
+
* number would lose information.
|
|
462
|
+
*
|
|
463
|
+
* Integer strings beyond `Number.MAX_SAFE_INTEGER` are rejected here on
|
|
464
|
+
* purpose: `Number('9007199254740992')` and `Number('9007199254740993')`
|
|
465
|
+
* are the same double, so a numeric comparison would report the second event
|
|
466
|
+
* as a duplicate of the first and drop it. Rejecting them hands the pair to
|
|
467
|
+
* {@link compareSequence}, which compares the digits as `bigint`.
|
|
468
|
+
*/
|
|
469
|
+
function toNumeric(version) {
|
|
470
|
+
if (typeof version === 'number')
|
|
471
|
+
return version;
|
|
472
|
+
const numeric = Number(version);
|
|
473
|
+
if (Number.isNaN(numeric))
|
|
474
|
+
return undefined;
|
|
475
|
+
if (INTEGER_STRING_PATTERN.test(version) && !Number.isSafeInteger(numeric))
|
|
476
|
+
return undefined;
|
|
477
|
+
return numeric;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Ids produced by the server-side `createEventIdSequence()`:
|
|
481
|
+
* `"<numeric epoch>-<zero-padded counter>"`.
|
|
482
|
+
*
|
|
483
|
+
* The epoch is required to be numeric so that ids in unrelated shapes are NOT
|
|
484
|
+
* mistaken for versions. A UUID, for example, has several dashes and hex digits
|
|
485
|
+
* and cannot match — which matters, because treating one as a version would
|
|
486
|
+
* order events at random and silently drop them as duplicates. The server-side
|
|
487
|
+
* generators (`createEventIdSequence`, `formatEventId`,
|
|
488
|
+
* `createRedisEventIdSequence`) refuse a non-numeric epoch for exactly this
|
|
489
|
+
* reason, so every id they produce is one this extractor can order. Any other
|
|
490
|
+
* id scheme declares `version.ofEvent` explicitly.
|
|
491
|
+
*/
|
|
492
|
+
const SEQUENCE_ID_PATTERN = /^(\d+)-(\d+)$/;
|
|
493
|
+
function toCounter(version) {
|
|
494
|
+
if (typeof version === 'number') {
|
|
495
|
+
return Number.isInteger(version) ? { epoch: 0n, counter: BigInt(version) } : undefined;
|
|
496
|
+
}
|
|
497
|
+
const parsed = SEQUENCE_ID_PATTERN.exec(version);
|
|
498
|
+
if (parsed) {
|
|
499
|
+
return { epoch: BigInt(parsed[1]), counter: BigInt(parsed[2]) };
|
|
500
|
+
}
|
|
501
|
+
return /^\d+$/.test(version) ? { epoch: 0n, counter: BigInt(version) } : undefined;
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Whether two versions belong to different ordering scopes (a writer restart,
|
|
505
|
+
* or a move to a differently-seeded sequence). Counters on either side of an
|
|
506
|
+
* epoch change measure nothing against each other, so neither "newer" nor
|
|
507
|
+
* "older" is meaningful — only "resync".
|
|
508
|
+
*/
|
|
509
|
+
function isEpochChange(a, b) {
|
|
510
|
+
const left = toCounter(a);
|
|
511
|
+
const right = toCounter(b);
|
|
512
|
+
if (left === undefined || right === undefined)
|
|
513
|
+
return false;
|
|
514
|
+
return left.epoch !== right.epoch;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* The version a bare SSE `id:` carries under the default extractor: a bare
|
|
518
|
+
* integer becomes a number, a `createEventIdSequence()` id stays a string
|
|
519
|
+
* (ordered by {@link defaultCompareVersions}), anything else carries no
|
|
520
|
+
* version at all.
|
|
521
|
+
*/
|
|
522
|
+
export function parseDefaultVersion(id) {
|
|
523
|
+
if (/^\d+$/.test(id)) {
|
|
524
|
+
const numeric = Number(id);
|
|
525
|
+
return Number.isSafeInteger(numeric) ? numeric : id;
|
|
526
|
+
}
|
|
527
|
+
return SEQUENCE_ID_PATTERN.test(id) ? id : undefined;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Numeric when both sides are numeric, epoch-then-counter when both are
|
|
531
|
+
* `createEventIdSequence()` ids, lexicographic otherwise.
|
|
532
|
+
*
|
|
533
|
+
* Comparing sequence ids by their parsed parts rather than lexicographically
|
|
534
|
+
* keeps ordering correct when the counter outgrows its zero padding, and
|
|
535
|
+
* makes a new epoch (a process restart) sort above the old one, so the
|
|
536
|
+
* restarted counter does not read as a flood of duplicates.
|
|
537
|
+
*/
|
|
538
|
+
export function defaultCompareVersions(a, b) {
|
|
539
|
+
const numeric = compareNumeric(a, b);
|
|
540
|
+
if (numeric !== undefined)
|
|
541
|
+
return numeric;
|
|
542
|
+
const sequence = compareSequence(a, b);
|
|
543
|
+
if (sequence !== undefined)
|
|
544
|
+
return sequence;
|
|
545
|
+
return compareLexicographic(String(a), String(b));
|
|
546
|
+
}
|
|
547
|
+
function compareNumeric(a, b) {
|
|
548
|
+
const aNumber = toNumeric(a);
|
|
549
|
+
const bNumber = toNumeric(b);
|
|
550
|
+
if (aNumber === undefined || bNumber === undefined)
|
|
551
|
+
return undefined;
|
|
552
|
+
return aNumber === bNumber ? 0 : aNumber < bNumber ? -1 : 1;
|
|
553
|
+
}
|
|
554
|
+
function compareSequence(a, b) {
|
|
555
|
+
const aParts = toCounter(a);
|
|
556
|
+
const bParts = toCounter(b);
|
|
557
|
+
if (!aParts || !bParts)
|
|
558
|
+
return undefined;
|
|
559
|
+
if (aParts.epoch !== bParts.epoch)
|
|
560
|
+
return aParts.epoch < bParts.epoch ? -1 : 1;
|
|
561
|
+
if (aParts.counter === bParts.counter)
|
|
562
|
+
return 0;
|
|
563
|
+
return aParts.counter < bParts.counter ? -1 : 1;
|
|
564
|
+
}
|
|
565
|
+
function compareLexicographic(a, b) {
|
|
566
|
+
return a === b ? 0 : a < b ? -1 : 1;
|
|
567
|
+
}
|
|
568
|
+
//# sourceMappingURL=reconciler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconciler.js","sourceRoot":"","sources":["../src/reconciler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BackoffConfig } from './bindingTypes.ts';
|
|
2
|
+
/** A single named resettable timer. */
|
|
3
|
+
export declare class ResettableTimer {
|
|
4
|
+
private handle;
|
|
5
|
+
private readonly onFire;
|
|
6
|
+
constructor(onFire: () => void);
|
|
7
|
+
/** (Re)arm the timer; a previously armed timeout is cancelled. */
|
|
8
|
+
arm(delayMs: number): void;
|
|
9
|
+
clear(): void;
|
|
10
|
+
get isArmed(): boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Full-jitter exponential backoff: `random() * min(cap, base * factor^attempt)`.
|
|
14
|
+
* Jitter prevents a thundering herd after a shared outage.
|
|
15
|
+
*/
|
|
16
|
+
export declare function backoffDelay(config: BackoffConfig, attempt: number, random: () => number): number;
|
|
17
|
+
/** Sleep that resolves early (with `false`) when the signal aborts. */
|
|
18
|
+
export declare function sleep(delayMs: number, signal: AbortSignal): Promise<boolean>;
|
|
19
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAStD,uCAAuC;AACvC,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAY;gBAEvB,MAAM,EAAE,MAAM,IAAI;IAI9B,kEAAkE;IAClE,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAO1B,KAAK,IAAI,IAAI;IAOb,IAAI,OAAO,IAAI,OAAO,CAErB;CACF;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,MAAM,GAAG,MAAM,CAGjG;AAED,uEAAuE;AACvE,wBAAgB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAiB5E"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** A single named resettable timer. */
|
|
2
|
+
export class ResettableTimer {
|
|
3
|
+
handle;
|
|
4
|
+
onFire;
|
|
5
|
+
constructor(onFire) {
|
|
6
|
+
this.onFire = onFire;
|
|
7
|
+
}
|
|
8
|
+
/** (Re)arm the timer; a previously armed timeout is cancelled. */
|
|
9
|
+
arm(delayMs) {
|
|
10
|
+
this.clear();
|
|
11
|
+
this.handle = setTimeout(this.onFire, delayMs);
|
|
12
|
+
this.handle.unref?.();
|
|
13
|
+
}
|
|
14
|
+
clear() {
|
|
15
|
+
if (this.handle !== undefined) {
|
|
16
|
+
clearTimeout(this.handle);
|
|
17
|
+
this.handle = undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
get isArmed() {
|
|
21
|
+
return this.handle !== undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Full-jitter exponential backoff: `random() * min(cap, base * factor^attempt)`.
|
|
26
|
+
* Jitter prevents a thundering herd after a shared outage.
|
|
27
|
+
*/
|
|
28
|
+
export function backoffDelay(config, attempt, random) {
|
|
29
|
+
const ceiling = Math.min(config.maxMs, config.baseMs * config.factor ** attempt);
|
|
30
|
+
return Math.max(0, Math.floor(random() * ceiling));
|
|
31
|
+
}
|
|
32
|
+
/** Sleep that resolves early (with `false`) when the signal aborts. */
|
|
33
|
+
export function sleep(delayMs, signal) {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
if (signal.aborted) {
|
|
36
|
+
resolve(false);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const timer = setTimeout(() => {
|
|
40
|
+
signal.removeEventListener('abort', onAbort);
|
|
41
|
+
resolve(true);
|
|
42
|
+
}, delayMs);
|
|
43
|
+
timer.unref?.();
|
|
44
|
+
const onAbort = () => {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
resolve(false);
|
|
47
|
+
};
|
|
48
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=scheduler.js.map
|