@novasamatech/host-container 0.7.4 → 0.7.5-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.
@@ -1,113 +1,104 @@
1
1
  import type { HexString } from '@novasamatech/host-api';
2
2
  import type { JsonRpcProvider } from 'polkadot-api';
3
- type JsonRpcConnection = ReturnType<JsonRpcProvider>;
4
- type PendingRequest = {
5
- resolve: (result: unknown) => void;
6
- reject: (error: unknown) => void;
7
- };
8
- type FollowSubscription = {
9
- chainSubId: string;
10
- eventListener: (event: unknown) => void;
11
- pendingRequestId?: string;
12
- };
13
- type ChainEntry = {
14
- connection: JsonRpcConnection;
15
- pendingRequests: Map<string, PendingRequest>;
16
- followSubscriptions: Map<string, FollowSubscription>;
17
- refCount: number;
18
- };
3
+ type FollowEvent = Record<string, unknown>;
19
4
  export type ChainConnectionManager = ReturnType<typeof createChainConnectionManager>;
20
- export declare function createChainConnectionManager(factory: (genesisHash: HexString) => JsonRpcProvider | null): {
21
- getOrCreateChain: (genesisHash: HexString) => ChainEntry | null;
22
- sendRequest: (genesisHash: HexString, method: string, params: unknown[]) => Promise<unknown>;
23
- startFollow: (genesisHash: HexString, withRuntime: boolean, onEvent: (event: unknown) => void) => {
5
+ export declare function createChainConnectionManager(factory: (genesisHash: HexString) => JsonRpcProvider | null, options?: {
6
+ refollowTimeoutMs?: number;
7
+ }): {
8
+ getOrCreateChain(genesisHash: HexString): boolean;
9
+ startFollow(genesisHash: HexString, withRuntime: boolean, onEvent: (event: FollowEvent) => void): {
24
10
  followId: string;
25
11
  };
26
- stopFollow: (genesisHash: HexString, followId: string) => void;
27
- getChainFollowSubId: (genesisHash: HexString) => string | null;
28
- releaseChain: (genesisHash: HexString) => void;
29
- dispose: () => void;
30
- convertJsonRpcEventToTyped: (event: Record<string, unknown>) => {
31
- tag: "Initialized";
32
- value: {
33
- readonly finalizedBlockHashes: HexString[];
34
- readonly finalizedBlockRuntime: unknown;
35
- };
36
- } | {
37
- tag: "NewBlock";
38
- value: {
39
- readonly blockHash: HexString;
40
- readonly parentBlockHash: HexString;
41
- readonly newRuntime: unknown;
42
- };
43
- } | {
44
- tag: "BestBlockChanged";
45
- value: {
46
- readonly bestBlockHash: HexString;
47
- };
48
- } | {
49
- tag: "Finalized";
50
- value: {
51
- readonly finalizedBlockHashes: HexString[];
52
- readonly prunedBlockHashes: HexString[];
53
- };
54
- } | {
55
- tag: "OperationBodyDone";
56
- value: {
57
- readonly operationId: string;
58
- readonly value: HexString[];
59
- };
60
- } | {
61
- tag: "OperationCallDone";
62
- value: {
63
- readonly operationId: string;
64
- readonly output: HexString;
65
- };
66
- } | {
67
- tag: "OperationStorageItems";
68
- value: {
69
- readonly operationId: string;
70
- readonly items: {
71
- key: HexString;
72
- value: `0x${string}`;
73
- hash: `0x${string}`;
74
- closestDescendantMerkleValue: `0x${string}`;
75
- }[];
76
- };
77
- } | {
78
- tag: "OperationStorageDone";
79
- value: {
80
- readonly operationId: string;
81
- };
82
- } | {
83
- tag: "OperationWaitingForContinue";
84
- value: {
85
- readonly operationId: string;
86
- };
87
- } | {
88
- tag: "OperationInaccessible";
89
- value: {
90
- readonly operationId: string;
91
- };
92
- } | {
93
- tag: "OperationError";
94
- value: {
95
- readonly operationId: string;
96
- readonly error: string;
97
- };
98
- } | {
99
- tag: "Stop";
100
- value: undefined;
12
+ stopFollow(genesisHash: HexString, followId: string): void;
13
+ hasActiveFollow(genesisHash: HexString): boolean;
14
+ chainHeadOp(genesisHash: HexString, method: string, params: unknown[]): Promise<unknown>;
15
+ sendRequest(genesisHash: HexString, method: string, params: unknown[]): Promise<unknown>;
16
+ releaseChain(genesisHash: HexString): void;
17
+ dispose(): void;
18
+ convertJsonRpcEventToTyped: typeof convertJsonRpcEventToTyped;
19
+ convertOperationStartedResult: typeof convertOperationStartedResult;
20
+ convertStorageQueryTypeToJsonRpc: typeof convertStorageQueryTypeToJsonRpc;
21
+ };
22
+ export declare function convertJsonRpcEventToTyped(event: Record<string, unknown>): {
23
+ tag: "Initialized";
24
+ value: {
25
+ readonly finalizedBlockHashes: HexString[];
26
+ readonly finalizedBlockRuntime: unknown;
27
+ };
28
+ } | {
29
+ tag: "NewBlock";
30
+ value: {
31
+ readonly blockHash: HexString;
32
+ readonly parentBlockHash: HexString;
33
+ readonly newRuntime: unknown;
34
+ };
35
+ } | {
36
+ tag: "BestBlockChanged";
37
+ value: {
38
+ readonly bestBlockHash: HexString;
39
+ };
40
+ } | {
41
+ tag: "Finalized";
42
+ value: {
43
+ readonly finalizedBlockHashes: HexString[];
44
+ readonly prunedBlockHashes: HexString[];
45
+ };
46
+ } | {
47
+ tag: "OperationBodyDone";
48
+ value: {
49
+ readonly operationId: string;
50
+ readonly value: HexString[];
101
51
  };
102
- convertOperationStartedResult: (result: unknown) => {
103
- tag: "Started";
104
- value: {
105
- readonly operationId: string;
106
- };
107
- } | {
108
- tag: "LimitReached";
109
- value: undefined;
52
+ } | {
53
+ tag: "OperationCallDone";
54
+ value: {
55
+ readonly operationId: string;
56
+ readonly output: HexString;
57
+ };
58
+ } | {
59
+ tag: "OperationStorageItems";
60
+ value: {
61
+ readonly operationId: string;
62
+ readonly items: {
63
+ key: HexString;
64
+ value: `0x${string}`;
65
+ hash: `0x${string}`;
66
+ closestDescendantMerkleValue: `0x${string}`;
67
+ }[];
68
+ };
69
+ } | {
70
+ tag: "OperationStorageDone";
71
+ value: {
72
+ readonly operationId: string;
73
+ };
74
+ } | {
75
+ tag: "OperationWaitingForContinue";
76
+ value: {
77
+ readonly operationId: string;
78
+ };
79
+ } | {
80
+ tag: "OperationInaccessible";
81
+ value: {
82
+ readonly operationId: string;
83
+ };
84
+ } | {
85
+ tag: "OperationError";
86
+ value: {
87
+ readonly operationId: string;
88
+ readonly error: string;
89
+ };
90
+ } | {
91
+ tag: "Stop";
92
+ value: undefined;
93
+ };
94
+ export declare function convertOperationStartedResult(result: unknown): {
95
+ tag: "Started";
96
+ value: {
97
+ readonly operationId: string;
110
98
  };
111
- convertStorageQueryTypeToJsonRpc: (type: string) => string;
99
+ } | {
100
+ tag: "LimitReached";
101
+ value: undefined;
112
102
  };
103
+ export declare function convertStorageQueryTypeToJsonRpc(type: string): string;
113
104
  export {};
@@ -1,310 +1,277 @@
1
1
  import { enumValue } from '@novasamatech/host-api';
2
- let instanceCounter = 0;
3
- export function createChainConnectionManager(factory) {
4
- const chains = new Map();
5
- const instanceId = instanceCounter++;
6
- let nextId = 0;
7
- function getNextId() {
8
- return `ccm_${instanceId}_${nextId++}`;
9
- }
10
- function getOrCreateChain(genesisHash) {
11
- const existing = chains.get(genesisHash);
12
- if (existing) {
13
- existing.refCount++;
14
- return existing;
15
- }
16
- const provider = factory(genesisHash);
17
- if (!provider)
18
- return null;
19
- const pendingRequests = new Map();
20
- const followSubscriptions = new Map();
21
- const entry = {
22
- connection: null,
23
- pendingRequests,
24
- followSubscriptions,
25
- refCount: 1,
26
- };
27
- entry.connection = provider(message => {
28
- // Response: has 'id' but no 'method'
29
- if ('id' in message && message.id != null && !('method' in message)) {
30
- const pending = pendingRequests.get(String(message.id));
31
- if (pending) {
32
- pendingRequests.delete(String(message.id));
33
- if ('error' in message) {
34
- pending.reject(message.error);
35
- }
36
- else {
37
- pending.resolve(message.result);
38
- }
39
- return;
40
- }
41
- }
42
- // Subscription notification: has 'method' and 'params'
43
- if ('method' in message && 'params' in message) {
44
- const params = message.params;
45
- if (params?.subscription) {
46
- const subId = String(params.subscription);
47
- for (const follow of followSubscriptions.values()) {
48
- if (follow.chainSubId === subId) {
49
- follow.eventListener(params.result);
50
- break;
51
- }
52
- }
2
+ import { StopError, createClient as createSubstrateClient } from '@polkadot-api/substrate-client';
3
+ // Operation events that signal a chain-head op is finished and its
4
+ // per-operation subscription can be torn down.
5
+ const TERMINAL_OPERATION_EVENTS = new Set([
6
+ 'operationBodyDone',
7
+ 'operationCallDone',
8
+ 'operationStorageDone',
9
+ 'operationError',
10
+ 'operationInaccessible',
11
+ ]);
12
+ // How long to hold a chain-head op when there's no active follow, waiting for
13
+ // the papp to issue a fresh Follow after a Stop. Long enough to absorb a
14
+ // realistic round-trip; short enough that a never-issuing papp fails fast.
15
+ const REFOLLOW_TIMEOUT_MS = 5_000;
16
+ function executeChainHeadOp(response, onEvent, method, params) {
17
+ return new Promise((resolve, reject) => {
18
+ response._request(method, params, {
19
+ onSuccess: (result, onSubscribeOperation) => {
20
+ const operationId = result?.operationId;
21
+ if (operationId) {
22
+ let unsub = () => undefined;
23
+ unsub = onSubscribeOperation(operationId, {
24
+ next: e => {
25
+ onEvent(e);
26
+ if (TERMINAL_OPERATION_EVENTS.has(e.event))
27
+ unsub();
28
+ },
29
+ error: () => unsub(),
30
+ });
53
31
  }
54
- }
55
- });
56
- chains.set(genesisHash, entry);
57
- return entry;
58
- }
59
- function sendRequest(genesisHash, method, params) {
60
- const entry = chains.get(genesisHash);
61
- if (!entry)
62
- return Promise.reject(new Error(`No connection for chain ${genesisHash}`));
63
- const id = getNextId();
64
- return new Promise((resolve, reject) => {
65
- entry.pendingRequests.set(id, { resolve, reject });
66
- entry.connection.send({ jsonrpc: '2.0', id, method, params });
67
- });
68
- }
69
- function startFollow(genesisHash, withRuntime, onEvent) {
70
- const entry = chains.get(genesisHash);
71
- if (!entry)
72
- throw new Error(`No connection for chain ${genesisHash}`);
73
- const followId = getNextId();
74
- const requestId = getNextId();
75
- const follow = {
76
- chainSubId: '', // will be set synchronously when response arrives
77
- eventListener: onEvent,
78
- pendingRequestId: requestId,
79
- };
80
- entry.followSubscriptions.set(followId, follow);
81
- // Bypass sendRequest to avoid Promise microtask deferral.
82
- // The response handler sets chainSubId synchronously, so when the next
83
- // message (the notification) is processed, chainSubId is already set.
84
- entry.pendingRequests.set(requestId, {
85
- resolve: result => {
86
- follow.chainSubId = result;
87
- follow.pendingRequestId = undefined;
88
- },
89
- reject: () => {
90
- follow.pendingRequestId = undefined;
91
- entry.followSubscriptions.delete(followId);
32
+ resolve(result);
92
33
  },
34
+ onError: reject,
93
35
  });
94
- entry.connection.send({
95
- jsonrpc: '2.0',
96
- id: requestId,
97
- method: 'chainHead_v1_follow',
98
- params: [withRuntime],
99
- });
100
- return { followId };
101
- }
102
- function stopFollow(genesisHash, followId) {
103
- const entry = chains.get(genesisHash);
104
- if (!entry)
105
- return;
106
- const follow = entry.followSubscriptions.get(followId);
107
- if (!follow)
108
- return;
109
- entry.followSubscriptions.delete(followId);
110
- if (follow.chainSubId) {
111
- const id = getNextId();
112
- entry.connection.send({
113
- jsonrpc: '2.0',
114
- id,
115
- method: 'chainHead_v1_unfollow',
116
- params: [follow.chainSubId],
117
- });
118
- }
119
- else if (follow.pendingRequestId) {
120
- // Follow response hasn't arrived yet — replace the pending resolve to send unfollow when it does
121
- entry.pendingRequests.set(follow.pendingRequestId, {
122
- resolve: result => {
123
- const chainSubId = result;
124
- if (chainSubId) {
125
- const unfollowId = getNextId();
126
- entry.connection.send({
127
- jsonrpc: '2.0',
128
- id: unfollowId,
129
- method: 'chainHead_v1_unfollow',
130
- params: [chainSubId],
131
- });
132
- }
133
- },
134
- reject: () => {
135
- /* follow already cleaned up */
136
- },
137
- });
36
+ });
37
+ }
38
+ export function createChainConnectionManager(factory, options = {}) {
39
+ const refollowTimeoutMs = options.refollowTimeoutMs ?? REFOLLOW_TIMEOUT_MS;
40
+ const chains = new Map();
41
+ let nextFollowId = 0;
42
+ function teardown(entry) {
43
+ for (const follow of entry.follows.values())
44
+ follow.response?.unfollow();
45
+ entry.follows.clear();
46
+ for (const op of entry.pendingOps) {
47
+ clearTimeout(op.timer);
48
+ op.reject(new Error('Chain disposed'));
138
49
  }
50
+ entry.pendingOps = [];
51
+ entry.client.destroy();
139
52
  }
140
- function getChainFollowSubId(genesisHash) {
53
+ function activeFollow(genesisHash) {
141
54
  const entry = chains.get(genesisHash);
142
55
  if (!entry)
143
56
  return null;
144
- // Return the first active follow subscription ID for this chain
145
- for (const follow of entry.followSubscriptions.values()) {
146
- if (follow.chainSubId)
147
- return follow.chainSubId;
57
+ for (const follow of entry.follows.values()) {
58
+ if (follow.response)
59
+ return follow;
148
60
  }
149
61
  return null;
150
62
  }
151
- function releaseChain(genesisHash) {
152
- const entry = chains.get(genesisHash);
153
- if (!entry)
154
- return;
155
- entry.refCount--;
156
- if (entry.refCount <= 0) {
157
- for (const follow of entry.followSubscriptions.values()) {
158
- if (follow.chainSubId) {
159
- const id = getNextId();
160
- entry.connection.send({
161
- jsonrpc: '2.0',
162
- id,
163
- method: 'chainHead_v1_unfollow',
164
- params: [follow.chainSubId],
165
- });
166
- }
63
+ return {
64
+ getOrCreateChain(genesisHash) {
65
+ const existing = chains.get(genesisHash);
66
+ if (existing) {
67
+ existing.refCount++;
68
+ return true;
167
69
  }
168
- entry.followSubscriptions.clear();
169
- entry.connection.disconnect();
170
- chains.delete(genesisHash);
171
- }
172
- }
173
- function dispose() {
174
- for (const entry of chains.values()) {
175
- for (const follow of entry.followSubscriptions.values()) {
176
- if (follow.chainSubId) {
177
- const id = getNextId();
178
- entry.connection.send({
179
- jsonrpc: '2.0',
180
- id,
181
- method: 'chainHead_v1_unfollow',
182
- params: [follow.chainSubId],
183
- });
70
+ const provider = factory(genesisHash);
71
+ if (!provider)
72
+ return false;
73
+ chains.set(genesisHash, {
74
+ client: createSubstrateClient(provider),
75
+ follows: new Map(),
76
+ pendingOps: [],
77
+ recoveringSince: null,
78
+ refCount: 1,
79
+ });
80
+ return true;
81
+ },
82
+ startFollow(genesisHash, withRuntime, onEvent) {
83
+ const entry = chains.get(genesisHash);
84
+ if (!entry)
85
+ throw new Error(`No connection for chain ${genesisHash}`);
86
+ const followId = `f${nextFollowId++}`;
87
+ const follow = { response: null, onEvent };
88
+ entry.follows.set(followId, follow);
89
+ const response = entry.client.chainHead(withRuntime,
90
+ // substrate-client renames the spec's `event` field to `type`. Restore it.
91
+ ({ type, ...rest }) => onEvent({ event: type, ...rest }), error => {
92
+ // The follow is dead. Drop the entry so it doesn't accumulate as a
93
+ // tombstone; clear the response field on the (now-detached)
94
+ // FollowEntry for any handle still holding it. For a spec stop,
95
+ // surface the event to the papp and open the recovery window so
96
+ // chain-head ops issued before the papp refollows queue rather
97
+ // than fail. Non-Stop errors get no recovery window — those are
98
+ // genuine failures, not protocol-driven restarts.
99
+ entry.follows.delete(followId);
100
+ follow.response = null;
101
+ if (error instanceof StopError) {
102
+ onEvent({ event: 'stop' });
103
+ entry.recoveringSince = Date.now();
104
+ }
105
+ });
106
+ follow.response = response;
107
+ // Fresh follow ends any in-flight recovery window. Drain ops queued
108
+ // during the gap through this follow's onEvent so operation events
109
+ // still reach the papp.
110
+ entry.recoveringSince = null;
111
+ if (entry.pendingOps.length > 0) {
112
+ const queued = entry.pendingOps;
113
+ entry.pendingOps = [];
114
+ for (const op of queued) {
115
+ clearTimeout(op.timer);
116
+ executeChainHeadOp(response, onEvent, op.method, op.params).then(op.resolve, op.reject);
184
117
  }
185
118
  }
186
- entry.followSubscriptions.clear();
187
- entry.connection.disconnect();
188
- }
189
- chains.clear();
190
- }
191
- // === JSON RPC ↔ typed conversion helpers ===
192
- function convertJsonRpcEventToTyped(event) {
193
- const eventType = event.event;
194
- switch (eventType) {
195
- case 'initialized':
196
- return enumValue('Initialized', {
197
- finalizedBlockHashes: event.finalizedBlockHashes,
198
- finalizedBlockRuntime: convertRuntime(event.finalizedBlockRuntime),
199
- });
200
- case 'newBlock':
201
- return enumValue('NewBlock', {
202
- blockHash: event.blockHash,
203
- parentBlockHash: event.parentBlockHash,
204
- newRuntime: convertRuntime(event.newRuntime),
205
- });
206
- case 'bestBlockChanged':
207
- return enumValue('BestBlockChanged', {
208
- bestBlockHash: event.bestBlockHash,
209
- });
210
- case 'finalized':
211
- return enumValue('Finalized', {
212
- finalizedBlockHashes: event.finalizedBlockHashes,
213
- prunedBlockHashes: event.prunedBlockHashes,
214
- });
215
- case 'operationBodyDone':
216
- return enumValue('OperationBodyDone', {
217
- operationId: event.operationId,
218
- value: event.value,
219
- });
220
- case 'operationCallDone':
221
- return enumValue('OperationCallDone', {
222
- operationId: event.operationId,
223
- output: event.output,
224
- });
225
- case 'operationStorageItems':
226
- return enumValue('OperationStorageItems', {
227
- operationId: event.operationId,
228
- items: event.items.map(item => ({
229
- key: item.key,
230
- value: item.value ?? null,
231
- hash: item.hash ?? null,
232
- closestDescendantMerkleValue: item.closestDescendantMerkleValue ?? null,
233
- })),
234
- });
235
- case 'operationStorageDone':
236
- return enumValue('OperationStorageDone', {
237
- operationId: event.operationId,
238
- });
239
- case 'operationWaitingForContinue':
240
- return enumValue('OperationWaitingForContinue', {
241
- operationId: event.operationId,
242
- });
243
- case 'operationInaccessible':
244
- return enumValue('OperationInaccessible', {
245
- operationId: event.operationId,
246
- });
247
- case 'operationError':
248
- return enumValue('OperationError', {
249
- operationId: event.operationId,
250
- error: event.error,
251
- });
252
- case 'stop':
253
- return enumValue('Stop', undefined);
254
- default:
255
- return enumValue('Stop', undefined);
256
- }
257
- }
258
- function convertRuntime(runtime) {
259
- if (!runtime || typeof runtime !== 'object')
260
- return undefined;
261
- const rt = runtime;
262
- if (rt.type === 'valid') {
263
- const spec = rt.spec;
264
- const apis = spec.apis;
265
- return enumValue('Valid', {
266
- specName: spec.specName,
267
- implName: spec.implName,
268
- specVersion: spec.specVersion,
269
- implVersion: spec.implVersion,
270
- transactionVersion: spec.transactionVersion,
271
- apis: apis ? Object.entries(apis).map(([name, version]) => [name, version]) : [],
119
+ return { followId };
120
+ },
121
+ stopFollow(genesisHash, followId) {
122
+ const entry = chains.get(genesisHash);
123
+ if (!entry)
124
+ return;
125
+ const follow = entry.follows.get(followId);
126
+ if (!follow)
127
+ return;
128
+ entry.follows.delete(followId);
129
+ follow.response?.unfollow();
130
+ },
131
+ hasActiveFollow(genesisHash) {
132
+ return activeFollow(genesisHash) !== null;
133
+ },
134
+ chainHeadOp(genesisHash, method, params) {
135
+ const follow = activeFollow(genesisHash);
136
+ if (follow?.response)
137
+ return executeChainHeadOp(follow.response, follow.onEvent, method, params);
138
+ // No active follow. If we're inside a recovery window opened by a
139
+ // recent Stop, hold the op so the brief gap before the papp's refollow
140
+ // doesn't surface as an error. Otherwise fail fast — no Stop, no
141
+ // expected refollow.
142
+ const entry = chains.get(genesisHash);
143
+ if (!entry || entry.recoveringSince === null) {
144
+ return Promise.reject(new Error('No active follow for this chain'));
145
+ }
146
+ if (Date.now() - entry.recoveringSince > refollowTimeoutMs) {
147
+ entry.recoveringSince = null;
148
+ return Promise.reject(new Error('No active follow for this chain'));
149
+ }
150
+ return new Promise((resolve, reject) => {
151
+ const op = {
152
+ method,
153
+ params,
154
+ resolve,
155
+ reject,
156
+ timer: setTimeout(() => {
157
+ const idx = entry.pendingOps.indexOf(op);
158
+ if (idx !== -1)
159
+ entry.pendingOps.splice(idx, 1);
160
+ reject(new Error('No active follow for this chain'));
161
+ }, refollowTimeoutMs),
162
+ };
163
+ entry.pendingOps.push(op);
272
164
  });
273
- }
274
- if (rt.type === 'invalid') {
275
- return enumValue('Invalid', { error: rt.error });
276
- }
277
- return undefined;
278
- }
279
- function convertOperationStartedResult(result) {
280
- if (typeof result === 'object' && result !== null) {
281
- const r = result;
282
- if (r.result === 'started') {
283
- return enumValue('Started', { operationId: r.operationId });
165
+ },
166
+ sendRequest(genesisHash, method, params) {
167
+ const entry = chains.get(genesisHash);
168
+ if (!entry)
169
+ return Promise.reject(new Error(`No connection for chain ${genesisHash}`));
170
+ return entry.client.request(method, params);
171
+ },
172
+ releaseChain(genesisHash) {
173
+ const entry = chains.get(genesisHash);
174
+ if (!entry)
175
+ return;
176
+ if (--entry.refCount <= 0) {
177
+ teardown(entry);
178
+ chains.delete(genesisHash);
284
179
  }
285
- }
286
- return enumValue('LimitReached', undefined);
287
- }
288
- function convertStorageQueryTypeToJsonRpc(type) {
289
- const map = {
290
- Value: 'value',
291
- Hash: 'hash',
292
- ClosestDescendantMerkleValue: 'closestDescendantMerkleValue',
293
- DescendantsValues: 'descendantsValues',
294
- DescendantsHashes: 'descendantsHashes',
295
- };
296
- return map[type] ?? 'value';
297
- }
298
- return {
299
- getOrCreateChain,
300
- sendRequest,
301
- startFollow,
302
- stopFollow,
303
- getChainFollowSubId,
304
- releaseChain,
305
- dispose,
180
+ },
181
+ dispose() {
182
+ for (const entry of chains.values())
183
+ teardown(entry);
184
+ chains.clear();
185
+ },
306
186
  convertJsonRpcEventToTyped,
307
187
  convertOperationStartedResult,
308
188
  convertStorageQueryTypeToJsonRpc,
309
189
  };
310
190
  }
191
+ // === JSON-RPC ↔ typed conversion (pure, no closure state) ===
192
+ function convertRuntime(runtime) {
193
+ if (!runtime || typeof runtime !== 'object')
194
+ return undefined;
195
+ const rt = runtime;
196
+ if (rt.type === 'valid') {
197
+ const spec = rt.spec;
198
+ const apis = spec.apis;
199
+ return enumValue('Valid', {
200
+ specName: spec.specName,
201
+ implName: spec.implName,
202
+ specVersion: spec.specVersion,
203
+ implVersion: spec.implVersion,
204
+ transactionVersion: spec.transactionVersion,
205
+ apis: apis ? Object.entries(apis).map(([name, version]) => [name, version]) : [],
206
+ });
207
+ }
208
+ if (rt.type === 'invalid')
209
+ return enumValue('Invalid', { error: rt.error });
210
+ return undefined;
211
+ }
212
+ export function convertJsonRpcEventToTyped(event) {
213
+ switch (event.event) {
214
+ case 'initialized':
215
+ return enumValue('Initialized', {
216
+ finalizedBlockHashes: event.finalizedBlockHashes,
217
+ finalizedBlockRuntime: convertRuntime(event.finalizedBlockRuntime),
218
+ });
219
+ case 'newBlock':
220
+ return enumValue('NewBlock', {
221
+ blockHash: event.blockHash,
222
+ parentBlockHash: event.parentBlockHash,
223
+ newRuntime: convertRuntime(event.newRuntime),
224
+ });
225
+ case 'bestBlockChanged':
226
+ return enumValue('BestBlockChanged', { bestBlockHash: event.bestBlockHash });
227
+ case 'finalized':
228
+ return enumValue('Finalized', {
229
+ finalizedBlockHashes: event.finalizedBlockHashes,
230
+ prunedBlockHashes: event.prunedBlockHashes,
231
+ });
232
+ case 'operationBodyDone':
233
+ return enumValue('OperationBodyDone', {
234
+ operationId: event.operationId,
235
+ value: event.value,
236
+ });
237
+ case 'operationCallDone':
238
+ return enumValue('OperationCallDone', {
239
+ operationId: event.operationId,
240
+ output: event.output,
241
+ });
242
+ case 'operationStorageItems':
243
+ return enumValue('OperationStorageItems', {
244
+ operationId: event.operationId,
245
+ items: event.items.map(item => ({
246
+ key: item.key,
247
+ value: item.value ?? null,
248
+ hash: item.hash ?? null,
249
+ closestDescendantMerkleValue: item.closestDescendantMerkleValue ?? null,
250
+ })),
251
+ });
252
+ case 'operationStorageDone':
253
+ return enumValue('OperationStorageDone', { operationId: event.operationId });
254
+ case 'operationWaitingForContinue':
255
+ return enumValue('OperationWaitingForContinue', { operationId: event.operationId });
256
+ case 'operationInaccessible':
257
+ return enumValue('OperationInaccessible', { operationId: event.operationId });
258
+ case 'operationError':
259
+ return enumValue('OperationError', {
260
+ operationId: event.operationId,
261
+ error: event.error,
262
+ });
263
+ case 'stop':
264
+ default:
265
+ return enumValue('Stop', undefined);
266
+ }
267
+ }
268
+ export function convertOperationStartedResult(result) {
269
+ const r = result;
270
+ return r?.result === 'started'
271
+ ? enumValue('Started', { operationId: r.operationId })
272
+ : enumValue('LimitReached', undefined);
273
+ }
274
+ // 'Value' → 'value', 'ClosestDescendantMerkleValue' → 'closestDescendantMerkleValue', …
275
+ export function convertStorageQueryTypeToJsonRpc(type) {
276
+ return type.charAt(0).toLowerCase() + type.slice(1);
277
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,317 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { createChainConnectionManager } from './chainConnectionManager.js';
3
+ const GENESIS = '0xabc';
4
+ const createMockProvider = () => {
5
+ const send = vi.fn();
6
+ const disconnect = vi.fn();
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ let onMessage = null;
9
+ const provider = cb => {
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ onMessage = cb;
12
+ return { send, disconnect };
13
+ };
14
+ return {
15
+ provider,
16
+ send,
17
+ disconnect,
18
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
19
+ simulateMessage: (msg) => onMessage?.(msg),
20
+ };
21
+ };
22
+ const sentMessages = (mock) => mock.send.mock.calls.map(([msg]) => msg);
23
+ const findCallId = (mock, method) => {
24
+ const match = sentMessages(mock).find(msg => msg.method === method);
25
+ if (!match)
26
+ throw new Error(`no '${method}' call found`);
27
+ return match.id;
28
+ };
29
+ describe('chainConnectionManager', () => {
30
+ describe('startFollow / stop event handling', () => {
31
+ it('sends chainHead_v1_follow on startFollow', () => {
32
+ const mock = createMockProvider();
33
+ const manager = createChainConnectionManager(() => mock.provider);
34
+ manager.getOrCreateChain(GENESIS);
35
+ manager.startFollow(GENESIS, true, vi.fn());
36
+ const calls = sentMessages(mock);
37
+ expect(calls).toHaveLength(1);
38
+ expect(calls[0]).toMatchObject({ method: 'chainHead_v1_follow', params: [true] });
39
+ });
40
+ it('hasActiveFollow becomes true on startFollow and stays true through the server confirmation', () => {
41
+ // The follow lifecycle is owned by substrate-client's chainHead, which
42
+ // queues chain-head ops on the underlying followSubscription Promise
43
+ // until the server assigns a chainSubId. So a follow is "active" from
44
+ // the moment startFollow returns; ops issued in the setup window are
45
+ // not rejected — they queue and fire once the chainSubId arrives.
46
+ const mock = createMockProvider();
47
+ const manager = createChainConnectionManager(() => mock.provider);
48
+ manager.getOrCreateChain(GENESIS);
49
+ manager.startFollow(GENESIS, true, vi.fn());
50
+ expect(manager.hasActiveFollow(GENESIS)).toBe(true);
51
+ mock.simulateMessage({ jsonrpc: '2.0', id: findCallId(mock, 'chainHead_v1_follow'), result: 'sub-id-1' });
52
+ expect(manager.hasActiveFollow(GENESIS)).toBe(true);
53
+ });
54
+ it('forwards a synthetic stop event to the listener and clears hasActiveFollow', () => {
55
+ const mock = createMockProvider();
56
+ const manager = createChainConnectionManager(() => mock.provider);
57
+ const events = [];
58
+ manager.getOrCreateChain(GENESIS);
59
+ manager.startFollow(GENESIS, true, e => events.push(e));
60
+ mock.simulateMessage({ jsonrpc: '2.0', id: findCallId(mock, 'chainHead_v1_follow'), result: 'sub-id-1' });
61
+ mock.simulateMessage({
62
+ jsonrpc: '2.0',
63
+ method: 'chainHead_v1_followEvent',
64
+ params: { subscription: 'sub-id-1', result: { event: 'stop' } },
65
+ });
66
+ expect(events).toEqual([{ event: 'stop' }]);
67
+ expect(manager.hasActiveFollow(GENESIS)).toBe(false);
68
+ });
69
+ it('forwards initialized/newBlock follow events with the spec-shape `event` field restored', () => {
70
+ const mock = createMockProvider();
71
+ const manager = createChainConnectionManager(() => mock.provider);
72
+ const events = [];
73
+ manager.getOrCreateChain(GENESIS);
74
+ manager.startFollow(GENESIS, true, e => events.push(e));
75
+ mock.simulateMessage({ jsonrpc: '2.0', id: findCallId(mock, 'chainHead_v1_follow'), result: 'sub-id-1' });
76
+ mock.simulateMessage({
77
+ jsonrpc: '2.0',
78
+ method: 'chainHead_v1_followEvent',
79
+ params: {
80
+ subscription: 'sub-id-1',
81
+ result: {
82
+ event: 'initialized',
83
+ finalizedBlockHashes: ['0xdead'],
84
+ finalizedBlockRuntime: {
85
+ type: 'valid',
86
+ spec: { specName: 'x', implName: 'y', specVersion: 1, implVersion: 1, apis: {} },
87
+ },
88
+ },
89
+ },
90
+ });
91
+ expect(events).toEqual([
92
+ expect.objectContaining({
93
+ event: 'initialized',
94
+ finalizedBlockHashes: ['0xdead'],
95
+ }),
96
+ ]);
97
+ });
98
+ it('drops the dead follow entry on stop so it does not leak as a tombstone', () => {
99
+ const mock = createMockProvider();
100
+ const manager = createChainConnectionManager(() => mock.provider);
101
+ manager.getOrCreateChain(GENESIS);
102
+ const { followId } = manager.startFollow(GENESIS, true, vi.fn());
103
+ mock.simulateMessage({ jsonrpc: '2.0', id: findCallId(mock, 'chainHead_v1_follow'), result: 'sub-id-1' });
104
+ mock.simulateMessage({
105
+ jsonrpc: '2.0',
106
+ method: 'chainHead_v1_followEvent',
107
+ params: { subscription: 'sub-id-1', result: { event: 'stop' } },
108
+ });
109
+ // stopFollow on the now-dead followId is a no-op (entry already cleaned
110
+ // up). The unfollow RPC must NOT be sent — the follow is already gone.
111
+ const beforeStop = sentMessages(mock).length;
112
+ manager.stopFollow(GENESIS, followId);
113
+ expect(sentMessages(mock)).toHaveLength(beforeStop);
114
+ });
115
+ it('a fresh follow after a stop is tracked under its new chainSubId', () => {
116
+ const mock = createMockProvider();
117
+ const manager = createChainConnectionManager(() => mock.provider);
118
+ manager.getOrCreateChain(GENESIS);
119
+ manager.startFollow(GENESIS, true, vi.fn());
120
+ mock.simulateMessage({ jsonrpc: '2.0', id: findCallId(mock, 'chainHead_v1_follow'), result: 'sub-id-1' });
121
+ mock.simulateMessage({
122
+ jsonrpc: '2.0',
123
+ method: 'chainHead_v1_followEvent',
124
+ params: { subscription: 'sub-id-1', result: { event: 'stop' } },
125
+ });
126
+ expect(manager.hasActiveFollow(GENESIS)).toBe(false);
127
+ manager.startFollow(GENESIS, true, vi.fn());
128
+ const followCalls = sentMessages(mock).filter(m => m.method === 'chainHead_v1_follow');
129
+ expect(followCalls).toHaveLength(2);
130
+ mock.simulateMessage({ jsonrpc: '2.0', id: followCalls[1].id, result: 'sub-id-2' });
131
+ expect(manager.hasActiveFollow(GENESIS)).toBe(true);
132
+ });
133
+ });
134
+ describe('chainHeadOp', () => {
135
+ it('rejects with "No active follow for this chain" when no follow is active', async () => {
136
+ const mock = createMockProvider();
137
+ const manager = createChainConnectionManager(() => mock.provider);
138
+ manager.getOrCreateChain(GENESIS);
139
+ // No startFollow.
140
+ await expect(manager.chainHeadOp(GENESIS, 'chainHead_v1_header', ['0xhash'])).rejects.toThrow('No active follow for this chain');
141
+ });
142
+ it('rejects after the refollow timeout elapses with no fresh Follow', async () => {
143
+ const mock = createMockProvider();
144
+ const manager = createChainConnectionManager(() => mock.provider, { refollowTimeoutMs: 30 });
145
+ manager.getOrCreateChain(GENESIS);
146
+ manager.startFollow(GENESIS, true, vi.fn());
147
+ mock.simulateMessage({ jsonrpc: '2.0', id: findCallId(mock, 'chainHead_v1_follow'), result: 'sub-id-1' });
148
+ mock.simulateMessage({
149
+ jsonrpc: '2.0',
150
+ method: 'chainHead_v1_followEvent',
151
+ params: { subscription: 'sub-id-1', result: { event: 'stop' } },
152
+ });
153
+ await expect(manager.chainHeadOp(GENESIS, 'chainHead_v1_header', ['0xhash'])).rejects.toThrow('No active follow for this chain');
154
+ });
155
+ it('still queues when the papp closes its dead subscription before opening a new one', async () => {
156
+ // Recovery window is gated on the most recent Stop, not on whether an
157
+ // entry happens to remain in the follows map. So even if the papp's
158
+ // pattern is "stopFollow on the old subscription, then startFollow on
159
+ // a new one", ops issued in the gap still queue.
160
+ const mock = createMockProvider();
161
+ const manager = createChainConnectionManager(() => mock.provider, { refollowTimeoutMs: 1_000 });
162
+ manager.getOrCreateChain(GENESIS);
163
+ const { followId } = manager.startFollow(GENESIS, true, vi.fn());
164
+ mock.simulateMessage({ jsonrpc: '2.0', id: findCallId(mock, 'chainHead_v1_follow'), result: 'sub-id-1' });
165
+ mock.simulateMessage({
166
+ jsonrpc: '2.0',
167
+ method: 'chainHead_v1_followEvent',
168
+ params: { subscription: 'sub-id-1', result: { event: 'stop' } },
169
+ });
170
+ // Papp closes its OLD subscription on Stop, BEFORE opening NEW.
171
+ manager.stopFollow(GENESIS, followId);
172
+ // Op issued in the gap must queue, not fail fast.
173
+ const opPromise = manager.chainHeadOp(GENESIS, 'chainHead_v1_header', ['0xhash']);
174
+ expect(sentMessages(mock).find(m => m.method === 'chainHead_v1_header')).toBeUndefined();
175
+ manager.startFollow(GENESIS, true, vi.fn());
176
+ const newFollowId = sentMessages(mock).filter(m => m.method === 'chainHead_v1_follow')[1].id;
177
+ mock.simulateMessage({ jsonrpc: '2.0', id: newFollowId, result: 'sub-id-2' });
178
+ await Promise.resolve();
179
+ const headerCall = sentMessages(mock).find(m => m.method === 'chainHead_v1_header');
180
+ expect(headerCall?.params).toEqual(['sub-id-2', '0xhash']);
181
+ mock.simulateMessage({ jsonrpc: '2.0', id: headerCall.id, result: '0xheader' });
182
+ await expect(opPromise).resolves.toBe('0xheader');
183
+ });
184
+ it('queues a chain-head op issued in the stop→refollow gap and drains it into the new follow', async () => {
185
+ const mock = createMockProvider();
186
+ const manager = createChainConnectionManager(() => mock.provider, { refollowTimeoutMs: 1_000 });
187
+ manager.getOrCreateChain(GENESIS);
188
+ manager.startFollow(GENESIS, true, vi.fn());
189
+ const firstFollowId = findCallId(mock, 'chainHead_v1_follow');
190
+ mock.simulateMessage({ jsonrpc: '2.0', id: firstFollowId, result: 'sub-id-1' });
191
+ // Stop invalidates the first follow but leaves the entry around so ops can queue.
192
+ mock.simulateMessage({
193
+ jsonrpc: '2.0',
194
+ method: 'chainHead_v1_followEvent',
195
+ params: { subscription: 'sub-id-1', result: { event: 'stop' } },
196
+ });
197
+ // Op issued while no follow is active — should not fail immediately.
198
+ const opPromise = manager.chainHeadOp(GENESIS, 'chainHead_v1_header', ['0xhash']);
199
+ // No header request hit the wire yet (still queued).
200
+ expect(sentMessages(mock).find(m => m.method === 'chainHead_v1_header')).toBeUndefined();
201
+ // Papp issues a fresh Follow.
202
+ manager.startFollow(GENESIS, true, vi.fn());
203
+ const secondFollowId = sentMessages(mock).filter(m => m.method === 'chainHead_v1_follow')[1].id;
204
+ mock.simulateMessage({ jsonrpc: '2.0', id: secondFollowId, result: 'sub-id-2' });
205
+ // The queued op drains into the new follow on the next microtask
206
+ // (substrate-client's fRequest waits on the followSubscription Promise).
207
+ await Promise.resolve();
208
+ const headerCall = sentMessages(mock).find(m => m.method === 'chainHead_v1_header');
209
+ expect(headerCall?.params).toEqual(['sub-id-2', '0xhash']);
210
+ mock.simulateMessage({ jsonrpc: '2.0', id: headerCall.id, result: '0xheader' });
211
+ await expect(opPromise).resolves.toBe('0xheader');
212
+ });
213
+ it('rejects queued ops when the chain is disposed before a refollow', async () => {
214
+ const mock = createMockProvider();
215
+ const manager = createChainConnectionManager(() => mock.provider, { refollowTimeoutMs: 1_000 });
216
+ manager.getOrCreateChain(GENESIS);
217
+ manager.startFollow(GENESIS, true, vi.fn());
218
+ mock.simulateMessage({ jsonrpc: '2.0', id: findCallId(mock, 'chainHead_v1_follow'), result: 'sub-id-1' });
219
+ mock.simulateMessage({
220
+ jsonrpc: '2.0',
221
+ method: 'chainHead_v1_followEvent',
222
+ params: { subscription: 'sub-id-1', result: { event: 'stop' } },
223
+ });
224
+ const opPromise = manager.chainHeadOp(GENESIS, 'chainHead_v1_header', ['0xhash']);
225
+ manager.releaseChain(GENESIS);
226
+ await expect(opPromise).rejects.toThrow('Chain disposed');
227
+ });
228
+ it('auto-prepends the chainSubId when forwarding a chain-head op to the provider', async () => {
229
+ const mock = createMockProvider();
230
+ const manager = createChainConnectionManager(() => mock.provider);
231
+ manager.getOrCreateChain(GENESIS);
232
+ manager.startFollow(GENESIS, true, vi.fn());
233
+ mock.simulateMessage({ jsonrpc: '2.0', id: findCallId(mock, 'chainHead_v1_follow'), result: 'sub-id-1' });
234
+ const promise = manager.chainHeadOp(GENESIS, 'chainHead_v1_header', ['0xblockHash']);
235
+ const headerCall = sentMessages(mock).find(m => m.method === 'chainHead_v1_header');
236
+ expect(headerCall?.params).toEqual(['sub-id-1', '0xblockHash']);
237
+ mock.simulateMessage({ jsonrpc: '2.0', id: headerCall.id, result: '0xheader' });
238
+ await expect(promise).resolves.toBe('0xheader');
239
+ });
240
+ it('forwards operation events streamed against the started operationId to the follow listener', async () => {
241
+ const mock = createMockProvider();
242
+ const manager = createChainConnectionManager(() => mock.provider);
243
+ const events = [];
244
+ manager.getOrCreateChain(GENESIS);
245
+ manager.startFollow(GENESIS, true, e => events.push(e));
246
+ mock.simulateMessage({ jsonrpc: '2.0', id: findCallId(mock, 'chainHead_v1_follow'), result: 'sub-id-1' });
247
+ const promise = manager.chainHeadOp(GENESIS, 'chainHead_v1_storage', ['0xhash', [], null]);
248
+ const storageCall = sentMessages(mock).find(m => m.method === 'chainHead_v1_storage');
249
+ mock.simulateMessage({
250
+ jsonrpc: '2.0',
251
+ id: storageCall.id,
252
+ result: { result: 'started', operationId: 'op-1', discardedItems: 0 },
253
+ });
254
+ await expect(promise).resolves.toMatchObject({ result: 'started', operationId: 'op-1' });
255
+ // Now operation events streamed via the follow notification must reach the listener.
256
+ const items = { event: 'operationStorageItems', operationId: 'op-1', items: [] };
257
+ mock.simulateMessage({
258
+ jsonrpc: '2.0',
259
+ method: 'chainHead_v1_followEvent',
260
+ params: { subscription: 'sub-id-1', result: items },
261
+ });
262
+ const done = { event: 'operationStorageDone', operationId: 'op-1' };
263
+ mock.simulateMessage({
264
+ jsonrpc: '2.0',
265
+ method: 'chainHead_v1_followEvent',
266
+ params: { subscription: 'sub-id-1', result: done },
267
+ });
268
+ expect(events).toContainEqual(items);
269
+ expect(events).toContainEqual(done);
270
+ });
271
+ });
272
+ describe('basic flows', () => {
273
+ it('sendRequest forwards a non-chainHead JSON-RPC and resolves on response', async () => {
274
+ const mock = createMockProvider();
275
+ const manager = createChainConnectionManager(() => mock.provider);
276
+ manager.getOrCreateChain(GENESIS);
277
+ const promise = manager.sendRequest(GENESIS, 'chainSpec_v1_genesisHash', []);
278
+ const call = sentMessages(mock).find(m => m.method === 'chainSpec_v1_genesisHash');
279
+ mock.simulateMessage({ jsonrpc: '2.0', id: call.id, result: '0xgenesis' });
280
+ await expect(promise).resolves.toBe('0xgenesis');
281
+ });
282
+ it('sendRequest rejects on error response', async () => {
283
+ const mock = createMockProvider();
284
+ const manager = createChainConnectionManager(() => mock.provider);
285
+ manager.getOrCreateChain(GENESIS);
286
+ const promise = manager.sendRequest(GENESIS, 'chainSpec_v1_genesisHash', []);
287
+ const call = sentMessages(mock).find(m => m.method === 'chainSpec_v1_genesisHash');
288
+ mock.simulateMessage({
289
+ jsonrpc: '2.0',
290
+ id: call.id,
291
+ error: { code: -32000, message: 'unsupported' },
292
+ });
293
+ await expect(promise).rejects.toBeInstanceOf(Error);
294
+ });
295
+ it('releaseChain only disconnects when refCount drops to zero', () => {
296
+ const mock = createMockProvider();
297
+ const manager = createChainConnectionManager(() => mock.provider);
298
+ manager.getOrCreateChain(GENESIS);
299
+ manager.getOrCreateChain(GENESIS);
300
+ manager.releaseChain(GENESIS);
301
+ expect(mock.disconnect).not.toHaveBeenCalled();
302
+ manager.releaseChain(GENESIS);
303
+ expect(mock.disconnect).toHaveBeenCalledTimes(1);
304
+ });
305
+ it('stopFollow sends chainHead_v1_unfollow with the current chainSubId', () => {
306
+ const mock = createMockProvider();
307
+ const manager = createChainConnectionManager(() => mock.provider);
308
+ manager.getOrCreateChain(GENESIS);
309
+ const { followId } = manager.startFollow(GENESIS, true, vi.fn());
310
+ mock.simulateMessage({ jsonrpc: '2.0', id: findCallId(mock, 'chainHead_v1_follow'), result: 'sub-id-1' });
311
+ manager.stopFollow(GENESIS, followId);
312
+ const unfollowCall = sentMessages(mock).find(m => m.method === 'chainHead_v1_unfollow');
313
+ expect(unfollowCall?.params).toEqual(['sub-id-1']);
314
+ expect(manager.hasActiveFollow(GENESIS)).toBe(false);
315
+ });
316
+ });
317
+ });
@@ -353,12 +353,11 @@ export function createContainer(provider) {
353
353
  return enumValue('v1', resultErr(new GenericError({ reason: UNSUPPORTED_MESSAGE_FORMAT_ERROR })));
354
354
  }
355
355
  const { genesisHash, hash } = message.value;
356
- const realSubId = manager.getChainFollowSubId(genesisHash);
357
- if (!realSubId) {
356
+ if (!manager.hasActiveFollow(genesisHash)) {
358
357
  return enumValue('v1', resultErr(new GenericError({ reason: 'No active follow for this chain' })));
359
358
  }
360
359
  try {
361
- const result = await manager.sendRequest(genesisHash, 'chainHead_v1_header', [realSubId, hash]);
360
+ const result = await manager.chainHeadOp(genesisHash, 'chainHead_v1_header', [hash]);
362
361
  return enumValue('v1', resultOk(result));
363
362
  }
364
363
  catch (e) {
@@ -371,12 +370,11 @@ export function createContainer(provider) {
371
370
  return enumValue('v1', resultErr(new GenericError({ reason: UNSUPPORTED_MESSAGE_FORMAT_ERROR })));
372
371
  }
373
372
  const { genesisHash, hash } = message.value;
374
- const realSubId = manager.getChainFollowSubId(genesisHash);
375
- if (!realSubId) {
373
+ if (!manager.hasActiveFollow(genesisHash)) {
376
374
  return enumValue('v1', resultErr(new GenericError({ reason: 'No active follow for this chain' })));
377
375
  }
378
376
  try {
379
- const result = await manager.sendRequest(genesisHash, 'chainHead_v1_body', [realSubId, hash]);
377
+ const result = await manager.chainHeadOp(genesisHash, 'chainHead_v1_body', [hash]);
380
378
  return enumValue('v1', resultOk(manager.convertOperationStartedResult(result)));
381
379
  }
382
380
  catch (e) {
@@ -389,8 +387,7 @@ export function createContainer(provider) {
389
387
  return enumValue('v1', resultErr(new GenericError({ reason: UNSUPPORTED_MESSAGE_FORMAT_ERROR })));
390
388
  }
391
389
  const { genesisHash, hash, items, childTrie } = message.value;
392
- const realSubId = manager.getChainFollowSubId(genesisHash);
393
- if (!realSubId) {
390
+ if (!manager.hasActiveFollow(genesisHash)) {
394
391
  return enumValue('v1', resultErr(new GenericError({ reason: 'No active follow for this chain' })));
395
392
  }
396
393
  const jsonRpcItems = items.map((item) => ({
@@ -398,8 +395,7 @@ export function createContainer(provider) {
398
395
  type: manager.convertStorageQueryTypeToJsonRpc(item.type),
399
396
  }));
400
397
  try {
401
- const result = await manager.sendRequest(genesisHash, 'chainHead_v1_storage', [
402
- realSubId,
398
+ const result = await manager.chainHeadOp(genesisHash, 'chainHead_v1_storage', [
403
399
  hash,
404
400
  jsonRpcItems,
405
401
  childTrie,
@@ -416,13 +412,11 @@ export function createContainer(provider) {
416
412
  return enumValue('v1', resultErr(new GenericError({ reason: UNSUPPORTED_MESSAGE_FORMAT_ERROR })));
417
413
  }
418
414
  const params = message.value;
419
- const realSubId = manager.getChainFollowSubId(params.genesisHash);
420
- if (!realSubId) {
415
+ if (!manager.hasActiveFollow(params.genesisHash)) {
421
416
  return enumValue('v1', resultErr(new GenericError({ reason: 'No active follow for this chain' })));
422
417
  }
423
418
  try {
424
- const result = await manager.sendRequest(params.genesisHash, 'chainHead_v1_call', [
425
- realSubId,
419
+ const result = await manager.chainHeadOp(params.genesisHash, 'chainHead_v1_call', [
426
420
  params.hash,
427
421
  params.function,
428
422
  params.callParameters,
@@ -439,12 +433,11 @@ export function createContainer(provider) {
439
433
  return enumValue('v1', resultErr(new GenericError({ reason: UNSUPPORTED_MESSAGE_FORMAT_ERROR })));
440
434
  }
441
435
  const { genesisHash, hashes } = message.value;
442
- const realSubId = manager.getChainFollowSubId(genesisHash);
443
- if (!realSubId) {
436
+ if (!manager.hasActiveFollow(genesisHash)) {
444
437
  return enumValue('v1', resultErr(new GenericError({ reason: 'No active follow for this chain' })));
445
438
  }
446
439
  try {
447
- await manager.sendRequest(genesisHash, 'chainHead_v1_unpin', [realSubId, hashes]);
440
+ await manager.chainHeadOp(genesisHash, 'chainHead_v1_unpin', [hashes]);
448
441
  return enumValue('v1', resultOk(undefined));
449
442
  }
450
443
  catch (e) {
@@ -457,12 +450,11 @@ export function createContainer(provider) {
457
450
  return enumValue('v1', resultErr(new GenericError({ reason: UNSUPPORTED_MESSAGE_FORMAT_ERROR })));
458
451
  }
459
452
  const { genesisHash, operationId } = message.value;
460
- const realSubId = manager.getChainFollowSubId(genesisHash);
461
- if (!realSubId) {
453
+ if (!manager.hasActiveFollow(genesisHash)) {
462
454
  return enumValue('v1', resultErr(new GenericError({ reason: 'No active follow for this chain' })));
463
455
  }
464
456
  try {
465
- await manager.sendRequest(genesisHash, 'chainHead_v1_continue', [realSubId, operationId]);
457
+ await manager.chainHeadOp(genesisHash, 'chainHead_v1_continue', [operationId]);
466
458
  return enumValue('v1', resultOk(undefined));
467
459
  }
468
460
  catch (e) {
@@ -475,12 +467,11 @@ export function createContainer(provider) {
475
467
  return enumValue('v1', resultErr(new GenericError({ reason: UNSUPPORTED_MESSAGE_FORMAT_ERROR })));
476
468
  }
477
469
  const { genesisHash, operationId } = message.value;
478
- const realSubId = manager.getChainFollowSubId(genesisHash);
479
- if (!realSubId) {
470
+ if (!manager.hasActiveFollow(genesisHash)) {
480
471
  return enumValue('v1', resultErr(new GenericError({ reason: 'No active follow for this chain' })));
481
472
  }
482
473
  try {
483
- await manager.sendRequest(genesisHash, 'chainHead_v1_stopOperation', [realSubId, operationId]);
474
+ await manager.chainHeadOp(genesisHash, 'chainHead_v1_stopOperation', [operationId]);
484
475
  return enumValue('v1', resultOk(undefined));
485
476
  }
486
477
  catch (e) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@novasamatech/host-container",
3
3
  "type": "module",
4
- "version": "0.7.4",
4
+ "version": "0.7.5-0",
5
5
  "description": "Host container for hosting and managing products within the Polkadot ecosystem.",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -27,7 +27,8 @@
27
27
  "dependencies": {
28
28
  "@noble/hashes": "2.2.0",
29
29
  "polkadot-api": ">=2",
30
- "@novasamatech/host-api": "0.7.4",
30
+ "@polkadot-api/substrate-client": "^0.7.0",
31
+ "@novasamatech/host-api": "0.7.5-0",
31
32
  "nanoid": "5.1.9",
32
33
  "neverthrow": "^8.2.0"
33
34
  },