@novasamatech/host-substrate-chain-connection 0.7.9-2 → 0.7.9-4

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@novasamatech/host-substrate-chain-connection",
3
3
  "type": "module",
4
- "version": "0.7.9-2",
4
+ "version": "0.7.9-4",
5
5
  "description": "Chain connection pool with ref counting and provider branching for Polkadot API",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -25,7 +25,7 @@
25
25
  "README.md"
26
26
  ],
27
27
  "dependencies": {
28
- "@novasamatech/storage-adapter": "0.7.9-2",
28
+ "@novasamatech/storage-adapter": "0.7.9-4",
29
29
  "@polkadot-api/ws-provider": "^0.9.0",
30
30
  "@polkadot-api/json-rpc-provider": "^0.2.0",
31
31
  "@polkadot-api/json-rpc-provider-proxy": "^0.4.0",
@@ -1,2 +0,0 @@
1
- import type { Middleware } from '@polkadot-api/ws-provider';
2
- export declare const nodeSubscriptionReplayMiddleware: Middleware;
@@ -1,105 +0,0 @@
1
- import { isResponse } from '@polkadot-api/json-rpc-provider';
2
- const isChainMethod = (method) => method.startsWith('chain_');
3
- const isSubscribeMethod = (method) => {
4
- if (isChainMethod(method))
5
- return false;
6
- const m = method.toLowerCase();
7
- return m.includes('subscribe') && !m.includes('unsubscribe');
8
- };
9
- const isUnsubscribeMethod = (method) => !isChainMethod(method) && method.toLowerCase().includes('unsubscribe');
10
- // Middleware that replays non-chain subscriptions across reconnects (chain_*
11
- // subscriptions are handled by PAPI itself).
12
- //
13
- // Outgoing subscribe requests are remembered. When the inner provider hands
14
- // over a new connection, every such request is re-sent so the server assigns
15
- // fresh subscription ids. The new server ids are remapped back to the id the
16
- // consumer first saw, keeping inbound notifications and outbound unsubscribes
17
- // transparent across the drop.
18
- export const nodeSubscriptionReplayMiddleware = base => {
19
- const subs = new Map();
20
- const remap = new Map();
21
- const pendingReplays = new Map();
22
- let counter = 0;
23
- const nextReplayId = () => `__replay_${counter++}`;
24
- const findReqIdByConsumerSub = (consumerSubId) => {
25
- for (const [reqId, sub] of subs)
26
- if (sub.consumerSubId === consumerSubId)
27
- return reqId;
28
- return null;
29
- };
30
- const consumerToServerSubId = (consumerSubId) => {
31
- for (const [server, consumer] of remap)
32
- if (consumer === consumerSubId)
33
- return server;
34
- return consumerSubId;
35
- };
36
- return (onMessage, onHalt) => {
37
- const handleMessage = (raw) => {
38
- let msg = raw;
39
- if (isResponse(msg)) {
40
- const pendingConsumerSubId = pendingReplays.get(msg.id);
41
- if (pendingConsumerSubId !== undefined) {
42
- pendingReplays.delete(msg.id);
43
- if ('result' in msg && msg.result !== pendingConsumerSubId)
44
- remap.set(msg.result, pendingConsumerSubId);
45
- return;
46
- }
47
- const sub = subs.get(msg.id);
48
- if (sub) {
49
- if ('result' in msg)
50
- sub.consumerSubId = msg.result;
51
- // subscribe failed — drop so we don't keep a zombie entry across reconnects
52
- else
53
- subs.delete(msg.id);
54
- }
55
- }
56
- else {
57
- const consumerSubId = remap.get(msg.params?.subscription);
58
- if (consumerSubId !== undefined)
59
- msg = { ...msg, params: { ...msg.params, subscription: consumerSubId } };
60
- }
61
- onMessage(msg);
62
- };
63
- // Server sub ids from the prior connection are dead now.
64
- remap.clear();
65
- pendingReplays.clear();
66
- const connection = base(handleMessage, onHalt);
67
- for (const { req, consumerSubId } of subs.values()) {
68
- if (consumerSubId === undefined)
69
- continue;
70
- const id = nextReplayId();
71
- pendingReplays.set(id, consumerSubId);
72
- connection.send({ ...req, id });
73
- }
74
- return {
75
- send(msg) {
76
- if (msg.id != null) {
77
- if (isSubscribeMethod(msg.method)) {
78
- subs.set(msg.id, { req: msg });
79
- }
80
- else if (isUnsubscribeMethod(msg.method)) {
81
- const consumerSubId = msg.params?.[0];
82
- if (typeof consumerSubId === 'string') {
83
- const reqId = findReqIdByConsumerSub(consumerSubId);
84
- if (reqId !== null)
85
- subs.delete(reqId);
86
- const serverSubId = consumerToServerSubId(consumerSubId);
87
- if (serverSubId !== consumerSubId) {
88
- remap.delete(serverSubId);
89
- connection.send({ ...msg, params: [serverSubId, ...msg.params.slice(1)] });
90
- return;
91
- }
92
- }
93
- }
94
- }
95
- connection.send(msg);
96
- },
97
- disconnect() {
98
- subs.clear();
99
- remap.clear();
100
- pendingReplays.clear();
101
- connection.disconnect();
102
- },
103
- };
104
- };
105
- };
@@ -1 +0,0 @@
1
- export {};
@@ -1,255 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import { nodeSubscriptionReplayMiddleware } from './nodeSubscriptionReplayMiddleware.js';
3
- const createMockBase = () => {
4
- const invocations = [];
5
- const base = (onMessage, onHalt) => {
6
- const send = vi.fn();
7
- const disconnect = vi.fn();
8
- invocations.push({ onMessage: onMessage, onHalt: onHalt, send, disconnect });
9
- return { send, disconnect };
10
- };
11
- return {
12
- base,
13
- invocations,
14
- get latest() {
15
- const last = invocations[invocations.length - 1];
16
- if (!last)
17
- throw new Error('no base connection has been created yet');
18
- return last;
19
- },
20
- };
21
- };
22
- const req = (id, method, params = []) => ({
23
- jsonrpc: '2.0',
24
- id,
25
- method,
26
- params,
27
- });
28
- const res = (id, result) => ({ jsonrpc: '2.0', id, result });
29
- const notify = (method, subscription, result = {}) => ({
30
- jsonrpc: '2.0',
31
- method,
32
- params: { subscription, result },
33
- });
34
- const firstSent = (send) => {
35
- const call = send.mock.calls[0];
36
- if (!call)
37
- throw new Error('expected send to have been called at least once');
38
- return call[0];
39
- };
40
- describe('nodeSubscriptionReplayMiddleware', () => {
41
- it('forwards incoming messages to onMessage unmodified', () => {
42
- const mock = createMockBase();
43
- const onMessage = vi.fn();
44
- nodeSubscriptionReplayMiddleware(mock.base)(onMessage, vi.fn());
45
- mock.latest.onMessage(res(1, '0xabc'));
46
- expect(onMessage).toHaveBeenCalledWith(res(1, '0xabc'));
47
- });
48
- it('forwards send calls to the underlying connection', () => {
49
- const mock = createMockBase();
50
- const conn = nodeSubscriptionReplayMiddleware(mock.base)(vi.fn(), vi.fn());
51
- conn.send(req(1, 'chain_getBlock'));
52
- expect(mock.latest.send).toHaveBeenCalledWith(req(1, 'chain_getBlock'));
53
- });
54
- it('passes onHalt through to the base connection', () => {
55
- const mock = createMockBase();
56
- const onHalt = vi.fn();
57
- nodeSubscriptionReplayMiddleware(mock.base)(vi.fn(), onHalt);
58
- expect(mock.latest.onHalt).toBe(onHalt);
59
- });
60
- it('does not replay non-subscription requests on reconnect', () => {
61
- const mock = createMockBase();
62
- const inner = nodeSubscriptionReplayMiddleware(mock.base);
63
- const conn = inner(vi.fn(), vi.fn());
64
- conn.send(req(1, 'chain_getBlock'));
65
- inner(vi.fn(), vi.fn());
66
- expect(mock.latest.send).not.toHaveBeenCalled();
67
- });
68
- it('does not replay pending subscriptions (no server response yet) on reconnect', () => {
69
- const mock = createMockBase();
70
- const inner = nodeSubscriptionReplayMiddleware(mock.base);
71
- const conn = inner(vi.fn(), vi.fn());
72
- conn.send(req(1, 'statement_subscribeStatement'));
73
- // intentionally no simulated response — subscription not confirmed by server
74
- inner(vi.fn(), vi.fn());
75
- expect(mock.latest.send).not.toHaveBeenCalled();
76
- });
77
- it('replays an active subscription on reconnect with a fresh internal id', () => {
78
- const mock = createMockBase();
79
- const inner = nodeSubscriptionReplayMiddleware(mock.base);
80
- const conn = inner(vi.fn(), vi.fn());
81
- conn.send(req(1, 'statement_subscribeStatement'));
82
- mock.latest.onMessage(res(1, 'srv-1'));
83
- inner(vi.fn(), vi.fn());
84
- expect(mock.latest.send).toHaveBeenCalledTimes(1);
85
- const resent = firstSent(mock.latest.send);
86
- expect(resent.method).toBe('statement_subscribeStatement');
87
- expect(resent.params).toEqual([]);
88
- // internal replay id, distinct from the consumer's original id
89
- expect(resent.id).not.toBe(1);
90
- expect(typeof resent.id).toBe('string');
91
- });
92
- it('replays multiple active subscriptions on reconnect', () => {
93
- const mock = createMockBase();
94
- const inner = nodeSubscriptionReplayMiddleware(mock.base);
95
- const conn = inner(vi.fn(), vi.fn());
96
- conn.send(req(1, 'statement_subscribeStatement'));
97
- mock.latest.onMessage(res(1, 'srv-1'));
98
- conn.send(req(2, 'state_subscribeStorage', [[]]));
99
- mock.latest.onMessage(res(2, 'srv-2'));
100
- inner(vi.fn(), vi.fn());
101
- const methods = mock.latest.send.mock.calls.map(c => c[0].method);
102
- expect(methods).toHaveLength(2);
103
- expect(methods).toContain('statement_subscribeStatement');
104
- expect(methods).toContain('state_subscribeStorage');
105
- });
106
- it('suppresses the replay subscribe response from reaching onMessage', () => {
107
- const mock = createMockBase();
108
- const inner = nodeSubscriptionReplayMiddleware(mock.base);
109
- const conn = inner(vi.fn(), vi.fn());
110
- conn.send(req(1, 'statement_subscribeStatement'));
111
- mock.latest.onMessage(res(1, 'srv-1'));
112
- const onMessage2 = vi.fn();
113
- inner(onMessage2, vi.fn());
114
- const replayId = firstSent(mock.latest.send).id;
115
- mock.latest.onMessage(res(replayId, 'srv-2'));
116
- expect(onMessage2).not.toHaveBeenCalled();
117
- });
118
- it('remaps notifications from the new server sub id to the original consumer id', () => {
119
- const mock = createMockBase();
120
- const inner = nodeSubscriptionReplayMiddleware(mock.base);
121
- const conn = inner(vi.fn(), vi.fn());
122
- conn.send(req(1, 'statement_subscribeStatement'));
123
- mock.latest.onMessage(res(1, 'srv-1'));
124
- const onMessage2 = vi.fn();
125
- inner(onMessage2, vi.fn());
126
- const replayId = firstSent(mock.latest.send).id;
127
- mock.latest.onMessage(res(replayId, 'srv-2'));
128
- mock.latest.onMessage(notify('chain_newHead', 'srv-2', { n: 42 }));
129
- expect(onMessage2).toHaveBeenCalledWith(notify('chain_newHead', 'srv-1', { n: 42 }));
130
- });
131
- it('forwards notifications with no remap entry unchanged', () => {
132
- const mock = createMockBase();
133
- const onMessage = vi.fn();
134
- const conn = nodeSubscriptionReplayMiddleware(mock.base)(onMessage, vi.fn());
135
- conn.send(req(1, 'statement_subscribeStatement'));
136
- mock.latest.onMessage(res(1, 'srv-1'));
137
- onMessage.mockClear();
138
- const n = notify('chain_newHead', 'srv-1', { n: 1 });
139
- mock.latest.onMessage(n);
140
- expect(onMessage).toHaveBeenCalledWith(n);
141
- });
142
- it('does not remap when the replay response returns the same sub id', () => {
143
- const mock = createMockBase();
144
- const inner = nodeSubscriptionReplayMiddleware(mock.base);
145
- const conn = inner(vi.fn(), vi.fn());
146
- conn.send(req(1, 'statement_subscribeStatement'));
147
- mock.latest.onMessage(res(1, 'srv-1'));
148
- const onMessage2 = vi.fn();
149
- inner(onMessage2, vi.fn());
150
- const replayId = firstSent(mock.latest.send).id;
151
- // server assigns the same sub id after reconnect
152
- mock.latest.onMessage(res(replayId, 'srv-1'));
153
- const n = notify('chain_newHead', 'srv-1', { n: 1 });
154
- mock.latest.onMessage(n);
155
- expect(onMessage2).toHaveBeenCalledWith(n);
156
- });
157
- it('translates the consumer sub id to the current server sub id on unsubscribe', () => {
158
- const mock = createMockBase();
159
- const inner = nodeSubscriptionReplayMiddleware(mock.base);
160
- const conn = inner(vi.fn(), vi.fn());
161
- conn.send(req(1, 'statement_subscribeStatement'));
162
- mock.latest.onMessage(res(1, 'srv-1'));
163
- const conn2 = inner(vi.fn(), vi.fn());
164
- const replayId = firstSent(mock.latest.send).id;
165
- mock.latest.onMessage(res(replayId, 'srv-2'));
166
- mock.latest.send.mockClear();
167
- conn2.send(req(2, 'statement_unsubscribeStatement', ['srv-1']));
168
- expect(mock.latest.send).toHaveBeenCalledWith(req(2, 'statement_unsubscribeStatement', ['srv-2']));
169
- });
170
- it('forwards unsubscribe unchanged when the sub id was never remapped', () => {
171
- const mock = createMockBase();
172
- const conn = nodeSubscriptionReplayMiddleware(mock.base)(vi.fn(), vi.fn());
173
- conn.send(req(1, 'statement_subscribeStatement'));
174
- mock.latest.onMessage(res(1, 'srv-1'));
175
- mock.latest.send.mockClear();
176
- conn.send(req(2, 'statement_unsubscribeStatement', ['srv-1']));
177
- expect(mock.latest.send).toHaveBeenCalledWith(req(2, 'statement_unsubscribeStatement', ['srv-1']));
178
- });
179
- it('preserves extra unsubscribe params while rewriting only the sub id', () => {
180
- const mock = createMockBase();
181
- const inner = nodeSubscriptionReplayMiddleware(mock.base);
182
- const conn = inner(vi.fn(), vi.fn());
183
- conn.send(req(1, 'some_subscribe'));
184
- mock.latest.onMessage(res(1, 'srv-1'));
185
- const conn2 = inner(vi.fn(), vi.fn());
186
- const replayId = firstSent(mock.latest.send).id;
187
- mock.latest.onMessage(res(replayId, 'srv-2'));
188
- mock.latest.send.mockClear();
189
- conn2.send(req(2, 'some_unsubscribe', ['srv-1', 'extra-arg']));
190
- expect(mock.latest.send).toHaveBeenCalledWith(req(2, 'some_unsubscribe', ['srv-2', 'extra-arg']));
191
- });
192
- it('does not replay a subscription after it has been unsubscribed', () => {
193
- const mock = createMockBase();
194
- const inner = nodeSubscriptionReplayMiddleware(mock.base);
195
- const conn = inner(vi.fn(), vi.fn());
196
- conn.send(req(1, 'statement_subscribeStatement'));
197
- mock.latest.onMessage(res(1, 'srv-1'));
198
- conn.send(req(2, 'statement_unsubscribeStatement', ['srv-1']));
199
- inner(vi.fn(), vi.fn());
200
- expect(mock.latest.send).not.toHaveBeenCalled();
201
- });
202
- it('removes the remap entry on unsubscribe so late notifications under the old server id pass through unchanged', () => {
203
- const mock = createMockBase();
204
- const inner = nodeSubscriptionReplayMiddleware(mock.base);
205
- const conn = inner(vi.fn(), vi.fn());
206
- conn.send(req(1, 'statement_subscribeStatement'));
207
- mock.latest.onMessage(res(1, 'srv-1'));
208
- const onMessage2 = vi.fn();
209
- const conn2 = inner(onMessage2, vi.fn());
210
- const replayId = firstSent(mock.latest.send).id;
211
- mock.latest.onMessage(res(replayId, 'srv-2'));
212
- conn2.send(req(2, 'statement_unsubscribeStatement', ['srv-1']));
213
- onMessage2.mockClear();
214
- const straggler = notify('chain_newHead', 'srv-2', { n: 9 });
215
- mock.latest.onMessage(straggler);
216
- expect(onMessage2).toHaveBeenCalledWith(straggler);
217
- });
218
- it('clears all state on disconnect', () => {
219
- const mock = createMockBase();
220
- const inner = nodeSubscriptionReplayMiddleware(mock.base);
221
- const conn = inner(vi.fn(), vi.fn());
222
- conn.send(req(1, 'statement_subscribeStatement'));
223
- mock.latest.onMessage(res(1, 'srv-1'));
224
- conn.disconnect();
225
- expect(mock.latest.disconnect).toHaveBeenCalledTimes(1);
226
- // a fresh invocation after disconnect should not replay anything
227
- inner(vi.fn(), vi.fn());
228
- expect(mock.latest.send).not.toHaveBeenCalled();
229
- });
230
- it('clears remap across reconnects so stale ids from the prior connection do not leak', () => {
231
- const mock = createMockBase();
232
- const inner = nodeSubscriptionReplayMiddleware(mock.base);
233
- const conn = inner(vi.fn(), vi.fn());
234
- conn.send(req(1, 'statement_subscribeStatement'));
235
- mock.latest.onMessage(res(1, 'srv-1'));
236
- // first reconnect: server re-assigns srv-2 (remap gains srv-2 -> srv-1)
237
- const onMessage2 = vi.fn();
238
- inner(onMessage2, vi.fn());
239
- const replayId2 = firstSent(mock.latest.send).id;
240
- mock.latest.onMessage(res(replayId2, 'srv-2'));
241
- // second reconnect: remap is cleared BEFORE replay; server assigns srv-3
242
- const onMessage3 = vi.fn();
243
- inner(onMessage3, vi.fn());
244
- const replayId3 = firstSent(mock.latest.send).id;
245
- mock.latest.onMessage(res(replayId3, 'srv-3'));
246
- // notification under srv-2 from the previous connection must not remap to srv-1
247
- const stale = notify('chain_newHead', 'srv-2', { n: 0 });
248
- mock.latest.onMessage(stale);
249
- expect(onMessage3).toHaveBeenCalledWith(stale);
250
- // notification under srv-3 is remapped to the consumer's original id (srv-1)
251
- onMessage3.mockClear();
252
- mock.latest.onMessage(notify('chain_newHead', 'srv-3', { n: 1 }));
253
- expect(onMessage3).toHaveBeenCalledWith(notify('chain_newHead', 'srv-1', { n: 1 }));
254
- });
255
- });
@@ -1,6 +0,0 @@
1
- import type { JsonRpcProvider } from '@polkadot-api/json-rpc-provider';
2
- import type { ConnectionStatus } from './types.js';
3
- export declare const createWsJsonRpcProvider: (options: {
4
- endpoints: string[];
5
- onStatusChanged?: (status: ConnectionStatus) => void;
6
- }) => JsonRpcProvider;
package/dist/providers.js DELETED
@@ -1,38 +0,0 @@
1
- import { withPolkadotSdkCompat } from 'polkadot-api/polkadot-sdk-compat';
2
- import { WsEvent, getWsProvider } from 'polkadot-api/ws-provider';
3
- import { withSubscriptionReplay } from './subscriptionReplayProvider.js';
4
- const noop = () => {
5
- // empty
6
- };
7
- export const createWsJsonRpcProvider = (options) => {
8
- let notifyReconnect = noop;
9
- const onReconnect = (cb) => {
10
- notifyReconnect = cb;
11
- return () => {
12
- notifyReconnect = noop;
13
- };
14
- };
15
- return withPolkadotSdkCompat(withSubscriptionReplay(getWsProvider(options.endpoints, {
16
- heartbeatTimeout: Number.POSITIVE_INFINITY,
17
- onStatusChanged: event => {
18
- let status;
19
- switch (event.type) {
20
- case WsEvent.CONNECTING:
21
- status = 'connecting';
22
- break;
23
- case WsEvent.CONNECTED:
24
- notifyReconnect();
25
- status = 'connected';
26
- break;
27
- case WsEvent.ERROR:
28
- case WsEvent.CLOSE:
29
- status = 'disconnected';
30
- break;
31
- default:
32
- status = 'disconnected';
33
- break;
34
- }
35
- options.onStatusChanged?.(status);
36
- },
37
- }), onReconnect));
38
- };