@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 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
- const sorted = [...page.items].sort(
443
- (a, b) => compareBigint(extractSlot(a), extractSlot(b))
444
- );
445
- for (const item of sorted) {
446
- const slot = extractSlot(item);
447
- const key = keyOf(item);
448
- if (slot < startSlot) continue;
449
- if (seenItem(slot, key)) {
450
- this.metrics.discardedDuplicates += 1;
451
- continue;
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 (page.done || cursor === void 0) backfillDone = true;
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 = slotLiteralFilter("event.slot", startSlot);
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(slotLiteralFilter("event.slot", startSlot), options.filter);
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) => streamResponseToEvent(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;