Skip to content

Commit d69d758

Browse files
authored
Merge pull request #66 from helius-labs/reconnection-overlap-fix
Fix: Subscription replacement instead of merge
2 parents fc031bf + 7f4fad1 commit d69d758

File tree

7 files changed

+483
-21
lines changed

7 files changed

+483
-21
lines changed

javascript/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

javascript/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "laserstream-napi"
3-
version = "0.2.4"
3+
version = "0.2.5"
44
edition = "2021"
55

66
[lib]

javascript/napi-src/stream.rs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -473,28 +473,28 @@ impl StreamInner {
473473
Ok(())
474474
}
475475

476-
/// Merges a subscription modification request into the current request.
477-
/// This ensures that modifications made via write() are preserved across reconnections.
476+
/// Replaces the current subscription request with a new one.
477+
/// This ensures modifications made via write() are preserved across reconnections.
478478
fn merge_subscribe_requests(
479479
current: &mut geyser::SubscribeRequest,
480480
modification: &geyser::SubscribeRequest,
481481
) {
482-
// Merge all subscription types
483-
current.accounts.extend(modification.accounts.clone());
484-
current.slots.extend(modification.slots.clone());
485-
current.transactions.extend(modification.transactions.clone());
486-
current.transactions_status.extend(modification.transactions_status.clone());
487-
current.blocks.extend(modification.blocks.clone());
488-
current.blocks_meta.extend(modification.blocks_meta.clone());
489-
current.entry.extend(modification.entry.clone());
490-
current.accounts_data_slice.extend(modification.accounts_data_slice.clone());
482+
// Replace all subscription types (Yellowstone gRPC replaces, not merges)
483+
current.accounts = modification.accounts.clone();
484+
current.slots = modification.slots.clone();
485+
current.transactions = modification.transactions.clone();
486+
current.transactions_status = modification.transactions_status.clone();
487+
current.blocks = modification.blocks.clone();
488+
current.blocks_meta = modification.blocks_meta.clone();
489+
current.entry = modification.entry.clone();
490+
current.accounts_data_slice = modification.accounts_data_slice.clone();
491491

492492
// Update commitment if specified
493493
if modification.commitment.is_some() {
494494
current.commitment = modification.commitment;
495495
}
496496

497-
// Note: from_slot and ping are not merged as they are connection-specific
497+
// Note: from_slot and ping are not replaced as they are connection-specific
498498
}
499499

