@pezkuwi/rpc-provider 16.5.5 → 16.5.6

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.
Files changed (125) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +10 -10
  3. package/bizinikiwi-connect/Health.js +259 -0
  4. package/{build/substrate-connect → bizinikiwi-connect}/index.d.ts +3 -3
  5. package/bizinikiwi-connect/index.js +319 -0
  6. package/{build/bundle.d.ts → bundle.d.ts} +1 -1
  7. package/{src/bundle.ts → bundle.js} +1 -4
  8. package/cjs/bizinikiwi-connect/Health.d.ts +7 -0
  9. package/cjs/bizinikiwi-connect/Health.js +264 -0
  10. package/cjs/bizinikiwi-connect/index.d.ts +22 -0
  11. package/cjs/bizinikiwi-connect/index.js +323 -0
  12. package/cjs/bizinikiwi-connect/types.d.ts +12 -0
  13. package/cjs/bizinikiwi-connect/types.js +2 -0
  14. package/cjs/bundle.d.ts +5 -0
  15. package/cjs/bundle.js +14 -0
  16. package/cjs/coder/error.js +53 -0
  17. package/cjs/coder/index.js +63 -0
  18. package/cjs/defaults.js +8 -0
  19. package/{build → cjs}/http/index.d.ts +1 -1
  20. package/cjs/http/index.js +196 -0
  21. package/cjs/http/types.js +2 -0
  22. package/cjs/index.js +5 -0
  23. package/cjs/lru.js +150 -0
  24. package/cjs/mock/index.js +196 -0
  25. package/cjs/mock/mockHttp.js +17 -0
  26. package/cjs/mock/mockWs.js +47 -0
  27. package/cjs/mock/types.js +2 -0
  28. package/cjs/package.json +3 -0
  29. package/cjs/packageDetect.d.ts +1 -0
  30. package/cjs/packageDetect.js +6 -0
  31. package/cjs/packageInfo.js +4 -0
  32. package/cjs/types.js +2 -0
  33. package/cjs/ws/errors.js +41 -0
  34. package/{build → cjs}/ws/index.d.ts +1 -1
  35. package/cjs/ws/index.js +529 -0
  36. package/coder/error.d.ts +29 -0
  37. package/coder/error.js +50 -0
  38. package/coder/index.d.ts +8 -0
  39. package/coder/index.js +58 -0
  40. package/defaults.d.ts +5 -0
  41. package/defaults.js +6 -0
  42. package/http/index.d.ts +81 -0
  43. package/http/index.js +191 -0
  44. package/http/types.d.ts +7 -0
  45. package/http/types.js +1 -0
  46. package/index.d.ts +2 -0
  47. package/index.js +2 -0
  48. package/lru.d.ts +15 -0
  49. package/lru.js +146 -0
  50. package/mock/index.d.ts +35 -0
  51. package/mock/index.js +191 -0
  52. package/mock/mockHttp.d.ts +9 -0
  53. package/mock/mockHttp.js +12 -0
  54. package/mock/mockWs.d.ts +26 -0
  55. package/mock/mockWs.js +43 -0
  56. package/mock/types.d.ts +23 -0
  57. package/mock/types.js +1 -0
  58. package/package.json +316 -15
  59. package/packageDetect.d.ts +1 -0
  60. package/packageDetect.js +4 -0
  61. package/packageInfo.d.ts +6 -0
  62. package/packageInfo.js +1 -0
  63. package/types.d.ts +85 -0
  64. package/types.js +1 -0
  65. package/ws/errors.d.ts +1 -0
  66. package/ws/errors.js +38 -0
  67. package/ws/index.d.ts +121 -0
  68. package/ws/index.js +524 -0
  69. package/src/coder/decodeResponse.spec.ts +0 -70
  70. package/src/coder/encodeJson.spec.ts +0 -20
  71. package/src/coder/encodeObject.spec.ts +0 -25
  72. package/src/coder/error.spec.ts +0 -111
  73. package/src/coder/error.ts +0 -66
  74. package/src/coder/index.ts +0 -88
  75. package/src/defaults.ts +0 -10
  76. package/src/http/index.spec.ts +0 -72
  77. package/src/http/index.ts +0 -238
  78. package/src/http/send.spec.ts +0 -61
  79. package/src/http/types.ts +0 -11
  80. package/src/index.ts +0 -6
  81. package/src/lru.spec.ts +0 -74
  82. package/src/lru.ts +0 -197
  83. package/src/mock/index.ts +0 -259
  84. package/src/mock/mockHttp.ts +0 -35
  85. package/src/mock/mockWs.ts +0 -92
  86. package/src/mock/on.spec.ts +0 -43
  87. package/src/mock/send.spec.ts +0 -38
  88. package/src/mock/subscribe.spec.ts +0 -81
  89. package/src/mock/types.ts +0 -36
  90. package/src/mock/unsubscribe.spec.ts +0 -57
  91. package/src/mod.ts +0 -4
  92. package/src/packageDetect.ts +0 -12
  93. package/src/packageInfo.ts +0 -6
  94. package/src/substrate-connect/Health.ts +0 -325
  95. package/src/substrate-connect/index.spec.ts +0 -638
  96. package/src/substrate-connect/index.ts +0 -415
  97. package/src/substrate-connect/types.ts +0 -16
  98. package/src/types.ts +0 -101
  99. package/src/ws/connect.spec.ts +0 -167
  100. package/src/ws/errors.ts +0 -41
  101. package/src/ws/index.spec.ts +0 -97
  102. package/src/ws/index.ts +0 -652
  103. package/src/ws/send.spec.ts +0 -126
  104. package/src/ws/state.spec.ts +0 -20
  105. package/src/ws/subscribe.spec.ts +0 -68
  106. package/src/ws/unsubscribe.spec.ts +0 -100
  107. package/tsconfig.build.json +0 -17
  108. package/tsconfig.build.tsbuildinfo +0 -1
  109. package/tsconfig.spec.json +0 -18
  110. /package/{build/substrate-connect → bizinikiwi-connect}/Health.d.ts +0 -0
  111. /package/{build/substrate-connect → bizinikiwi-connect}/types.d.ts +0 -0
  112. /package/{build/packageDetect.d.ts → bizinikiwi-connect/types.js} +0 -0
  113. /package/{build → cjs}/coder/error.d.ts +0 -0
  114. /package/{build → cjs}/coder/index.d.ts +0 -0
  115. /package/{build → cjs}/defaults.d.ts +0 -0
  116. /package/{build → cjs}/http/types.d.ts +0 -0
  117. /package/{build → cjs}/index.d.ts +0 -0
  118. /package/{build → cjs}/lru.d.ts +0 -0
  119. /package/{build → cjs}/mock/index.d.ts +0 -0
  120. /package/{build → cjs}/mock/mockHttp.d.ts +0 -0
  121. /package/{build → cjs}/mock/mockWs.d.ts +0 -0
  122. /package/{build → cjs}/mock/types.d.ts +0 -0
  123. /package/{build → cjs}/packageInfo.d.ts +0 -0
  124. /package/{build → cjs}/types.d.ts +0 -0
  125. /package/{build → cjs}/ws/errors.d.ts +0 -0
