@novasamatech/host-substrate-chain-connection 0.7.0 → 0.7.1-0

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.
@@ -15,5 +15,13 @@ export type ChainConnection<C extends ChainConfig, T = PolkadotClient> = {
15
15
  getProvider(chain: C): JsonRpcProvider;
16
16
  status(genesisHash: string): ConnectionStatus;
17
17
  onStatusChanged(genesisHash: string, callback: (status: ConnectionStatus) => void): VoidFunction;
18
+ /**
19
+ * Drop the inner socket of every active provider that supports pausing
20
+ * (e.g. providers built via `createWsJsonRpcProvider`). Clients and
21
+ * refcounts are preserved; tracked subscriptions are re-sent on
22
+ * {@link resumeAll} via the replay wrapper.
23
+ */
24
+ pauseAll(): void;
25
+ resumeAll(): void;
18
26
  };
19
27
  export declare const createChainConnection: <C extends ChainConfig, T = PolkadotClient>({ resolve, clientOptions, createProvider, destroyDelay, }: ChainConnectionConfig<C, T>) => ChainConnection<C, T>;
@@ -3,6 +3,7 @@ import { createClient } from 'polkadot-api';
3
3
  import { createBranchedProvider } from './branchedProvider.js';
4
4
  import { createConnectionManager } from './connectionManager.js';
5
5
  import { createRefCounter } from './refCounter.js';
6
+ import { isPausable } from './wsProvider.js';
6
7
  export const createChainConnection = ({ resolve, clientOptions, createProvider, destroyDelay = 0, }) => {
7
8
  const connections = createConnectionManager();
8
9
  const refCounter = createRefCounter();
@@ -22,10 +23,10 @@ export const createChainConnection = ({ resolve, clientOptions, createProvider,
22
23
  const existing = existingClients.get(chain.genesisHash);
23
24
  if (existing)
24
25
  return existing;
25
- const provider = createProvider(chain, status => connections.update(chain.genesisHash, status));
26
- const branchedProvider = createBranchedProvider(provider);
26
+ const rootProvider = createProvider(chain, status => connections.update(chain.genesisHash, status));
27
+ const branchedProvider = createBranchedProvider(rootProvider);
27
28
  const client = createClient(branchedProvider.branch(), clientOptions?.(chain));
28
- const pooled = { client, provider: branchedProvider };
29
+ const pooled = { client, provider: branchedProvider, rootProvider };
29
30
  existingClients.set(chain.genesisHash, pooled);
30
31
  return pooled;
31
32
  };
@@ -123,5 +124,17 @@ export const createChainConnection = ({ resolve, clientOptions, createProvider,
123
124
  onStatusChanged(genesisHash, callback) {
124
125
  return connections.onStatusChange(genesisHash, callback);
125
126
  },
127
+ pauseAll() {
128
+ for (const { rootProvider } of existingClients.values()) {
129
+ if (isPausable(rootProvider))
130
+ rootProvider.pause();
131
+ }
132
+ },
133
+ resumeAll() {
134
+ for (const { rootProvider } of existingClients.values()) {
135
+ if (isPausable(rootProvider))
136
+ rootProvider.resume();
137
+ }
138
+ },
126
139
  };
127
140
  };
@@ -205,4 +205,59 @@ describe('createChainConnection', () => {
205
205
  });
206
206
  });
207
207
  });
