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 +9 -1
- package/README_CN.md +8 -1
- package/dist/grpc/client.d.ts +10 -0
- package/dist/grpc/client.js +124 -3
- package/package.json +1 -1
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.
|
|
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.
|
|
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`、
|
package/dist/grpc/client.d.ts
CHANGED
|
@@ -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 {
|
package/dist/grpc/client.js
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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