@novasamatech/host-substrate-chain-connection 0.7.7 → 0.7.8-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.
@@ -239,14 +239,19 @@ describe('createChainConnection', () => {
239
239
  expect(chainA.resume).toHaveBeenCalledTimes(1);
240
240
  unlock();
241
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();
242
+ it('skips non-pausable providers without preventing pause on the rest', async () => {
243
+ // Mixed pool: 'a' is non-pausable, 'b' is pausable. The pausable one must
244
+ // still receive pause/resume even though the other is silently skipped.
245
+ const pausable = createPausableMockProvider();
246
+ const connection = createChainConnection({
247
+ createProvider: c => (c.genesisHash === 'b' ? pausable.provider : createMockProvider().provider),
248
+ });
249
+ await connection.lockApi(testChain('a'));
250
+ await connection.lockApi(testChain('b'));
251
+ connection.pauseAll();
252
+ connection.resumeAll();
253
+ expect(pausable.pause).toHaveBeenCalledTimes(1);
254
+ expect(pausable.resume).toHaveBeenCalledTimes(1);
250
255
  });
251
256
  it('does not call pause on providers for chains that have been destroyed', async () => {
252
257
  const chainA = createPausableMockProvider();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,106 @@
1
+ import { getSyncProvider } from '@polkadot-api/json-rpc-provider-proxy';
2
+ import { describe, expect, it, vi } from 'vitest';
3
+ import { createPauseController } from './pauseController.js';
4
+ const flushMicroAndTimers = async () => {
5
+ await vi.advanceTimersByTimeAsync(0);
6
+ };
7
+ const req = (id, method, params = []) => ({
8
+ jsonrpc: '2.0',
9
+ id,
10
+ method,
11
+ params,
12
+ });
13
+ const followNotif = (subscription, result) => ({
14
+ jsonrpc: '2.0',
15
+ method: 'chainHead_v1_followEvent',
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ params: { subscription, result },
18
+ });
19
+ describe('pauseController × getSyncProvider integration', () => {
20
+ it('routes follow events to the new listener after pause+resume+refollow (best blocks must keep flowing)', async () => {
21
+ vi.useFakeTimers();
22
+ try {
23
+ const invocations = [];
24
+ const base = (onMessage, onHalt) => {
25
+ const send = vi.fn();
26
+ const disconnect = vi.fn();
27
+ invocations.push({
28
+ onMessage: onMessage,
29
+ onHalt: onHalt,
30
+ send,
31
+ disconnect,
32
+ });
33
+ return { send, disconnect };
34
+ };
35
+ const pc = createPauseController();
36
+ const inner = pc.middleware(base);
37
+ // Wrap the pausable inner in getSyncProvider so we get the same proxy /
38
+ // synthetic-Stop machinery the WS provider gets via getWsProvider.
39
+ const provider = getSyncProvider(onReady => {
40
+ onReady((onMsg, onHalt) => inner(onMsg, onHalt));
41
+ return () => {
42
+ /* no teardown for the scheduling step */
43
+ };
44
+ });
45
+ const events = [];
46
+ const conn = provider(msg => events.push(msg));
47
+ // getSyncProvider schedules input via setTimeout(0); flush it so the
48
+ // base receives onMessage/onHalt and the proxy reaches Connected.
49
+ await flushMicroAndTimers();
50
+ expect(invocations).toHaveLength(1);
51
+ // Initial follow: consumer sends chainHead_v1_follow, the base "chain"
52
+ // assigns sub-id-1, and a bestBlockChanged event arrives.
53
+ conn.send(req(1, 'chainHead_v1_follow', [true]));
54
+ expect(invocations[0].send).toHaveBeenCalledWith(req(1, 'chainHead_v1_follow', [true]));
55
+ invocations[0].onMessage({ jsonrpc: '2.0', id: 1, result: 'sub-id-1' });
56
+ invocations[0].onMessage(followNotif('sub-id-1', { event: 'bestBlockChanged', bestBlockHash: '0xaa' }));
57
+ expect(events).toContainEqual({ jsonrpc: '2.0', id: 1, result: 'sub-id-1' });
58
+ expect(events).toContainEqual(followNotif('sub-id-1', { event: 'bestBlockChanged', bestBlockHash: '0xaa' }));
59
+ // === pause ===
60
+ // pauseController.pause() disconnects the live socket and manually fires
61
+ // onHalt({type:'paused'}). The proxy reacts by synthesizing a stop
62
+ // notification for every active chainHead — which is what surfaces to
63
+ // the consumer as "the follow died, please refollow".
64
+ events.length = 0;
65
+ pc.pause();
66
+ const syntheticStop = events.find(m =>
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ m.method === 'chainHead_v1_follow' &&
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ m.params?.subscription === 'sub-id-1' &&
71
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
+ m.params?.result?.event === 'stop');
73
+ expect(syntheticStop).toBeDefined();
74
+ expect(invocations[0].disconnect).toHaveBeenCalledTimes(1);
75
+ // Consumer's substrate-client refollows synchronously inside the stop
76
+ // handler. The new chainHead_v1_follow goes into the proxy's pending
77
+ // queue while the base is paused.
78
+ conn.send(req(2, 'chainHead_v1_follow', [true]));
79
+ expect(invocations[0].send).not.toHaveBeenCalledWith(req(2, 'chainHead_v1_follow', [true]));
80
+ // === resume ===
81
+ pc.resume();
82
+ // After resume, the base is re-invoked (new "connection") and the
83
+ // queued send is flushed. proxy.start() has its own setTimeout backoff;
84
+ // flush all timers so the new connection is established.
85
+ await vi.advanceTimersByTimeAsync(10_000);
86
+ expect(invocations.length).toBeGreaterThanOrEqual(2);
87
+ const fresh = invocations[invocations.length - 1];
88
+ // The buffered chainHead_v1_follow must hit the fresh base.
89
+ expect(fresh.send).toHaveBeenCalledWith(req(2, 'chainHead_v1_follow', [true]));
90
+ // Server assigns a NEW subId on the new connection.
91
+ fresh.onMessage({ jsonrpc: '2.0', id: 2, result: 'sub-id-2' });
92
+ // bestBlockChanged on sub-id-2 must reach the consumer — this is the
93
+ // assertion that fails if pause/resume drops best-block flow.
94
+ events.length = 0;
95
+ fresh.onMessage(followNotif('sub-id-2', { event: 'bestBlockChanged', bestBlockHash: '0xcc' }));
96
+ expect(events).toContainEqual(followNotif('sub-id-2', { event: 'bestBlockChanged', bestBlockHash: '0xcc' }));
97
+ // And no stale sub-id-1 events should have leaked through after refollow.
98
+ expect(events.some(m =>
99
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
+ m.method === 'chainHead_v1_follow' && m.params?.subscription === 'sub-id-1')).toBe(false);
101
+ }
102
+ finally {
103
+ vi.useRealTimers();
104
+ }
105
+ });
106
+ });
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.7",
4
+ "version": "0.7.8-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,7 @@
25
25
  "README.md"
26
26
  ],
27
27
  "dependencies": {
28
- "@novasamatech/storage-adapter": "0.7.7",
28
+ "@novasamatech/storage-adapter": "0.7.8-0",
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",