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