@novasamatech/host-substrate-chain-connection 0.6.18 → 0.7.0-1

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.
@@ -1,4 +1,4 @@
1
- import type { JsonRpcProvider } from '@polkadot-api/json-rpc-provider';
1
+ import type { JsonRpcProvider } from 'polkadot-api';
2
2
  import type { BranchedProvider } from './types.js';
3
3
  type Params = {
4
4
  enhanceBranch?(branch: JsonRpcProvider): JsonRpcProvider;
@@ -3,8 +3,10 @@ import { createBranchedProvider } from './branchedProvider.js';
3
3
  const createMockProvider = () => {
4
4
  const send = vi.fn();
5
5
  const disconnect = vi.fn();
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
7
  let onMessage = null;
7
8
  const provider = cb => {
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
10
  onMessage = cb;
9
11
  return { send, disconnect };
10
12
  };
@@ -12,6 +14,7 @@ const createMockProvider = () => {
12
14
  provider,
13
15
  send,
14
16
  disconnect,
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
18
  simulateMessage: (msg) => onMessage?.(msg),
16
19
  };
17
20
  };
@@ -21,8 +24,8 @@ describe('createBranchedProvider', () => {
21
24
  const branched = createBranchedProvider(mock.provider);
22
25
  const onMessage = vi.fn();
23
26
  branched.branch()(onMessage);
24
- mock.simulateMessage('{"id":1}');
25
- expect(onMessage).toHaveBeenCalledWith('{"id":1}');
27
+ mock.simulateMessage({ id: 1 });
28
+ expect(onMessage).toHaveBeenCalledWith({ id: 1 });
26
29
  });
27
30
  it('reuses connection for multiple branches', () => {
28
31
  const mock = createMockProvider();
@@ -39,9 +42,9 @@ describe('createBranchedProvider', () => {
39
42
  const cb2 = vi.fn();
40
43
  branched.branch()(cb1);
41
44
  branched.branch()(cb2);
42
- mock.simulateMessage('{"id":1}');
43
- expect(cb1).toHaveBeenCalledWith('{"id":1}');
44
- expect(cb2).toHaveBeenCalledWith('{"id":1}');
45
+ mock.simulateMessage({ id: 1 });
46
+ expect(cb1).toHaveBeenCalledWith({ id: 1 });
47
+ expect(cb2).toHaveBeenCalledWith({ id: 1 });
45
48
  });
46
49
  it('disconnected branch stops receiving messages', () => {
47
50
  const mock = createMockProvider();
@@ -50,7 +53,7 @@ describe('createBranchedProvider', () => {
50
53
  const conn1 = branched.branch()(cb1);
51
54
  branched.branch()(vi.fn()); // keep connection alive
52
55
  conn1.disconnect();
53
- mock.simulateMessage('{"id":1}');
56
+ mock.simulateMessage({ id: 1 });
54
57
  expect(cb1).not.toHaveBeenCalled();
55
58
  });
56
59
  it('disconnecting one branch does not affect others', () => {
@@ -60,8 +63,8 @@ describe('createBranchedProvider', () => {
60
63
  const cb2 = vi.fn();
61
64
  branched.branch()(cb2);
62
65
  conn1.disconnect();
63
- mock.simulateMessage('{"id":1}');
64
- expect(cb2).toHaveBeenCalledWith('{"id":1}');
66
+ mock.simulateMessage({ id: 1 });
67
+ expect(cb2).toHaveBeenCalledWith({ id: 1 });
65
68
  expect(mock.disconnect).not.toHaveBeenCalled();
66
69
  });
67
70
  it('disconnecting last branch tears down underlying connection', () => {
@@ -96,8 +99,9 @@ describe('createBranchedProvider', () => {
96
99
  const mock = createMockProvider();
97
100
  const branched = createBranchedProvider(mock.provider);
98
101
  const conn = branched.branch()(vi.fn());
99
- conn.send('{"method":"test"}');
100
- expect(mock.send).toHaveBeenCalledWith('{"method":"test"}');
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ conn.send({ method: 'test' });
104
+ expect(mock.send).toHaveBeenCalledWith({ method: 'test' });
101
105
  });
102
106
  it('calls enhanceBranch for each new branch independently', () => {
103
107
  const mock = createMockProvider();
@@ -1,5 +1,4 @@
1
- import type { JsonRpcProvider } from '@polkadot-api/json-rpc-provider';
2
- import type { PolkadotClient } from 'polkadot-api';
1
+ import type { JsonRpcProvider, PolkadotClient } from 'polkadot-api';
3
2
  import { createClient } from 'polkadot-api';
4
3
  import type { ChainConfig, ConnectionStatus } from './types.js';
5
4
  export type ChainConnectionConfig<C extends ChainConfig, T = PolkadotClient> = {
@@ -104,9 +104,17 @@ export const createChainConnection = ({ resolve, clientOptions, createProvider,
104
104
  }
105
105
  },
106
106
  getProvider(chain) {
107
- return getSyncProvider(async () => {
108
- const { pooled, unlock } = await rawAcquire(chain);
109
- return pooled.provider.branch(unlock);
107
+ return getSyncProvider(onResult => {
108
+ rawAcquire(chain)
109
+ .then(({ pooled, unlock }) => {
110
+ onResult((onMessage, _onHalt) => pooled.provider.branch(unlock)(onMessage));
111
+ })
112
+ .catch(() => {
113
+ onResult(null);
114
+ });
115
+ return () => {
116
+ /* empty */
117
+ };
110
118
  });
111
119
  },
112
120
  status(genesisHash) {
@@ -7,11 +7,14 @@ const createMockClient = () => ({ destroy: vi.fn() });
7
7
  const createMockProvider = () => {
8
8
  const send = vi.fn();
9
9
  const disconnect = vi.fn();
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
11
  let onMessage = null;
11
12
  const provider = cb => {
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
14
  onMessage = cb;
13
15
  return { send, disconnect };
14
16
  };
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
18
  return { provider, send, disconnect, simulateMessage: (msg) => onMessage?.(msg) };
16
19
  };
17
20
  const testChain = (id) => ({ genesisHash: id });
@@ -1,2 +1,2 @@
1
- import type { JsonRpcProvider } from '@polkadot-api/json-rpc-provider';
1
+ import type { JsonRpcProvider } from 'polkadot-api';
2
2
  export declare const withSubscriptionReplay: (provider: JsonRpcProvider, onReconnect: (callback: VoidFunction) => VoidFunction) => JsonRpcProvider;
@@ -2,17 +2,22 @@ const isSubscribeMethod = (method) => method === 'chainHead_v1_follow' ||
2
2
  (method.toLowerCase().includes('subscribe') && !method.toLowerCase().includes('unsubscribe'));
3
3
  const isUnsubscribeMethod = (method) => method === 'chainHead_v1_unfollow' || method.toLowerCase().includes('unsubscribe');
4
4
  export const withSubscriptionReplay = (provider, onReconnect) => onMessage => {
5
- // request id → raw JSON message (sent, awaiting server subscription ID)
5
+ // request id → request object (sent, awaiting server subscription ID)
6
6
  const pendingSubscriptions = new Map();
7
- // server subscription ID → raw JSON message (confirmed by server)
7
+ // server subscription ID → request object (confirmed by server)
8
8
  const activeSubscriptions = new Map();
9
9
  const conn = provider(message => {
10
- const parsed = JSON.parse(message);
11
- if (parsed.id !== undefined && typeof parsed.result === 'string') {
12
- const pending = pendingSubscriptions.get(parsed.id);
10
+ // 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') {
16
+ const id = message.id;
17
+ const pending = pendingSubscriptions.get(id);
13
18
  if (pending !== undefined) {
14
- pendingSubscriptions.delete(parsed.id);
15
- activeSubscriptions.set(parsed.result, { id: parsed.id, payload: pending });
19
+ pendingSubscriptions.delete(id);
20
+ activeSubscriptions.set(message.result, { id, payload: pending });
16
21
  }
17
22
  }
18
23
  onMessage(message);
@@ -27,7 +32,7 @@ export const withSubscriptionReplay = (provider, onReconnect) => onMessage => {
27
32
  });
28
33
  return {
29
34
  send(message) {
30
- const { method, id, params } = JSON.parse(message);
35
+ const { method, id, params } = message;
31
36
  if (method) {
32
37
  if (isSubscribeMethod(method)) {
33
38
  if (id !== undefined)
@@ -3,8 +3,10 @@ import { withSubscriptionReplay } from './subscriptionReplayProvider.js';
3
3
  const createMockProvider = () => {
4
4
  const send = vi.fn();
5
5
  const disconnect = vi.fn();
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
7
  let onMessage = null;
7
8
  const provider = cb => {
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
10
  onMessage = cb;
9
11
  return { send, disconnect };
10
12
  };
@@ -12,6 +14,7 @@ const createMockProvider = () => {
12
14
  provider,
13
15
  send,
14
16
  disconnect,
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
15
18
  simulateMessage: (msg) => onMessage?.(msg),
16
19
  };
17
20
  };
@@ -30,27 +33,30 @@ const createReconnectControl = () => {
30
33
  unsubscribe,
31
34
  };
32
35
  };
36
+ // Helper to create typed request objects
37
+ const req = (id, method, params = []) => ({ jsonrpc: '2.0', id, method, params });
38
+ const res = (id, result) => ({ jsonrpc: '2.0', id, result });
33
39
  describe('withSubscriptionReplay', () => {
34
40
  it('forwards all incoming messages to onMessage unmodified', () => {
35
41
  const mock = createMockProvider();
36
42
  const { onReconnect } = createReconnectControl();
37
43
  const onMessage = vi.fn();
38
44
  withSubscriptionReplay(mock.provider, onReconnect)(onMessage);
39
- mock.simulateMessage('{"jsonrpc":"2.0","id":1,"result":"0xabc"}');
40
- expect(onMessage).toHaveBeenCalledWith('{"jsonrpc":"2.0","id":1,"result":"0xabc"}');
45
+ mock.simulateMessage(res(1, '0xabc'));
46
+ expect(onMessage).toHaveBeenCalledWith(res(1, '0xabc'));
41
47
  });
42
48
  it('forwards send calls to the underlying provider', () => {
43
49
  const mock = createMockProvider();
44
50
  const { onReconnect } = createReconnectControl();
45
51
  const conn = withSubscriptionReplay(mock.provider, onReconnect)(vi.fn());
46
- conn.send('{"id":1,"method":"chain_getBlock","params":[]}');
47
- expect(mock.send).toHaveBeenCalledWith('{"id":1,"method":"chain_getBlock","params":[]}');
52
+ conn.send(req(1, 'chain_getBlock'));
53
+ expect(mock.send).toHaveBeenCalledWith(req(1, 'chain_getBlock'));
48
54
  });
49
55
  it('does not replay non-subscription requests on reconnect', () => {
50
56
  const mock = createMockProvider();
51
57
  const control = createReconnectControl();
52
58
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
53
- conn.send('{"id":1,"method":"chain_getBlock","params":[]}');
59
+ conn.send(req(1, 'chain_getBlock'));
54
60
  mock.send.mockClear();
55
61
  control.triggerReconnect();
56
62
  expect(mock.send).not.toHaveBeenCalled();
@@ -59,7 +65,7 @@ describe('withSubscriptionReplay', () => {
59
65
  const mock = createMockProvider();
60
66
  const control = createReconnectControl();
61
67
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
62
- conn.send('{"id":1,"method":"chain_subscribeNewHeads","params":[]}');
68
+ conn.send(req(1, 'chain_subscribeNewHeads'));
63
69
  // intentionally no simulateMessage — subscription not confirmed by server
64
70
  mock.send.mockClear();
65
71
  control.triggerReconnect();
@@ -69,9 +75,9 @@ describe('withSubscriptionReplay', () => {
69
75
  const mock = createMockProvider();
70
76
  const control = createReconnectControl();
71
77
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
72
- const subscribeMsg = '{"id":1,"method":"chain_subscribeNewHeads","params":[]}';
78
+ const subscribeMsg = req(1, 'chain_subscribeNewHeads');
73
79
  conn.send(subscribeMsg);
74
- mock.simulateMessage('{"jsonrpc":"2.0","id":1,"result":"sub-id-1"}');
80
+ mock.simulateMessage(res(1, 'sub-id-1'));
75
81
  mock.send.mockClear();
76
82
  control.triggerReconnect();
77
83
  expect(mock.send).toHaveBeenCalledWith(subscribeMsg);
@@ -80,12 +86,12 @@ describe('withSubscriptionReplay', () => {
80
86
  const mock = createMockProvider();
81
87
  const control = createReconnectControl();
82
88
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
83
- const msg1 = '{"id":1,"method":"chain_subscribeNewHeads","params":[]}';
84
- const msg2 = '{"id":2,"method":"state_subscribeStorage","params":[[]]}';
89
+ const msg1 = req(1, 'chain_subscribeNewHeads');
90
+ const msg2 = req(2, 'state_subscribeStorage', [[]]);
85
91
  conn.send(msg1);
86
- mock.simulateMessage('{"jsonrpc":"2.0","id":1,"result":"sub-id-1"}');
92
+ mock.simulateMessage(res(1, 'sub-id-1'));
87
93
  conn.send(msg2);
88
- mock.simulateMessage('{"jsonrpc":"2.0","id":2,"result":"sub-id-2"}');
94
+ mock.simulateMessage(res(2, 'sub-id-2'));
89
95
  mock.send.mockClear();
90
96
  control.triggerReconnect();
91
97
  expect(mock.send).toHaveBeenCalledTimes(2);
@@ -96,15 +102,15 @@ describe('withSubscriptionReplay', () => {
96
102
  const mock = createMockProvider();
97
103
  const control = createReconnectControl();
98
104
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
99
- const subscribeMsg = '{"id":1,"method":"chain_subscribeNewHeads","params":[]}';
105
+ const subscribeMsg = req(1, 'chain_subscribeNewHeads');
100
106
  conn.send(subscribeMsg);
101
- mock.simulateMessage('{"jsonrpc":"2.0","id":1,"result":"old-sub-id"}');
107
+ mock.simulateMessage(res(1, 'old-sub-id'));
102
108
  // First reconnect — replays and moves to pending
103
109
  control.triggerReconnect();
104
110
  // Server assigns new subscription ID
105
- mock.simulateMessage('{"jsonrpc":"2.0","id":1,"result":"new-sub-id"}');
111
+ mock.simulateMessage(res(1, 'new-sub-id'));
106
112
  // Unsubscribing with the old ID should have no effect (old ID is no longer tracked)
107
- conn.send('{"id":2,"method":"chain_unsubscribeNewHeads","params":["old-sub-id"]}');
113
+ conn.send(req(2, 'chain_unsubscribeNewHeads', ['old-sub-id']));
108
114
  mock.send.mockClear();
109
115
  // Second reconnect — subscription is still active (new-sub-id was not removed)
110
116
  control.triggerReconnect();
@@ -114,9 +120,9 @@ describe('withSubscriptionReplay', () => {
114
120
  const mock = createMockProvider();
115
121
  const control = createReconnectControl();
116
122
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
117
- conn.send('{"id":1,"method":"chain_subscribeNewHeads","params":[]}');
118
- mock.simulateMessage('{"jsonrpc":"2.0","id":1,"result":"sub-id-1"}');
119
- conn.send('{"id":2,"method":"chain_unsubscribeNewHeads","params":["sub-id-1"]}');
123
+ conn.send(req(1, 'chain_subscribeNewHeads'));
124
+ mock.simulateMessage(res(1, 'sub-id-1'));
125
+ conn.send(req(2, 'chain_unsubscribeNewHeads', ['sub-id-1']));
120
126
  mock.send.mockClear();
121
127
  control.triggerReconnect();
122
128
  expect(mock.send).not.toHaveBeenCalled();
@@ -125,8 +131,8 @@ describe('withSubscriptionReplay', () => {
125
131
  const mock = createMockProvider();
126
132
  const control = createReconnectControl();
127
133
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
128
- conn.send('{"id":1,"method":"chain_subscribeNewHeads","params":[]}');
129
- mock.simulateMessage('{"jsonrpc":"2.0","id":1,"result":"sub-id-1"}');
134
+ conn.send(req(1, 'chain_subscribeNewHeads'));
135
+ mock.simulateMessage(res(1, 'sub-id-1'));
130
136
  conn.disconnect();
131
137
  expect(mock.disconnect).toHaveBeenCalledTimes(1);
132
138
  expect(control.unsubscribe).toHaveBeenCalledTimes(1);
@@ -139,7 +145,7 @@ describe('withSubscriptionReplay', () => {
139
145
  const mock = createMockProvider();
140
146
  const control = createReconnectControl();
141
147
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
142
- conn.send('{"id":1,"method":"chainHead_v1_follow","params":[true]}');
148
+ conn.send(req(1, 'chainHead_v1_follow', [true]));
143
149
  // intentionally no simulateMessage — subscription not confirmed by server
144
150
  mock.send.mockClear();
145
151
  control.triggerReconnect();
@@ -149,9 +155,9 @@ describe('withSubscriptionReplay', () => {
149
155
  const mock = createMockProvider();
150
156
  const control = createReconnectControl();
151
157
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
152
- const followMsg = '{"id":1,"method":"chainHead_v1_follow","params":[true]}';
158
+ const followMsg = req(1, 'chainHead_v1_follow', [true]);
153
159
  conn.send(followMsg);
154
- mock.simulateMessage('{"jsonrpc":"2.0","id":1,"result":"follow-sub-id"}');
160
+ mock.simulateMessage(res(1, 'follow-sub-id'));
155
161
  mock.send.mockClear();
156
162
  control.triggerReconnect();
157
163
  expect(mock.send).toHaveBeenCalledWith(followMsg);
@@ -160,9 +166,9 @@ describe('withSubscriptionReplay', () => {
160
166
  const mock = createMockProvider();
161
167
  const control = createReconnectControl();
162
168
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
163
- conn.send('{"id":1,"method":"chainHead_v1_follow","params":[true]}');
164
- mock.simulateMessage('{"jsonrpc":"2.0","id":1,"result":"follow-sub-id"}');
165
- conn.send('{"id":2,"method":"chainHead_v1_unfollow","params":["follow-sub-id"]}');
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']));
166
172
  mock.send.mockClear();
167
173
  control.triggerReconnect();
168
174
  expect(mock.send).not.toHaveBeenCalled();
@@ -171,10 +177,10 @@ describe('withSubscriptionReplay', () => {
171
177
  const mock = createMockProvider();
172
178
  const control = createReconnectControl();
173
179
  const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
174
- const followMsg = '{"id":1,"method":"chainHead_v1_follow","params":[true]}';
180
+ const followMsg = req(1, 'chainHead_v1_follow', [true]);
175
181
  conn.send(followMsg);
176
- mock.simulateMessage('{"jsonrpc":"2.0","id":1,"result":"follow-sub-id"}');
177
- conn.send('{"id":2,"method":"chainHead_v1_unfollow","params":["wrong-sub-id"]}');
182
+ mock.simulateMessage(res(1, 'follow-sub-id'));
183
+ conn.send(req(2, 'chainHead_v1_unfollow', ['wrong-sub-id']));
178
184
  mock.send.mockClear();
179
185
  control.triggerReconnect();
180
186
  expect(mock.send).toHaveBeenCalledWith(followMsg);
package/dist/types.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import type { JsonRpcProvider } from '@polkadot-api/json-rpc-provider';
2
- import type { PolkadotClient } from 'polkadot-api';
1
+ import type { JsonRpcProvider, PolkadotClient } from 'polkadot-api';
3
2
  export type ChainConfig = {
4
3
  genesisHash: string;
5
4
  };
@@ -1,4 +1,4 @@
1
- import type { JsonRpcProvider } from '@polkadot-api/json-rpc-provider';
1
+ import type { JsonRpcProvider } from 'polkadot-api';
2
2
  import type { ConnectionStatus } from './types.js';
3
3
  export declare const createWsJsonRpcProvider: (options: {
4
4
  endpoints: string[];
@@ -1,4 +1,4 @@
1
- import { WsEvent, getWsProvider } from '@polkadot-api/ws-provider';
1
+ import { WsEvent, getWsProvider } from 'polkadot-api/ws';
2
2
  import { noop } from './helpers.js';
3
3
  import { withSubscriptionReplay } from './subscriptionReplayProvider.js';
4
4
  export const createWsJsonRpcProvider = (options) => {
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.6.18",
4
+ "version": "0.7.0-1",
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,12 +25,10 @@
25
25
  "README.md"
26
26
  ],
27
27
  "dependencies": {
28
- "@novasamatech/storage-adapter": "0.6.18",
29
- "@polkadot-api/json-rpc-provider": "^0.0.4",
30
- "@polkadot-api/json-rpc-provider-proxy": "^0.2.8",
31
- "@polkadot-api/ws-provider": "^0.7.5",
28
+ "@novasamatech/storage-adapter": "0.7.0-1",
29
+ "@polkadot-api/json-rpc-provider-proxy": "^0.4.0",
32
30
  "nanoevents": "^9.1.0",
33
- "polkadot-api": "^1.23.3"
31
+ "polkadot-api": ">=2"
34
32
  },
35
33
  "publishConfig": {
36
34
  "access": "public"