@pezkuwi/rpc-provider 16.5.5
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 +68 -0
- package/build/bundle.d.ts +5 -0
- package/build/coder/error.d.ts +29 -0
- package/build/coder/index.d.ts +8 -0
- package/build/defaults.d.ts +5 -0
- package/build/http/index.d.ts +81 -0
- package/build/http/types.d.ts +7 -0
- package/build/index.d.ts +2 -0
- package/build/lru.d.ts +15 -0
- package/build/mock/index.d.ts +35 -0
- package/build/mock/mockHttp.d.ts +9 -0
- package/build/mock/mockWs.d.ts +26 -0
- package/build/mock/types.d.ts +23 -0
- package/build/packageDetect.d.ts +1 -0
- package/build/packageInfo.d.ts +6 -0
- package/build/substrate-connect/Health.d.ts +7 -0
- package/build/substrate-connect/index.d.ts +22 -0
- package/build/substrate-connect/types.d.ts +12 -0
- package/build/types.d.ts +85 -0
- package/build/ws/errors.d.ts +1 -0
- package/build/ws/index.d.ts +121 -0
- package/package.json +43 -0
- package/src/bundle.ts +8 -0
- package/src/coder/decodeResponse.spec.ts +70 -0
- package/src/coder/encodeJson.spec.ts +20 -0
- package/src/coder/encodeObject.spec.ts +25 -0
- package/src/coder/error.spec.ts +111 -0
- package/src/coder/error.ts +66 -0
- package/src/coder/index.ts +88 -0
- package/src/defaults.ts +10 -0
- package/src/http/index.spec.ts +72 -0
- package/src/http/index.ts +238 -0
- package/src/http/send.spec.ts +61 -0
- package/src/http/types.ts +11 -0
- package/src/index.ts +6 -0
- package/src/lru.spec.ts +74 -0
- package/src/lru.ts +197 -0
- package/src/mock/index.ts +259 -0
- package/src/mock/mockHttp.ts +35 -0
- package/src/mock/mockWs.ts +92 -0
- package/src/mock/on.spec.ts +43 -0
- package/src/mock/send.spec.ts +38 -0
- package/src/mock/subscribe.spec.ts +81 -0
- package/src/mock/types.ts +36 -0
- package/src/mock/unsubscribe.spec.ts +57 -0
- package/src/mod.ts +4 -0
- package/src/packageDetect.ts +12 -0
- package/src/packageInfo.ts +6 -0
- package/src/substrate-connect/Health.ts +325 -0
- package/src/substrate-connect/index.spec.ts +638 -0
- package/src/substrate-connect/index.ts +415 -0
- package/src/substrate-connect/types.ts +16 -0
- package/src/types.ts +101 -0
- package/src/ws/connect.spec.ts +167 -0
- package/src/ws/errors.ts +41 -0
- package/src/ws/index.spec.ts +97 -0
- package/src/ws/index.ts +652 -0
- package/src/ws/send.spec.ts +126 -0
- package/src/ws/state.spec.ts +20 -0
- package/src/ws/subscribe.spec.ts +68 -0
- package/src/ws/unsubscribe.spec.ts +100 -0
- package/tsconfig.build.json +17 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.spec.json +18 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// Copyright 2017-2025 @polkadot/rpc-provider authors & contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { HealthChecker, SmoldotHealth } from './types.js';
|
|
5
|
+
|
|
6
|
+
import { stringify } from '@pezkuwi/util';
|
|
7
|
+
|
|
8
|
+
interface JSONRequest {
|
|
9
|
+
id: string;
|
|
10
|
+
jsonrpc: '2.0',
|
|
11
|
+
method: string;
|
|
12
|
+
params: unknown[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* Creates a new health checker.
|
|
17
|
+
*
|
|
18
|
+
* The role of the health checker is to report to the user the health of a smoldot chain.
|
|
19
|
+
*
|
|
20
|
+
* In order to use it, start by creating a health checker, and call `setSendJsonRpc` to set the
|
|
21
|
+
* way to send a JSON-RPC request to a chain. The health checker is disabled by default. Use
|
|
22
|
+
* `start()` in order to start the health checks. The `start()` function must be passed a callback called
|
|
23
|
+
* when an update to the health of the node is available.
|
|
24
|
+
*
|
|
25
|
+
* In order to send a JSON-RPC request to the chain, you **must** use the `sendJsonRpc` function
|
|
26
|
+
* of the health checker. The health checker rewrites the `id` of the requests it receives.
|
|
27
|
+
*
|
|
28
|
+
* When the chain send a JSON-RPC response, it must be passed to `responsePassThrough()`. This
|
|
29
|
+
* function intercepts the responses destined to the requests that have been emitted by the health
|
|
30
|
+
* checker and returns `null`. If the response doesn't concern the health checker, the response is
|
|
31
|
+
* simply returned by the function.
|
|
32
|
+
*
|
|
33
|
+
* # How it works
|
|
34
|
+
*
|
|
35
|
+
* The health checker periodically calls the `system_health` JSON-RPC call in order to determine
|
|
36
|
+
* the health of the chain.
|
|
37
|
+
*
|
|
38
|
+
* In addition to this, as long as the health check reports that `isSyncing` is `true`, the
|
|
39
|
+
* health checker also maintains a subscription to new best blocks using `chain_subscribeNewHeads`.
|
|
40
|
+
* Whenever a new block is notified, a health check is performed immediately in order to determine
|
|
41
|
+
* whether `isSyncing` has changed to `false`.
|
|
42
|
+
*
|
|
43
|
+
* Thanks to this subscription, the latency of the report of the switch from `isSyncing: true` to
|
|
44
|
+
* `isSyncing: false` is very low.
|
|
45
|
+
*
|
|
46
|
+
*/
|
|
47
|
+
export function healthChecker (): HealthChecker {
|
|
48
|
+
// `null` if health checker is not started.
|
|
49
|
+
let checker: null | InnerChecker = null;
|
|
50
|
+
let sendJsonRpc: null | ((request: string) => void) = null;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
responsePassThrough: (jsonRpcResponse) => {
|
|
54
|
+
if (checker === null) {
|
|
55
|
+
return jsonRpcResponse;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return checker.responsePassThrough(jsonRpcResponse);
|
|
59
|
+
},
|
|
60
|
+
sendJsonRpc: (request) => {
|
|
61
|
+
if (!sendJsonRpc) {
|
|
62
|
+
throw new Error('setSendJsonRpc must be called before sending requests');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (checker === null) {
|
|
66
|
+
sendJsonRpc(request);
|
|
67
|
+
} else {
|
|
68
|
+
checker.sendJsonRpc(request);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
setSendJsonRpc: (cb) => {
|
|
72
|
+
sendJsonRpc = cb;
|
|
73
|
+
},
|
|
74
|
+
start: (healthCallback) => {
|
|
75
|
+
if (checker !== null) {
|
|
76
|
+
throw new Error("Can't start the health checker multiple times in parallel");
|
|
77
|
+
} else if (!sendJsonRpc) {
|
|
78
|
+
throw new Error('setSendJsonRpc must be called before starting the health checks');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
checker = new InnerChecker(healthCallback, sendJsonRpc);
|
|
82
|
+
checker.update(true);
|
|
83
|
+
},
|
|
84
|
+
stop: () => {
|
|
85
|
+
if (checker === null) {
|
|
86
|
+
return;
|
|
87
|
+
} // Already stopped.
|
|
88
|
+
|
|
89
|
+
checker.destroy();
|
|
90
|
+
checker = null;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
class InnerChecker {
|
|
96
|
+
#healthCallback: (health: SmoldotHealth) => void;
|
|
97
|
+
#currentHealthCheckId: string | null = null;
|
|
98
|
+
#currentHealthTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
99
|
+
#currentSubunsubRequestId: string | null = null;
|
|
100
|
+
#currentSubscriptionId: string | null = null;
|
|
101
|
+
#requestToSmoldot: (request: JSONRequest) => void;
|
|
102
|
+
#isSyncing = false;
|
|
103
|
+
#nextRequestId = 0;
|
|
104
|
+
|
|
105
|
+
constructor (healthCallback: (health: SmoldotHealth) => void, requestToSmoldot: (request: string) => void) {
|
|
106
|
+
this.#healthCallback = healthCallback;
|
|
107
|
+
this.#requestToSmoldot = (request: JSONRequest) => requestToSmoldot(stringify(request));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
sendJsonRpc = (request: string): void => {
|
|
111
|
+
// Replace the `id` in the request to prefix the request ID with `extern:`.
|
|
112
|
+
let parsedRequest: JSONRequest;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
parsedRequest = JSON.parse(request) as JSONRequest;
|
|
116
|
+
} catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (parsedRequest.id) {
|
|
121
|
+
const newId = 'extern:' + stringify(parsedRequest.id);
|
|
122
|
+
|
|
123
|
+
parsedRequest.id = newId;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.#requestToSmoldot(parsedRequest);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
responsePassThrough = (jsonRpcResponse: string): string | null => {
|
|
130
|
+
let parsedResponse: {id: string, result?: SmoldotHealth, params?: { subscription: string }};
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
parsedResponse = JSON.parse(jsonRpcResponse) as { id: string, result?: SmoldotHealth };
|
|
134
|
+
} catch {
|
|
135
|
+
return jsonRpcResponse;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check whether response is a response to `system_health`.
|
|
139
|
+
if (parsedResponse.id && this.#currentHealthCheckId === parsedResponse.id) {
|
|
140
|
+
this.#currentHealthCheckId = null;
|
|
141
|
+
|
|
142
|
+
// Check whether query was successful. It is possible for queries to fail for
|
|
143
|
+
// various reasons, such as the client being overloaded.
|
|
144
|
+
if (!parsedResponse.result) {
|
|
145
|
+
this.update(false);
|
|
146
|
+
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.#healthCallback(parsedResponse.result);
|
|
151
|
+
this.#isSyncing = parsedResponse.result.isSyncing;
|
|
152
|
+
this.update(false);
|
|
153
|
+
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check whether response is a response to the subscription or unsubscription.
|
|
158
|
+
if (
|
|
159
|
+
parsedResponse.id &&
|
|
160
|
+
this.#currentSubunsubRequestId === parsedResponse.id
|
|
161
|
+
) {
|
|
162
|
+
this.#currentSubunsubRequestId = null;
|
|
163
|
+
|
|
164
|
+
// Check whether query was successful. It is possible for queries to fail for
|
|
165
|
+
// various reasons, such as the client being overloaded.
|
|
166
|
+
if (!parsedResponse.result) {
|
|
167
|
+
this.update(false);
|
|
168
|
+
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (this.#currentSubscriptionId) {
|
|
173
|
+
this.#currentSubscriptionId = null;
|
|
174
|
+
} else {
|
|
175
|
+
this.#currentSubscriptionId = parsedResponse.result as unknown as string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.update(false);
|
|
179
|
+
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check whether response is a notification to a subscription.
|
|
184
|
+
if (
|
|
185
|
+
parsedResponse.params &&
|
|
186
|
+
this.#currentSubscriptionId &&
|
|
187
|
+
parsedResponse.params.subscription === this.#currentSubscriptionId
|
|
188
|
+
) {
|
|
189
|
+
// Note that after a successful subscription, a notification containing
|
|
190
|
+
// the current best block is always returned. Considering that a
|
|
191
|
+
// subscription is performed in response to a health check, calling
|
|
192
|
+
// `startHealthCheck()` here will lead to a second health check.
|
|
193
|
+
// It might seem redundant to perform two health checks in a quick
|
|
194
|
+
// succession, but doing so doesn't lead to any problem, and it is
|
|
195
|
+
// actually possible for the health to have changed in between as the
|
|
196
|
+
// current best block might have been updated during the subscription
|
|
197
|
+
// request.
|
|
198
|
+
this.update(true);
|
|
199
|
+
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Response doesn't concern us.
|
|
204
|
+
if (parsedResponse.id) {
|
|
205
|
+
const id: string = parsedResponse.id;
|
|
206
|
+
|
|
207
|
+
// Need to remove the `extern:` prefix.
|
|
208
|
+
if (!id.startsWith('extern:')) {
|
|
209
|
+
throw new Error('State inconsistency in health checker');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const newId = JSON.parse(id.slice('extern:'.length)) as string;
|
|
213
|
+
|
|
214
|
+
parsedResponse.id = newId;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return stringify(parsedResponse);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
update = (startNow: boolean): void => {
|
|
221
|
+
// If `startNow`, clear `#currentHealthTimeout` so that it is set below.
|
|
222
|
+
if (startNow && this.#currentHealthTimeout) {
|
|
223
|
+
clearTimeout(this.#currentHealthTimeout);
|
|
224
|
+
this.#currentHealthTimeout = null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!this.#currentHealthTimeout) {
|
|
228
|
+
const startHealthRequest = () => {
|
|
229
|
+
this.#currentHealthTimeout = null;
|
|
230
|
+
|
|
231
|
+
// No matter what, don't start a health request if there is already one in progress.
|
|
232
|
+
// This is sane to do because receiving a response to a health request calls `update()`.
|
|
233
|
+
if (this.#currentHealthCheckId) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Actual request starting.
|
|
238
|
+
this.#currentHealthCheckId = `health-checker:${this.#nextRequestId}`;
|
|
239
|
+
this.#nextRequestId += 1;
|
|
240
|
+
|
|
241
|
+
this.#requestToSmoldot({
|
|
242
|
+
id: this.#currentHealthCheckId,
|
|
243
|
+
jsonrpc: '2.0',
|
|
244
|
+
method: 'system_health',
|
|
245
|
+
params: []
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
if (startNow) {
|
|
250
|
+
startHealthRequest();
|
|
251
|
+
} else {
|
|
252
|
+
this.#currentHealthTimeout = setTimeout(startHealthRequest, 1000);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (
|
|
257
|
+
this.#isSyncing &&
|
|
258
|
+
!this.#currentSubscriptionId &&
|
|
259
|
+
!this.#currentSubunsubRequestId
|
|
260
|
+
) {
|
|
261
|
+
this.startSubscription();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (
|
|
265
|
+
!this.#isSyncing &&
|
|
266
|
+
this.#currentSubscriptionId &&
|
|
267
|
+
!this.#currentSubunsubRequestId
|
|
268
|
+
) {
|
|
269
|
+
this.endSubscription();
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
startSubscription = (): void => {
|
|
274
|
+
if (this.#currentSubunsubRequestId || this.#currentSubscriptionId) {
|
|
275
|
+
throw new Error('Internal error in health checker');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
this.#currentSubunsubRequestId = `health-checker:${this.#nextRequestId}`;
|
|
279
|
+
this.#nextRequestId += 1;
|
|
280
|
+
|
|
281
|
+
this.#requestToSmoldot({
|
|
282
|
+
id: this.#currentSubunsubRequestId,
|
|
283
|
+
jsonrpc: '2.0',
|
|
284
|
+
method: 'chain_subscribeNewHeads',
|
|
285
|
+
params: []
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
endSubscription = (): void => {
|
|
290
|
+
if (this.#currentSubunsubRequestId || !this.#currentSubscriptionId) {
|
|
291
|
+
throw new Error('Internal error in health checker');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
this.#currentSubunsubRequestId = `health-checker:${this.#nextRequestId}`;
|
|
295
|
+
this.#nextRequestId += 1;
|
|
296
|
+
|
|
297
|
+
this.#requestToSmoldot({
|
|
298
|
+
id: this.#currentSubunsubRequestId,
|
|
299
|
+
jsonrpc: '2.0',
|
|
300
|
+
method: 'chain_unsubscribeNewHeads',
|
|
301
|
+
params: [this.#currentSubscriptionId]
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
destroy = (): void => {
|
|
306
|
+
if (this.#currentHealthTimeout) {
|
|
307
|
+
clearTimeout(this.#currentHealthTimeout);
|
|
308
|
+
this.#currentHealthTimeout = null;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export class HealthCheckError extends Error {
|
|
314
|
+
readonly #cause: unknown;
|
|
315
|
+
|
|
316
|
+
getCause (): unknown {
|
|
317
|
+
return this.#cause;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
constructor (response: unknown, message = 'Got error response asking for system health') {
|
|
321
|
+
super(message);
|
|
322
|
+
|
|
323
|
+
this.#cause = response;
|
|
324
|
+
}
|
|
325
|
+
}
|