500500
pub fn cancel(&self) -> Result<()> {

javascript/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "helius-laserstream",
3-
"version": "0.2.4",
3+
"version": "0.2.5",
44
"description": "High-performance Laserstream gRPC client with automatic reconnection",
55
"main": "client.js",
66
"types": "client.d.ts",
@@ -83,12 +83,12 @@
8383
}
8484
},
8585
"optionalDependencies": {
86-
"helius-laserstream-darwin-arm64": "0.2.4",
87-
"helius-laserstream-darwin-x64": "0.2.4",
88-
"helius-laserstream-linux-arm64-gnu": "0.2.4",
89-
"helius-laserstream-linux-arm64-musl": "0.2.4",
90-
"helius-laserstream-linux-x64-gnu": "0.2.4",
91-
"helius-laserstream-linux-x64-musl": "0.2.4"
86+
"helius-laserstream-darwin-arm64": "0.2.5",
87+
"helius-laserstream-darwin-x64": "0.2.5",
88+
"helius-laserstream-linux-arm64-gnu": "0.2.5",
89+
"helius-laserstream-linux-arm64-musl": "0.2.5",
90+
"helius-laserstream-linux-x64-gnu": "0.2.5",
91+
"helius-laserstream-linux-x64-musl": "0.2.5"
9292
},
9393
"dependencies": {
9494
"@types/protobufjs": "^6.0.0",
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { subscribe, CommitmentLevel, SubscribeUpdate, LaserstreamConfig } from '../client';
2+
3+
const credentials = require('../test-config');
4+
5+
/**
6+
* Test for DataSliceOverlap bug fix
7+
*
8+
* This test verifies that calling write() multiple times with accountsDataSlice
9+
* does NOT cause duplication, which would lead to "DataSliceOverlap" errors
10+
* during reconnection.
11+
*
12+
* Before fix: accountsDataSlice would extend/accumulate: [slice, slice, slice, slice]
13+
* After fix: accountsDataSlice replaces: [slice]
14+
*/
15+
async function testDataSliceOverlapBugFix() {
16+
17+
const config: LaserstreamConfig = {
18+
apiKey: credentials.laserstream.apiKey,
19+
endpoint: credentials.laserstream.endpoint,
20+
replay: true,
21+
};
22+
23+
let reconnectionAttempts = 0;
24+
let dataSliceOverlapError = false;
25+
let reconnectionSucceeded = false;
26+
let firstReconnectDetected = false;
27+
28+
const USDC_MINT = 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v';
29+
30+
const stream = await subscribe(
31+
config,
32+
{
33+
accounts: {
34+
"usdc-accounts": {
35+
account: [],
36+
owner: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
37+
filters: [
38+
{ datasize: 165 },
39+
{ memcmp: { offset: 0, base58: USDC_MINT } }
40+
]
41+
}
42+
},
43+
commitment: CommitmentLevel.PROCESSED,
44+
slots: {},
45+
transactions: {},
46+
transactionsStatus: {},
47+
blocks: {},
48+
blocksMeta: {},
49+
entry: {},
50+
accountsDataSlice: [
51+
{
52+
offset: 0,
53+
length: 165
54+
}
55+
]
56+
},
57+
async (update: SubscribeUpdate) => {
58+
// Track that we're receiving data after reconnection
59+
if (firstReconnectDetected && reconnectionAttempts > 0) {
60+
reconnectionSucceeded = true;
61+
}
62+
},
63+
(error) => {
64+
if (error.message.includes('DataSliceOverlap')) {
65+
dataSliceOverlapError = true;
66+
}
67+
68+
if (error.message.includes('Connection error')) {
69+
if (error.message.includes('attempt 1')) {
70+
firstReconnectDetected = true;
71+
}
72+
const match = error.message.match(/attempt (\d+)/);
73+
if (match) {
74+
reconnectionAttempts = Math.max(reconnectionAttempts, parseInt(match[1]));
75+
}
76+
}
77+
}
78+
);
79+
80+
await new Promise(resolve => setTimeout(resolve, 3000));
81+
82+
for (let i = 1; i <= 3; i++) {
83+
await stream.write({
84+
accounts: {
85+
"usdc-accounts": {
86+
account: [],
87+
owner: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
88+
filters: [
89+
{ datasize: 165 },
90+
{ memcmp: { offset: 0, base58: USDC_MINT } }
91+
]
92+
}
93+
},
94+
commitment: CommitmentLevel.PROCESSED,
95+
slots: {},
96+
transactions: {},
97+
transactionsStatus: {},
98+
blocks: {},
99+
blocksMeta: {},
100+
entry: {},
101+
accountsDataSlice: [
102+
{
103+
offset: 0,
104+
length: 165
105+
}
106+
]
107+
});
108+
await new Promise(resolve => setTimeout(resolve, 500));
109+
}
110+
111+
await new Promise(resolve => setTimeout(resolve, 60000));
112+
113+
stream.cancel();
114+
115+
if (dataSliceOverlapError) {
116+
throw new Error('DataSliceOverlap error occurred');
117+
}
118+
119+
if (reconnectionAttempts > 0 && !reconnectionSucceeded) {
120+
throw new Error('Reconnection attempted but did not succeed');
121+
}
122+
123+
if (reconnectionAttempts === 0) {
124+
console.log('Warning: No reconnection triggered. Chaos proxy may not be running.');
125+
}
126+
127+
console.log('Test passed');
128+
}
129+
130+
testDataSliceOverlapBugFix().catch(err => {
131+
console.error('Test failed:', err.message);
132+
process.exit(1);
133+
});

0 commit comments

Comments
 (0)