208
+ describe('pauseAll / resumeAll', () => {
209
+ const createPausableMockProvider = () => {
210
+ const pause = vi.fn();
211
+ const resume = vi.fn();
212
+ const provider = Object.assign(() => ({ send: vi.fn(), disconnect: vi.fn() }), {
213
+ pause,
214
+ resume,
215
+ });
216
+ return { provider, pause, resume };
217
+ };
218
+ it('calls pause on every pausable provider created so far', async () => {
219
+ const chainA = createPausableMockProvider();
220
+ const chainB = createPausableMockProvider();
221
+ const providerByChain = { a: chainA.provider, b: chainB.provider };
222
+ const connection = createChainConnection({
223
+ createProvider: chain => providerByChain[chain.genesisHash],
224
+ });
225
+ const { unlock: u1 } = await connection.lockApi(testChain('a'));
226
+ const { unlock: u2 } = await connection.lockApi(testChain('b'));
227
+ connection.pauseAll();
228
+ expect(chainA.pause).toHaveBeenCalledTimes(1);
229
+ expect(chainB.pause).toHaveBeenCalledTimes(1);
230
+ u1();
231
+ u2();
232
+ });
233
+ it('calls resume on every pausable provider', async () => {
234
+ const chainA = createPausableMockProvider();
235
+ const connection = createChainConnection({ createProvider: () => chainA.provider });
236
+ const { unlock } = await connection.lockApi(testChain('a'));
237
+ connection.pauseAll();
238
+ connection.resumeAll();
239
+ expect(chainA.resume).toHaveBeenCalledTimes(1);
240
+ unlock();
241
+ });
242
+ it('skips providers that do not expose pause/resume', async () => {
243
+ const { connection } = createTestConnection();
244
+ const { unlock } = await connection.lockApi(testChain('a'));
245
+ expect(() => {
246
+ connection.pauseAll();
247
+ connection.resumeAll();
248
+ }).not.toThrow();
249
+ unlock();
250
+ });
251
+ it('does not call pause on providers for chains that have been destroyed', async () => {
252
+ const chainA = createPausableMockProvider();
253
+ const connection = createChainConnection({
254
+ createProvider: () => chainA.provider,
255
+ destroyDelay: 0,
256
+ });
257
+ const { unlock } = await connection.lockApi(testChain('a'));
258
+ unlock();
259
+ connection.pauseAll();
260
+ expect(chainA.pause).not.toHaveBeenCalled();
261
+ });
262
+ });
208
263
  });
