@novasamatech/host-substrate-chain-connection 0.10.1 → 0.10.2
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/dist/connectionPool.d.ts +16 -0
- package/dist/connectionPool.js +37 -6
- package/dist/connectionPool.spec.js +122 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/restartableProvider.d.ts +33 -0
- package/dist/restartableProvider.js +107 -0
- package/dist/restartableProvider.spec.d.ts +1 -0
- package/dist/restartableProvider.spec.js +192 -0
- package/dist/restartableProviderWsIntegration.spec.d.ts +1 -0
- package/dist/restartableProviderWsIntegration.spec.js +80 -0
- package/dist/subscriptionReplayProvider.js +10 -9
- package/dist/subscriptionReplayProvider.spec.js +19 -1
- package/dist/types.d.ts +2 -1
- package/package.json +2 -2
package/dist/connectionPool.d.ts
CHANGED
|
@@ -25,5 +25,21 @@ export type ChainConnection<C extends ChainConfig, T = PolkadotClient> = {
|
|
|
25
25
|
*/
|
|
26
26
|
pauseAll(): void;
|
|
27
27
|
resumeAll(): void;
|
|
28
|
+
/**
|
|
29
|
+
* Rebuild the transport of the chains named by `genesisHashes`, or of every
|
|
30
|
+
* chain currently held when it is omitted. `createProvider` runs again, so a
|
|
31
|
+
* host that picks its transport from settings (a light client versus RPC
|
|
32
|
+
* nodes, say) applies the new choice here.
|
|
33
|
+
*
|
|
34
|
+
* Pooled clients, resolved apis and refcounts all survive: consumers keep the
|
|
35
|
+
* references they already hold and see the same interruption a dropped socket
|
|
36
|
+
* would have caused. A chain nobody holds is skipped — one the pool never
|
|
37
|
+
* connected to, and one still waiting out `destroyDelay` — and picks the new
|
|
38
|
+
* transport up when it is next acquired.
|
|
39
|
+
*
|
|
40
|
+
* The old transports close at once, the new ones come up on the reconnect
|
|
41
|
+
* backoff; see `restart` in `createRestartableProvider`.
|
|
42
|
+
*/
|
|
43
|
+
reconnect(genesisHashes?: readonly string[]): void;
|
|
28
44
|
};
|
|
29
45
|
export declare const createChainConnection: <C extends ChainConfig, T = PolkadotClient>({ resolve, clientOptions, createProvider, createClient: makeClient, destroyDelay, }: ChainConnectionConfig<C, T>) => ChainConnection<C, T>;
|
package/dist/connectionPool.js
CHANGED
|
@@ -3,7 +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 {
|
|
6
|
+
import { createRestartableProvider } from './restartableProvider.js';
|
|
7
7
|
export const createChainConnection = ({ resolve, clientOptions, createProvider, createClient: makeClient = createClient, destroyDelay = 0, }) => {
|
|
8
8
|
const connections = createConnectionManager();
|
|
9
9
|
const refCounter = createRefCounter();
|
|
@@ -23,7 +23,28 @@ export const createChainConnection = ({ resolve, clientOptions, createProvider,
|
|
|
23
23
|
const existing = existingClients.get(chain.genesisHash);
|
|
24
24
|
if (existing)
|
|
25
25
|
return existing;
|
|
26
|
-
|
|
26
|
+
// A provider replaced by `reconnect` can still report a trailing
|
|
27
|
+
// `disconnected` while its successor is already connecting, so status is
|
|
28
|
+
// scoped to the generation that produced it and stale updates are dropped.
|
|
29
|
+
let generation = 0;
|
|
30
|
+
const rootProvider = createRestartableProvider(() => {
|
|
31
|
+
const current = ++generation;
|
|
32
|
+
const update = (status) => {
|
|
33
|
+
if (current === generation)
|
|
34
|
+
connections.update(chain.genesisHash, status);
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
return createProvider(chain, update);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
// The bump above already muted the previous transport's updates, so a
|
|
41
|
+
// factory that throws would otherwise leave the chain reporting the
|
|
42
|
+
// status of a transport that no longer exists. `createRestartableProvider`
|
|
43
|
+
// retries, and the next attempt reports for itself.
|
|
44
|
+
update('disconnected');
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
27
48
|
const branchedProvider = createBranchedProvider(rootProvider);
|
|
28
49
|
const client = makeClient(branchedProvider.branch(), clientOptions?.(chain));
|
|
29
50
|
const pooled = { client, provider: branchedProvider, rootProvider };
|
|
@@ -142,14 +163,24 @@ export const createChainConnection = ({ resolve, clientOptions, createProvider,
|
|
|
142
163
|
},
|
|
143
164
|
pauseAll() {
|
|
144
165
|
for (const { rootProvider } of existingClients.values()) {
|
|
145
|
-
|
|
146
|
-
rootProvider.pause();
|
|
166
|
+
rootProvider.pause();
|
|
147
167
|
}
|
|
148
168
|
},
|
|
149
169
|
resumeAll() {
|
|
150
170
|
for (const { rootProvider } of existingClients.values()) {
|
|
151
|
-
|
|
152
|
-
|
|
171
|
+
rootProvider.resume();
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
reconnect(genesisHashes) {
|
|
175
|
+
for (const [genesisHash, { rootProvider }] of existingClients) {
|
|
176
|
+
if (genesisHashes && !genesisHashes.includes(genesisHash))
|
|
177
|
+
continue;
|
|
178
|
+
// Nobody holds this one any more — it is only waiting out `destroyDelay`.
|
|
179
|
+
// Rebuilding its transport would open a socket the destruction timer is
|
|
180
|
+
// about to throw away.
|
|
181
|
+
if (destructionTimers.has(genesisHash))
|
|
182
|
+
continue;
|
|
183
|
+
rootProvider.restart();
|
|
153
184
|
}
|
|
154
185
|
},
|
|
155
186
|
};
|
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { createChainConnection } from './connectionPool.js';
|
|
3
|
+
import { noop } from './helpers.js';
|
|
3
4
|
// An in-memory stand-in for polkadot-api's `createClient`: one client object per
|
|
4
5
|
// call, each counting its own `destroy()`.
|
|
5
6
|
const createClientFactory = () => {
|
|
6
7
|
const clients = [];
|
|
7
|
-
const factory = ((
|
|
8
|
+
const factory = ((provider, _options) => {
|
|
8
9
|
const client = { destroyed: 0 };
|
|
9
10
|
clients.push(client);
|
|
10
|
-
|
|
11
|
+
// The real one connects up front and drops the connection on destroy; the
|
|
12
|
+
// pool only builds a transport once something actually connects.
|
|
13
|
+
const connection = provider(noop);
|
|
14
|
+
return {
|
|
15
|
+
destroy: () => {
|
|
16
|
+
client.destroyed++;
|
|
17
|
+
connection.disconnect();
|
|
18
|
+
},
|
|
19
|
+
};
|
|
11
20
|
});
|
|
12
21
|
return { createClient: factory, last: () => clients[clients.length - 1] };
|
|
13
22
|
};
|
|
23
|
+
// The transport is built on the proxy's first connect attempt, which it
|
|
24
|
+
// schedules rather than runs inline.
|
|
25
|
+
const connected = () => new Promise(resolve => setTimeout(resolve, 0));
|
|
14
26
|
const createMockProvider = () => {
|
|
15
27
|
const send = vi.fn();
|
|
16
28
|
const disconnect = vi.fn();
|
|
@@ -125,9 +137,13 @@ describe('createChainConnection', () => {
|
|
|
125
137
|
return createMockProvider().provider;
|
|
126
138
|
},
|
|
127
139
|
});
|
|
128
|
-
|
|
140
|
+
// The lock is held across the wait: releasing it destroys the client, and
|
|
141
|
+
// a destroyed pool never gets round to building a transport at all.
|
|
142
|
+
const { unlock } = await connection.lockApi(testChain('a'));
|
|
143
|
+
await connected();
|
|
129
144
|
statusCb('connected');
|
|
130
145
|
expect(connection.status('a')).toBe('connected');
|
|
146
|
+
unlock();
|
|
131
147
|
});
|
|
132
148
|
it('onStatusChanged returns unsubscribe function', async () => {
|
|
133
149
|
let statusCb;
|
|
@@ -137,7 +153,8 @@ describe('createChainConnection', () => {
|
|
|
137
153
|
return createMockProvider().provider;
|
|
138
154
|
},
|
|
139
155
|
});
|
|
140
|
-
await connection.lockApi(testChain('a'))
|
|
156
|
+
const { unlock } = await connection.lockApi(testChain('a'));
|
|
157
|
+
await connected();
|
|
141
158
|
const callback = vi.fn();
|
|
142
159
|
const unsub = connection.onStatusChanged('a', callback);
|
|
143
160
|
statusCb('connected');
|
|
@@ -146,6 +163,7 @@ describe('createChainConnection', () => {
|
|
|
146
163
|
callback.mockClear();
|
|
147
164
|
statusCb('disconnected');
|
|
148
165
|
expect(callback).not.toHaveBeenCalled();
|
|
166
|
+
unlock();
|
|
149
167
|
});
|
|
150
168
|
});
|
|
151
169
|
describe('lockApi — connection lifecycle', () => {
|
|
@@ -214,6 +232,7 @@ describe('createChainConnection', () => {
|
|
|
214
232
|
});
|
|
215
233
|
const { unlock: u1 } = await connection.lockApi(testChain('a'));
|
|
216
234
|
const { unlock: u2 } = await connection.lockApi(testChain('b'));
|
|
235
|
+
await connected();
|
|
217
236
|
connection.pauseAll();
|
|
218
237
|
expect(chainA.pause).toHaveBeenCalledTimes(1);
|
|
219
238
|
expect(chainB.pause).toHaveBeenCalledTimes(1);
|
|
@@ -227,6 +246,7 @@ describe('createChainConnection', () => {
|
|
|
227
246
|
createClient: createClientFactory().createClient,
|
|
228
247
|
});
|
|
229
248
|
const { unlock } = await connection.lockApi(testChain('a'));
|
|
249
|
+
await connected();
|
|
230
250
|
connection.pauseAll();
|
|
231
251
|
connection.resumeAll();
|
|
232
252
|
expect(chainA.resume).toHaveBeenCalledTimes(1);
|
|
@@ -242,6 +262,7 @@ describe('createChainConnection', () => {
|
|
|
242
262
|
});
|
|
243
263
|
await connection.lockApi(testChain('a'));
|
|
244
264
|
await connection.lockApi(testChain('b'));
|
|
265
|
+
await connected();
|
|
245
266
|
connection.pauseAll();
|
|
246
267
|
connection.resumeAll();
|
|
247
268
|
expect(pausable.pause).toHaveBeenCalledTimes(1);
|
|
@@ -260,4 +281,101 @@ describe('createChainConnection', () => {
|
|
|
260
281
|
expect(chainA.pause).not.toHaveBeenCalled();
|
|
261
282
|
});
|
|
262
283
|
});
|
|
284
|
+
describe('reconnect', () => {
|
|
285
|
+
// A restart rides the proxy's halt path, which backs off when halts land on
|
|
286
|
+
// top of each other. Running fake time past that window keeps these cases
|
|
287
|
+
// about the swap rather than about the backoff.
|
|
288
|
+
beforeEach(() => vi.useFakeTimers());
|
|
289
|
+
afterEach(() => vi.useRealTimers());
|
|
290
|
+
const settle = () => vi.advanceTimersByTimeAsync(1_000);
|
|
291
|
+
it('asks createProvider for a new transport and closes the old one', async () => {
|
|
292
|
+
const first = createMockProvider();
|
|
293
|
+
const second = createMockProvider();
|
|
294
|
+
const transports = [first, second];
|
|
295
|
+
const createProvider = vi.fn(() => transports.shift().provider);
|
|
296
|
+
const { connection } = createTestConnection({ createProvider });
|
|
297
|
+
const { unlock } = await connection.lockApi(testChain('a'));
|
|
298
|
+
await settle();
|
|
299
|
+
connection.reconnect();
|
|
300
|
+
await settle();
|
|
301
|
+
expect(createProvider).toHaveBeenCalledTimes(2);
|
|
302
|
+
expect(first.disconnect).toHaveBeenCalledTimes(1);
|
|
303
|
+
unlock();
|
|
304
|
+
});
|
|
305
|
+
it('keeps the pooled client — the point is not restarting the app', async () => {
|
|
306
|
+
const { connection, clients } = createTestConnection();
|
|
307
|
+
const { api, unlock } = await connection.lockApi(testChain('a'));
|
|
308
|
+
await settle();
|
|
309
|
+
connection.reconnect();
|
|
310
|
+
await settle();
|
|
311
|
+
const { api: after, unlock: unlockAfter } = await connection.lockApi(testChain('a'));
|
|
312
|
+
expect(clients.last().destroyed).toBe(0);
|
|
313
|
+
expect(after).toBe(api);
|
|
314
|
+
unlock();
|
|
315
|
+
unlockAfter();
|
|
316
|
+
});
|
|
317
|
+
it('rebuilds only the chains it was given', async () => {
|
|
318
|
+
const built = [];
|
|
319
|
+
const connection = createChainConnection({
|
|
320
|
+
createProvider: chain => {
|
|
321
|
+
built.push(chain.genesisHash);
|
|
322
|
+
return createMockProvider().provider;
|
|
323
|
+
},
|
|
324
|
+
createClient: createClientFactory().createClient,
|
|
325
|
+
});
|
|
326
|
+
const { unlock: unlockA } = await connection.lockApi(testChain('a'));
|
|
327
|
+
const { unlock: unlockB } = await connection.lockApi(testChain('b'));
|
|
328
|
+
await settle();
|
|
329
|
+
expect(built).toEqual(['a', 'b']);
|
|
330
|
+
connection.reconnect(['a']);
|
|
331
|
+
await settle();
|
|
332
|
+
expect(built).toEqual(['a', 'b', 'a']);
|
|
333
|
+
unlockA();
|
|
334
|
+
unlockB();
|
|
335
|
+
});
|
|
336
|
+
it('ignores a chain the pool is not holding', async () => {
|
|
337
|
+
const createProvider = vi.fn(() => createMockProvider().provider);
|
|
338
|
+
const connection = createChainConnection({
|
|
339
|
+
createProvider,
|
|
340
|
+
createClient: createClientFactory().createClient,
|
|
341
|
+
});
|
|
342
|
+
expect(() => connection.reconnect(['nothing-here'])).not.toThrow();
|
|
343
|
+
expect(createProvider).not.toHaveBeenCalled();
|
|
344
|
+
});
|
|
345
|
+
it('skips a chain that is only waiting out destroyDelay', async () => {
|
|
346
|
+
const createProvider = vi.fn(() => createMockProvider().provider);
|
|
347
|
+
const connection = createChainConnection({
|
|
348
|
+
createProvider,
|
|
349
|
+
createClient: createClientFactory().createClient,
|
|
350
|
+
destroyDelay: 10_000,
|
|
351
|
+
});
|
|
352
|
+
const { unlock } = await connection.lockApi(testChain('a'));
|
|
353
|
+
await settle();
|
|
354
|
+
unlock();
|
|
355
|
+
connection.reconnect();
|
|
356
|
+
await settle();
|
|
357
|
+
// Nobody holds it; the destruction timer is about to discard the client,
|
|
358
|
+
// so a fresh transport would be opened only to be thrown away.
|
|
359
|
+
expect(createProvider).toHaveBeenCalledTimes(1);
|
|
360
|
+
});
|
|
361
|
+
it('drops a status the replaced transport reports after the swap', async () => {
|
|
362
|
+
const statusCallbacks = [];
|
|
363
|
+
const { connection } = createTestConnection({
|
|
364
|
+
createProvider: (_chain, onStatusChanged) => {
|
|
365
|
+
statusCallbacks.push(onStatusChanged);
|
|
366
|
+
return createMockProvider().provider;
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
const { unlock } = await connection.lockApi(testChain('a'));
|
|
370
|
+
await settle();
|
|
371
|
+
connection.reconnect();
|
|
372
|
+
await settle();
|
|
373
|
+
statusCallbacks[1]('connected');
|
|
374
|
+
// The transport that was replaced has no say any more — otherwise its
|
|
375
|
+
// parting 'disconnected' would land on top of its successor's 'connected'.
|
|
376
|
+
statusCallbacks[0]('disconnected');
|
|
377
|
+
expect(connection.status('a')).toBe('connected');
|
|
378
|
+
unlock();
|
|
379
|
+
});
|
|
380
|
+
});
|
|
263
381
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { type ChainConnection, type ChainConnectionConfig, createChainConnection } from './connectionPool.js';
|
|
2
2
|
export { type MetadataCache, createMetadataCache } from './metadataCache.js';
|
|
3
|
+
export { type RestartableJsonRpcProvider, createRestartableProvider } from './restartableProvider.js';
|
|
3
4
|
export { withSubscriptionReplay } from './subscriptionReplayProvider.js';
|
|
4
5
|
export { createWsJsonRpcProvider } from './wsProvider.js';
|
|
5
6
|
export { type ChainConfig, type ConnectionStatus } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { createChainConnection } from './connectionPool.js';
|
|
2
2
|
export { createMetadataCache } from './metadataCache.js';
|
|
3
|
+
export { createRestartableProvider } from './restartableProvider.js';
|
|
3
4
|
export { withSubscriptionReplay } from './subscriptionReplayProvider.js';
|
|
4
5
|
export { createWsJsonRpcProvider } from './wsProvider.js';
|
|
5
6
|
export {} from './types.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { JsonRpcProvider } from 'polkadot-api';
|
|
2
|
+
export type RestartableJsonRpcProvider = JsonRpcProvider & {
|
|
3
|
+
/**
|
|
4
|
+
* Throw the live transport away and build a new one from the factory, without
|
|
5
|
+
* tearing down the consumers hanging off this provider. Callers use it to move
|
|
6
|
+
* a chain onto a different transport — a light client to an RPC node, say —
|
|
7
|
+
* while clients, subscriptions and refcounts stay where they are.
|
|
8
|
+
*
|
|
9
|
+
* The old transport closes synchronously; the new one does not. The swap goes
|
|
10
|
+
* through the proxy's reconnect path, so the factory is called on its backoff
|
|
11
|
+
* — 500 ms after this returns, and a step further out for each restart that
|
|
12
|
+
* lands before the chain has delivered a message on the transport before it
|
|
13
|
+
* (`getSyncProvider` counts those as a flapping connection). Next to opening a
|
|
14
|
+
* socket or booting a light client the first step is noise, but a caller
|
|
15
|
+
* driving this from a UI toggle should not treat it as instantaneous.
|
|
16
|
+
*/
|
|
17
|
+
restart(): void;
|
|
18
|
+
/** Forwarded to the current transport when it supports it; otherwise a no-op. */
|
|
19
|
+
pause(): void;
|
|
20
|
+
resume(): void;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* A provider whose underlying transport can be replaced at any time.
|
|
24
|
+
*
|
|
25
|
+
* The swap is dressed up as a connection drop, which is the one event every
|
|
26
|
+
* layer above already knows how to survive: `getSyncProvider`'s proxy reports
|
|
27
|
+
* the active `chainHead` follows as stopped and re-sends the requests that were
|
|
28
|
+
* in flight, so polkadot-api re-follows on the new transport by itself, and the
|
|
29
|
+
* subscription-replay wrapper re-establishes the legacy subscriptions that
|
|
30
|
+
* `chainHead` bookkeeping does not cover. Consumers see the same interruption a
|
|
31
|
+
* dropped socket would have caused, and nothing above has to be rebuilt.
|
|
32
|
+
*/
|
|
33
|
+
export declare const createRestartableProvider: (createTransport: () => JsonRpcProvider) => RestartableJsonRpcProvider;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { getSyncProvider } from '@polkadot-api/json-rpc-provider-proxy';
|
|
2
|
+
import { noop } from './helpers.js';
|
|
3
|
+
import { withSubscriptionReplay } from './subscriptionReplayProvider.js';
|
|
4
|
+
import { isPausable } from './wsProvider.js';
|
|
5
|
+
/**
|
|
6
|
+
* A provider whose underlying transport can be replaced at any time.
|
|
7
|
+
*
|
|
8
|
+
* The swap is dressed up as a connection drop, which is the one event every
|
|
9
|
+
* layer above already knows how to survive: `getSyncProvider`'s proxy reports
|
|
10
|
+
* the active `chainHead` follows as stopped and re-sends the requests that were
|
|
11
|
+
* in flight, so polkadot-api re-follows on the new transport by itself, and the
|
|
12
|
+
* subscription-replay wrapper re-establishes the legacy subscriptions that
|
|
13
|
+
* `chainHead` bookkeeping does not cover. Consumers see the same interruption a
|
|
14
|
+
* dropped socket would have caused, and nothing above has to be rebuilt.
|
|
15
|
+
*/
|
|
16
|
+
export const createRestartableProvider = (createTransport) => {
|
|
17
|
+
let active = null;
|
|
18
|
+
let notifyReconnect = noop;
|
|
19
|
+
// Survives a restart: a transport built while the host has the pool paused
|
|
20
|
+
// must come up paused too, or backgrounding the app would stop holding.
|
|
21
|
+
let paused = false;
|
|
22
|
+
const onReconnect = (callback) => {
|
|
23
|
+
notifyReconnect = callback;
|
|
24
|
+
return () => {
|
|
25
|
+
notifyReconnect = noop;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
const core = getSyncProvider(onResult => {
|
|
29
|
+
let built = false;
|
|
30
|
+
onResult((onMessage, halt) => {
|
|
31
|
+
let provider;
|
|
32
|
+
let connection;
|
|
33
|
+
try {
|
|
34
|
+
provider = createTransport();
|
|
35
|
+
// Paused before the transport is handed its message callback: a
|
|
36
|
+
// transport that opens its connection right there (a light client, say)
|
|
37
|
+
// would otherwise be up for as long as it takes to pause it again,
|
|
38
|
+
// which is the socket-behind-a-backgrounded-app this guards against.
|
|
39
|
+
if (paused && isPausable(provider))
|
|
40
|
+
provider.pause();
|
|
41
|
+
connection = provider(onMessage);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
// The proxy is mid-connect and stores whatever we return here, so a
|
|
45
|
+
// factory that throws has to be reported through `halt`: it puts the
|
|
46
|
+
// proxy back to connecting — buffering sends rather than pushing them
|
|
47
|
+
// at a transport that was never built — and schedules the next attempt.
|
|
48
|
+
// Without it the chain would keep a live-looking client wired to
|
|
49
|
+
// nothing, for good.
|
|
50
|
+
halt(error);
|
|
51
|
+
return { send: noop, disconnect: noop };
|
|
52
|
+
}
|
|
53
|
+
// The transport's own halt channel is deliberately not wired up: a
|
|
54
|
+
// transport either recovers on its own (the ws provider proxies its
|
|
55
|
+
// reconnects internally) or reports `disconnected` and stays down. `halt`
|
|
56
|
+
// is kept for `restart`, which is the only reason this layer replaces one.
|
|
57
|
+
const transport = { provider, connection, halt };
|
|
58
|
+
active = transport;
|
|
59
|
+
built = true;
|
|
60
|
+
return {
|
|
61
|
+
send: message => connection.send(message),
|
|
62
|
+
disconnect: () => {
|
|
63
|
+
if (active === transport)
|
|
64
|
+
active = null;
|
|
65
|
+
connection.disconnect();
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
// `onResult` runs the proxy's connect synchronously, so by here the proxy is
|
|
70
|
+
// connected and the replayed subscribes go out on the new transport rather
|
|
71
|
+
// than into a buffer that a failed connect would discard. A failed attempt
|
|
72
|
+
// skips the replay entirely — the halt above has already queued another one,
|
|
73
|
+
// which will do it against a transport that exists.
|
|
74
|
+
if (built)
|
|
75
|
+
notifyReconnect();
|
|
76
|
+
// Nothing to cancel: `onResult` above already resolved this attempt.
|
|
77
|
+
return noop;
|
|
78
|
+
});
|
|
79
|
+
const provider = withSubscriptionReplay(core, onReconnect);
|
|
80
|
+
return Object.assign(provider, {
|
|
81
|
+
restart() {
|
|
82
|
+
const transport = active;
|
|
83
|
+
// Nothing to replace while the proxy is between transports — whatever it
|
|
84
|
+
// asks the factory for next is already built from the current settings.
|
|
85
|
+
if (!transport)
|
|
86
|
+
return;
|
|
87
|
+
active = null;
|
|
88
|
+
// Close before halting: the halt schedules the next `createTransport`, and
|
|
89
|
+
// a transport left open would keep its socket (or light client) running
|
|
90
|
+
// alongside its replacement.
|
|
91
|
+
transport.connection.disconnect();
|
|
92
|
+
transport.halt();
|
|
93
|
+
},
|
|
94
|
+
pause() {
|
|
95
|
+
paused = true;
|
|
96
|
+
const transport = active?.provider;
|
|
97
|
+
if (transport && isPausable(transport))
|
|
98
|
+
transport.pause();
|
|
99
|
+
},
|
|
100
|
+
resume() {
|
|
101
|
+
paused = false;
|
|
102
|
+
const transport = active?.provider;
|
|
103
|
+
if (transport && isPausable(transport))
|
|
104
|
+
transport.resume();
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createRestartableProvider } from './restartableProvider.js';
|
|
3
|
+
const createFakeTransport = ({ pausable = false } = {}) => {
|
|
4
|
+
const send = vi.fn();
|
|
5
|
+
const disconnect = vi.fn();
|
|
6
|
+
let onMessage = null;
|
|
7
|
+
const provider = cb => {
|
|
8
|
+
onMessage = cb;
|
|
9
|
+
return { send, disconnect };
|
|
10
|
+
};
|
|
11
|
+
const transport = { provider, send, disconnect, emit: message => onMessage?.(message) };
|
|
12
|
+
if (pausable) {
|
|
13
|
+
transport.pause = vi.fn();
|
|
14
|
+
transport.resume = vi.fn();
|
|
15
|
+
Object.assign(provider, { pause: transport.pause, resume: transport.resume });
|
|
16
|
+
}
|
|
17
|
+
return transport;
|
|
18
|
+
};
|
|
19
|
+
// `getSyncProvider` schedules every connect attempt on a timer, and treats
|
|
20
|
+
// halts that land on top of each other as a flapping connection worth backing
|
|
21
|
+
// off from. Letting fake time run past that window keeps these cases about the
|
|
22
|
+
// swap rather than about the backoff.
|
|
23
|
+
const settle = () => vi.advanceTimersByTimeAsync(1_000);
|
|
24
|
+
const connect = async (provider) => {
|
|
25
|
+
const messages = [];
|
|
26
|
+
const connection = provider(message => messages.push(message));
|
|
27
|
+
await settle();
|
|
28
|
+
return { connection, messages };
|
|
29
|
+
};
|
|
30
|
+
describe('createRestartableProvider', () => {
|
|
31
|
+
beforeEach(() => vi.useFakeTimers());
|
|
32
|
+
afterEach(() => vi.useRealTimers());
|
|
33
|
+
it('builds a transport lazily and routes traffic through it', async () => {
|
|
34
|
+
const transport = createFakeTransport();
|
|
35
|
+
const provider = createRestartableProvider(() => transport.provider);
|
|
36
|
+
const { connection, messages } = await connect(provider);
|
|
37
|
+
connection.send({ jsonrpc: '2.0', id: 1, method: 'system_chain', params: [] });
|
|
38
|
+
transport.emit({ jsonrpc: '2.0', id: 1, result: 'polkadot' });
|
|
39
|
+
expect(transport.send).toHaveBeenCalledOnce();
|
|
40
|
+
expect(messages).toContainEqual({ jsonrpc: '2.0', id: 1, result: 'polkadot' });
|
|
41
|
+
});
|
|
42
|
+
it('replaces the transport on restart, closing the old one exactly once', async () => {
|
|
43
|
+
const first = createFakeTransport();
|
|
44
|
+
const second = createFakeTransport();
|
|
45
|
+
const transports = [first, second];
|
|
46
|
+
const provider = createRestartableProvider(() => transports.shift().provider);
|
|
47
|
+
await connect(provider);
|
|
48
|
+
provider.restart();
|
|
49
|
+
await settle();
|
|
50
|
+
expect(first.disconnect).toHaveBeenCalledOnce();
|
|
51
|
+
expect(second.disconnect).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
it('keeps the consumer connection alive across a restart', async () => {
|
|
54
|
+
const first = createFakeTransport();
|
|
55
|
+
const second = createFakeTransport();
|
|
56
|
+
const transports = [first, second];
|
|
57
|
+
const provider = createRestartableProvider(() => transports.shift().provider);
|
|
58
|
+
const { connection, messages } = await connect(provider);
|
|
59
|
+
provider.restart();
|
|
60
|
+
await settle();
|
|
61
|
+
connection.send({ jsonrpc: '2.0', id: 2, method: 'system_chain', params: [] });
|
|
62
|
+
second.emit({ jsonrpc: '2.0', id: 2, result: 'kusama' });
|
|
63
|
+
expect(second.send).toHaveBeenCalledOnce();
|
|
64
|
+
expect(messages).toContainEqual({ jsonrpc: '2.0', id: 2, result: 'kusama' });
|
|
65
|
+
});
|
|
66
|
+
it('reports the active chainHead follows as stopped so the client re-follows', async () => {
|
|
67
|
+
const first = createFakeTransport();
|
|
68
|
+
const second = createFakeTransport();
|
|
69
|
+
const transports = [first, second];
|
|
70
|
+
const provider = createRestartableProvider(() => transports.shift().provider);
|
|
71
|
+
const { connection, messages } = await connect(provider);
|
|
72
|
+
connection.send({ jsonrpc: '2.0', id: 3, method: 'chainHead_v1_follow', params: [true] });
|
|
73
|
+
first.emit({ jsonrpc: '2.0', id: 3, result: 'follow-1' });
|
|
74
|
+
provider.restart();
|
|
75
|
+
await settle();
|
|
76
|
+
expect(messages).toContainEqual(expect.objectContaining({
|
|
77
|
+
method: 'chainHead_v1_follow',
|
|
78
|
+
params: { subscription: 'follow-1', result: { event: 'stop', internal: true } },
|
|
79
|
+
}));
|
|
80
|
+
});
|
|
81
|
+
it('re-establishes a legacy subscription on the new transport', async () => {
|
|
82
|
+
const first = createFakeTransport();
|
|
83
|
+
const second = createFakeTransport();
|
|
84
|
+
const transports = [first, second];
|
|
85
|
+
const provider = createRestartableProvider(() => transports.shift().provider);
|
|
86
|
+
const { connection, messages } = await connect(provider);
|
|
87
|
+
const subscribe = { jsonrpc: '2.0', id: 4, method: 'state_subscribeStorage', params: [['0x00']] };
|
|
88
|
+
connection.send(subscribe);
|
|
89
|
+
first.emit({ jsonrpc: '2.0', id: 4, result: 'sub-1' });
|
|
90
|
+
provider.restart();
|
|
91
|
+
await settle();
|
|
92
|
+
expect(second.send).toHaveBeenCalledWith(expect.objectContaining({ method: 'state_subscribeStorage' }));
|
|
93
|
+
// The consumer only ever learned `sub-1`, so notifications from the new
|
|
94
|
+
// transport's id have to arrive back under the one it is routing on.
|
|
95
|
+
second.emit({ jsonrpc: '2.0', id: 4, result: 'sub-2' });
|
|
96
|
+
second.emit({
|
|
97
|
+
jsonrpc: '2.0',
|
|
98
|
+
method: 'state_storage',
|
|
99
|
+
params: { subscription: 'sub-2', result: '0x01' },
|
|
100
|
+
});
|
|
101
|
+
expect(messages).toContainEqual(expect.objectContaining({ params: { subscription: 'sub-1', result: '0x01' } }));
|
|
102
|
+
// The re-confirmation is the consumer's own subscribe id — surfacing it
|
|
103
|
+
// would look like a second, unasked-for response.
|
|
104
|
+
expect(messages.filter(message => 'result' in message && message.result === 'sub-2')).toHaveLength(0);
|
|
105
|
+
});
|
|
106
|
+
it('retries instead of wedging when the transport factory throws', async () => {
|
|
107
|
+
const transport = createFakeTransport();
|
|
108
|
+
const created = vi.fn(() => {
|
|
109
|
+
if (created.mock.calls.length === 1)
|
|
110
|
+
throw new Error('no WebSocket class');
|
|
111
|
+
return transport.provider;
|
|
112
|
+
});
|
|
113
|
+
const provider = createRestartableProvider(created);
|
|
114
|
+
const { connection, messages } = await connect(provider);
|
|
115
|
+
connection.send({ jsonrpc: '2.0', id: 5, method: 'system_chain', params: [] });
|
|
116
|
+
transport.emit({ jsonrpc: '2.0', id: 5, result: 'polkadot' });
|
|
117
|
+
expect(created).toHaveBeenCalledTimes(2);
|
|
118
|
+
expect(transport.send).toHaveBeenCalledOnce();
|
|
119
|
+
expect(messages).toContainEqual({ jsonrpc: '2.0', id: 5, result: 'polkadot' });
|
|
120
|
+
});
|
|
121
|
+
it('does not replay subscriptions into a transport the factory failed to build', async () => {
|
|
122
|
+
const first = createFakeTransport();
|
|
123
|
+
const second = createFakeTransport();
|
|
124
|
+
const transports = [first, null, second];
|
|
125
|
+
const provider = createRestartableProvider(() => {
|
|
126
|
+
const next = transports.shift();
|
|
127
|
+
if (!next)
|
|
128
|
+
throw new Error('transport unavailable');
|
|
129
|
+
return next.provider;
|
|
130
|
+
});
|
|
131
|
+
const { connection } = await connect(provider);
|
|
132
|
+
const subscribe = { jsonrpc: '2.0', id: 6, method: 'state_subscribeStorage', params: [['0x00']] };
|
|
133
|
+
connection.send(subscribe);
|
|
134
|
+
first.emit({ jsonrpc: '2.0', id: 6, result: 'sub-1' });
|
|
135
|
+
provider.restart();
|
|
136
|
+
// Two windows: the failed attempt in between pushes the next one out by a
|
|
137
|
+
// further backoff step.
|
|
138
|
+
await settle();
|
|
139
|
+
await settle();
|
|
140
|
+
expect(second.send).toHaveBeenCalledTimes(1);
|
|
141
|
+
expect(second.send).toHaveBeenCalledWith(expect.objectContaining({ method: 'state_subscribeStorage' }));
|
|
142
|
+
});
|
|
143
|
+
it('is a no-op before anything has connected', async () => {
|
|
144
|
+
const transport = createFakeTransport();
|
|
145
|
+
const created = vi.fn(() => transport.provider);
|
|
146
|
+
const provider = createRestartableProvider(created);
|
|
147
|
+
provider.restart();
|
|
148
|
+
await connect(provider);
|
|
149
|
+
expect(created).toHaveBeenCalledOnce();
|
|
150
|
+
});
|
|
151
|
+
it('forwards pause and resume to a pausable transport', async () => {
|
|
152
|
+
const transport = createFakeTransport({ pausable: true });
|
|
153
|
+
const provider = createRestartableProvider(() => transport.provider);
|
|
154
|
+
await connect(provider);
|
|
155
|
+
provider.pause();
|
|
156
|
+
provider.resume();
|
|
157
|
+
expect(transport.pause).toHaveBeenCalledOnce();
|
|
158
|
+
expect(transport.resume).toHaveBeenCalledOnce();
|
|
159
|
+
});
|
|
160
|
+
it('pauses a transport built while paused before handing it the message callback', async () => {
|
|
161
|
+
const pause = vi.fn();
|
|
162
|
+
const pausedAtInvocation = [];
|
|
163
|
+
const transport = Object.assign(() => {
|
|
164
|
+
pausedAtInvocation.push(pause.mock.calls.length > 0);
|
|
165
|
+
return { send: vi.fn(), disconnect: vi.fn() };
|
|
166
|
+
}, { pause, resume: vi.fn() });
|
|
167
|
+
const provider = createRestartableProvider(() => transport);
|
|
168
|
+
provider.pause();
|
|
169
|
+
await connect(provider);
|
|
170
|
+
expect(pausedAtInvocation).toEqual([true]);
|
|
171
|
+
});
|
|
172
|
+
it('brings a transport built while paused up paused', async () => {
|
|
173
|
+
const first = createFakeTransport({ pausable: true });
|
|
174
|
+
const second = createFakeTransport({ pausable: true });
|
|
175
|
+
const transports = [first, second];
|
|
176
|
+
const provider = createRestartableProvider(() => transports.shift().provider);
|
|
177
|
+
await connect(provider);
|
|
178
|
+
provider.pause();
|
|
179
|
+
provider.restart();
|
|
180
|
+
await settle();
|
|
181
|
+
expect(second.pause).toHaveBeenCalledOnce();
|
|
182
|
+
});
|
|
183
|
+
it('tolerates pause and resume on a transport that cannot pause', async () => {
|
|
184
|
+
const transport = createFakeTransport();
|
|
185
|
+
const provider = createRestartableProvider(() => transport.provider);
|
|
186
|
+
await connect(provider);
|
|
187
|
+
expect(() => {
|
|
188
|
+
provider.pause();
|
|
189
|
+
provider.resume();
|
|
190
|
+
}).not.toThrow();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createRestartableProvider } from './restartableProvider.js';
|
|
3
|
+
import { createWsJsonRpcProvider } from './wsProvider.js';
|
|
4
|
+
class FakeWebSocket {
|
|
5
|
+
static instances = [];
|
|
6
|
+
sent = [];
|
|
7
|
+
url;
|
|
8
|
+
listeners = new Map();
|
|
9
|
+
constructor(url) {
|
|
10
|
+
this.url = url;
|
|
11
|
+
FakeWebSocket.instances.push(this);
|
|
12
|
+
}
|
|
13
|
+
addEventListener(type, listener) {
|
|
14
|
+
const forType = this.listeners.get(type) ?? new Set();
|
|
15
|
+
forType.add(listener);
|
|
16
|
+
this.listeners.set(type, forType);
|
|
17
|
+
}
|
|
18
|
+
removeEventListener(type, listener) {
|
|
19
|
+
this.listeners.get(type)?.delete(listener);
|
|
20
|
+
}
|
|
21
|
+
send(data) {
|
|
22
|
+
this.sent.push(data);
|
|
23
|
+
}
|
|
24
|
+
close() {
|
|
25
|
+
this.emit('close', { type: 'close' });
|
|
26
|
+
}
|
|
27
|
+
open() {
|
|
28
|
+
this.emit('open', { type: 'open' });
|
|
29
|
+
}
|
|
30
|
+
deliver(message) {
|
|
31
|
+
this.emit('message', { data: JSON.stringify(message) });
|
|
32
|
+
}
|
|
33
|
+
requests(method) {
|
|
34
|
+
return this.sent.map(raw => JSON.parse(raw)).filter(message => message.method === method);
|
|
35
|
+
}
|
|
36
|
+
emit(type, event) {
|
|
37
|
+
for (const listener of [...(this.listeners.get(type) ?? [])])
|
|
38
|
+
listener(event);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const subscribe = { jsonrpc: '2.0', id: 1, method: 'state_subscribeStorage', params: [['0x00']] };
|
|
42
|
+
// The proxy schedules each connect attempt on a timer, and a restart lands on
|
|
43
|
+
// its reconnect backoff — one window covers both.
|
|
44
|
+
const settle = () => vi.advanceTimersByTimeAsync(1_000);
|
|
45
|
+
describe('createRestartableProvider × createWsJsonRpcProvider', () => {
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
vi.useFakeTimers();
|
|
48
|
+
FakeWebSocket.instances.length = 0;
|
|
49
|
+
});
|
|
50
|
+
afterEach(() => vi.useRealTimers());
|
|
51
|
+
it('re-establishes a legacy subscription exactly once on the new socket', async () => {
|
|
52
|
+
const provider = createRestartableProvider(() => createWsJsonRpcProvider({
|
|
53
|
+
endpoints: ['wss://example.test'],
|
|
54
|
+
websocketClass: FakeWebSocket,
|
|
55
|
+
}));
|
|
56
|
+
const messages = [];
|
|
57
|
+
const connection = provider(message => messages.push(message));
|
|
58
|
+
await settle();
|
|
59
|
+
const first = FakeWebSocket.instances[0];
|
|
60
|
+
first.open();
|
|
61
|
+
connection.send(subscribe);
|
|
62
|
+
first.deliver({ jsonrpc: '2.0', id: 1, result: 'sub-1' });
|
|
63
|
+
provider.restart();
|
|
64
|
+
await settle();
|
|
65
|
+
const second = FakeWebSocket.instances[1];
|
|
66
|
+
second.open();
|
|
67
|
+
// Twice would leave the second subId subscribed server-side with nothing
|
|
68
|
+
// holding it: the ws proxy drops the duplicate response, so neither replay
|
|
69
|
+
// layer ever learns that id and neither will unsubscribe it.
|
|
70
|
+
expect(second.requests('state_subscribeStorage')).toHaveLength(1);
|
|
71
|
+
// And the consumer keeps routing on the subId it was given.
|
|
72
|
+
second.deliver({ jsonrpc: '2.0', id: 1, result: 'sub-2' });
|
|
73
|
+
second.deliver({
|
|
74
|
+
jsonrpc: '2.0',
|
|
75
|
+
method: 'state_storage',
|
|
76
|
+
params: { subscription: 'sub-2', result: '0x01' },
|
|
77
|
+
});
|
|
78
|
+
expect(messages).toContainEqual(expect.objectContaining({ params: { subscription: 'sub-1', result: '0x01' } }));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -69,16 +69,17 @@ export const withSubscriptionReplay = (provider, onReconnect) => onMessage => {
|
|
|
69
69
|
onMessage(message);
|
|
70
70
|
});
|
|
71
71
|
const unsubReconnect = onReconnect(() => {
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// Replay confirmed subscriptions: the server returns a fresh subId, which
|
|
80
|
-
// we map back to the consumer's stable subId.
|
|
72
|
+
// Only confirmed subscriptions are replayed. An unconfirmed subscribe is
|
|
73
|
+
// still owned by the transport underneath — every provider this wraps is
|
|
74
|
+
// built on `getSyncProvider`, whose proxy re-sends its in-flight requests
|
|
75
|
+
// across a reconnect and buffers them until the next one is up — so
|
|
76
|
+
// re-sending it here would put the same subscribe on the wire twice and
|
|
77
|
+
// leave the second, unmatched subId subscribed server-side forever.
|
|
78
|
+
// Its pending entry is kept: the response still has to be recognised.
|
|
81
79
|
for (const [consumerSubId, sub] of activeSubscriptions) {
|
|
80
|
+
// The server returns a fresh subId, which we map back to the consumer's
|
|
81
|
+
// stable subId. `reconnectFor` keeps these entries apart from the
|
|
82
|
+
// unconfirmed ones already in the map, which all carry null.
|
|
82
83
|
pendingSubscriptions.set(sub.id, { payload: sub.payload, reconnectFor: consumerSubId });
|
|
83
84
|
conn.send(sub.payload);
|
|
84
85
|
}
|
|
@@ -61,7 +61,7 @@ describe('withSubscriptionReplay', () => {
|
|
|
61
61
|
control.triggerReconnect();
|
|
62
62
|
expect(mock.send).not.toHaveBeenCalled();
|
|
63
63
|
});
|
|
64
|
-
it('
|
|
64
|
+
it('leaves unconfirmed subscribes to the transport underneath on reconnect', () => {
|
|
65
65
|
const mock = createMockProvider();
|
|
66
66
|
const control = createReconnectControl();
|
|
67
67
|
const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(vi.fn());
|
|
@@ -70,6 +70,24 @@ describe('withSubscriptionReplay', () => {
|
|
|
70
70
|
// intentionally no simulateMessage — subscription not confirmed by server
|
|
71
71
|
mock.send.mockClear();
|
|
72
72
|
control.triggerReconnect();
|
|
73
|
+
// `getSyncProvider`'s proxy re-sends it on its own; a second copy from here
|
|
74
|
+
// would leave an unmatched subscription live on the server.
|
|
75
|
+
expect(mock.send).not.toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
it('still recognises the response to an unconfirmed subscribe the transport re-sent', () => {
|
|
78
|
+
const mock = createMockProvider();
|
|
79
|
+
const control = createReconnectControl();
|
|
80
|
+
const onMessage = vi.fn();
|
|
81
|
+
const conn = withSubscriptionReplay(mock.provider, control.onReconnect)(onMessage);
|
|
82
|
+
const subscribeMsg = req(1, 'statement_subscribeStatement');
|
|
83
|
+
conn.send(subscribeMsg);
|
|
84
|
+
control.triggerReconnect();
|
|
85
|
+
// The transport's own replay puts it back on the wire; the answer arrives
|
|
86
|
+
// against the id we are still holding a pending entry for.
|
|
87
|
+
mock.simulateMessage(res(1, 'sub-1'));
|
|
88
|
+
mock.send.mockClear();
|
|
89
|
+
control.triggerReconnect();
|
|
90
|
+
expect(onMessage).toHaveBeenCalledWith(res(1, 'sub-1'));
|
|
73
91
|
expect(mock.send).toHaveBeenCalledWith(subscribeMsg);
|
|
74
92
|
});
|
|
75
93
|
it('replays active subscriptions on reconnect', () => {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { JsonRpcProvider, PolkadotClient } from 'polkadot-api';
|
|
2
|
+
import type { RestartableJsonRpcProvider } from './restartableProvider.js';
|
|
2
3
|
export type ChainConfig = {
|
|
3
4
|
genesisHash: string;
|
|
4
5
|
};
|
|
@@ -9,5 +10,5 @@ export type BranchedProvider = {
|
|
|
9
10
|
export type PooledClient = {
|
|
10
11
|
client: PolkadotClient;
|
|
11
12
|
provider: BranchedProvider;
|
|
12
|
-
rootProvider:
|
|
13
|
+
rootProvider: RestartableJsonRpcProvider;
|
|
13
14
|
};
|
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.10.
|
|
4
|
+
"version": "0.10.2",
|
|
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.10.
|
|
28
|
+
"@novasamatech/storage-adapter": "0.10.2",
|
|
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",
|