@novasamatech/host-substrate-chain-connection 0.9.3 → 0.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -86,6 +86,7 @@ function createChainConnection<C extends ChainConfig, T = PolkadotClient>(
86
86
  |---|---|---|
87
87
  | `createProvider` | `(chain: C, onStatusChanged: (status: ConnectionStatus) => void) => JsonRpcProvider` | Factory for the underlying JSON-RPC transport. Called once per chain. Use `onStatusChanged` to feed connection status back into the pool. |
88
88
  | `clientOptions` | `(chain: C) => ClientOptions` | Optional. Returns [polkadot-api client options](https://papi.how/) - typically metadata cache hooks (`getMetadata` / `setMetadata`). |
89
+ | `createClient` | `typeof createClient` | Optional. Overrides polkadot-api's `createClient`. Defaults to it; an injection point for tests and for wrapping client construction. |
89
90
  | `resolve` | `(chain: C, client: PolkadotClient) => Promise<T>` | Optional. Transforms the raw `PolkadotClient` into your app's API type. The result is cached per chain. If omitted, `T` defaults to `PolkadotClient`. |
90
91
  | `destroyDelay` | `number` | Optional. Milliseconds to wait before destroying a connection after the last caller releases. Defaults to `0` (destroy immediately). Useful to avoid reconnect churn when callers release and re-acquire in quick succession. |
91
92
 
@@ -4,6 +4,8 @@ import type { ChainConfig, ConnectionStatus } from './types.js';
4
4
  export type ChainConnectionConfig<C extends ChainConfig, T = PolkadotClient> = {
5
5
  createProvider(chain: C, onStatusChanged: (status: ConnectionStatus) => void): JsonRpcProvider;
6
6
  clientOptions?(chain: C): Parameters<typeof createClient>[1];
7
+ /** Defaults to polkadot-api's `createClient`. An injection point for tests and for wrapping client construction — polkadot-api is imported either way. */
8
+ createClient?: typeof createClient;
7
9
  resolve?(chain: C, client: PolkadotClient): Promise<T>;
8
10
  destroyDelay?: number;
9
11
  };
@@ -24,4 +26,4 @@ export type ChainConnection<C extends ChainConfig, T = PolkadotClient> = {
24
26
  pauseAll(): void;
25
27
  resumeAll(): void;
26
28
  };
27
- export declare const createChainConnection: <C extends ChainConfig, T = PolkadotClient>({ resolve, clientOptions, createProvider, destroyDelay, }: ChainConnectionConfig<C, T>) => ChainConnection<C, T>;
29
+ export declare const createChainConnection: <C extends ChainConfig, T = PolkadotClient>({ resolve, clientOptions, createProvider, createClient: makeClient, destroyDelay, }: ChainConnectionConfig<C, T>) => ChainConnection<C, T>;
@@ -4,7 +4,7 @@ import { createBranchedProvider } from './branchedProvider.js';
4
4
  import { createConnectionManager } from './connectionManager.js';
5
5
  import { createRefCounter } from './refCounter.js';
6
6
  import { isPausable } from './wsProvider.js';
7
- export const createChainConnection = ({ resolve, clientOptions, createProvider, destroyDelay = 0, }) => {
7
+ export const createChainConnection = ({ resolve, clientOptions, createProvider, createClient: makeClient = createClient, destroyDelay = 0, }) => {
8
8
  const connections = createConnectionManager();
9
9
  const refCounter = createRefCounter();
10
10
  const existingClients = new Map();
@@ -25,7 +25,7 @@ export const createChainConnection = ({ resolve, clientOptions, createProvider,
25
25
  return existing;
26
26
  const rootProvider = createProvider(chain, status => connections.update(chain.genesisHash, status));
27
27
  const branchedProvider = createBranchedProvider(rootProvider);
28
- const client = createClient(branchedProvider.branch(), clientOptions?.(chain));
28
+ const client = makeClient(branchedProvider.branch(), clientOptions?.(chain));
29
29
  const pooled = { client, provider: branchedProvider, rootProvider };
30
30
  existingClients.set(chain.genesisHash, pooled);
31
31
  return pooled;
@@ -1,9 +1,16 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import { createChainConnection } from './connectionPool.js';
3
- vi.mock('polkadot-api', () => ({
4
- createClient: vi.fn((_provider, _options) => createMockClient()),
5
- }));
6
- const createMockClient = () => ({ destroy: vi.fn() });
3
+ // An in-memory stand-in for polkadot-api's `createClient`: one client object per
4
+ // call, each counting its own `destroy()`.
5
+ const createClientFactory = () => {
6
+ const clients = [];
7
+ const factory = ((_provider, _options) => {
8
+ const client = { destroyed: 0 };
9
+ clients.push(client);
10
+ return { destroy: () => client.destroyed++ };
11
+ });
12
+ return { createClient: factory, last: () => clients[clients.length - 1] };
13
+ };
7
14
  const createMockProvider = () => {
8
15
  const send = vi.fn();
9
16
  const disconnect = vi.fn();
@@ -20,11 +27,13 @@ const createMockProvider = () => {
20
27
  const testChain = (id) => ({ genesisHash: id });
21
28
  const createTestConnection = (overrides) => {
22
29
  const mockProvider = createMockProvider();
30
+ const clientFactory = createClientFactory();
23
31
  const connection = createChainConnection({
24
32
  createProvider: () => mockProvider.provider,
33
+ createClient: clientFactory.createClient,
25
34
  ...overrides,
26
35
  });
27
- return { connection, mockProvider };
36
+ return { connection, mockProvider, clients: clientFactory };
28
37
  };
29
38
  describe('createChainConnection', () => {
30
39
  describe('lockApi', () => {
@@ -141,30 +150,20 @@ describe('createChainConnection', () => {
141
150
  });
142
151
  describe('lockApi — connection lifecycle', () => {
143
152
  it('destroys client synchronously when last lock is released (no destroyDelay)', async () => {
144
- const destroyFn = vi.fn();
145
- vi.mocked(await import('polkadot-api')).createClient.mockReturnValueOnce({
146
- getBestBlocks: vi.fn().mockResolvedValue([]),
147
- destroy: destroyFn,
148
- });
149
- const { connection } = createTestConnection();
153
+ const { connection, clients } = createTestConnection();
150
154
  const { unlock } = await connection.lockApi(testChain('a'));
151
155
  unlock();
152
- expect(destroyFn).toHaveBeenCalledOnce();
156
+ expect(clients.last().destroyed).toBe(1);
153
157
  });
154
158
  it('does not destroy while any lock is still held', async () => {
155
- const destroyFn = vi.fn();
156
- vi.mocked(await import('polkadot-api')).createClient.mockReturnValueOnce({
157
- getBestBlocks: vi.fn().mockResolvedValue([]),
158
- destroy: destroyFn,
159
- });
160
- const { connection } = createTestConnection();
159
+ const { connection, clients } = createTestConnection();
161
160
  const chain = testChain('a');
162
161
  const { unlock: u1 } = await connection.lockApi(chain);
163
162
  const { unlock: u2 } = await connection.lockApi(chain);
164
163
  u1();
165
- expect(destroyFn).not.toHaveBeenCalled();
164
+ expect(clients.last().destroyed).toBe(0);
166
165
  u2();
167
- expect(destroyFn).toHaveBeenCalledOnce();
166
+ expect(clients.last().destroyed).toBe(1);
168
167
  });
169
168
  describe('with destroyDelay', () => {
170
169
  beforeEach(() => {
@@ -174,32 +173,22 @@ describe('createChainConnection', () => {
174
173
  vi.useRealTimers();
175
174
  });
176
175
  it('defers destruction by destroyDelay ms', async () => {
177
- const destroyFn = vi.fn();
178
- vi.mocked(await import('polkadot-api')).createClient.mockReturnValueOnce({
179
- getBestBlocks: vi.fn().mockResolvedValue([]),
180
- destroy: destroyFn,
181
- });
182
- const { connection } = createTestConnection({ destroyDelay: 1000 });
176
+ const { connection, clients } = createTestConnection({ destroyDelay: 1000 });
183
177
  const { unlock } = await connection.lockApi(testChain('a'));
184
178
  unlock();
185
- expect(destroyFn).not.toHaveBeenCalled();
179
+ expect(clients.last().destroyed).toBe(0);
186
180
  vi.advanceTimersByTime(1000);
187
- expect(destroyFn).toHaveBeenCalledOnce();
181
+ expect(clients.last().destroyed).toBe(1);
188
182
  });
189
183
  it('cancels destruction timer when connection is re-acquired before delay elapses', async () => {
190
- const destroyFn = vi.fn();
191
- vi.mocked(await import('polkadot-api')).createClient.mockReturnValueOnce({
192
- getBestBlocks: vi.fn().mockResolvedValue([]),
193
- destroy: destroyFn,
194
- });
195
- const { connection } = createTestConnection({ destroyDelay: 1000 });
184
+ const { connection, clients } = createTestConnection({ destroyDelay: 1000 });
196
185
  const chain = testChain('a');
197
186
  const { api: api1, unlock: u1 } = await connection.lockApi(chain);
198
187
  u1();
199
188
  // Re-acquire before timer fires — same client must be returned, no destruction
200
189
  const { api: api2, unlock: u2 } = await connection.lockApi(chain);
201
190
  vi.advanceTimersByTime(1000);
202
- expect(destroyFn).not.toHaveBeenCalled();
191
+ expect(clients.last().destroyed).toBe(0);
203
192
  expect(api1).toBe(api2);
204
193
  u2();
205
194
  });
@@ -221,6 +210,7 @@ describe('createChainConnection', () => {
221
210
  const providerByChain = { a: chainA.provider, b: chainB.provider };
222
211
  const connection = createChainConnection({
223
212
  createProvider: chain => providerByChain[chain.genesisHash],
213
+ createClient: createClientFactory().createClient,
224
214
  });
225
215
  const { unlock: u1 } = await connection.lockApi(testChain('a'));
226
216
  const { unlock: u2 } = await connection.lockApi(testChain('b'));
@@ -232,7 +222,10 @@ describe('createChainConnection', () => {
232
222
  });
233
223
  it('calls resume on every pausable provider', async () => {
234
224
  const chainA = createPausableMockProvider();
235
- const connection = createChainConnection({ createProvider: () => chainA.provider });
225
+ const connection = createChainConnection({
226
+ createProvider: () => chainA.provider,
227
+ createClient: createClientFactory().createClient,
228
+ });
236
229
  const { unlock } = await connection.lockApi(testChain('a'));
237
230
  connection.pauseAll();
238
231
  connection.resumeAll();
@@ -245,6 +238,7 @@ describe('createChainConnection', () => {
245
238
  const pausable = createPausableMockProvider();
246
239
  const connection = createChainConnection({
247
240
  createProvider: c => (c.genesisHash === 'b' ? pausable.provider : createMockProvider().provider),
241
+ createClient: createClientFactory().createClient,
248
242
  });
249
243
  await connection.lockApi(testChain('a'));
250
244
  await connection.lockApi(testChain('b'));
@@ -257,6 +251,7 @@ describe('createChainConnection', () => {
257
251
  const chainA = createPausableMockProvider();
258
252
  const connection = createChainConnection({
259
253
  createProvider: () => chainA.provider,
254
+ createClient: createClientFactory().createClient,
260
255
  destroyDelay: 0,
261
256
  });
262
257
  const { unlock } = await connection.lockApi(testChain('a'));
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.9.3",
4
+ "version": "0.9.4",
5
5
  "description": "Chain connection pool with ref counting and provider branching for Polkadot API",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -25,7 +25,7 @@
25
25
  "README.md"
26
26
  ],
27
27
  "dependencies": {
28
- "@novasamatech/storage-adapter": "0.9.3",
28
+ "@novasamatech/storage-adapter": "0.9.4",
29
29
  "@polkadot-api/ws-provider": "^0.9.0",
30
30
  "@polkadot-api/json-rpc-provider": "^0.2.0",
31
31
  "@polkadot-api/json-rpc-provider-proxy": "^0.4.0",