@@ -0,0 +1,319 @@
1
+ import { EventEmitter } from 'eventemitter3';
2
+ import { isError, isFunction, isObject, logger, noop, objectSpread } from '@pezkuwi/util';
3
+ import { RpcCoder } from '../coder/index.js';
4
+ import { healthChecker } from './Health.js';
5
+ const l = logger('api-bizinikiwi-connect');
6
+ const subscriptionUnsubscriptionMethods = new Map([
7
+ ['author_submitAndWatchExtrinsic', 'author_unwatchExtrinsic'],
8
+ ['chain_subscribeAllHeads', 'chain_unsubscribeAllHeads'],
9
+ ['chain_subscribeFinalizedHeads', 'chain_unsubscribeFinalizedHeads'],
10
+ ['chain_subscribeFinalisedHeads', 'chain_subscribeFinalisedHeads'],
11
+ ['chain_subscribeNewHeads', 'chain_unsubscribeNewHeads'],
12
+ ['chain_subscribeNewHead', 'chain_unsubscribeNewHead'],
13
+ ['chain_subscribeRuntimeVersion', 'chain_unsubscribeRuntimeVersion'],
14
+ ['subscribe_newHead', 'unsubscribe_newHead'],
15
+ ['state_subscribeRuntimeVersion', 'state_unsubscribeRuntimeVersion'],
16
+ ['state_subscribeStorage', 'state_unsubscribeStorage']
17
+ ]);
18
+ const scClients = new WeakMap();
19
+ export class ScProvider {
20
+ #Sc;
21
+ #coder = new RpcCoder();
22
+ #spec;
23
+ #sharedSandbox;
24
+ #subscriptions = new Map();
25
+ #resubscribeMethods = new Map();
26
+ #requests = new Map();
27
+ #wellKnownChains;
28
+ #eventemitter = new EventEmitter();
29
+ #chain = null;
30
+ #isChainReady = false;
31
+ constructor(Sc, spec, sharedSandbox) {
32
+ if (!isObject(Sc) || !isObject(Sc.WellKnownChain) || !isFunction(Sc.createScClient)) {
33
+ throw new Error('Expected an @bizinikiwi/connect interface as first parameter to ScProvider');
34
+ }
35
+ this.#Sc = Sc;
36
+ this.#spec = spec;
37
+ this.#sharedSandbox = sharedSandbox;
38
+ this.#wellKnownChains = new Set(Object.values(Sc.WellKnownChain));
39
+ }
40
+ get hasSubscriptions() {
41
+ // Indicates that subscriptions are supported
42
+ return !!true;
43
+ }
44
+ get isClonable() {
45
+ return !!false;
46
+ }
47
+ get isConnected() {
48
+ return !!this.#chain && this.#isChainReady;
49
+ }
50
+ clone() {
51
+ throw new Error('clone() is not supported.');
52
+ }
53
+ // Config details can be found in @bizinikiwi/connect repo following the link:
54
+ // https://github.com/pezkuwichain/bizinikiwi-connect/blob/main/packages/connect/src/connector/index.ts
55
+ async connect(config, checkerFactory = healthChecker) {
56
+ if (this.isConnected) {
57
+ throw new Error('Already connected!');
58
+ }
59
+ // it could happen that after emitting `disconnected` due to the fact that
60
+ // smoldot is syncing, the consumer tries to reconnect after a certain amount
61
+ // of time... In which case we want to make sure that we don't create a new
62
+ // chain.
63
+ if (this.#chain) {
64
+ await this.#chain;
65
+ return;
66
+ }
67
+ if (this.#sharedSandbox && !this.#sharedSandbox.isConnected) {
68
+ await this.#sharedSandbox.connect();
69
+ }
70
+ const client = this.#sharedSandbox
71
+ ? scClients.get(this.#sharedSandbox)
72
+ : this.#Sc.createScClient(config);
73
+ if (!client) {
74
+ throw new Error('Unknown ScProvider!');
75
+ }
76
+ scClients.set(this, client);
77
+ const hc = checkerFactory();
78
+ const onResponse = (res) => {
79
+ const hcRes = hc.responsePassThrough(res);
80
+ if (!hcRes) {
81
+ return;
82
+ }
83
+ const response = JSON.parse(hcRes);
84
+ let decodedResponse;
85
+ try {
86
+ decodedResponse = this.#coder.decodeResponse(response);
87
+ }
88
+ catch (e) {
89
+ decodedResponse = e;
90
+ }
91
+ // It's not a subscription message, but rather a standar RPC response
92
+ if (response.params?.subscription === undefined || !response.method) {
93
+ return this.#requests.get(response.id)?.(decodedResponse);
94
+ }
95
+ // We are dealing with a subscription message
96
+ const subscriptionId = `${response.method}::${response.params.subscription}`;
97
+ const callback = this.#subscriptions.get(subscriptionId)?.[0];
98
+ callback?.(decodedResponse);
99
+ };
100
+ const addChain = this.#sharedSandbox
101
+ ? (async (...args) => {
102
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
103
+ const source = this.#sharedSandbox;
104
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
105
+ return (await source.#chain).addChain(...args);
106
+ })
107
+ : this.#wellKnownChains.has(this.#spec)
108
+ ? client.addWellKnownChain
109
+ : client.addChain;
110
+ this.#chain = addChain(this.#spec).then((chain) => {
111
+ hc.setSendJsonRpc(chain.sendJsonRpc);
112
+ // Start async response processing loop
113
+ // This replaces the callback-based API from older @substrate/connect versions
114
+ (async () => {
115
+ try {
116
+ for await (const res of chain.jsonRpcResponses) {
117
+ onResponse(res);
118
+ }
119
+ }
120
+ catch {
121
+ // Chain was removed or connection closed - this is expected
122
+ }
123
+ })();
124
+ this.#isChainReady = false;
125
+ const cleanup = () => {
126
+ // If there are any callbacks left, we have to reject/error them.
127
+ // Otherwise, that would cause a memory leak.
128
+ const disconnectionError = new Error('Disconnected');
129
+ this.#requests.forEach((cb) => cb(disconnectionError));
130
+ this.#subscriptions.forEach(([cb]) => cb(disconnectionError));
131
+ this.#subscriptions.clear();
132
+ };
133
+ const staleSubscriptions = [];
134
+ const killStaleSubscriptions = () => {
135
+ if (staleSubscriptions.length === 0) {
136
+ return;
137
+ }
138
+ const stale = staleSubscriptions.pop();
139
+ if (!stale) {
140
+ throw new Error('Unable to get stale subscription');
141
+ }
142
+ const { id, unsubscribeMethod } = stale;
143
+ Promise
144
+ .race([
145
+ this.send(unsubscribeMethod, [id]).catch(noop),
146
+ new Promise((resolve) => setTimeout(resolve, 500))
147
+ ])
148
+ .then(killStaleSubscriptions)
149
+ .catch(noop);
150
+ };
151
+ hc.start((health) => {
152
+ const isReady = !health.isSyncing && (health.peers > 0 || !health.shouldHavePeers);
153
+ // if it's the same as before, then nothing has changed and we are done
154
+ if (this.#isChainReady === isReady) {
155
+ return;
156
+ }
157
+ this.#isChainReady = isReady;
158
+ if (!isReady) {
159
+ // If we've reached this point, that means that the chain used to be "ready"
160
+ // and now we are about to emit `disconnected`.
161
+ //
162
+ // This will cause the PezkuwiJs API think that the connection is
163
+ // actually dead. In reality the smoldot chain is not dead, of course.
164
+ // However, we have to cleanup all the existing callbacks because when
165
+ // the smoldot chain stops syncing, then we will emit `connected` and
166
+ // the PezkuwiJs API will try to re-create the previous
167
+ // subscriptions and requests. Although, now is not a good moment
168
+ // to be sending unsubscription messages to the smoldot chain, we
169
+ // should wait until is no longer syncing to send the unsubscription
170
+ // messages from the stale subscriptions of the previous connection.
171
+ //
172
+ // That's why -before we perform the cleanup of `this.#subscriptions`-
173
+ // we keep the necessary information that we will need later on to
174
+ // kill the stale subscriptions.
175
+ [...this.#subscriptions.values()].forEach((s) => {
176
+ staleSubscriptions.push(s[1]);
177
+ });
178
+ cleanup();
179
+ this.#eventemitter.emit('disconnected');
180
+ }
181
+ else {
182
+ killStaleSubscriptions();
183
+ this.#eventemitter.emit('connected');
184
+ if (this.#resubscribeMethods.size) {
185
+ this.#resubscribe();
186
+ }
187
+ }
188
+ });
189
+ return objectSpread({}, chain, {
190
+ remove: () => {
191
+ hc.stop();
192
+ chain.remove();
193
+ cleanup();
194
+ },
195
+ sendJsonRpc: hc.sendJsonRpc.bind(hc)
196
+ });
197
+ });
198
+ try {
199
+ await this.#chain;
200
+ }
201
+ catch (e) {
202
+ this.#chain = null;
203
+ this.#eventemitter.emit('error', e);
204
+ throw e;
205
+ }
206
+ }
207
+ #resubscribe = () => {
208
+ const promises = [];
209
+ this.#resubscribeMethods.forEach((subDetails) => {
210
+ // only re-create subscriptions which are not in author (only area where
211
+ // transactions are created, i.e. submissions such as 'author_submitAndWatchExtrinsic'
212
+ // are not included (and will not be re-broadcast)
213
+ if (subDetails.type.startsWith('author_')) {
214
+ return;
215
+ }
216
+ try {
217
+ const promise = new Promise((resolve) => {
218
+ this.subscribe(subDetails.type, subDetails.method, subDetails.params, subDetails.callback).catch((error) => console.log(error));
219
+ resolve();
220
+ });
221
+ promises.push(promise);
222
+ }
223
+ catch (error) {
224
+ l.error(error);
225
+ }
226
+ });
227
+ Promise.all(promises).catch((err) => l.log(err));
228
+ };
229
+ async disconnect() {
230
+ if (!this.#chain) {
231
+ return;
232
+ }
233
+ const chain = await this.#chain;
234
+ this.#chain = null;
235
+ this.#isChainReady = false;
236
+ try {
237
+ chain.remove();
238
+ }
239
+ catch (_) { }
240
+ this.#eventemitter.emit('disconnected');
241
+ }
242
+ on(type, sub) {
243
+ // It's possible. Although, quite unlikely, that by the time that pezkuwi
244
+ // subscribes to the `connected` event, the Provider is already connected.
245
+ // In that case, we must emit to let the consumer know that we are connected.
246
+ if (type === 'connected' && this.isConnected) {
247
+ sub();
248
+ }
249
+ this.#eventemitter.on(type, sub);
250
+ return () => {
251
+ this.#eventemitter.removeListener(type, sub);
252
+ };
253
+ }
254
+ async send(method, params) {
255
+ if (!this.isConnected || !this.#chain) {
256
+ throw new Error('Provider is not connected');
257
+ }
258
+ const chain = await this.#chain;
259
+ const [id, json] = this.#coder.encodeJson(method, params);
260
+ const result = new Promise((resolve, reject) => {
261
+ this.#requests.set(id, (response) => {
262
+ (isError(response) ? reject : resolve)(response);
263
+ });
264
+ try {
265
+ chain.sendJsonRpc(json);
266
+ }
267
+ catch (e) {
268
+ this.#chain = null;
269
+ try {
270
+ chain.remove();
271
+ }
272
+ catch (_) { }
273
+ this.#eventemitter.emit('error', e);
274
+ }
275
+ });
276
+ try {
277
+ return await result;
278
+ }
279
+ finally {
280
+ // let's ensure that once the Promise is resolved/rejected, then we remove
281
+ // remove its entry from the internal #requests
282
+ this.#requests.delete(id);
283
+ }
284
+ }
285
+ async subscribe(type, method, params, callback) {
286
+ if (!subscriptionUnsubscriptionMethods.has(method)) {
287
+ throw new Error(`Unsupported subscribe method: ${method}`);
288
+ }
289
+ const id = await this.send(method, params);
290
+ const subscriptionId = `${type}::${id}`;
291
+ const cb = (response) => {
292
+ if (response instanceof Error) {
293
+ callback(response, undefined);
294
+ }
295
+ else {
296
+ callback(null, response);
297
+ }
298
+ };
299
+ const unsubscribeMethod = subscriptionUnsubscriptionMethods.get(method);
300
+ if (!unsubscribeMethod) {
301
+ throw new Error('Invalid unsubscribe method found');
302
+ }
303
+ this.#resubscribeMethods.set(subscriptionId, { callback, method, params, type });
304
+ this.#subscriptions.set(subscriptionId, [cb, { id, unsubscribeMethod }]);
305
+ return id;
306
+ }
307
+ unsubscribe(type, method, id) {
308
+ if (!this.isConnected) {
309
+ throw new Error('Provider is not connected');
310
+ }
311
+ const subscriptionId = `${type}::${id}`;
312
+ if (!this.#subscriptions.has(subscriptionId)) {
313
+ return Promise.reject(new Error(`Unable to find active subscription=${subscriptionId}`));
314
+ }
315
+ this.#resubscribeMethods.delete(subscriptionId);
316
+ this.#subscriptions.delete(subscriptionId);
317
+ return this.send(method, [id]);
318
+ }
319
+ }
@@ -1,5 +1,5 @@
1
1
  export { HttpProvider } from './http/index.js';