@@ -0,0 +1,2 @@
1
+ import type { Middleware } from '@polkadot-api/ws-provider';
2
+ export declare const nodeSubscriptionReplayMiddleware: Middleware;
@@ -0,0 +1,105 @@
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
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,255 @@
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
+ });
@@ -0,0 +1,8 @@
1
+ import type { Middleware } from '@polkadot-api/ws-provider';
2
+ export type PauseController = {
3
+ middleware: Middleware;
4
+ pause: () => void;
5
+ resume: () => void;
6
+ isPaused: () => boolean;
7
+ };
8
+ export declare const createPauseController: () => PauseController;
@@ -0,0 +1,67 @@
1
+ // Plugs into getWsProvider's `middleware` option. Pause closes the live socket
2
+ // and stalls auto-reconnect; resume opens a fresh socket and flushes buffered
3
+ // sends.
4
+ export const createPauseController = () => {
5
+ let paused = false;
6
+ let destroyed = false;
7
+ let base = null;
8
+ let onMessage = null;
9
+ let onHalt = null;
10
+ let real = null;
11
+ let buffer = [];
12
+ // a halt we fired already scheduled a middleware re-invocation — resume
13
+ // must defer to it rather than reusing the stale onMessage/onHalt pair
14
+ let reinvocationPending = false;
15
+ const connect = () => {
16
+ if (!base || !onMessage || !onHalt)
17
+ return;
18
+ real = base(onMessage, onHalt);
19
+ const q = buffer;
20
+ buffer = [];
21
+ for (const m of q)
22
+ real.send(m);
23
+ };
24
+ const middleware = b => {
25
+ base = b;
26
+ return (onMsg, onH) => {
27
+ reinvocationPending = false;
28
+ onMessage = onMsg;
29
+ onHalt = onH;
30
+ real = null;
31
+ if (!paused)
32
+ connect();
33
+ return {
34
+ send: m => (real ? real.send(m) : buffer.push(m)),
35
+ disconnect: () => {
36
+ destroyed = true;
37
+ paused = false;
38
+ buffer = [];
39
+ real?.disconnect();
40
+ real = null;
41
+ },
42
+ };
43
+ };
44
+ };
45
+ const pause = () => {
46
+ if (paused || destroyed)
47
+ return;
48
+ paused = true;
49
+ if (!real || !onHalt)
50
+ return;
51
+ reinvocationPending = true;
52
+ const r = real;
53
+ real = null;
54
+ // withSocket.disconnect detaches listeners before close — no halt fires
55
+ // from there, so trigger it manually to drive getProxy's replay.
56
+ r.disconnect();
57
+ onHalt({ type: 'paused' });
58
+ };
59
+ const resume = () => {
60
+ if (destroyed || !paused)
61
+ return;
62
+ paused = false;
63
+ if (!reinvocationPending)
64
+ connect();
65
+ };
66
+ return { middleware, pause, resume, isPaused: () => paused };
67
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,177 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { createPauseController } from './pauseController.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
+ describe('createPauseController', () => {
29
+ it('connects on first inner invocation and forwards sends', () => {
30
+ const mock = createMockBase();
31
+ const pc = createPauseController();
32
+ const inner = pc.middleware(mock.base);
33
+ expect(mock.invocations).toHaveLength(0);
34
+ const conn = inner(vi.fn(), vi.fn());
35
+ expect(mock.invocations).toHaveLength(1);
36
+ expect(pc.isPaused()).toBe(false);
37
+ conn.send(req(1, 'foo'));
38
+ expect(mock.latest.send).toHaveBeenCalledWith(req(1, 'foo'));
39
+ });
40
+ it('forwards inbound messages from the base to the stored onMessage', () => {
41
+ const mock = createMockBase();
42
+ const pc = createPauseController();
43
+ const onMessage = vi.fn();
44
+ pc.middleware(mock.base)(onMessage, vi.fn());
45
+ const message = { jsonrpc: '2.0', id: 1, result: 'x' };
46
+ mock.latest.onMessage(message);
47
+ expect(onMessage).toHaveBeenCalledWith(message);
48
+ });
49
+ it('forwards halts from the base to the stored onHalt', () => {
50
+ const mock = createMockBase();
51
+ const pc = createPauseController();
52
+ const onHalt = vi.fn();
53
+ pc.middleware(mock.base)(vi.fn(), onHalt);
54
+ const haltReason = { type: 'socket-closed' };
55
+ mock.latest.onHalt(haltReason);
56
+ expect(onHalt).toHaveBeenCalledWith(haltReason);
57
+ });
58
+ it('pause disconnects the live socket and fires a paused halt', () => {
59
+ const mock = createMockBase();
60
+ const pc = createPauseController();
61
+ const onHalt = vi.fn();
62
+ pc.middleware(mock.base)(vi.fn(), onHalt);
63
+ pc.pause();
64
+ expect(pc.isPaused()).toBe(true);
65
+ expect(mock.latest.disconnect).toHaveBeenCalledTimes(1);
66
+ expect(onHalt).toHaveBeenCalledWith({ type: 'paused' });
67
+ });
68
+ it('pause is a no-op when already paused', () => {
69
+ const mock = createMockBase();
70
+ const pc = createPauseController();
71
+ const onHalt = vi.fn();
72
+ pc.middleware(mock.base)(vi.fn(), onHalt);
73
+ pc.pause();
74
+ onHalt.mockClear();
75
+ pc.pause();
76
+ expect(mock.latest.disconnect).toHaveBeenCalledTimes(1);
77
+ expect(onHalt).not.toHaveBeenCalled();
78
+ });
79
+ it('pause before the inner provider has been invoked does not fire a halt', () => {
80
+ const mock = createMockBase();
81
+ const pc = createPauseController();
82
+ pc.middleware(mock.base);
83
+ pc.pause();
84
+ expect(pc.isPaused()).toBe(true);
85
+ expect(mock.invocations).toHaveLength(0);
86
+ });
87
+ it('buffers sends while paused and flushes them in order after the post-halt re-invocation + resume', () => {
88
+ const mock = createMockBase();
89
+ const pc = createPauseController();
90
+ const inner = pc.middleware(mock.base);
91
+ const conn = inner(vi.fn(), vi.fn());
92
+ pc.pause();
93
+ conn.send(req(1, 'a'));
94
+ conn.send(req(2, 'b'));
95
+ // simulate the re-invocation that onHalt → getProxy would drive
96
+ inner(vi.fn(), vi.fn());
97
+ expect(mock.invocations).toHaveLength(1); // still paused — no new base connection yet
98
+ pc.resume();
99
+ expect(mock.invocations).toHaveLength(2);
100
+ expect(mock.latest.send).toHaveBeenNthCalledWith(1, req(1, 'a'));
101
+ expect(mock.latest.send).toHaveBeenNthCalledWith(2, req(2, 'b'));
102
+ expect(pc.isPaused()).toBe(false);
103
+ });
104
+ it('re-invocation while paused does not open a new socket', () => {
105
+ const mock = createMockBase();
106
+ const pc = createPauseController();
107
+ const inner = pc.middleware(mock.base);
108
+ inner(vi.fn(), vi.fn());
109
+ pc.pause();
110
+ inner(vi.fn(), vi.fn());
111
+ inner(vi.fn(), vi.fn());
112
+ expect(mock.invocations).toHaveLength(1);
113
+ });
114
+ it('resume without a pending re-invocation connects directly', () => {
115
+ const mock = createMockBase();
116
+ const pc = createPauseController();
117
+ const inner = pc.middleware(mock.base);
118
+ // pause before any real connection exists — no halt fires, so no re-invocation is pending
119
+ pc.pause();
120
+ inner(vi.fn(), vi.fn());
121
+ expect(mock.invocations).toHaveLength(0);
122
+ pc.resume();
123
+ expect(mock.invocations).toHaveLength(1);
124
+ expect(pc.isPaused()).toBe(false);
125
+ });
126
+ it('resume is a no-op when not paused', () => {
127
+ const mock = createMockBase();
128
+ const pc = createPauseController();
129
+ const inner = pc.middleware(mock.base);
130
+ inner(vi.fn(), vi.fn());
131
+ pc.resume();
132
+ expect(mock.invocations).toHaveLength(1);
133
+ expect(pc.isPaused()).toBe(false);
134
+ });
135
+ it('after a re-invocation, inbound messages from the new base reach the new onMessage', () => {
136
+ const mock = createMockBase();
137
+ const pc = createPauseController();
138
+ const inner = pc.middleware(mock.base);
139
+ const firstOnMessage = vi.fn();
140
+ inner(firstOnMessage, vi.fn());
141
+ pc.pause();
142
+ const secondOnMessage = vi.fn();
143
+ inner(secondOnMessage, vi.fn());
144
+ pc.resume();
145
+ const message = { jsonrpc: '2.0', id: 9, result: 'y' };
146
+ mock.latest.onMessage(message);
147
+ expect(secondOnMessage).toHaveBeenCalledWith(message);
148
+ expect(firstOnMessage).not.toHaveBeenCalled();
149
+ });
150
+ it('disconnect tears down and makes subsequent pause/resume no-ops', () => {
151
+ const mock = createMockBase();
152
+ const pc = createPauseController();
153
+ const inner = pc.middleware(mock.base);
154
+ const onHalt = vi.fn();
155
+ const conn = inner(vi.fn(), onHalt);
156
+ conn.disconnect();
157
+ expect(mock.latest.disconnect).toHaveBeenCalledTimes(1);
158
+ pc.pause();
159
+ expect(pc.isPaused()).toBe(false);
160
+ expect(onHalt).not.toHaveBeenCalled();
161
+ pc.resume();
162
+ expect(mock.invocations).toHaveLength(1);
163
+ });
164
+ it('disconnect drops buffered sends so a later reconnect does not flush stale messages', () => {
165
+ const mock = createMockBase();
166
+ const pc = createPauseController();
167
+ const inner = pc.middleware(mock.base);
168
+ const conn = inner(vi.fn(), vi.fn());
169
+ pc.pause();
170
+ conn.send(req(1, 'lost'));
171
+ conn.disconnect();
172
+ // a new controller wouldn't share buffer, but even a fresh inner call on the same
173
+ // controller (as might happen if getProxy retries) must not replay pre-disconnect buffer
174
+ inner(vi.fn(), vi.fn());
175
+ expect(mock.latest.send).not.toHaveBeenCalled();
176
+ });
177
+ });
@@ -1,6 +1,12 @@
1
- const isSubscribeMethod = (method) => method === 'chainHead_v1_follow' ||
2
- (method.toLowerCase().includes('subscribe') && !method.toLowerCase().includes('unsubscribe'));
3
- const isUnsubscribeMethod = (method) => method === 'chainHead_v1_unfollow' || method.toLowerCase().includes('unsubscribe');
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');
4
10
  export const withSubscriptionReplay = (provider, onReconnect) => onMessage => {
5
11
  // request id → request object (sent, awaiting server subscription ID)
6
12
  const pendingSubscriptions = new Map();
@@ -8,11 +14,7 @@ export const withSubscriptionReplay = (provider, onReconnect) => onMessage => {
8
14
  const activeSubscriptions = new Map();
9
15
  const conn = provider(message => {
10
16
  // Response with a string result means it's a subscription confirmation
11
- if ('id' in message &&
12
- message.id &&
13
- !('method' in message) &&
14
- 'result' in message &&
15
- typeof message.result === 'string') {
17
+ if (message.id && isResponse(message) && 'result' in message && typeof message.result === 'string') {
16
18
  const id = message.id;
17
19
  const pending = pendingSubscriptions.get(id);
18
20
  if (pending !== undefined) {
@@ -65,7 +65,7 @@ describe('withSubscriptionReplay', () => {
65
65
  const mock = createMockProvider();
66
66
  const control = createReconnectControl();
67
67
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
68
- conn.send(req(1, 'chain_subscribeNewHeads'));
68
+ conn.send(req(1, 'statement_subscribeStatement'));
69
69
  // intentionally no simulateMessage — subscription not confirmed by server
70
70
  mock.send.mockClear();
71
71
  control.triggerReconnect();
@@ -75,7 +75,7 @@ describe('withSubscriptionReplay', () => {
75
75
  const mock = createMockProvider();
76
76
  const control = createReconnectControl();
77
77
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
78
- const subscribeMsg = req(1, 'chain_subscribeNewHeads');
78
+ const subscribeMsg = req(1, 'statement_subscribeStatement');
79
79
  conn.send(subscribeMsg);
80
80
  mock.simulateMessage(res(1, 'sub-id-1'));
81
81
  mock.send.mockClear();
@@ -86,7 +86,7 @@ describe('withSubscriptionReplay', () => {
86
86
  const mock = createMockProvider();
87
87
  const control = createReconnectControl();
88
88
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
89
- const msg1 = req(1, 'chain_subscribeNewHeads');
89
+ const msg1 = req(1, 'statement_subscribeStatement');
90
90
  const msg2 = req(2, 'state_subscribeStorage', [[]]);
91
91
  conn.send(msg1);
92
92
  mock.simulateMessage(res(1, 'sub-id-1'));
@@ -102,7 +102,7 @@ describe('withSubscriptionReplay', () => {
102
102
  const mock = createMockProvider();
103
103
  const control = createReconnectControl();
104
104
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
105
- const subscribeMsg = req(1, 'chain_subscribeNewHeads');
105
+ const subscribeMsg = req(1, 'statement_subscribeStatement');
106
106
  conn.send(subscribeMsg);
107
107
  mock.simulateMessage(res(1, 'old-sub-id'));
108
108
  // First reconnect — replays and moves to pending
@@ -110,7 +110,7 @@ describe('withSubscriptionReplay', () => {
110
110
  // Server assigns new subscription ID
111
111
  mock.simulateMessage(res(1, 'new-sub-id'));
112
112
  // Unsubscribing with the old ID should have no effect (old ID is no longer tracked)
113
- conn.send(req(2, 'chain_unsubscribeNewHeads', ['old-sub-id']));
113
+ conn.send(req(2, 'statement_unsubscribeStatement', ['old-sub-id']));
114
114
  mock.send.mockClear();
115
115
  // Second reconnect — subscription is still active (new-sub-id was not removed)
116
116
  control.triggerReconnect();
@@ -120,9 +120,9 @@ describe('withSubscriptionReplay', () => {
120
120
  const mock = createMockProvider();
121
121
  const control = createReconnectControl();
122
122
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
123
- conn.send(req(1, 'chain_subscribeNewHeads'));
123
+ conn.send(req(1, 'statement_subscribeStatement'));
124
124
  mock.simulateMessage(res(1, 'sub-id-1'));
125
- conn.send(req(2, 'chain_unsubscribeNewHeads', ['sub-id-1']));
125
+ conn.send(req(2, 'statement_unsubscribeStatement', ['sub-id-1']));
126
126
  mock.send.mockClear();
127
127
  control.triggerReconnect();
128
128
  expect(mock.send).not.toHaveBeenCalled();
@@ -131,7 +131,7 @@ describe('withSubscriptionReplay', () => {
131
131
  const mock = createMockProvider();
132
132
  const control = createReconnectControl();
133
133
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
134
- conn.send(req(1, 'chain_subscribeNewHeads'));
134
+ conn.send(req(1, 'statement_subscribeStatement'));
135
135
  mock.simulateMessage(res(1, 'sub-id-1'));
136
136
  conn.disconnect();
137
137
  expect(mock.disconnect).toHaveBeenCalledTimes(1);
@@ -141,48 +141,15 @@ describe('withSubscriptionReplay', () => {
141
141
  control.triggerReconnect();
142
142
  expect(mock.send).not.toHaveBeenCalled();
143
143
  });
144
- it('does not replay chainHead_v1_follow when pending (no server response yet)', () => {
145
- const mock = createMockProvider();
146
- const control = createReconnectControl();
147
- const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
148
- conn.send(req(1, 'chainHead_v1_follow', [true]));
149
- // intentionally no simulateMessage — subscription not confirmed by server
150
- mock.send.mockClear();
151
- control.triggerReconnect();
152
- expect(mock.send).not.toHaveBeenCalled();
153
- });
154
- it('replays active chainHead_v1_follow subscription on reconnect', () => {
155
- const mock = createMockProvider();
156
- const control = createReconnectControl();
157
- const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
158
- const followMsg = req(1, 'chainHead_v1_follow', [true]);
159
- conn.send(followMsg);
160
- mock.simulateMessage(res(1, 'follow-sub-id'));
161
- mock.send.mockClear();
162
- control.triggerReconnect();
163
- expect(mock.send).toHaveBeenCalledWith(followMsg);
164
- });
165
- it('does not replay chainHead_v1_follow after chainHead_v1_unfollow', () => {
166
- const mock = createMockProvider();
167
- const control = createReconnectControl();
168
- const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
169
- conn.send(req(1, 'chainHead_v1_follow', [true]));
170
- mock.simulateMessage(res(1, 'follow-sub-id'));
171
- conn.send(req(2, 'chainHead_v1_unfollow', ['follow-sub-id']));
172
- mock.send.mockClear();
173
- control.triggerReconnect();
174
- expect(mock.send).not.toHaveBeenCalled();
175
- });
176
- it('keeps chainHead_v1_follow active when chainHead_v1_unfollow uses wrong sub-id', () => {
144
+ it('should ignore chain subscriptions', () => {
177
145
  const mock = createMockProvider();
178
146
  const control = createReconnectControl();
179
147
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
180
- const followMsg = req(1, 'chainHead_v1_follow', [true]);
181
- conn.send(followMsg);
182
- mock.simulateMessage(res(1, 'follow-sub-id'));
183
- conn.send(req(2, 'chainHead_v1_unfollow', ['wrong-sub-id']));
148
+ const subscribeMsg = req(1, 'chain_subscribeNewHeads');
149
+ conn.send(subscribeMsg);
150
+ mock.simulateMessage(res(1, 'sub-id-1'));
184
151
  mock.send.mockClear();
185
152
  control.triggerReconnect();
186
- expect(mock.send).toHaveBeenCalledWith(followMsg);
153
+ expect(mock.send).not.toHaveBeenCalledWith(subscribeMsg);
187
154
  });
188
155
  });
package/dist/types.d.ts CHANGED
@@ -4,9 +4,10 @@ export type ChainConfig = {
4
4
  };
5
5
  export type ConnectionStatus = 'connecting' | 'connected' | 'disconnected';
6
6
  export type BranchedProvider = {
7
- branch(onDisconnect?: VoidFunction): JsonRpcProvider;
7
+ branch(onHalt?: VoidFunction): JsonRpcProvider;
8
8
  };
9
9
  export type PooledClient = {
10
10
  client: PolkadotClient;
11
11
  provider: BranchedProvider;
12
+ rootProvider: JsonRpcProvider;
12
13
  };
@@ -1,6 +1,15 @@
1
+ import type { SocketLoggerFn } from '@polkadot-api/ws-provider';
1
2
  import type { JsonRpcProvider } from 'polkadot-api';
2
3
  import type { ConnectionStatus } from './types.js';
4
+ export type PausableJsonRpcProvider = JsonRpcProvider & {
5
+ pause(): void;
6
+ resume(): void;
7
+ };
8
+ export declare const isPausable: (provider: JsonRpcProvider) => provider is PausableJsonRpcProvider;
3
9
  export declare const createWsJsonRpcProvider: (options: {
4
10
  endpoints: string[];
5
11
  onStatusChanged?: (status: ConnectionStatus) => void;
6
- }) => JsonRpcProvider;
12
+ websocketClass?: typeof WebSocket;
13
+ heartbeatTimeout?: number;
14
+ logger?: SocketLoggerFn;
15
+ }) => PausableJsonRpcProvider;
@@ -1,6 +1,17 @@
1
- import { WsEvent, getWsProvider } from 'polkadot-api/ws';
1
+ import { WsEvent, getWsProvider } from '@polkadot-api/ws-provider';
2
2
  import { noop } from './helpers.js';
3
+ import { createPauseController } from './pauseController.js';
3
4
  import { withSubscriptionReplay } from './subscriptionReplayProvider.js';
5
+ export const isPausable = (provider) => {
6
+ const maybe = provider;
7
+ return typeof maybe.pause === 'function' && typeof maybe.resume === 'function';
8
+ };
9
+ const STATUS_BY_WS_EVENT = {
10
+ [WsEvent.CONNECTING]: 'connecting',
11
+ [WsEvent.CONNECTED]: 'connected',
12
+ [WsEvent.ERROR]: 'disconnected',
13
+ [WsEvent.CLOSE]: 'disconnected',
14
+ };
4
15
  export const createWsJsonRpcProvider = (options) => {
5
16
  let notifyReconnect = noop;
6
17
  const onReconnect = (cb) => {
@@ -9,27 +20,23 @@ export const createWsJsonRpcProvider = (options) => {
9
20
  notifyReconnect = noop;
10
21
  };
11
22
  };
12
- return withSubscriptionReplay(getWsProvider(options.endpoints, {
13
- heartbeatTimeout: Number.POSITIVE_INFINITY,
23
+ const pauseController = createPauseController();
24
+ const baseProvider = getWsProvider(options.endpoints, {
25
+ logger: options.logger,
26
+ heartbeatTimeout: options.heartbeatTimeout,
27
+ middleware: inner => pauseController.middleware(inner),
28
+ websocketClass: options.websocketClass,
14
29
  onStatusChanged: event => {
15
- let status;
16
- switch (event.type) {
17
- case WsEvent.CONNECTING:
18
- status = 'connecting';
19
- break;
20
- case WsEvent.CONNECTED:
21
- notifyReconnect();
22
- status = 'connected';
23
- break;
24
- case WsEvent.ERROR:
25
- case WsEvent.CLOSE:
26
- status = 'disconnected';
27
- break;
28
- default:
29
- status = 'disconnected';
30
- break;
30
+ const status = STATUS_BY_WS_EVENT[event.type];
31
+ if (status === 'connected') {
32
+ notifyReconnect();
31
33
  }
32
34
  options.onStatusChanged?.(status);
33
35
  },
34
- }), onReconnect);
36
+ });
37
+ const replayProvider = withSubscriptionReplay(baseProvider, onReconnect);
38
+ return Object.assign(replayProvider, {
39
+ pause: () => pauseController.pause(),
40
+ resume: () => pauseController.resume(),
41
+ });
35
42
  };
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.0",
4
+ "version": "0.7.1-0",
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,9 @@
25
25
  "README.md"
26
26
  ],
27
27
  "dependencies": {
28
- "@novasamatech/storage-adapter": "0.7.0",
28
+ "@novasamatech/storage-adapter": "0.7.1-0",
29
+ "@polkadot-api/ws-provider": "^0.9.0",
30
+ "@polkadot-api/json-rpc-provider": "^0.2.0",
29
31
  "@polkadot-api/json-rpc-provider-proxy": "^0.4.0",
30
32
  "nanoevents": "^9.1.0",
31
33
  "polkadot-api": ">=2"