@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
package/cjs/ws/index.js
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WsProvider = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const eventemitter3_1 = require("eventemitter3");
|
|
6
|
+
const util_1 = require("@pezkuwi/util");
|
|
7
|
+
const x_global_1 = require("@pezkuwi/x-global");
|
|
8
|
+
const x_ws_1 = require("@pezkuwi/x-ws");
|
|
9
|
+
const index_js_1 = require("../coder/index.js");
|
|
10
|
+
const defaults_js_1 = tslib_1.__importDefault(require("../defaults.js"));
|
|
11
|
+
const lru_js_1 = require("../lru.js");
|
|
12
|
+
const errors_js_1 = require("./errors.js");
|
|
13
|
+
const ALIASES = {
|
|
14
|
+
chain_finalisedHead: 'chain_finalizedHead',
|
|
15
|
+
chain_subscribeFinalisedHeads: 'chain_subscribeFinalizedHeads',
|
|
16
|
+
chain_unsubscribeFinalisedHeads: 'chain_unsubscribeFinalizedHeads'
|
|
17
|
+
};
|
|
18
|
+
const RETRY_DELAY = 2_500;
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 60 * 1000;
|
|
20
|
+
const TIMEOUT_INTERVAL = 5_000;
|
|
21
|
+
const l = (0, util_1.logger)('api-ws');
|
|
22
|
+
/** @internal Clears a Record<*> of all keys, optionally with all callback on clear */
|
|
23
|
+
function eraseRecord(record, cb) {
|
|
24
|
+
Object.keys(record).forEach((key) => {
|
|
25
|
+
if (cb) {
|
|
26
|
+
cb(record[key]);
|
|
27
|
+
}
|
|
28
|
+
delete record[key];
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/** @internal Creates a default/empty stats object */
|
|
32
|
+
function defaultEndpointStats() {
|
|
33
|
+
return { bytesRecv: 0, bytesSent: 0, cached: 0, errors: 0, requests: 0, subscriptions: 0, timeout: 0 };
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* # @pezkuwi/rpc-provider/ws
|
|
37
|
+
*
|
|
38
|
+
* @name WsProvider
|
|
39
|
+
*
|
|
40
|
+
* @description The WebSocket Provider allows sending requests using WebSocket to a WebSocket RPC server TCP port. Unlike the [[HttpProvider]], it does support subscriptions and allows listening to events such as new blocks or balance changes.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* <BR>
|
|
44
|
+
*
|
|
45
|
+
* ```javascript
|
|
46
|
+
* import Api from '@pezkuwi/api/promise';
|
|
47
|
+
* import { WsProvider } from '@pezkuwi/rpc-provider/ws';
|
|
48
|
+
*
|
|
49
|
+
* const provider = new WsProvider('ws://127.0.0.1:9944');
|
|
50
|
+
* const api = new Api(provider);
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @see [[HttpProvider]]
|
|
54
|
+
*/
|
|
55
|
+
class WsProvider {
|
|
56
|
+
#callCache;
|
|
57
|
+
#coder;
|
|
58
|
+
#endpoints;
|
|
59
|
+
#headers;
|
|
60
|
+
#eventemitter;
|
|
61
|
+
#handlers = {};
|
|
62
|
+
#isReadyPromise;
|
|
63
|
+
#stats;
|
|
64
|
+
#waitingForId = {};
|
|
65
|
+
#cacheCapacity;
|
|
66
|
+
#ttl;
|
|
67
|
+
#autoConnectMs;
|
|
68
|
+
#endpointIndex;
|
|
69
|
+
#endpointStats;
|
|
70
|
+
#isConnected = false;
|
|
71
|
+
#subscriptions = {};
|
|
72
|
+
#timeoutId = null;
|
|
73
|
+
#websocket;
|
|
74
|
+
#timeout;
|
|
75
|
+
/**
|
|
76
|
+
* @param {string | string[]} endpoint The endpoint url. Usually `ws://ip:9944` or `wss://ip:9944`, may provide an array of endpoint strings.
|
|
77
|
+
* @param {number | false} autoConnectMs Whether to connect automatically or not (default). Provided value is used as a delay between retries.
|
|
78
|
+
* @param {Record<string, string>} headers The headers provided to the underlying WebSocket
|
|
79
|
+
* @param {number} [timeout] Custom timeout value used per request . Defaults to `DEFAULT_TIMEOUT_MS`
|
|
80
|
+
* @param {number} [cacheCapacity] Custom size of the WsProvider LRUCache. Defaults to `DEFAULT_CAPACITY` (1024)
|
|
81
|
+
* @param {number} [cacheTtl] Custom TTL of the WsProvider LRUCache. Determines how long an object can live in the cache. Defaults to DEFAULT_TTL` (30000)
|
|
82
|
+
*/
|
|
83
|
+
constructor(endpoint = defaults_js_1.default.WS_URL, autoConnectMs = RETRY_DELAY, headers = {}, timeout, cacheCapacity, cacheTtl) {
|
|
84
|
+
const endpoints = Array.isArray(endpoint)
|
|
85
|
+
? endpoint
|
|
86
|
+
: [endpoint];
|
|
87
|
+
if (endpoints.length === 0) {
|
|
88
|
+
throw new Error('WsProvider requires at least one Endpoint');
|
|
89
|
+
}
|
|
90
|
+
endpoints.forEach((endpoint) => {
|
|
91
|
+
if (!/^(wss|ws):\/\//.test(endpoint)) {
|
|
92
|
+
throw new Error(`Endpoint should start with 'ws://', received '${endpoint}'`);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
const ttl = cacheTtl === undefined ? lru_js_1.DEFAULT_TTL : cacheTtl;
|
|
96
|
+
this.#callCache = new lru_js_1.LRUCache(cacheCapacity === 0 ? 0 : cacheCapacity || lru_js_1.DEFAULT_CAPACITY, ttl);
|
|
97
|
+
this.#ttl = cacheTtl;
|
|
98
|
+
this.#cacheCapacity = cacheCapacity || lru_js_1.DEFAULT_CAPACITY;
|
|
99
|
+
this.#eventemitter = new eventemitter3_1.EventEmitter();
|
|
100
|
+
this.#autoConnectMs = autoConnectMs || 0;
|
|
101
|
+
this.#coder = new index_js_1.RpcCoder();
|
|
102
|
+
this.#endpointIndex = -1;
|
|
103
|
+
this.#endpoints = endpoints;
|
|
104
|
+
this.#headers = headers;
|
|
105
|
+
this.#websocket = null;
|
|
106
|
+
this.#stats = {
|
|
107
|
+
active: { requests: 0, subscriptions: 0 },
|
|
108
|
+
total: defaultEndpointStats()
|
|
109
|
+
};
|
|
110
|
+
this.#endpointStats = defaultEndpointStats();
|
|
111
|
+
this.#timeout = timeout || DEFAULT_TIMEOUT_MS;
|
|
112
|
+
if (autoConnectMs && autoConnectMs > 0) {
|
|
113
|
+
this.connectWithRetry().catch(util_1.noop);
|
|
114
|
+
}
|
|
115
|
+
this.#isReadyPromise = new Promise((resolve) => {
|
|
116
|
+
this.#eventemitter.once('connected', () => {
|
|
117
|
+
resolve(this);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* @summary `true` when this provider supports subscriptions
|
|
123
|
+
*/
|
|
124
|
+
get hasSubscriptions() {
|
|
125
|
+
return !!true;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* @summary `true` when this provider supports clone()
|
|
129
|
+
*/
|
|
130
|
+
get isClonable() {
|
|
131
|
+
return !!true;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* @summary Whether the node is connected or not.
|
|
135
|
+
* @return {boolean} true if connected
|
|
136
|
+
*/
|
|
137
|
+
get isConnected() {
|
|
138
|
+
return this.#isConnected;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* @description Promise that resolves the first time we are connected and loaded
|
|
142
|
+
*/
|
|
143
|
+
get isReady() {
|
|
144
|
+
return this.#isReadyPromise;
|
|
145
|
+
}
|
|
146
|
+
get endpoint() {
|
|
147
|
+
return this.#endpoints[this.#endpointIndex];
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* @description Returns a clone of the object
|
|
151
|
+
*/
|
|
152
|
+
clone() {
|
|
153
|
+
return new WsProvider(this.#endpoints);
|
|
154
|
+
}
|
|
155
|
+
selectEndpointIndex(endpoints) {
|
|
156
|
+
return (this.#endpointIndex + 1) % endpoints.length;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* @summary Manually connect
|
|
160
|
+
* @description The [[WsProvider]] connects automatically by default, however if you decided otherwise, you may
|
|
161
|
+
* connect manually using this method.
|
|
162
|
+
*/
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
164
|
+
async connect() {
|
|
165
|
+
if (this.#websocket) {
|
|
166
|
+
throw new Error('WebSocket is already connected');
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
this.#endpointIndex = this.selectEndpointIndex(this.#endpoints);
|
|
170
|
+
// the as here is Deno-specific - not available on the globalThis
|
|
171
|
+
this.#websocket = typeof x_global_1.xglobal.WebSocket !== 'undefined' && (0, util_1.isChildClass)(x_global_1.xglobal.WebSocket, x_ws_1.WebSocket)
|
|
172
|
+
? new x_ws_1.WebSocket(this.endpoint)
|
|
173
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
174
|
+
// @ts-ignore - WS may be an instance of ws, which supports options
|
|
175
|
+
: new x_ws_1.WebSocket(this.endpoint, undefined, {
|
|
176
|
+
headers: this.#headers
|
|
177
|
+
});
|
|
178
|
+
if (this.#websocket) {
|
|
179
|
+
this.#websocket.onclose = this.#onSocketClose;
|
|
180
|
+
this.#websocket.onerror = this.#onSocketError;
|
|
181
|
+
this.#websocket.onmessage = this.#onSocketMessage;
|
|
182
|
+
this.#websocket.onopen = this.#onSocketOpen;
|
|
183
|
+
}
|
|
184
|
+
// timeout any handlers that have not had a response
|
|
185
|
+
this.#timeoutId = setInterval(() => this.#timeoutHandlers(), TIMEOUT_INTERVAL);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
l.error(error);
|
|
189
|
+
this.#emit('error', error);
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* @description Connect, never throwing an error, but rather forcing a retry
|
|
195
|
+
*/
|
|
196
|
+
async connectWithRetry() {
|
|
197
|
+
if (this.#autoConnectMs > 0) {
|
|
198
|
+
try {
|
|
199
|
+
await this.connect();
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
setTimeout(() => {
|
|
203
|
+
this.connectWithRetry().catch(util_1.noop);
|
|
204
|
+
}, this.#autoConnectMs);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* @description Manually disconnect from the connection, clearing auto-connect logic
|
|
210
|
+
*/
|
|
211
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
212
|
+
async disconnect() {
|
|
213
|
+
// switch off autoConnect, we are in manual mode now
|
|
214
|
+
this.#autoConnectMs = 0;
|
|
215
|
+
try {
|
|
216
|
+
if (this.#websocket) {
|
|
217
|
+
// 1000 - Normal closure; the connection successfully completed
|
|
218
|
+
this.#websocket.close(1000);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
l.error(error);
|
|
223
|
+
this.#emit('error', error);
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* @description Returns the connection stats
|
|
229
|
+
*/
|
|
230
|
+
get stats() {
|
|
231
|
+
return {
|
|
232
|
+
active: {
|
|
233
|
+
requests: Object.keys(this.#handlers).length,
|
|
234
|
+
subscriptions: Object.keys(this.#subscriptions).length
|
|
235
|
+
},
|
|
236
|
+
total: this.#stats.total
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* @description Returns the connection stats
|
|
241
|
+
*/
|
|
242
|
+
get ttl() {
|
|
243
|
+
return this.#ttl;
|
|
244
|
+
}
|
|
245
|
+
get endpointStats() {
|
|
246
|
+
return this.#endpointStats;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* @summary Listens on events after having subscribed using the [[subscribe]] function.
|
|
250
|
+
* @param {ProviderInterfaceEmitted} type Event
|
|
251
|
+
* @param {ProviderInterfaceEmitCb} sub Callback
|
|
252
|
+
* @return unsubscribe function
|
|
253
|
+
*/
|
|
254
|
+
on(type, sub) {
|
|
255
|
+
this.#eventemitter.on(type, sub);
|
|
256
|
+
return () => {
|
|
257
|
+
this.#eventemitter.removeListener(type, sub);
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* @summary Send JSON data using WebSockets to configured HTTP Endpoint or queue.
|
|
262
|
+
* @param method The RPC methods to execute
|
|
263
|
+
* @param params Encoded parameters as applicable for the method
|
|
264
|
+
* @param subscription Subscription details (internally used)
|
|
265
|
+
*/
|
|
266
|
+
send(method, params, isCacheable, subscription) {
|
|
267
|
+
this.#endpointStats.requests++;
|
|
268
|
+
this.#stats.total.requests++;
|
|
269
|
+
const [id, body] = this.#coder.encodeJson(method, params);
|
|
270
|
+
if (this.#cacheCapacity === 0) {
|
|
271
|
+
return this.#send(id, body, method, params, subscription);
|
|
272
|
+
}
|
|
273
|
+
const cacheKey = isCacheable ? `${method}::${(0, util_1.stringify)(params)}` : '';
|
|
274
|
+
let resultPromise = isCacheable
|
|
275
|
+
? this.#callCache.get(cacheKey)
|
|
276
|
+
: null;
|
|
277
|
+
if (!resultPromise) {
|
|
278
|
+
resultPromise = this.#send(id, body, method, params, subscription);
|
|
279
|
+
if (isCacheable) {
|
|
280
|
+
this.#callCache.set(cacheKey, resultPromise);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
this.#endpointStats.cached++;
|
|
285
|
+
this.#stats.total.cached++;
|
|
286
|
+
}
|
|
287
|
+
return resultPromise;
|
|
288
|
+
}
|
|
289
|
+
async #send(id, body, method, params, subscription) {
|
|
290
|
+
return new Promise((resolve, reject) => {
|
|
291
|
+
try {
|
|
292
|
+
if (!this.isConnected || this.#websocket === null) {
|
|
293
|
+
throw new Error('WebSocket is not connected');
|
|
294
|
+
}
|
|
295
|
+
const callback = (error, result) => {
|
|
296
|
+
error
|
|
297
|
+
? reject(error)
|
|
298
|
+
: resolve(result);
|
|
299
|
+
};
|
|
300
|
+
l.debug(() => ['calling', method, body]);
|
|
301
|
+
this.#handlers[id] = {
|
|
302
|
+
callback,
|
|
303
|
+
method,
|
|
304
|
+
params,
|
|
305
|
+
start: Date.now(),
|
|
306
|
+
subscription
|
|
307
|
+
};
|
|
308
|
+
const bytesSent = body.length;
|
|
309
|
+
this.#endpointStats.bytesSent += bytesSent;
|
|
310
|
+
this.#stats.total.bytesSent += bytesSent;
|
|
311
|
+
this.#websocket.send(body);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
this.#endpointStats.errors++;
|
|
315
|
+
this.#stats.total.errors++;
|
|
316
|
+
const rpcError = error;
|
|
317
|
+
const failedRequest = `\nFailed WS Request: ${JSON.stringify({ method, params })}`;
|
|
318
|
+
// Provide WS Request alongside the error
|
|
319
|
+
rpcError.message = `${rpcError.message}${failedRequest}`;
|
|
320
|
+
reject(rpcError);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* @name subscribe
|
|
326
|
+
* @summary Allows subscribing to a specific event.
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* <BR>
|
|
330
|
+
*
|
|
331
|
+
* ```javascript
|
|
332
|
+
* const provider = new WsProvider('ws://127.0.0.1:9944');
|
|
333
|
+
* const rpc = new Rpc(provider);
|
|
334
|
+
*
|
|
335
|
+
* rpc.state.subscribeStorage([[storage.system.account, <Address>]], (_, values) => {
|
|
336
|
+
* console.log(values)
|
|
337
|
+
* }).then((subscriptionId) => {
|
|
338
|
+
* console.log('balance changes subscription id: ', subscriptionId)
|
|
339
|
+
* })
|
|
340
|
+
* ```
|
|
341
|
+
*/
|
|
342
|
+
subscribe(type, method, params, callback) {
|
|
343
|
+
this.#endpointStats.subscriptions++;
|
|
344
|
+
this.#stats.total.subscriptions++;
|
|
345
|
+
// subscriptions are not cached, LRU applies to .at(<blockHash>) only
|
|
346
|
+
return this.send(method, params, false, { callback, type });
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* @summary Allows unsubscribing to subscriptions made with [[subscribe]].
|
|
350
|
+
*/
|
|
351
|
+
async unsubscribe(type, method, id) {
|
|
352
|
+
const subscription = `${type}::${id}`;
|
|
353
|
+
// FIXME This now could happen with re-subscriptions. The issue is that with a re-sub
|
|
354
|
+
// the assigned id now does not match what the API user originally received. It has
|
|
355
|
+
// a slight complication in solving - since we cannot rely on the send id, but rather
|
|
356
|
+
// need to find the actual subscription id to map it
|
|
357
|
+
if ((0, util_1.isUndefined)(this.#subscriptions[subscription])) {
|
|
358
|
+
l.debug(() => `Unable to find active subscription=${subscription}`);
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
delete this.#subscriptions[subscription];
|
|
362
|
+
try {
|
|
363
|
+
return this.isConnected && !(0, util_1.isNull)(this.#websocket)
|
|
364
|
+
? this.send(method, [id])
|
|
365
|
+
: true;
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
#emit = (type, ...args) => {
|
|
372
|
+
this.#eventemitter.emit(type, ...args);
|
|
373
|
+
};
|
|
374
|
+
#onSocketClose = (event) => {
|
|
375
|
+
const error = new Error(`disconnected from ${this.endpoint}: ${event.code}:: ${event.reason || (0, errors_js_1.getWSErrorString)(event.code)}`);
|
|
376
|
+
if (this.#autoConnectMs > 0) {
|
|
377
|
+
l.error(error.message);
|
|
378
|
+
}
|
|
379
|
+
this.#isConnected = false;
|
|
380
|
+
if (this.#websocket) {
|
|
381
|
+
this.#websocket.onclose = null;
|
|
382
|
+
this.#websocket.onerror = null;
|
|
383
|
+
this.#websocket.onmessage = null;
|
|
384
|
+
this.#websocket.onopen = null;
|
|
385
|
+
this.#websocket = null;
|
|
386
|
+
}
|
|
387
|
+
if (this.#timeoutId) {
|
|
388
|
+
clearInterval(this.#timeoutId);
|
|
389
|
+
this.#timeoutId = null;
|
|
390
|
+
}
|
|
391
|
+
// reject all hanging requests
|
|
392
|
+
eraseRecord(this.#handlers, (h) => {
|
|
393
|
+
try {
|
|
394
|
+
h.callback(error, undefined);
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
// does not throw
|
|
398
|
+
l.error(err);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
eraseRecord(this.#waitingForId);
|
|
402
|
+
// Reset stats for active endpoint
|
|
403
|
+
this.#endpointStats = defaultEndpointStats();
|
|
404
|
+
this.#emit('disconnected');
|
|
405
|
+
if (this.#autoConnectMs > 0) {
|
|
406
|
+
setTimeout(() => {
|
|
407
|
+
this.connectWithRetry().catch(util_1.noop);
|
|
408
|
+
}, this.#autoConnectMs);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
#onSocketError = (error) => {
|
|
412
|
+
l.debug(() => ['socket error', error]);
|
|
413
|
+
this.#emit('error', error);
|
|
414
|
+
};
|
|
415
|
+
#onSocketMessage = (message) => {
|
|
416
|
+
l.debug(() => ['received', message.data]);
|
|
417
|
+
const bytesRecv = message.data.length;
|
|
418
|
+
this.#endpointStats.bytesRecv += bytesRecv;
|
|
419
|
+
this.#stats.total.bytesRecv += bytesRecv;
|
|
420
|
+
const response = JSON.parse(message.data);
|
|
421
|
+
return (0, util_1.isUndefined)(response.method)
|
|
422
|
+
? this.#onSocketMessageResult(response)
|
|
423
|
+
: this.#onSocketMessageSubscribe(response);
|
|
424
|
+
};
|
|
425
|
+
#onSocketMessageResult = (response) => {
|
|
426
|
+
const handler = this.#handlers[response.id];
|
|
427
|
+
if (!handler) {
|
|
428
|
+
l.debug(() => `Unable to find handler for id=${response.id}`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
try {
|
|
432
|
+
const { method, params, subscription } = handler;
|
|
433
|
+
const result = this.#coder.decodeResponse(response);
|
|
434
|
+
// first send the result - in case of subs, we may have an update
|
|
435
|
+
// immediately if we have some queued results already
|
|
436
|
+
handler.callback(null, result);
|
|
437
|
+
if (subscription) {
|
|
438
|
+
const subId = `${subscription.type}::${result}`;
|
|
439
|
+
this.#subscriptions[subId] = (0, util_1.objectSpread)({}, subscription, {
|
|
440
|
+
method,
|
|
441
|
+
params
|
|
442
|
+
});
|
|
443
|
+
// if we have a result waiting for this subscription already
|
|
444
|
+
if (this.#waitingForId[subId]) {
|
|
445
|
+
this.#onSocketMessageSubscribe(this.#waitingForId[subId]);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
this.#endpointStats.errors++;
|
|
451
|
+
this.#stats.total.errors++;
|
|
452
|
+
handler.callback(error, undefined);
|
|
453
|
+
}
|
|
454
|
+
delete this.#handlers[response.id];
|
|
455
|
+
};
|
|
456
|
+
#onSocketMessageSubscribe = (response) => {
|
|
457
|
+
if (!response.method) {
|
|
458
|
+
throw new Error('No method found in JSONRPC response');
|
|
459
|
+
}
|
|
460
|
+
const method = ALIASES[response.method] || response.method;
|
|
461
|
+
const subId = `${method}::${response.params.subscription}`;
|
|
462
|
+
const handler = this.#subscriptions[subId];
|
|
463
|
+
if (!handler) {
|
|
464
|
+
// store the JSON, we could have out-of-order subid coming in
|
|
465
|
+
this.#waitingForId[subId] = response;
|
|
466
|
+
l.debug(() => `Unable to find handler for subscription=${subId}`);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
// housekeeping
|
|
470
|
+
delete this.#waitingForId[subId];
|
|
471
|
+
try {
|
|
472
|
+
const result = this.#coder.decodeResponse(response);
|
|
473
|
+
handler.callback(null, result);
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
this.#endpointStats.errors++;
|
|
477
|
+
this.#stats.total.errors++;
|
|
478
|
+
handler.callback(error, undefined);
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
#onSocketOpen = () => {
|
|
482
|
+
if (this.#websocket === null) {
|
|
483
|
+
throw new Error('WebSocket cannot be null in onOpen');
|
|
484
|
+
}
|
|
485
|
+
l.debug(() => ['connected to', this.endpoint]);
|
|
486
|
+
this.#isConnected = true;
|
|
487
|
+
this.#resubscribe();
|
|
488
|
+
this.#emit('connected');
|
|
489
|
+
return true;
|
|
490
|
+
};
|
|
491
|
+
#resubscribe = () => {
|
|
492
|
+
const subscriptions = this.#subscriptions;
|
|
493
|
+
this.#subscriptions = {};
|
|
494
|
+
Promise.all(Object.keys(subscriptions).map(async (id) => {
|
|
495
|
+
const { callback, method, params, type } = subscriptions[id];
|
|
496
|
+
// only re-create subscriptions which are not in author (only area where
|
|
497
|
+
// transactions are created, i.e. submissions such as 'author_submitAndWatchExtrinsic'
|
|
498
|
+
// are not included (and will not be re-broadcast)
|
|
499
|
+
if (type.startsWith('author_')) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
await this.subscribe(type, method, params, callback);
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
l.error(error);
|
|
507
|
+
}
|
|
508
|
+
})).catch(l.error);
|
|
509
|
+
};
|
|
510
|
+
#timeoutHandlers = () => {
|
|
511
|
+
const now = Date.now();
|
|
512
|
+
const ids = Object.keys(this.#handlers);
|
|
513
|
+
for (let i = 0, count = ids.length; i < count; i++) {
|
|
514
|
+
const handler = this.#handlers[ids[i]];
|
|
515
|
+
if ((now - handler.start) > this.#timeout) {
|
|
516
|
+
try {
|
|
517
|
+
handler.callback(new Error(`No response received from RPC endpoint in ${this.#timeout / 1000}s`), undefined);
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
// ignore
|
|
521
|
+
}
|
|
522
|
+
this.#endpointStats.timeout++;
|
|
523
|
+
this.#stats.total.timeout++;
|
|
524
|
+
delete this.#handlers[ids[i]];
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
exports.WsProvider = WsProvider;
|
package/coder/error.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RpcErrorInterface } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* @name RpcError
|
|
4
|
+
* @summary Extension to the basic JS Error.
|
|
5
|
+
* @description
|
|
6
|
+
* The built-in JavaScript Error class is extended by adding a code to allow for Error categorization. In addition to the normal `stack`, `message`, the numeric `code` and `data` (any types) parameters are available on the object.
|
|
7
|
+
* @example
|
|
8
|
+
* <BR>
|
|
9
|
+
*
|
|
10
|
+
* ```javascript
|
|
11
|
+
* const { RpcError } from '@pezkuwi/util');
|
|
12
|
+
*
|
|
13
|
+
* throw new RpcError('some message', RpcError.CODES.METHOD_NOT_FOUND); // => error.code = -32601
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export default class RpcError<T = never> extends Error implements RpcErrorInterface<T> {
|
|
17
|
+
code: number;
|
|
18
|
+
data?: T;
|
|
19
|
+
message: string;
|
|
20
|
+
name: string;
|
|
21
|
+
stack: string;
|
|
22
|
+
constructor(message?: string, code?: number, data?: T);
|
|
23
|
+
static CODES: {
|
|
24
|
+
ASSERT: number;
|
|
25
|
+
INVALID_JSONRPC: number;
|
|
26
|
+
METHOD_NOT_FOUND: number;
|
|
27
|
+
UNKNOWN: number;
|
|
28
|
+
};
|
|
29
|
+
}
|
package/coder/error.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isFunction } from '@pezkuwi/util';
|
|
2
|
+
const UNKNOWN = -99999;
|
|
3
|
+
function extend(that, name, value) {
|
|
4
|
+
Object.defineProperty(that, name, {
|
|
5
|
+
configurable: true,
|
|
6
|
+
enumerable: false,
|
|
7
|
+
value
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* @name RpcError
|
|
12
|
+
* @summary Extension to the basic JS Error.
|
|
13
|
+
* @description
|
|
14
|
+
* The built-in JavaScript Error class is extended by adding a code to allow for Error categorization. In addition to the normal `stack`, `message`, the numeric `code` and `data` (any types) parameters are available on the object.
|
|
15
|
+
* @example
|
|
16
|
+
* <BR>
|
|
17
|
+
*
|
|
18
|
+
* ```javascript
|
|
19
|
+
* const { RpcError } from '@pezkuwi/util');
|
|
20
|
+
*
|
|
21
|
+
* throw new RpcError('some message', RpcError.CODES.METHOD_NOT_FOUND); // => error.code = -32601
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export default class RpcError extends Error {
|
|
25
|
+
code;
|
|
26
|
+
data;
|
|
27
|
+
message;
|
|
28
|
+
name;
|
|
29
|
+
stack;
|
|
30
|
+
constructor(message = '', code = UNKNOWN, data) {
|
|
31
|
+
super();
|
|
32
|
+
extend(this, 'message', String(message));
|
|
33
|
+
extend(this, 'name', this.constructor.name);
|
|
34
|
+
extend(this, 'data', data);
|
|
35
|
+
extend(this, 'code', code);
|
|
36
|
+
if (isFunction(Error.captureStackTrace)) {
|
|
37
|
+
Error.captureStackTrace(this, this.constructor);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const { stack } = new Error(message);
|
|
41
|
+
stack && extend(this, 'stack', stack);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
static CODES = {
|
|
45
|
+
ASSERT: -90009,
|
|
46
|
+
INVALID_JSONRPC: -99998,
|
|
47
|
+
METHOD_NOT_FOUND: -32601, // Rust client
|
|
48
|
+
UNKNOWN
|
|
49
|
+
};
|
|
50
|
+
}
|
package/coder/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { JsonRpcRequest, JsonRpcResponse } from '../types.js';
|
|
2
|
+
/** @internal */
|
|
3
|
+
export declare class RpcCoder {
|
|
4
|
+
#private;
|
|
5
|
+
decodeResponse<T>(response?: JsonRpcResponse<T>): T;
|
|
6
|
+
encodeJson(method: string, params: unknown[]): [number, string];
|
|
7
|
+
encodeObject(method: string, params: unknown[]): [number, JsonRpcRequest];
|
|
8
|
+
}
|
package/coder/index.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isNumber, isString, isUndefined, stringify } from '@pezkuwi/util';
|
|
2
|
+
import RpcError from './error.js';
|
|
3
|
+
function formatErrorData(data) {
|
|
4
|
+
if (isUndefined(data)) {
|
|
5
|
+
return '';
|
|
6
|
+
}
|
|
7
|
+
const formatted = `: ${isString(data)
|
|
8
|
+
? data.replace(/Error\("/g, '').replace(/\("/g, '(').replace(/"\)/g, ')').replace(/\(/g, ', ').replace(/\)/g, '')
|
|
9
|
+
: stringify(data)}`;
|
|
10
|
+
// We need some sort of cut-off here since these can be very large and
|
|
11
|
+
// very nested, pick a number and trim the result display to it
|
|
12
|
+
return formatted.length <= 256
|
|
13
|
+
? formatted
|
|
14
|
+
: `${formatted.substring(0, 255)}…`;
|
|
15
|
+
}
|
|
16
|
+
function checkError(error) {
|
|
17
|
+
if (error) {
|
|
18
|
+
const { code, data, message } = error;
|
|
19
|
+
throw new RpcError(`${code}: ${message}${formatErrorData(data)}`, code, data);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** @internal */
|
|
23
|
+
export class RpcCoder {
|
|
24
|
+
#id = 0;
|
|
25
|
+
decodeResponse(response) {
|
|
26
|
+
if (!response || response.jsonrpc !== '2.0') {
|
|
27
|
+
throw new Error('Invalid jsonrpc field in decoded object');
|
|
28
|
+
}
|
|
29
|
+
const isSubscription = !isUndefined(response.params) && !isUndefined(response.method);
|
|
30
|
+
if (!isNumber(response.id) &&
|
|
31
|
+
(!isSubscription || (!isNumber(response.params.subscription) &&
|
|
32
|
+
!isString(response.params.subscription)))) {
|
|
33
|
+
throw new Error('Invalid id field in decoded object');
|
|
34
|
+
}
|
|
35
|
+
checkError(response.error);
|
|
36
|
+
if (response.result === undefined && !isSubscription) {
|
|
37
|
+
throw new Error('No result found in jsonrpc response');
|
|
38
|
+
}
|
|
39
|
+
if (isSubscription) {
|
|
40
|
+
checkError(response.params.error);
|
|
41
|
+
return response.params.result;
|
|
42
|
+
}
|
|
43
|
+
return response.result;
|
|
44
|
+
}
|
|
45
|
+
encodeJson(method, params) {
|
|
46
|
+
const [id, data] = this.encodeObject(method, params);
|
|
47
|
+
return [id, stringify(data)];
|
|
48
|
+
}
|
|
49
|
+
encodeObject(method, params) {
|
|
50
|
+
const id = ++this.#id;
|
|
51
|
+
return [id, {
|
|
52
|
+
id,
|
|
53
|
+
jsonrpc: '2.0',
|
|
54
|
+
method,
|
|
55
|
+
params
|
|
56
|
+
}];
|
|
57
|
+
}
|
|
58
|
+
}
|