2
2
  export { DEFAULT_CAPACITY, LRUCache } from './lru.js';
3
3
  export { packageInfo } from './packageInfo.js';
4
- export { ScProvider } from './substrate-connect/index.js';
4
+ export { ScProvider } from './bizinikiwi-connect/index.js';
5
5
  export { WsProvider } from './ws/index.js';
@@ -1,8 +1,5 @@
1
- // Copyright 2017-2025 @polkadot/rpc-provider authors & contributors
2
- // SPDX-License-Identifier: Apache-2.0
3
-
4
1
  export { HttpProvider } from './http/index.js';
5
2
  export { DEFAULT_CAPACITY, LRUCache } from './lru.js';
6
3
  export { packageInfo } from './packageInfo.js';
7
- export { ScProvider } from './substrate-connect/index.js';
4
+ export { ScProvider } from './bizinikiwi-connect/index.js';
8
5
  export { WsProvider } from './ws/index.js';
@@ -0,0 +1,7 @@
1
+ import type { HealthChecker } from './types.js';
2
+ export declare function healthChecker(): HealthChecker;
3
+ export declare class HealthCheckError extends Error {
4
+ #private;
5
+ getCause(): unknown;
6
+ constructor(response: unknown, message?: string);
7
+ }
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HealthCheckError = void 0;
4
+ exports.healthChecker = healthChecker;
5
+ const util_1 = require("@pezkuwi/util");
6
+ /*
7
+ * Creates a new health checker.
8
+ *
9
+ * The role of the health checker is to report to the user the health of a smoldot chain.
10
+ *
11
+ * In order to use it, start by creating a health checker, and call `setSendJsonRpc` to set the
12
+ * way to send a JSON-RPC request to a chain. The health checker is disabled by default. Use
13
+ * `start()` in order to start the health checks. The `start()` function must be passed a callback called
14
+ * when an update to the health of the node is available.
15
+ *
16
+ * In order to send a JSON-RPC request to the chain, you **must** use the `sendJsonRpc` function
17
+ * of the health checker. The health checker rewrites the `id` of the requests it receives.
18
+ *
19
+ * When the chain send a JSON-RPC response, it must be passed to `responsePassThrough()`. This
20
+ * function intercepts the responses destined to the requests that have been emitted by the health
21
+ * checker and returns `null`. If the response doesn't concern the health checker, the response is
22
+ * simply returned by the function.
23
+ *
24
+ * # How it works
25
+ *
26
+ * The health checker periodically calls the `system_health` JSON-RPC call in order to determine
27
+ * the health of the chain.
28
+ *
29
+ * In addition to this, as long as the health check reports that `isSyncing` is `true`, the
30
+ * health checker also maintains a subscription to new best blocks using `chain_subscribeNewHeads`.
31
+ * Whenever a new block is notified, a health check is performed immediately in order to determine
32
+ * whether `isSyncing` has changed to `false`.
33
+ *
34
+ * Thanks to this subscription, the latency of the report of the switch from `isSyncing: true` to
35
+ * `isSyncing: false` is very low.
36
+ *
37
+ */
38
+ function healthChecker() {
39
+ // `null` if health checker is not started.
40
+ let checker = null;
41
+ let sendJsonRpc = null;
42
+ return {
43
+ responsePassThrough: (jsonRpcResponse) => {
44
+ if (checker === null) {
45
+ return jsonRpcResponse;
46
+ }
47
+ return checker.responsePassThrough(jsonRpcResponse);
48
+ },
49
+ sendJsonRpc: (request) => {
50
+ if (!sendJsonRpc) {
51
+ throw new Error('setSendJsonRpc must be called before sending requests');
52
+ }
53
+ if (checker === null) {
54
+ sendJsonRpc(request);
55
+ }
56
+ else {
57
+ checker.sendJsonRpc(request);
58
+ }
59
+ },
60
+ setSendJsonRpc: (cb) => {
61
+ sendJsonRpc = cb;
62
+ },
63
+ start: (healthCallback) => {
64
+ if (checker !== null) {
65
+ throw new Error("Can't start the health checker multiple times in parallel");
66
+ }
67
+ else if (!sendJsonRpc) {
68
+ throw new Error('setSendJsonRpc must be called before starting the health checks');
69
+ }
70
+ checker = new InnerChecker(healthCallback, sendJsonRpc);
71
+ checker.update(true);
72
+ },
73
+ stop: () => {
74
+ if (checker === null) {
75
+ return;
76
+ } // Already stopped.
77
+ checker.destroy();
78
+ checker = null;
79
+ }
80
+ };
81
+ }
82
+ class InnerChecker {
83
+ #healthCallback;
84
+ #currentHealthCheckId = null;
85
+ #currentHealthTimeout = null;
86
+ #currentSubunsubRequestId = null;
87
+ #currentSubscriptionId = null;
88
+ #requestToSmoldot;
89
+ #isSyncing = false;
90
+ #nextRequestId = 0;
91
+ constructor(healthCallback, requestToSmoldot) {
92
+ this.#healthCallback = healthCallback;
93
+ this.#requestToSmoldot = (request) => requestToSmoldot((0, util_1.stringify)(request));
94
+ }
95
+ sendJsonRpc = (request) => {
96
+ // Replace the `id` in the request to prefix the request ID with `extern:`.
97
+ let parsedRequest;
98
+ try {
99
+ parsedRequest = JSON.parse(request);
100
+ }
101
+ catch {
102
+ return;
103
+ }
104
+ if (parsedRequest.id) {
105
+ const newId = 'extern:' + (0, util_1.stringify)(parsedRequest.id);
106
+ parsedRequest.id = newId;
107
+ }
108
+ this.#requestToSmoldot(parsedRequest);
109
+ };
110
+ responsePassThrough = (jsonRpcResponse) => {
111
+ let parsedResponse;
112
+ try {
113
+ parsedResponse = JSON.parse(jsonRpcResponse);
114
+ }
115
+ catch {
116
+ return jsonRpcResponse;
117
+ }
118
+ // Check whether response is a response to `system_health`.
119
+ if (parsedResponse.id && this.#currentHealthCheckId === parsedResponse.id) {
120
+ this.#currentHealthCheckId = null;
121
+ // Check whether query was successful. It is possible for queries to fail for
122
+ // various reasons, such as the client being overloaded.
123
+ if (!parsedResponse.result) {
124
+ this.update(false);
125
+ return null;
126
+ }
127
+ this.#healthCallback(parsedResponse.result);
128
+ this.#isSyncing = parsedResponse.result.isSyncing;
129
+ this.update(false);
130
+ return null;
131
+ }
132
+ // Check whether response is a response to the subscription or unsubscription.
133
+ if (parsedResponse.id &&
134
+ this.#currentSubunsubRequestId === parsedResponse.id) {
135
+ this.#currentSubunsubRequestId = null;
136
+ // Check whether query was successful. It is possible for queries to fail for
137
+ // various reasons, such as the client being overloaded.
138
+ if (!parsedResponse.result) {
139
+ this.update(false);
140
+ return null;
141
+ }
142
+ if (this.#currentSubscriptionId) {
143
+ this.#currentSubscriptionId = null;
144
+ }
145
+ else {
146
+ this.#currentSubscriptionId = parsedResponse.result;
147
+ }
148
+ this.update(false);
149
+ return null;
150
+ }
151
+ // Check whether response is a notification to a subscription.
152
+ if (parsedResponse.params &&
153
+ this.#currentSubscriptionId &&
154
+ parsedResponse.params.subscription === this.#currentSubscriptionId) {
155
+ // Note that after a successful subscription, a notification containing
156
+ // the current best block is always returned. Considering that a
157
+ // subscription is performed in response to a health check, calling
158
+ // `startHealthCheck()` here will lead to a second health check.
159
+ // It might seem redundant to perform two health checks in a quick
160
+ // succession, but doing so doesn't lead to any problem, and it is
161
+ // actually possible for the health to have changed in between as the
162
+ // current best block might have been updated during the subscription
163
+ // request.
164
+ this.update(true);
165
+ return null;
166
+ }
167
+ // Response doesn't concern us.
168
+ if (parsedResponse.id) {
169
+ const id = parsedResponse.id;
170
+ // Need to remove the `extern:` prefix.
171
+ if (!id.startsWith('extern:')) {
172
+ throw new Error('State inconsistency in health checker');
173
+ }
174
+ const newId = JSON.parse(id.slice('extern:'.length));
175
+ parsedResponse.id = newId;
176
+ }
177
+ return (0, util_1.stringify)(parsedResponse);
178
+ };
179
+ update = (startNow) => {
180
+ // If `startNow`, clear `#currentHealthTimeout` so that it is set below.
181
+ if (startNow && this.#currentHealthTimeout) {
182
+ clearTimeout(this.#currentHealthTimeout);
183
+ this.#currentHealthTimeout = null;
184
+ }
185
+ if (!this.#currentHealthTimeout) {
186
+ const startHealthRequest = () => {
187
+ this.#currentHealthTimeout = null;
188
+ // No matter what, don't start a health request if there is already one in progress.
189
+ // This is sane to do because receiving a response to a health request calls `update()`.
190
+ if (this.#currentHealthCheckId) {
191
+ return;
192
+ }
193
+ // Actual request starting.
194
+ this.#currentHealthCheckId = `health-checker:${this.#nextRequestId}`;
195
+ this.#nextRequestId += 1;
196
+ this.#requestToSmoldot({
197
+ id: this.#currentHealthCheckId,
198
+ jsonrpc: '2.0',
199
+ method: 'system_health',
200
+ params: []
201
+ });
202
+ };
203
+ if (startNow) {
204
+ startHealthRequest();
205
+ }
206
+ else {
207
+ this.#currentHealthTimeout = setTimeout(startHealthRequest, 1000);
208
+ }
209
+ }
210
+ if (this.#isSyncing &&
211
+ !this.#currentSubscriptionId &&
212
+ !this.#currentSubunsubRequestId) {
213
+ this.startSubscription();
214
+ }
215
+ if (!this.#isSyncing &&
216
+ this.#currentSubscriptionId &&
217
+ !this.#currentSubunsubRequestId) {
218
+ this.endSubscription();
219
+ }
220
+ };
221
+ startSubscription = () => {
222
+ if (this.#currentSubunsubRequestId || this.#currentSubscriptionId) {
223
+ throw new Error('Internal error in health checker');
224
+ }
225
+ this.#currentSubunsubRequestId = `health-checker:${this.#nextRequestId}`;
226
+ this.#nextRequestId += 1;
227
+ this.#requestToSmoldot({
228
+ id: this.#currentSubunsubRequestId,
229
+ jsonrpc: '2.0',
230
+ method: 'chain_subscribeNewHeads',
231
+ params: []
232
+ });
233
+ };
234
+ endSubscription = () => {
235
+ if (this.#currentSubunsubRequestId || !this.#currentSubscriptionId) {
236
+ throw new Error('Internal error in health checker');
237
+ }
238
+ this.#currentSubunsubRequestId = `health-checker:${this.#nextRequestId}`;
239
+ this.#nextRequestId += 1;
240
+ this.#requestToSmoldot({
241
+ id: this.#currentSubunsubRequestId,
242
+ jsonrpc: '2.0',
243
+ method: 'chain_unsubscribeNewHeads',
244
+ params: [this.#currentSubscriptionId]
245
+ });
246
+ };
247
+ destroy = () => {
248
+ if (this.#currentHealthTimeout) {
249
+ clearTimeout(this.#currentHealthTimeout);
250
+ this.#currentHealthTimeout = null;
251
+ }
252
+ };
253
+ }
254
+ class HealthCheckError extends Error {
255
+ #cause;
256
+ getCause() {
257
+ return this.#cause;
258
+ }
259
+ constructor(response, message = 'Got error response asking for system health') {
260
+ super(message);
261
+ this.#cause = response;
262
+ }
263
+ }
264
+ exports.HealthCheckError = HealthCheckError;
@@ -0,0 +1,22 @@
1
+ import type * as ScType from '@bizinikiwi/connect';
2
+ import type { ProviderInterface, ProviderInterfaceCallback, ProviderInterfaceEmitCb, ProviderInterfaceEmitted } from '../types.js';
3
+ import { healthChecker } from './Health.js';
4
+ interface BizinikiwiConnect {
5
+ WellKnownChain: typeof ScType['WellKnownChain'];
6
+ createScClient: typeof ScType['createScClient'];
7
+ }
8
+ export declare class ScProvider implements ProviderInterface {
9
+ #private;
10
+ constructor(Sc: BizinikiwiConnect, spec: string | ScType.WellKnownChain, sharedSandbox?: ScProvider);
11
+ get hasSubscriptions(): boolean;
12
+ get isClonable(): boolean;
13
+ get isConnected(): boolean;
14
+ clone(): ProviderInterface;
15
+ connect(config?: ScType.Config, checkerFactory?: typeof healthChecker): Promise<void>;
16
+ disconnect(): Promise<void>;
17
+ on(type: ProviderInterfaceEmitted, sub: ProviderInterfaceEmitCb): () => void;
18
+ send<T = any>(method: string, params: unknown[]): Promise<T>;
19
+ subscribe(type: string, method: string, params: any[], callback: ProviderInterfaceCallback): Promise<number | string>;
20
+ unsubscribe(type: string, method: string, id: number | string): Promise<boolean>;
21
+ }
22
+ export {};