sol-parser-sdk 0.5.12 → 0.5.13

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/README.md CHANGED
@@ -75,7 +75,7 @@
75
75
  **From npm**
76
76
 
77
77
  ```bash
78
- npm install sol-parser-sdk@0.5.12
78
+ npm install sol-parser-sdk@0.5.13
79
79
  ```
80
80
 
81
81
  **From source** (folder may be named `sol-parser-sdk-ts` in a monorepo)
@@ -176,6 +176,14 @@ queue with `sub.len()` / `sub.eventDropped()` and the pre-parser queue with
176
176
  `sub.ingressLen()` / `sub.ingressDropped()`. `sub.dropped()` is the combined count. Every overflow
177
177
  is counted and periodically reported through `sub.errors`.
178
178
 
179
+ Automatic reconnect resumes from the last successfully parsed slot by default and deduplicates
180
+ replayed transaction/account updates. Monitor transport continuity with `sub.isStreamConnected()`,
181
+ `sub.streamDisconnects()`, `sub.reconnects()`, `sub.replayedUpdates()`, and
182
+ `sub.continuityBreaks()`. A zero local drop count does not describe updates lost before the gRPC
183
+ `data` callback. If a provider rejects Yellowstone `fromSlot`, the SDK reports the failure through
184
+ `sub.errors`, increments `continuityBreaks()`, and reconnects live. Set `replayOnReconnect: false`
185
+ only when duplicate-free live delivery is more important than recovering a disconnect window.
186
+
179
187
  For precise local-only measurements, set `config.enable_metrics = true`. Event metadata then includes
180
188
  `local_queue_latency_us`, `parse_duration_us`, and `local_processing_latency_us`. These values use
181
189
  Node's monotonic high-resolution clock. They measure only local queueing and parsing; the hot path
package/README_CN.md CHANGED
@@ -75,7 +75,7 @@
75
75
  **npm**
76
76
 
77
77
  ```bash
78
- npm install sol-parser-sdk@0.5.12
78
+ npm install sol-parser-sdk@0.5.13
79
79
  ```
80
80
 
81
81
  **源码**(monorepo 里目录可能是 `sol-parser-sdk-ts`)
