@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.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
- const sorted = [...page.items].sort(
442
- (a, b) => compareBigint(extractSlot(a), extractSlot(b))
443
- );
444
- for (const item of sorted) {
445
- const slot = extractSlot(item);
446
- const key = keyOf(item);
447
- if (slot < startSlot) continue;
448
- if (seenItem(slot, key)) {
449
- this.metrics.discardedDuplicates += 1;
450
- continue;
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 (page.done || cursor === void 0) backfillDone = true;
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);
@@ -863,7 +910,7 @@ function eventBackfillFilter(startSlot, resumeAfter) {
863
910
  return slotLiteralFilter("event.slot", startSlot);
864
911
  }
865
912
  return create(FilterSchema, {
866
- 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()}))))`
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()}))))`
867
914
  });
868
915
  }
869
916
  function parseEventId(resumeAfter) {
@@ -872,23 +919,27 @@ function parseEventId(resumeAfter) {
872
919
  }
873
920
  function parseCanonicalEventId(eventId, expectedSlot) {
874
921
  if (!eventId) return null;
875
- const match = /^ts(\d+)_(\d+)_(\d+)$/.exec(eventId);
876
- if (!match) return null;
877
- const [slotPart, blockOffsetPart, callIdxPart] = match.slice(1);
878
- const slot = BigInt(slotPart);
879
- if (slot !== expectedSlot) return null;
880
- return {
881
- slot,
882
- blockOffset: BigInt(blockOffsetPart),
883
- callIdx: BigInt(callIdxPart)
884
- };
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;
885
935
  }
886
936
  function isAfterBoundary(event, boundary) {
887
937
  if (event.slot !== boundary.slot) return event.slot > boundary.slot;
888
938
  if (event.blockOffset !== boundary.blockOffset) {
889
939
  return event.blockOffset > boundary.blockOffset;
890
940
  }
891
- return event.callIdx > boundary.callIdx;
941
+ if (event.callIdx !== boundary.callIdx) return event.callIdx > boundary.callIdx;
942
+ return event.eventIdx > boundary.eventIdx;
892
943
  }
893
944
  function shouldEmitLiveEvent(event, startSlot, resumeAfter) {
894
945
  const boundary = parseEventId(resumeAfter);