@thru/replay 0.2.19 → 0.2.21
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/dist/index.cjs +117 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.mjs +117 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -165,6 +165,11 @@ interface EventReplayOptions {
|
|
|
165
165
|
/** Factory to create fresh clients on reconnection. Enables robust reconnection. */
|
|
166
166
|
clientFactory?: () => EventSource;
|
|
167
167
|
startSlot: Slot;
|
|
168
|
+
/** Last fully processed event. Allows backfill to resume within a slot. */
|
|
169
|
+
resumeAfter?: {
|
|
170
|
+
slot: Slot;
|
|
171
|
+
eventId: string;
|
|
172
|
+
};
|
|
168
173
|
safetyMargin?: bigint;
|
|
169
174
|
pageSize?: number;
|
|
170
175
|
filter?: Filter;
|
package/dist/index.d.ts
CHANGED
|
@@ -165,6 +165,11 @@ interface EventReplayOptions {
|
|
|
165
165
|
/** Factory to create fresh clients on reconnection. Enables robust reconnection. */
|
|
166
166
|
clientFactory?: () => EventSource;
|
|
167
167
|
startSlot: Slot;
|
|
168
|
+
/** Last fully processed event. Allows backfill to resume within a slot. */
|
|
169
|
+
resumeAfter?: {
|
|
170
|
+
slot: Slot;
|
|
171
|
+
eventId: string;
|
|
172
|
+
};
|
|
168
173
|
safetyMargin?: bigint;
|
|
169
174
|
pageSize?: number;
|
|
170
175
|
filter?: Filter;
|
package/dist/index.mjs
CHANGED
|
@@ -358,6 +358,27 @@ function compareBigint(a, b) {
|
|
|
358
358
|
if (a === b) return 0;
|
|
359
359
|
return a < b ? -1 : 1;
|
|
360
360
|
}
|
|
361
|
+
function isNonDecreasing(items, extractSlot) {
|
|
362
|
+
for (let idx = 1; idx < items.length; idx += 1) {
|
|
363
|
+
if (extractSlot(items[idx]) < extractSlot(items[idx - 1])) return false;
|
|
364
|
+
}
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
function assertBackfillPageOrder(previousPage, currentPage, extractSlot) {
|
|
368
|
+
if (!isNonDecreasing(currentPage, extractSlot)) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
"backfill source returned a page that is not ordered by ascending slot"
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
if (!previousPage?.length || !currentPage.length) return;
|
|
374
|
+
const previousMaxSlot = extractSlot(previousPage[previousPage.length - 1]);
|
|
375
|
+
const currentMinSlot = extractSlot(currentPage[0]);
|
|
376
|
+
if (currentMinSlot < previousMaxSlot) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
`backfill source returned pages out of ascending slot order: page minimum slot ${currentMinSlot} is before previous page maximum slot ${previousMaxSlot}`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
361
382
|
var ReplayStream = class {
|
|
362
383
|
config;
|
|
363
384
|
logger;
|
|
@@ -418,6 +439,29 @@ var ReplayStream = class {
|
|
|
418
439
|
this.logger.info(
|
|
419
440
|
`replay entering BACKFILLING state (startSlot=${startSlot}, safetyMargin=${safetyMargin})`
|
|
420
441
|
);
|
|
442
|
+
let pendingOrderedPage = null;
|
|
443
|
+
const emitBackfillItems = async function* (self, items) {
|
|
444
|
+
for (const item of items) {
|
|
445
|
+
const slot = extractSlot(item);
|
|
446
|
+
const key = keyOf(item);
|
|
447
|
+
if (slot < startSlot) continue;
|
|
448
|
+
if (seenItem(slot, key)) {
|
|
449
|
+
self.metrics.discardedDuplicates += 1;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
currentSlot = slot;
|
|
453
|
+
recordEmission(slot, key);
|
|
454
|
+
self.metrics.emittedBackfill += 1;
|
|
455
|
+
yield item;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
const flushPendingBackfill = async function* (self) {
|
|
459
|
+
if (!pendingOrderedPage) return;
|
|
460
|
+
for await (const item of emitBackfillItems(self, pendingOrderedPage)) {
|
|
461
|
+
yield item;
|
|
462
|
+
}
|
|
463
|
+
pendingOrderedPage = null;
|
|
464
|
+
};
|
|
421
465
|
let emptyPageRetries = 0;
|
|
422
466
|
const MAX_EMPTY_PAGE_RETRIES = 10;
|
|
423
467
|
while (!backfillDone) {
|
|
@@ -428,6 +472,9 @@ var ReplayStream = class {
|
|
|
428
472
|
this.logger.error(
|
|
429
473
|
`backfill returned ${MAX_EMPTY_PAGE_RETRIES} consecutive empty pages; treating as done`
|
|
430
474
|
);
|
|
475
|
+
for await (const item of flushPendingBackfill(this)) {
|
|
476
|
+
yield item;
|
|
477
|
+
}
|
|
431
478
|
break;
|
|
432
479
|
}
|
|
433
480
|
const backoffMs = calculateBackoff(emptyPageRetries - 1, DEFAULT_RETRY_CONFIG);
|
|
@@ -438,21 +485,18 @@ var ReplayStream = class {
|
|
|
438
485
|
continue;
|
|
439
486
|
}
|
|
440
487
|
emptyPageRetries = 0;
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
488
|
+
assertBackfillPageOrder(pendingOrderedPage, page.items, extractSlot);
|
|
489
|
+
if (pendingOrderedPage !== null) {
|
|
490
|
+
for await (const item of flushPendingBackfill(this)) {
|
|
491
|
+
yield item;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
pendingOrderedPage = [...page.items];
|
|
495
|
+
const reachedEnd = page.done || page.cursor === void 0;
|
|
496
|
+
if (reachedEnd) {
|
|
497
|
+
for await (const item of flushPendingBackfill(this)) {
|
|
498
|
+
yield item;
|
|
451
499
|
}
|
|
452
|
-
currentSlot = slot;
|
|
453
|
-
recordEmission(slot, key);
|
|
454
|
-
this.metrics.emittedBackfill += 1;
|
|
455
|
-
yield item;
|
|
456
500
|
}
|
|
457
501
|
const duplicatesTrimmed = livePump.discardBufferedUpTo(currentSlot);
|
|
458
502
|
this.metrics.discardedDuplicates += duplicatesTrimmed;
|
|
@@ -461,13 +505,16 @@ var ReplayStream = class {
|
|
|
461
505
|
if (maxStreamSlot !== null) {
|
|
462
506
|
const catchUpSlot = maxStreamSlot > safetyMargin ? maxStreamSlot - safetyMargin : 0n;
|
|
463
507
|
if (currentSlot >= catchUpSlot) {
|
|
508
|
+
for await (const item of flushPendingBackfill(this)) {
|
|
509
|
+
yield item;
|
|
510
|
+
}
|
|
464
511
|
this.logger.info(
|
|
465
512
|
`replay reached SWITCHING threshold (currentSlot=${currentSlot}, maxStreamSlot=${maxStreamSlot}, catchUpSlot=${catchUpSlot})`
|
|
466
513
|
);
|
|
467
514
|
backfillDone = true;
|
|
468
515
|
}
|
|
469
516
|
}
|
|
470
|
-
if (
|
|
517
|
+
if (reachedEnd) backfillDone = true;
|
|
471
518
|
}
|
|
472
519
|
this.logger.info(`replay entering SWITCHING state (currentSlot=${currentSlot})`);
|
|
473
520
|
const { drained, discarded } = livePump.enableStreaming(currentSlot);
|
|
@@ -796,7 +843,7 @@ function createEventReplay(options) {
|
|
|
796
843
|
orderBy: PAGE_ORDER_ASC3,
|
|
797
844
|
pageToken: cursor
|
|
798
845
|
});
|
|
799
|
-
const baseFilter =
|
|
846
|
+
const baseFilter = eventBackfillFilter(startSlot, options.resumeAfter);
|
|
800
847
|
const mergedFilter = combineFilters(baseFilter, options.filter);
|
|
801
848
|
const response = await client.listEvents(
|
|
802
849
|
create(ListEventsRequestSchema, {
|
|
@@ -807,13 +854,19 @@ function createEventReplay(options) {
|
|
|
807
854
|
return backfillPage(response.events, response.page);
|
|
808
855
|
};
|
|
809
856
|
const createSubscribeLive = (client) => (startSlot) => {
|
|
810
|
-
const mergedFilter = combineFilters(
|
|
857
|
+
const mergedFilter = combineFilters(
|
|
858
|
+
slotLiteralFilter("event.slot", startSlot),
|
|
859
|
+
options.filter
|
|
860
|
+
);
|
|
811
861
|
const request = create(StreamEventsRequestSchema, {
|
|
812
862
|
filter: mergedFilter
|
|
813
863
|
});
|
|
814
864
|
return mapAsyncIterable(
|
|
815
865
|
client.streamEvents(request),
|
|
816
|
-
(resp) =>
|
|
866
|
+
(resp) => {
|
|
867
|
+
const event = streamResponseToEvent(resp);
|
|
868
|
+
return shouldEmitLiveEvent(event, startSlot, options.resumeAfter) ? event : null;
|
|
869
|
+
}
|
|
817
870
|
);
|
|
818
871
|
};
|
|
819
872
|
const onReconnect = options.clientFactory ? () => {
|
|
@@ -851,6 +904,52 @@ function eventKey(event) {
|
|
|
851
904
|
const slotPart = event.slot?.toString() ?? "0";
|
|
852
905
|
return `${slotPart}:${event.callIdx ?? 0}`;
|
|
853
906
|
}
|
|
907
|
+
function eventBackfillFilter(startSlot, resumeAfter) {
|
|
908
|
+
const boundary = parseEventId(resumeAfter);
|
|
909
|
+
if (!boundary || startSlot > boundary.slot) {
|
|
910
|
+
return slotLiteralFilter("event.slot", startSlot);
|
|
911
|
+
}
|
|
912
|
+
return create(FilterSchema, {
|
|
913
|
+
expression: `event.slot > uint(${boundary.slot.toString()}) || (event.slot == uint(${boundary.slot.toString()}) && (event.block_offset > uint(${boundary.blockOffset.toString()}) || (event.block_offset == uint(${boundary.blockOffset.toString()}) && event.call_idx >= uint(${boundary.callIdx.toString()}))))`
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
function parseEventId(resumeAfter) {
|
|
917
|
+
if (!resumeAfter?.eventId) return null;
|
|
918
|
+
return parseCanonicalEventId(resumeAfter.eventId, resumeAfter.slot);
|
|
919
|
+
}
|
|
920
|
+
function parseCanonicalEventId(eventId, expectedSlot) {
|
|
921
|
+
if (!eventId) return null;
|
|
922
|
+
const parts = eventId.split(":");
|
|
923
|
+
if (parts.length === 5) {
|
|
924
|
+
const [, slotPart, blockOffsetPart, callIdxPart, eventIdxPart] = parts;
|
|
925
|
+
const slot = BigInt(slotPart);
|
|
926
|
+
if (slot !== expectedSlot) return null;
|
|
927
|
+
return {
|
|
928
|
+
slot,
|
|
929
|
+
blockOffset: BigInt(blockOffsetPart),
|
|
930
|
+
callIdx: BigInt(callIdxPart),
|
|
931
|
+
eventIdx: BigInt(eventIdxPart)
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
function isAfterBoundary(event, boundary) {
|
|
937
|
+
if (event.slot !== boundary.slot) return event.slot > boundary.slot;
|
|
938
|
+
if (event.blockOffset !== boundary.blockOffset) {
|
|
939
|
+
return event.blockOffset > boundary.blockOffset;
|
|
940
|
+
}
|
|
941
|
+
if (event.callIdx !== boundary.callIdx) return event.callIdx > boundary.callIdx;
|
|
942
|
+
return event.eventIdx > boundary.eventIdx;
|
|
943
|
+
}
|
|
944
|
+
function shouldEmitLiveEvent(event, startSlot, resumeAfter) {
|
|
945
|
+
const boundary = parseEventId(resumeAfter);
|
|
946
|
+
if (!boundary || startSlot > boundary.slot) return true;
|
|
947
|
+
const eventSlot = event.slot ?? 0n;
|
|
948
|
+
if (eventSlot > boundary.slot) return true;
|
|
949
|
+
if (eventSlot < boundary.slot) return false;
|
|
950
|
+
const eventPosition = parseCanonicalEventId(event.eventId, boundary.slot);
|
|
951
|
+
return eventPosition ? isAfterBoundary(eventPosition, boundary) : false;
|
|
952
|
+
}
|
|
854
953
|
|
|
855
954
|
// src/page-assembler.ts
|
|
856
955
|
var PAGE_SIZE = 4096;
|