@@ -174,6 +174,13 @@ for await (const event of sub) {
174
174
  公开事件队列,通过 `sub.ingressLen()` / `sub.ingressDropped()` 监控解析前入口队列;
175
175
  `sub.dropped()` 返回两者之和。每次溢出都会计数,并定期通过 `sub.errors` 报告。
176
176
 
177
+ 自动重连默认从最后一个成功解析的 slot 继续订阅,并对回放的 transaction/account update
178
+ 去重。可通过 `sub.isStreamConnected()`、`sub.streamDisconnects()`、`sub.reconnects()`、
179
+ `sub.replayedUpdates()` 和 `sub.continuityBreaks()` 监控传输连续性。本地 drop 为零并不能说明
180
+ 进入 gRPC `data` 回调之前没有丢数据。如果 provider 拒绝 Yellowstone `fromSlot`,SDK 会通过
181
+ `sub.errors` 明确报告,增加 `continuityBreaks()`,再降级为 live 重连。只有在“绝不接收回放重复”
182
+ 比“补偿断线窗口”更重要时,才应设置 `replayOnReconnect: false`。
183
+
177
184
  如需精确测量纯本地延迟,请设置 `config.enable_metrics = true`。事件 metadata 将包含
178
185
  `local_queue_latency_us`、`parse_duration_us` 和 `local_processing_latency_us`,这些字段使用
179
186
  Node 单调高精度时钟,只测量本地排队与解析。热路径不会调用 `getSlot`、`getTransaction`、
@@ -18,6 +18,12 @@ export interface DexEventSubscription extends AsyncIterable<DexEvent> {
18
18
  eventDropped(): number;
19
19
  ingressLen(): number;
20
20
  ingressDropped(): number;
21
+ isStreamConnected(): boolean;
22
+ streamDisconnects(): number;
23
+ reconnects(): number;
24
+ replayedUpdates(): number;
25
+ /** Reconnects that could not resume from the last parsed slot. */
26
+ continuityBreaks(): number;
21
27
  }
22
28
  export interface SubscribeDexEventsOptions {
23
29
  autoReconnect?: boolean;
@@ -30,6 +36,10 @@ export interface SubscribeDexEventsOptions {
30
36
  ingressBufferSize?: number;
31
37
  /** Maximum raw updates parsed before yielding back to Node's event loop. Defaults to 8. */
32
38
  parserBatchSize?: number;
39
+ /** Resume from the last parsed slot after reconnect. Defaults to true. */
40
+ replayOnReconnect?: boolean;
41
+ /** Parsed slots retained for replay deduplication. Defaults to 128. */
42
+ replayDedupeSlots?: number;
33
43
  }
34
44
  /** Yellowstone gRPC 客户端包装器 */
35
45
  export declare class YellowstoneGrpc {
@@ -42,6 +42,7 @@ exports.YellowstoneGrpc = void 0;
42
42
  */
43
43
  const yellowstone_grpc_1 = __importStar(require("@triton-one/yellowstone-grpc"));
44
44
  const bs58_1 = __importDefault(require("bs58"));
45
+ const node_buffer_1 = require("node:buffer");
45
46
  const dex_event_js_1 = require("../core/dex_event.js");
46
47
  const metadata_js_1 = require("../core/metadata.js");
47
48
  const mod_js_1 = require("../accounts/mod.js");
@@ -66,6 +67,35 @@ function positiveIntegerOption(value, fallback, name) {
66
67
  }
67
68
  return normalized;
68
69
  }
70
+ function replayUnsupported(error) {
71
+ const grpcError = error;
72
+ if (grpcError.code === 3 || grpcError.code === 11 || grpcError.code === 12)
73
+ return true;
74
+ return /from.?slot.*(?:unsupported|disabled|unavailable|invalid)/i.test(`${grpcError.message ?? ""} ${grpcError.details ?? ""}`);
75
+ }
76
+ function updateSlot(update) {
77
+ const value = update.transaction?.slot ?? update.account?.slot;
78
+ if (value === undefined || value === null)
79
+ return undefined;
80
+ try {
81
+ return BigInt(value);
82
+ }
83
+ catch {
84
+ return undefined;
85
+ }
86
+ }
87
+ function bytesKey(value) {
88
+ return node_buffer_1.Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("base64");
89
+ }
90
+ function updateIdentity(update) {
91
+ const transaction = update.transaction?.transaction;
92
+ if (transaction?.signature)
93
+ return `t:${bytesKey(transaction.signature)}`;
94
+ const account = update.account?.account;
95
+ if (!account?.pubkey)
96
+ return undefined;
97
+ return `a:${bytesKey(account.pubkey)}:${String(update.account?.slot ?? 0)}:${String(account.writeVersion ?? 0)}`;
98
+ }
69
99
  /** Yellowstone gRPC 客户端包装器 */
70
100
  class YellowstoneGrpc {
71
101
  client;
@@ -440,12 +470,24 @@ class YellowstoneGrpc {
440
470
  const events = new async_event_queue_js_1.AsyncEventQueue(Math.max(1, this.config.buffer_size), options?.queueOverflowStrategy);
441
471
  const ingressBufferSize = positiveIntegerOption(options?.ingressBufferSize, Math.min(this.config.buffer_size, 1024), "ingressBufferSize");
442
472
  const parserBatchSize = positiveIntegerOption(options?.parserBatchSize, 8, "parserBatchSize");
473
+ const replayDedupeSlots = positiveIntegerOption(options?.replayDedupeSlots, 128, "replayDedupeSlots");
474
+ const replayOnReconnect = options?.replayOnReconnect !== false;
443
475
  const ingress = new async_event_queue_js_1.AsyncEventQueue(ingressBufferSize, options?.queueOverflowStrategy);
444
476
  const errors = new async_event_queue_js_1.AsyncEventQueue(Math.max(1, this.config.buffer_size));
445
477
  const order = new order_buffer_js_1.OrderDispatcher(this.config);
446
478
  const eventDropped = events.dropped.bind(events);
447
479
  let nextEventDropWarningAt = 0;
448
480
  let nextIngressDropWarningAt = 0;
481
+ let streamConnected = false;
482
+ let streamDisconnectCount = 0;
483
+ let reconnectCount = 0;
484
+ let replayedUpdateCount = 0;
485
+ let continuityBreakCount = 0;
486
+ let lastParsedSlot;
487
+ let highestRememberedSlot = 0n;
488
+ let seenHead = 0;
489
+ const seenUpdates = new Map();
490
+ const seenOrder = [];
449
491
  const reportError = (error) => errors.push(error);
450
492
  const emitEvent = (event) => {
451
493
  if (events.push(event))
@@ -490,7 +532,37 @@ class YellowstoneGrpc {
490
532
  metadata.local_processing_latency_us = processingUs;
491
533
  }
492
534
  };
535
+ const rememberParsedUpdate = (identity, slot) => {
536
+ if (slot !== undefined && (lastParsedSlot === undefined || slot > lastParsedSlot)) {
537
+ lastParsedSlot = slot;
538
+ }
539
+ if (!replayOnReconnect || identity === undefined || slot === undefined)
540
+ return;
541
+ seenUpdates.set(identity, slot);
542
+ seenOrder.push({ identity, slot });
543
+ if (slot > highestRememberedSlot)
544
+ highestRememberedSlot = slot;
545
+ const retainFrom = highestRememberedSlot > BigInt(replayDedupeSlots)
546
+ ? highestRememberedSlot - BigInt(replayDedupeSlots)
547
+ : 0n;
548
+ while (seenHead < seenOrder.length && seenOrder[seenHead].slot < retainFrom) {
549
+ const stale = seenOrder[seenHead++];
550
+ if (seenUpdates.get(stale.identity) === stale.slot)
551
+ seenUpdates.delete(stale.identity);
552
+ }
553
+ if (seenHead >= 4096 && seenHead * 2 >= seenOrder.length) {
554
+ seenOrder.splice(0, seenHead);
555
+ seenHead = 0;
556
+ }
557
+ };
493
558
  const processUpdate = ({ update, grpcRecvUs, grpcRecvNs, blockTimeUs }) => {
559
+ const identity = updateIdentity(update);
560
+ const slotForReplay = updateSlot(update);
561
+ if (replayOnReconnect && identity !== undefined && seenUpdates.has(identity)) {
562
+ replayedUpdateCount++;
563
+ rememberParsedUpdate(undefined, slotForReplay);
564
+ return;
565
+ }
494
566
  if (order.needsTimer)
495
567
  order.flushDue(emitEvent);
496
568
  const txUpdate = update.transaction;
@@ -525,6 +597,7 @@ class YellowstoneGrpc {
525
597
  if (ev)
526
598
  emitEvent(ev);
527
599
  }
600
+ rememberParsedUpdate(identity, slotForReplay);
528
601
  };
529
602
  const scheduleIngressDrain = () => {
530
603
  if (isCancelled || ingressDrainScheduled || ingress.len() === 0)
@@ -618,25 +691,39 @@ class YellowstoneGrpc {
618
691
  const autoReconnect = options?.autoReconnect !== false;
619
692
  const maxBackoffMs = 60_000;
620
693
  let backoffMs = this.config.retry_delay_ms;
694
+ let replayAllowed = replayOnReconnect;
695
+ let attempt = 0;
621
696
  try {
622
697
  while (!isCancelled) {
698
+ let attemptedReplay = false;
699
+ let receivedData = false;
623
700
  try {
701
+ attempt++;
702
+ if (attempt > 1)
703
+ reconnectCount++;
624
704
  const stream = await this.client.subscribe();
625
705
  if (isCancelled) {
626
706
  cancelSubscribeStream(stream);
627
707
  break;
628
708
  }
629
709
  streamHolder.current = stream;
710
+ const request = (0, subscribe_builder_js_1.buildSubscribeRequest)(currentTransactionFilters, currentAccountFilters);
711
+ if (attempt > 1 && replayAllowed && lastParsedSlot !== undefined) {
712
+ request.fromSlot = lastParsedSlot.toString();
713
+ attemptedReplay = true;
714
+ }
630
715
  await new Promise((resolve, reject) => {
631
- stream.write((0, subscribe_builder_js_1.buildSubscribeRequest)(currentTransactionFilters, currentAccountFilters), (err) => {
716
+ stream.write(request, (err) => {
632
717
  if (err)
633
718
  reject(err);
634
719
  else
635
720
  resolve();
636
721
  });
637
722
  });
723
+ streamConnected = true;
638
724
  await new Promise((resolve, reject) => {
639
725
  const cleanup = () => {
726
+ streamConnected = false;
640
727
  stream.removeAllListeners();
641
728
  if (streamHolder.current === stream) {
642
729
  streamHolder.current = null;
@@ -645,6 +732,7 @@ class YellowstoneGrpc {
645
732
  stream.on("data", (update) => {
646
733
  if (isCancelled)
647
734
  return;
735
+ receivedData = true;
648
736
  const grpcRecvNs = this.config.enable_metrics
649
737
  ? process.hrtime.bigint()
650
738
  : undefined;
@@ -676,11 +764,15 @@ class YellowstoneGrpc {
676
764
  scheduleIngressDrain();
677
765
  });
678
766
  stream.on("error", (err) => {
767
+ streamDisconnectCount++;
679
768
  cleanup();
680
769
  reject(err);
681
770
  });
682
771
  stream.on("end", () => {
772
+ streamDisconnectCount++;
683
773
  cleanup();
774
+ if (!isCancelled)
775
+ reportError(new Error("Yellowstone stream ended; reconnecting"));
684
776
  resolve();
685
777
  });
686
778
  });
@@ -690,15 +782,38 @@ class YellowstoneGrpc {
690
782
  break;
691
783
  cancelSubscribeStream(streamHolder.current);
692
784
  streamHolder.current = null;
785
+ streamConnected = false;
693
786
  reportError(err instanceof Error ? err : new Error(String(err)));
694
- if (!autoReconnect)
787
+ await waitForIngressIdle();
788
+ if (!autoReconnect) {
789
+ if (lastParsedSlot !== undefined)
790
+ continuityBreakCount++;
695
791
  break;
792
+ }
793
+ let continuityBreakRecorded = false;
794
+ if (attemptedReplay && !receivedData && replayUnsupported(err)) {
795
+ replayAllowed = false;
796
+ continuityBreakCount++;
797
+ continuityBreakRecorded = true;
798
+ reportError(new Error("Yellowstone provider rejected fromSlot replay; reconnecting live and continuity is not guaranteed"));
799
+ }
800
+ if (!replayAllowed && lastParsedSlot !== undefined && !continuityBreakRecorded) {
801
+ continuityBreakCount++;
802
+ }
696
803
  await waitBeforeReconnect(backoffMs);
697
804
  backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
698
805
  continue;
699
806
  }
700
- if (isCancelled || !autoReconnect)
807
+ await waitForIngressIdle();
808
+ if (isCancelled)
701
809
  break;
810
+ if (!autoReconnect) {
811
+ if (lastParsedSlot !== undefined)
812
+ continuityBreakCount++;
813
+ break;
814
+ }
815
+ if (!replayAllowed && lastParsedSlot !== undefined)
816
+ continuityBreakCount++;
702
817
  await waitBeforeReconnect(backoffMs);
703
818
  backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
704
819
  }
@@ -706,6 +821,7 @@ class YellowstoneGrpc {
706
821
  finally {
707
822
  cancelSubscribeStream(streamHolder.current);
708
823
  streamHolder.current = null;
824
+ streamConnected = false;
709
825
  if (flushTimer)
710
826
  clearInterval(flushTimer);
711
827
  if (!isCancelled) {
@@ -725,6 +841,11 @@ class YellowstoneGrpc {
725
841
  eventDropped,
726
842
  ingressLen: () => ingress.len(),
727
843
  ingressDropped: () => ingress.dropped(),
844
+ isStreamConnected: () => streamConnected,
845
+ streamDisconnects: () => streamDisconnectCount,
846
+ reconnects: () => reconnectCount,
847
+ replayedUpdates: () => replayedUpdateCount,
848
+ continuityBreaks: () => continuityBreakCount,
728
849
  cancel: () => {
729
850
  cancel();
730
851
  this.dexSubscribers.delete(id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sol-parser-sdk",
3
- "version": "0.5.12",
3
+ "version": "0.5.13",
4
4
  "description": "High-performance Solana DEX event parser for Node.js/TypeScript (Yellowstone gRPC, log parsing)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",