@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.
- package/LICENSE +201 -0
- package/README.md +10 -10
- package/bizinikiwi-connect/Health.js +259 -0
- package/{build/substrate-connect → bizinikiwi-connect}/index.d.ts +3 -3
- package/bizinikiwi-connect/index.js +319 -0
- package/{build/bundle.d.ts → bundle.d.ts} +1 -1
- package/{src/bundle.ts → bundle.js} +1 -4
- package/cjs/bizinikiwi-connect/Health.d.ts +7 -0
- package/cjs/bizinikiwi-connect/Health.js +264 -0
- package/cjs/bizinikiwi-connect/index.d.ts +22 -0
- package/cjs/bizinikiwi-connect/index.js +323 -0
- package/cjs/bizinikiwi-connect/types.d.ts +12 -0
- package/cjs/bizinikiwi-connect/types.js +2 -0
- package/cjs/bundle.d.ts +5 -0
- package/cjs/bundle.js +14 -0
- package/cjs/coder/error.js +53 -0
- package/cjs/coder/index.js +63 -0
- package/cjs/defaults.js +8 -0
- package/{build → cjs}/http/index.d.ts +1 -1
- package/cjs/http/index.js +196 -0
- package/cjs/http/types.js +2 -0
- package/cjs/index.js +5 -0
- package/cjs/lru.js +150 -0
- package/cjs/mock/index.js +196 -0
- package/cjs/mock/mockHttp.js +17 -0
- package/cjs/mock/mockWs.js +47 -0
- package/cjs/mock/types.js +2 -0
- package/cjs/package.json +3 -0
- package/cjs/packageDetect.d.ts +1 -0
- package/cjs/packageDetect.js +6 -0
- package/cjs/packageInfo.js +4 -0
- package/cjs/types.js +2 -0
- package/cjs/ws/errors.js +41 -0
- package/{build → cjs}/ws/index.d.ts +1 -1
- package/cjs/ws/index.js +529 -0
- package/coder/error.d.ts +29 -0
- package/coder/error.js +50 -0
- package/coder/index.d.ts +8 -0
- package/coder/index.js +58 -0
- package/defaults.d.ts +5 -0
- package/defaults.js +6 -0
- package/http/index.d.ts +81 -0
- package/http/index.js +191 -0
- package/http/types.d.ts +7 -0
- package/http/types.js +1 -0
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/lru.d.ts +15 -0
- package/lru.js +146 -0
- package/mock/index.d.ts +35 -0
- package/mock/index.js +191 -0
- package/mock/mockHttp.d.ts +9 -0
- package/mock/mockHttp.js +12 -0
- package/mock/mockWs.d.ts +26 -0
- package/mock/mockWs.js +43 -0
- package/mock/types.d.ts +23 -0
- package/mock/types.js +1 -0
- package/package.json +316 -15
- package/packageDetect.d.ts +1 -0
- package/packageDetect.js +4 -0
- package/packageInfo.d.ts +6 -0
- package/packageInfo.js +1 -0
- package/types.d.ts +85 -0
- package/types.js +1 -0
- package/ws/errors.d.ts +1 -0
- package/ws/errors.js +38 -0
- package/ws/index.d.ts +121 -0
- package/ws/index.js +524 -0
- package/src/coder/decodeResponse.spec.ts +0 -70
- package/src/coder/encodeJson.spec.ts +0 -20
- package/src/coder/encodeObject.spec.ts +0 -25
- package/src/coder/error.spec.ts +0 -111
- package/src/coder/error.ts +0 -66
- package/src/coder/index.ts +0 -88
- package/src/defaults.ts +0 -10
- package/src/http/index.spec.ts +0 -72
- package/src/http/index.ts +0 -238
- package/src/http/send.spec.ts +0 -61
- package/src/http/types.ts +0 -11
- package/src/index.ts +0 -6
- package/src/lru.spec.ts +0 -74
- package/src/lru.ts +0 -197
- package/src/mock/index.ts +0 -259
- package/src/mock/mockHttp.ts +0 -35
- package/src/mock/mockWs.ts +0 -92
- package/src/mock/on.spec.ts +0 -43
- package/src/mock/send.spec.ts +0 -38
- package/src/mock/subscribe.spec.ts +0 -81
- package/src/mock/types.ts +0 -36
- package/src/mock/unsubscribe.spec.ts +0 -57
- package/src/mod.ts +0 -4
- package/src/packageDetect.ts +0 -12
- package/src/packageInfo.ts +0 -6
- package/src/substrate-connect/Health.ts +0 -325
- package/src/substrate-connect/index.spec.ts +0 -638
- package/src/substrate-connect/index.ts +0 -415
- package/src/substrate-connect/types.ts +0 -16
- package/src/types.ts +0 -101
- package/src/ws/connect.spec.ts +0 -167
- package/src/ws/errors.ts +0 -41
- package/src/ws/index.spec.ts +0 -97
- package/src/ws/index.ts +0 -652
- package/src/ws/send.spec.ts +0 -126
- package/src/ws/state.spec.ts +0 -20
- package/src/ws/subscribe.spec.ts +0 -68
- package/src/ws/unsubscribe.spec.ts +0 -100
- package/tsconfig.build.json +0 -17
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.spec.json +0 -18
- /package/{build/substrate-connect → bizinikiwi-connect}/Health.d.ts +0 -0
- /package/{build/substrate-connect → bizinikiwi-connect}/types.d.ts +0 -0
- /package/{build/packageDetect.d.ts → bizinikiwi-connect/types.js} +0 -0
- /package/{build → cjs}/coder/error.d.ts +0 -0
- /package/{build → cjs}/coder/index.d.ts +0 -0
- /package/{build → cjs}/defaults.d.ts +0 -0
- /package/{build → cjs}/http/types.d.ts +0 -0
- /package/{build → cjs}/index.d.ts +0 -0
- /package/{build → cjs}/lru.d.ts +0 -0
- /package/{build → cjs}/mock/index.d.ts +0 -0
- /package/{build → cjs}/mock/mockHttp.d.ts +0 -0
- /package/{build → cjs}/mock/mockWs.d.ts +0 -0
- /package/{build → cjs}/mock/types.d.ts +0 -0
- /package/{build → cjs}/packageInfo.d.ts +0 -0
- /package/{build → cjs}/types.d.ts +0 -0
- /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 './
|
|
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 './
|
|
4
|
+
export { ScProvider } from './bizinikiwi-connect/index.js';
|
|
8
5
|
export { WsProvider } from './ws/index.js';
|
|
@@ -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 {};
|