@thru/replay 0.2.20 → 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);
@@ -864,7 +911,7 @@ function eventBackfillFilter(startSlot, resumeAfter) {
864
911
  return slotLiteralFilter("event.slot", startSlot);
865
912
  }
866
913
  return protobuf.create(proto.FilterSchema, {
867
- 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
+ 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()}))))`
868
915
  });
869
916
  }
870
917
  function parseEventId(resumeAfter) {
@@ -873,23 +920,27 @@ function parseEventId(resumeAfter) {
873
920
  }
874
921
  function parseCanonicalEventId(eventId, expectedSlot) {
875
922
  if (!eventId) return null;
876
- const match = /^ts(\d+)_(\d+)_(\d+)$/.exec(eventId);
877
- if (!match) return null;
878
- const [slotPart, blockOffsetPart, callIdxPart] = match.slice(1);
879
- const slot = BigInt(slotPart);
880
- if (slot !== expectedSlot) return null;
881
- return {
882
- slot,
883
- blockOffset: BigInt(blockOffsetPart),
884
- callIdx: BigInt(callIdxPart)
885
- };
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;
886
936
  }
887
937
  function isAfterBoundary(event, boundary) {
888
938
  if (event.slot !== boundary.slot) return event.slot > boundary.slot;
889
939
  if (event.blockOffset !== boundary.blockOffset) {
890
940
  return event.blockOffset > boundary.blockOffset;
891
941
  }
892
- return event.callIdx > boundary.callIdx;
942
+ if (event.callIdx !== boundary.callIdx) return event.callIdx > boundary.callIdx;
943
+ return event.eventIdx > boundary.eventIdx;
893
944
  }
894
945
  function shouldEmitLiveEvent(event, startSlot, resumeAfter) {
895
946
  const boundary = parseEventId(resumeAfter);