@pezkuwi/rpc-provider 16.5.16 → 16.5.18
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/build/LICENSE +201 -0
- package/build/README.md +68 -0
- package/build/bizinikiwi-connect/Health.js +259 -0
- package/build/bizinikiwi-connect/index.js +319 -0
- package/build/bundle.js +5 -0
- package/build/cjs/bizinikiwi-connect/Health.d.ts +7 -0
- package/build/cjs/bizinikiwi-connect/Health.js +264 -0
- package/build/cjs/bizinikiwi-connect/index.d.ts +22 -0
- package/build/cjs/bizinikiwi-connect/index.js +323 -0
- package/build/cjs/bizinikiwi-connect/types.d.ts +12 -0
- package/build/cjs/bizinikiwi-connect/types.js +2 -0
- package/build/cjs/bundle.d.ts +5 -0
- package/build/cjs/bundle.js +14 -0
- package/build/cjs/coder/error.js +53 -0
- package/build/cjs/coder/index.js +63 -0
- package/build/cjs/defaults.js +8 -0
- package/build/cjs/http/index.js +196 -0
- package/build/cjs/http/types.js +2 -0
- package/build/cjs/index.js +5 -0
- package/build/cjs/lru.js +150 -0
- package/build/cjs/mock/index.js +196 -0
- package/build/cjs/mock/mockHttp.js +17 -0
- package/build/cjs/mock/mockWs.js +47 -0
- package/build/cjs/mock/types.js +2 -0
- package/build/cjs/packageDetect.d.ts +1 -0
- package/build/cjs/packageInfo.js +4 -0
- package/build/cjs/types.js +2 -0
- package/build/cjs/ws/errors.js +41 -0
- package/build/cjs/ws/index.js +529 -0
- package/build/coder/error.d.ts +29 -0
- package/build/coder/error.js +50 -0
- package/build/coder/index.d.ts +8 -0
- package/build/coder/index.js +58 -0
- package/build/defaults.d.ts +5 -0
- package/build/defaults.js +6 -0
- package/build/http/index.d.ts +81 -0
- package/build/http/index.js +191 -0
- package/build/http/types.d.ts +7 -0
- package/build/http/types.js +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +2 -0
- package/build/lru.d.ts +15 -0
- package/build/lru.js +146 -0
- package/build/mock/index.d.ts +35 -0
- package/build/mock/index.js +191 -0
- package/build/mock/mockHttp.d.ts +9 -0
- package/build/mock/mockHttp.js +12 -0
- package/build/mock/mockWs.d.ts +26 -0
- package/build/mock/mockWs.js +43 -0
- package/build/mock/types.d.ts +23 -0
- package/build/mock/types.js +1 -0
- package/build/package.json +344 -0
- package/build/packageDetect.d.ts +1 -0
- package/build/packageDetect.js +4 -0
- package/build/packageInfo.d.ts +6 -0
- package/build/packageInfo.js +1 -0
- package/build/types.d.ts +85 -0
- package/build/types.js +1 -0
- package/build/ws/errors.d.ts +1 -0
- package/build/ws/errors.js +38 -0
- package/build/ws/index.d.ts +121 -0
- package/build/ws/index.js +524 -0
- package/build-deno/README.md +66 -0
- package/build-deno/bizinikiwi-connect/Health.ts +323 -0
- package/build-deno/bizinikiwi-connect/index.ts +417 -0
- package/build-deno/bizinikiwi-connect/types.ts +14 -0
- package/build-deno/bundle.ts +6 -0
- package/build-deno/coder/error.ts +64 -0
- package/build-deno/coder/index.ts +86 -0
- package/build-deno/defaults.ts +8 -0
- package/build-deno/http/index.ts +236 -0
- package/build-deno/http/types.ts +9 -0
- package/build-deno/index.ts +4 -0
- package/build-deno/lru.ts +189 -0
- package/build-deno/mock/index.ts +257 -0
- package/build-deno/mock/mockHttp.ts +33 -0
- package/build-deno/mock/mockWs.ts +87 -0
- package/build-deno/mock/types.ts +34 -0
- package/build-deno/mod.ts +2 -0
- package/build-deno/packageDetect.ts +8 -0
- package/build-deno/packageInfo.ts +3 -0
- package/build-deno/types.ts +99 -0
- package/build-deno/ws/errors.ts +38 -0
- package/build-deno/ws/index.ts +650 -0
- package/build-tsc-cjs/packageDetect.js +6 -0
- package/{cjs → build-tsc-cjs}/packageInfo.js +1 -1
- package/{packageInfo.js → build-tsc-esm/packageInfo.js} +1 -1
- package/package.json +16 -16
- package/src/bizinikiwi-connect/Health.ts +325 -0
- package/src/bizinikiwi-connect/index.spec.ts +675 -0
- package/src/bizinikiwi-connect/index.ts +427 -0
- package/src/bizinikiwi-connect/types.ts +16 -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/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
- package/tsconfig.spec.tsbuildinfo +1 -0
- /package/{cjs → build}/bizinikiwi-connect/Health.d.ts +0 -0
- /package/{cjs → build}/bizinikiwi-connect/index.d.ts +0 -0
- /package/{cjs → build}/bizinikiwi-connect/types.d.ts +0 -0
- /package/{packageDetect.d.ts → build/bizinikiwi-connect/types.js} +0 -0
- /package/{cjs → build}/bundle.d.ts +0 -0
- /package/{coder → build/cjs/coder}/error.d.ts +0 -0
- /package/{coder → build/cjs/coder}/index.d.ts +0 -0
- /package/{defaults.d.ts → build/cjs/defaults.d.ts} +0 -0
- /package/{http → build/cjs/http}/index.d.ts +0 -0
- /package/{http → build/cjs/http}/types.d.ts +0 -0
- /package/{index.d.ts → build/cjs/index.d.ts} +0 -0
- /package/{lru.d.ts → build/cjs/lru.d.ts} +0 -0
- /package/{mock → build/cjs/mock}/index.d.ts +0 -0
- /package/{mock → build/cjs/mock}/mockHttp.d.ts +0 -0
- /package/{mock → build/cjs/mock}/mockWs.d.ts +0 -0
- /package/{mock → build/cjs/mock}/types.d.ts +0 -0
- /package/{cjs → build/cjs}/package.json +0 -0
- /package/{cjs → build/cjs}/packageDetect.js +0 -0
- /package/{packageInfo.d.ts → build/cjs/packageInfo.d.ts} +0 -0
- /package/{types.d.ts → build/cjs/types.d.ts} +0 -0
- /package/{ws → build/cjs/ws}/errors.d.ts +0 -0
- /package/{ws → build/cjs/ws}/index.d.ts +0 -0
- /package/{bizinikiwi-connect → build-tsc/bizinikiwi-connect}/Health.d.ts +0 -0
- /package/{bizinikiwi-connect → build-tsc/bizinikiwi-connect}/index.d.ts +0 -0
- /package/{bizinikiwi-connect → build-tsc/bizinikiwi-connect}/types.d.ts +0 -0
- /package/{bundle.d.ts → build-tsc/bundle.d.ts} +0 -0
- /package/{cjs → build-tsc}/coder/error.d.ts +0 -0
- /package/{cjs → build-tsc}/coder/index.d.ts +0 -0
- /package/{cjs → build-tsc}/defaults.d.ts +0 -0
- /package/{cjs → build-tsc}/http/index.d.ts +0 -0
- /package/{cjs → build-tsc}/http/types.d.ts +0 -0
- /package/{cjs → build-tsc}/index.d.ts +0 -0
- /package/{cjs → build-tsc}/lru.d.ts +0 -0
- /package/{cjs → build-tsc}/mock/index.d.ts +0 -0
- /package/{cjs → build-tsc}/mock/mockHttp.d.ts +0 -0
- /package/{cjs → build-tsc}/mock/mockWs.d.ts +0 -0
- /package/{cjs → build-tsc}/mock/types.d.ts +0 -0
- /package/{cjs → build-tsc}/packageDetect.d.ts +0 -0
- /package/{cjs → build-tsc}/packageInfo.d.ts +0 -0
- /package/{cjs → build-tsc}/types.d.ts +0 -0
- /package/{cjs → build-tsc}/ws/errors.d.ts +0 -0
- /package/{cjs → build-tsc}/ws/index.d.ts +0 -0
- /package/{cjs → build-tsc-cjs}/bizinikiwi-connect/Health.js +0 -0
- /package/{cjs → build-tsc-cjs}/bizinikiwi-connect/index.js +0 -0
- /package/{cjs → build-tsc-cjs}/bizinikiwi-connect/types.js +0 -0
- /package/{cjs → build-tsc-cjs}/bundle.js +0 -0
- /package/{cjs → build-tsc-cjs}/coder/error.js +0 -0
- /package/{cjs → build-tsc-cjs}/coder/index.js +0 -0
- /package/{cjs → build-tsc-cjs}/defaults.js +0 -0
- /package/{cjs → build-tsc-cjs}/http/index.js +0 -0
- /package/{cjs → build-tsc-cjs}/http/types.js +0 -0
- /package/{cjs → build-tsc-cjs}/index.js +0 -0
- /package/{cjs → build-tsc-cjs}/lru.js +0 -0
- /package/{cjs → build-tsc-cjs}/mock/index.js +0 -0
- /package/{cjs → build-tsc-cjs}/mock/mockHttp.js +0 -0
- /package/{cjs → build-tsc-cjs}/mock/mockWs.js +0 -0
- /package/{cjs → build-tsc-cjs}/mock/types.js +0 -0
- /package/{cjs → build-tsc-cjs}/types.js +0 -0
- /package/{cjs → build-tsc-cjs}/ws/errors.js +0 -0
- /package/{cjs → build-tsc-cjs}/ws/index.js +0 -0
- /package/{bizinikiwi-connect → build-tsc-esm/bizinikiwi-connect}/Health.js +0 -0
- /package/{bizinikiwi-connect → build-tsc-esm/bizinikiwi-connect}/index.js +0 -0
- /package/{bizinikiwi-connect → build-tsc-esm/bizinikiwi-connect}/types.js +0 -0
- /package/{bundle.js → build-tsc-esm/bundle.js} +0 -0
- /package/{coder → build-tsc-esm/coder}/error.js +0 -0
- /package/{coder → build-tsc-esm/coder}/index.js +0 -0
- /package/{defaults.js → build-tsc-esm/defaults.js} +0 -0
- /package/{http → build-tsc-esm/http}/index.js +0 -0
- /package/{http → build-tsc-esm/http}/types.js +0 -0
- /package/{index.js → build-tsc-esm/index.js} +0 -0
- /package/{lru.js → build-tsc-esm/lru.js} +0 -0
- /package/{mock → build-tsc-esm/mock}/index.js +0 -0
- /package/{mock → build-tsc-esm/mock}/mockHttp.js +0 -0
- /package/{mock → build-tsc-esm/mock}/mockWs.js +0 -0
- /package/{mock → build-tsc-esm/mock}/types.js +0 -0
- /package/{packageDetect.js → build-tsc-esm/packageDetect.js} +0 -0
- /package/{types.js → build-tsc-esm/types.js} +0 -0
- /package/{ws → build-tsc-esm/ws}/errors.js +0 -0
- /package/{ws → build-tsc-esm/ws}/index.js +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Copyright 2017-2025 @pezkuwi/rpc-provider authors & contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { RpcErrorInterface } from '../types.js';
|
|
5
|
+
|
|
6
|
+
import { isFunction } from '@pezkuwi/util';
|
|
7
|
+
|
|
8
|
+
const UNKNOWN = -99999;
|
|
9
|
+
|
|
10
|
+
function extend<Data, K extends keyof RpcError<Data>> (that: RpcError<Data>, name: K, value: RpcError<Data>[K]): void {
|
|
11
|
+
Object.defineProperty(that, name, {
|
|
12
|
+
configurable: true,
|
|
13
|
+
enumerable: false,
|
|
14
|
+
value
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @name RpcError
|
|
20
|
+
* @summary Extension to the basic JS Error.
|
|
21
|
+
* @description
|
|
22
|
+
* 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.
|
|
23
|
+
* @example
|
|
24
|
+
* <BR>
|
|
25
|
+
*
|
|
26
|
+
* ```javascript
|
|
27
|
+
* const { RpcError } from '@pezkuwi/util');
|
|
28
|
+
*
|
|
29
|
+
* throw new RpcError('some message', RpcError.CODES.METHOD_NOT_FOUND); // => error.code = -32601
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export default class RpcError<T = never> extends Error implements RpcErrorInterface<T> {
|
|
33
|
+
public code!: number;
|
|
34
|
+
|
|
35
|
+
public data?: T;
|
|
36
|
+
|
|
37
|
+
public override message!: string;
|
|
38
|
+
|
|
39
|
+
public override name!: string;
|
|
40
|
+
|
|
41
|
+
public override stack!: string;
|
|
42
|
+
|
|
43
|
+
public constructor (message = '', code: number = UNKNOWN, data?: T) {
|
|
44
|
+
super();
|
|
45
|
+
|
|
46
|
+
extend(this, 'message', String(message));
|
|
47
|
+
extend(this, 'name', this.constructor.name);
|
|
48
|
+
extend(this, 'data', data);
|
|
49
|
+
extend(this, 'code', code);
|
|
50
|
+
|
|
51
|
+
if (isFunction(Error.captureStackTrace)) {
|
|
52
|
+
Error.captureStackTrace(this, this.constructor);
|
|
53
|
+
} else {
|
|
54
|
+
const { stack } = new Error(message);
|
|
55
|
+
|
|
56
|
+
stack && extend(this, 'stack', stack);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public static CODES = {
|
|
61
|
+
ASSERT: -90009,
|
|
62
|
+
INVALID_JSONRPC: -99998,
|
|
63
|
+
METHOD_NOT_FOUND: -32601, // Rust client
|
|
64
|
+
UNKNOWN
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Copyright 2017-2025 @pezkuwi/rpc-provider authors & contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { JsonRpcRequest, JsonRpcResponse, JsonRpcResponseBaseError } from '../types.js';
|
|
5
|
+
|
|
6
|
+
import { isNumber, isString, isUndefined, stringify } from '@pezkuwi/util';
|
|
7
|
+
|
|
8
|
+
import RpcError from './error.js';
|
|
9
|
+
|
|
10
|
+
function formatErrorData (data?: string | number): string {
|
|
11
|
+
if (isUndefined(data)) {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const formatted = `: ${isString(data)
|
|
16
|
+
? data.replace(/Error\("/g, '').replace(/\("/g, '(').replace(/"\)/g, ')').replace(/\(/g, ', ').replace(/\)/g, '')
|
|
17
|
+
: stringify(data)}`;
|
|
18
|
+
|
|
19
|
+
// We need some sort of cut-off here since these can be very large and
|
|
20
|
+
// very nested, pick a number and trim the result display to it
|
|
21
|
+
return formatted.length <= 256
|
|
22
|
+
? formatted
|
|
23
|
+
: `${formatted.substring(0, 255)}…`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function checkError (error?: JsonRpcResponseBaseError): void {
|
|
27
|
+
if (error) {
|
|
28
|
+
const { code, data, message } = error;
|
|
29
|
+
|
|
30
|
+
throw new RpcError(`${code}: ${message}${formatErrorData(data)}`, code, data);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** @internal */
|
|
35
|
+
export class RpcCoder {
|
|
36
|
+
#id = 0;
|
|
37
|
+
|
|
38
|
+
public decodeResponse <T> (response?: JsonRpcResponse<T>): T {
|
|
39
|
+
if (!response || response.jsonrpc !== '2.0') {
|
|
40
|
+
throw new Error('Invalid jsonrpc field in decoded object');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const isSubscription = !isUndefined(response.params) && !isUndefined(response.method);
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
!isNumber(response.id) &&
|
|
47
|
+
(
|
|
48
|
+
!isSubscription || (
|
|
49
|
+
!isNumber(response.params.subscription) &&
|
|
50
|
+
!isString(response.params.subscription)
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
) {
|
|
54
|
+
throw new Error('Invalid id field in decoded object');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
checkError(response.error);
|
|
58
|
+
|
|
59
|
+
if (response.result === undefined && !isSubscription) {
|
|
60
|
+
throw new Error('No result found in jsonrpc response');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (isSubscription) {
|
|
64
|
+
checkError(response.params.error);
|
|
65
|
+
|
|
66
|
+
return response.params.result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return response.result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public encodeJson (method: string, params: unknown[]): [number, string] {
|
|
73
|
+
const [id, data] = this.encodeObject(method, params);
|
|
74
|
+
|
|
75
|
+
return [id, stringify(data)];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public encodeObject (method: string, params: unknown[]): [number, JsonRpcRequest] {
|
|
79
|
+
const id = ++this.#id;
|
|
80
|
+
|
|
81
|
+
return [id, {
|
|
82
|
+
id,
|
|
83
|
+
jsonrpc: '2.0',
|
|
84
|
+
method,
|
|
85
|
+
params
|
|
86
|
+
}];
|
|
87
|
+
}
|
|
88
|
+
}
|
package/src/defaults.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Copyright 2017-2025 @pezkuwi/rpc-provider authors & contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
|
|
5
|
+
|
|
6
|
+
import { TEST_HTTP_URL } from '../mock/mockHttp.js';
|
|
7
|
+
import { HttpProvider } from './index.js';
|
|
8
|
+
|
|
9
|
+
describe('Http', (): void => {
|
|
10
|
+
let http: HttpProvider;
|
|
11
|
+
|
|
12
|
+
beforeEach((): void => {
|
|
13
|
+
http = new HttpProvider(TEST_HTTP_URL);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('requires an http:// prefixed endpoint', (): void => {
|
|
17
|
+
expect(
|
|
18
|
+
() => new HttpProvider('ws://')
|
|
19
|
+
).toThrow(/with 'http/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('allows https:// endpoints', (): void => {
|
|
23
|
+
expect(
|
|
24
|
+
() => new HttpProvider('https://')
|
|
25
|
+
).not.toThrow();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('allows custom headers', (): void => {
|
|
29
|
+
expect(
|
|
30
|
+
() => new HttpProvider('https://', { foo: 'bar' })
|
|
31
|
+
).not.toThrow();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should throw error on negative cache capacity or TTL', () => {
|
|
35
|
+
expect(() =>
|
|
36
|
+
new HttpProvider(TEST_HTTP_URL, {}, -5, 30000)
|
|
37
|
+
).toThrow(/'capacity' must be a non-negative integer/);
|
|
38
|
+
|
|
39
|
+
expect(() =>
|
|
40
|
+
new HttpProvider(TEST_HTTP_URL, {}, 1024, -1000)
|
|
41
|
+
).toThrow(/'ttl' must be between 0 and 1800000 ms or null to disable/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('allow clone', (): void => {
|
|
45
|
+
const clone = http.clone();
|
|
46
|
+
/* eslint-disable */
|
|
47
|
+
expect((clone as any)['#endpoint']).toEqual((http as any)['#endpoint']);
|
|
48
|
+
expect((clone as any)['#headers']).toEqual((http as any)['#headers']);
|
|
49
|
+
/* eslint-enable */
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('always returns isConnected true', (): void => {
|
|
53
|
+
expect(http.isConnected).toEqual(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('does not (yet) support subscribe', async (): Promise<void> => {
|
|
57
|
+
await http.subscribe('', '', [], (cb): void => {
|
|
58
|
+
// eslint-disable-next-line jest/no-conditional-expect
|
|
59
|
+
expect(cb).toEqual(expect.anything());
|
|
60
|
+
}).catch((error): void => {
|
|
61
|
+
// eslint-disable-next-line jest/no-conditional-expect
|
|
62
|
+
expect((error as Error).message).toMatch(/does not have subscriptions/);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('does not (yet) support unsubscribe', async (): Promise<void> => {
|
|
67
|
+
await http.unsubscribe('', '', 0).catch((error): void => {
|
|
68
|
+
// eslint-disable-next-line jest/no-conditional-expect
|
|
69
|
+
expect((error as Error).message).toMatch(/does not have subscriptions/);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// Copyright 2017-2025 @pezkuwi/rpc-provider authors & contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type RpcError from '../coder/error.js';
|
|
5
|
+
import type { JsonRpcResponse, ProviderInterface, ProviderInterfaceCallback, ProviderInterfaceEmitCb, ProviderInterfaceEmitted, ProviderStats } from '../types.js';
|
|
6
|
+
|
|
7
|
+
import { logger, noop, stringify } from '@pezkuwi/util';
|
|
8
|
+
import { fetch } from '@pezkuwi/x-fetch';
|
|
9
|
+
|
|
10
|
+
import { RpcCoder } from '../coder/index.js';
|
|
11
|
+
import defaults from '../defaults.js';
|
|
12
|
+
import { DEFAULT_CAPACITY, DEFAULT_TTL, LRUCache } from '../lru.js';
|
|
13
|
+
|
|
14
|
+
const ERROR_SUBSCRIBE = 'HTTP Provider does not have subscriptions, use WebSockets instead';
|
|
15
|
+
|
|
16
|
+
const l = logger('api-http');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* # @pezkuwi/rpc-provider
|
|
20
|
+
*
|
|
21
|
+
* @name HttpProvider
|
|
22
|
+
*
|
|
23
|
+
* @description The HTTP Provider allows sending requests using HTTP to a HTTP RPC server TCP port. It does not support subscriptions so you won't be able to listen to events such as new blocks or balance changes. It is usually preferable using the [[WsProvider]].
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* <BR>
|
|
27
|
+
*
|
|
28
|
+
* ```javascript
|
|
29
|
+
* import Api from '@pezkuwi/api/promise';
|
|
30
|
+
* import { HttpProvider } from '@pezkuwi/rpc-provider';
|
|
31
|
+
*
|
|
32
|
+
* const provider = new HttpProvider('http://127.0.0.1:9933');
|
|
33
|
+
* const api = new Api(provider);
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @see [[WsProvider]]
|
|
37
|
+
*/
|
|
38
|
+
export class HttpProvider implements ProviderInterface {
|
|
39
|
+
readonly #callCache: LRUCache;
|
|
40
|
+
readonly #cacheCapacity: number;
|
|
41
|
+
readonly #coder: RpcCoder;
|
|
42
|
+
readonly #endpoint: string;
|
|
43
|
+
readonly #headers: Record<string, string>;
|
|
44
|
+
readonly #stats: ProviderStats;
|
|
45
|
+
readonly #ttl: number | null | undefined;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} endpoint The endpoint url starting with http://
|
|
49
|
+
* @param {Record<string, string>} headers The headers provided to the underlying Http Endpoint
|
|
50
|
+
* @param {number} [cacheCapacity] Custom size of the HttpProvider LRUCache. Defaults to `DEFAULT_CAPACITY` (1024)
|
|
51
|
+
* @param {number} [cacheTtl] Custom TTL of the HttpProvider LRUCache. Determines how long an object can live in the cache. Defaults to `DEFAULT_TTL` (30000)
|
|
52
|
+
*/
|
|
53
|
+
constructor (endpoint: string = defaults.HTTP_URL, headers: Record<string, string> = {}, cacheCapacity?: number, cacheTtl?: number | null) {
|
|
54
|
+
if (!/^(https|http):\/\//.test(endpoint)) {
|
|
55
|
+
throw new Error(`Endpoint should start with 'http://' or 'https://', received '${endpoint}'`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.#coder = new RpcCoder();
|
|
59
|
+
this.#endpoint = endpoint;
|
|
60
|
+
this.#headers = headers;
|
|
61
|
+
this.#cacheCapacity = cacheCapacity === 0 ? 0 : cacheCapacity || DEFAULT_CAPACITY;
|
|
62
|
+
|
|
63
|
+
const ttl = cacheTtl === undefined ? DEFAULT_TTL : cacheTtl;
|
|
64
|
+
|
|
65
|
+
this.#callCache = new LRUCache(cacheCapacity === 0 ? 0 : cacheCapacity || DEFAULT_CAPACITY, ttl);
|
|
66
|
+
this.#ttl = cacheTtl;
|
|
67
|
+
|
|
68
|
+
this.#stats = {
|
|
69
|
+
active: { requests: 0, subscriptions: 0 },
|
|
70
|
+
total: { bytesRecv: 0, bytesSent: 0, cached: 0, errors: 0, requests: 0, subscriptions: 0, timeout: 0 }
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @summary `true` when this provider supports subscriptions
|
|
76
|
+
*/
|
|
77
|
+
public get hasSubscriptions (): boolean {
|
|
78
|
+
return !!false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @description Returns a clone of the object
|
|
83
|
+
*/
|
|
84
|
+
public clone (): HttpProvider {
|
|
85
|
+
return new HttpProvider(this.#endpoint, this.#headers);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @description Manually connect from the connection
|
|
90
|
+
*/
|
|
91
|
+
public async connect (): Promise<void> {
|
|
92
|
+
// noop
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @description Manually disconnect from the connection
|
|
97
|
+
*/
|
|
98
|
+
public async disconnect (): Promise<void> {
|
|
99
|
+
// noop
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @description Returns the connection stats
|
|
104
|
+
*/
|
|
105
|
+
public get stats (): ProviderStats {
|
|
106
|
+
return this.#stats;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @description Returns the connection stats
|
|
111
|
+
*/
|
|
112
|
+
public get ttl (): number | null | undefined {
|
|
113
|
+
return this.#ttl;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @summary `true` when this provider supports clone()
|
|
118
|
+
*/
|
|
119
|
+
public get isClonable (): boolean {
|
|
120
|
+
return !!true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* @summary Whether the node is connected or not.
|
|
125
|
+
* @return {boolean} true if connected
|
|
126
|
+
*/
|
|
127
|
+
public get isConnected (): boolean {
|
|
128
|
+
return !!true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @summary Events are not supported with the HttpProvider, see [[WsProvider]].
|
|
133
|
+
* @description HTTP Provider does not have 'on' emitters. WebSockets should be used instead.
|
|
134
|
+
*/
|
|
135
|
+
public on (_type: ProviderInterfaceEmitted, _sub: ProviderInterfaceEmitCb): () => void {
|
|
136
|
+
l.error('HTTP Provider does not have \'on\' emitters, use WebSockets instead');
|
|
137
|
+
|
|
138
|
+
return noop;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @summary Send HTTP POST Request with Body to configured HTTP Endpoint.
|
|
143
|
+
*/
|
|
144
|
+
public async send <T> (method: string, params: unknown[], isCacheable?: boolean): Promise<T> {
|
|
145
|
+
this.#stats.total.requests++;
|
|
146
|
+
|
|
147
|
+
const [, body] = this.#coder.encodeJson(method, params);
|
|
148
|
+
|
|
149
|
+
if (this.#cacheCapacity === 0) {
|
|
150
|
+
return this.#send(body);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const cacheKey = isCacheable ? `${method}::${stringify(params)}` : '';
|
|
154
|
+
let resultPromise: Promise<T> | null = isCacheable
|
|
155
|
+
? this.#callCache.get(cacheKey)
|
|
156
|
+
: null;
|
|
157
|
+
|
|
158
|
+
if (!resultPromise) {
|
|
159
|
+
resultPromise = this.#send(body);
|
|
160
|
+
|
|
161
|
+
if (isCacheable) {
|
|
162
|
+
this.#callCache.set(cacheKey, resultPromise);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
this.#stats.total.cached++;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return resultPromise;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async #send <T> (body: string): Promise<T> {
|
|
172
|
+
this.#stats.active.requests++;
|
|
173
|
+
this.#stats.total.bytesSent += body.length;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const response = await fetch(this.#endpoint, {
|
|
177
|
+
body,
|
|
178
|
+
headers: {
|
|
179
|
+
Accept: 'application/json',
|
|
180
|
+
'Content-Length': `${body.length}`,
|
|
181
|
+
'Content-Type': 'application/json',
|
|
182
|
+
...this.#headers
|
|
183
|
+
},
|
|
184
|
+
method: 'POST'
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
throw new Error(`[${response.status}]: ${response.statusText}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const result = await response.text();
|
|
192
|
+
|
|
193
|
+
this.#stats.total.bytesRecv += result.length;
|
|
194
|
+
|
|
195
|
+
const decoded = this.#coder.decodeResponse(JSON.parse(result) as JsonRpcResponse<T>);
|
|
196
|
+
|
|
197
|
+
this.#stats.active.requests--;
|
|
198
|
+
|
|
199
|
+
return decoded;
|
|
200
|
+
} catch (e) {
|
|
201
|
+
this.#stats.active.requests--;
|
|
202
|
+
this.#stats.total.errors++;
|
|
203
|
+
|
|
204
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
205
|
+
const { method, params } = JSON.parse(body);
|
|
206
|
+
|
|
207
|
+
const rpcError: RpcError = e as RpcError;
|
|
208
|
+
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
210
|
+
const failedRequest = `\nFailed HTTP Request: ${JSON.stringify({ method, params })}`;
|
|
211
|
+
|
|
212
|
+
// Provide HTTP Request alongside the error
|
|
213
|
+
rpcError.message = `${rpcError.message}${failedRequest}`;
|
|
214
|
+
|
|
215
|
+
throw rpcError;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @summary Subscriptions are not supported with the HttpProvider, see [[WsProvider]].
|
|
221
|
+
*/
|
|
222
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
223
|
+
public async subscribe (_types: string, _method: string, _params: unknown[], _cb: ProviderInterfaceCallback): Promise<number> {
|
|
224
|
+
l.error(ERROR_SUBSCRIBE);
|
|
225
|
+
|
|
226
|
+
throw new Error(ERROR_SUBSCRIBE);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @summary Subscriptions are not supported with the HttpProvider, see [[WsProvider]].
|
|
231
|
+
*/
|
|
232
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
233
|
+
public async unsubscribe (_type: string, _method: string, _id: number): Promise<boolean> {
|
|
234
|
+
l.error(ERROR_SUBSCRIBE);
|
|
235
|
+
|
|
236
|
+
throw new Error(ERROR_SUBSCRIBE);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Copyright 2017-2025 @pezkuwi/rpc-provider authors & contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
|
|
5
|
+
|
|
6
|
+
import type { Mock } from '../mock/types.js';
|
|
7
|
+
|
|
8
|
+
import { mockHttp, TEST_HTTP_URL } from '../mock/mockHttp.js';
|
|
9
|
+
import { HttpProvider } from './index.js';
|
|
10
|
+
|
|
11
|
+
// Does not work with Node 18 (native fetch)
|
|
12
|
+
// See https://github.com/nock/nock/issues/2397
|
|
13
|
+
// eslint-disable-next-line jest/no-disabled-tests
|
|
14
|
+
describe.skip('send', (): void => {
|
|
15
|
+
let http: HttpProvider;
|
|
16
|
+
let mock: Mock;
|
|
17
|
+
|
|
18
|
+
beforeEach((): void => {
|
|
19
|
+
http = new HttpProvider(TEST_HTTP_URL);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
if (mock) {
|
|
24
|
+
await mock.done();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('passes the body through correctly', (): Promise<void> => {
|
|
29
|
+
mock = mockHttp([{
|
|
30
|
+
method: 'test_body',
|
|
31
|
+
reply: {
|
|
32
|
+
result: 'ok'
|
|
33
|
+
}
|
|
34
|
+
}]);
|
|
35
|
+
|
|
36
|
+
return http
|
|
37
|
+
.send('test_body', ['param'])
|
|
38
|
+
.then((): void => {
|
|
39
|
+
expect(mock.body['test_body']).toEqual({
|
|
40
|
+
id: 1,
|
|
41
|
+
jsonrpc: '2.0',
|
|
42
|
+
method: 'test_body',
|
|
43
|
+
params: ['param']
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('throws error when !response.ok', async (): Promise<any> => {
|
|
49
|
+
mock = mockHttp([{
|
|
50
|
+
code: 500,
|
|
51
|
+
method: 'test_error'
|
|
52
|
+
}]);
|
|
53
|
+
|
|
54
|
+
return http
|
|
55
|
+
.send('test_error', [])
|
|
56
|
+
.catch((error): void => {
|
|
57
|
+
// eslint-disable-next-line jest/no-conditional-expect
|
|
58
|
+
expect((error as Error).message).toMatch(/\[500\]/);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Copyright 2017-2025 @pezkuwi/rpc-provider authors & contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { Logger } from '@pezkuwi/util/types';
|
|
5
|
+
import type { RpcCoder } from '../coder/index.js';
|
|
6
|
+
|
|
7
|
+
export interface HttpState {
|
|
8
|
+
coder: RpcCoder;
|
|
9
|
+
endpoint: string;
|
|
10
|
+
l: Logger;
|
|
11
|
+
}
|
package/src/index.ts
ADDED
package/src/lru.spec.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Copyright 2017-2025 @pezkuwi/rpc-provider authors & contributors
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/// <reference types="@pezkuwi/dev-test/globals.d.ts" />
|
|
5
|
+
|
|
6
|
+
import { LRUCache } from './lru.js';
|
|
7
|
+
|
|
8
|
+
describe('LRUCache', (): void => {
|
|
9
|
+
let lru: LRUCache | undefined;
|
|
10
|
+
|
|
11
|
+
beforeEach((): void => {
|
|
12
|
+
lru = new LRUCache(4, 500);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('allows getting of items below capacity', (): void => {
|
|
16
|
+
const keys = ['1', '2', '3', '4'];
|
|
17
|
+
|
|
18
|
+
keys.forEach((k) => lru?.set(k, `${k}${k}${k}`));
|
|
19
|
+
const lruKeys = lru?.keys();
|
|
20
|
+
|
|
21
|
+
expect(lruKeys?.join(', ')).toBe(keys.reverse().join(', '));
|
|
22
|
+
expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true);
|
|
23
|
+
|
|
24
|
+
keys.forEach((k) => expect(lru?.get(k)).toEqual(`${k}${k}${k}`));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('drops items when at capacity', (): void => {
|
|
28
|
+
const keys = ['1', '2', '3', '4', '5', '6'];
|
|
29
|
+
|
|
30
|
+
keys.forEach((k) => lru?.set(k, `${k}${k}${k}`));
|
|
31
|
+
|
|
32
|
+
expect(lru?.keys().join(', ')).toEqual(keys.slice(2).reverse().join(', '));
|
|
33
|
+
expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true);
|
|
34
|
+
|
|
35
|
+
keys.slice(2).forEach((k) => expect(lru?.get(k)).toEqual(`${k}${k}${k}`));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('adjusts the order as they are used', (): void => {
|
|
39
|
+
const keys = ['1', '2', '3', '4', '5'];
|
|
40
|
+
|
|
41
|
+
keys.forEach((k) => lru?.set(k, `${k}${k}${k}`));
|
|
42
|
+
|
|
43
|
+
expect(lru?.entries()).toEqual([['5', '555'], ['4', '444'], ['3', '333'], ['2', '222']]);
|
|
44
|
+
expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true);
|
|
45
|
+
|
|
46
|
+
lru?.get('3');
|
|
47
|
+
|
|
48
|
+
expect(lru?.entries()).toEqual([['3', '333'], ['5', '555'], ['4', '444'], ['2', '222']]);
|
|
49
|
+
expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true);
|
|
50
|
+
|
|
51
|
+
lru?.set('4', '4433');
|
|
52
|
+
|
|
53
|
+
expect(lru?.entries()).toEqual([['4', '4433'], ['3', '333'], ['5', '555'], ['2', '222']]);
|
|
54
|
+
expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true);
|
|
55
|
+
|
|
56
|
+
lru?.set('6', '666');
|
|
57
|
+
|
|
58
|
+
expect(lru?.entries()).toEqual([['6', '666'], ['4', '4433'], ['3', '333'], ['5', '555']]);
|
|
59
|
+
expect(lru?.length === lru?.lengthData && lru?.length === lru?.lengthRefs).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('evicts items with TTL', (): void => {
|
|
63
|
+
const keys = ['1', '2', '3', '4', '5'];
|
|
64
|
+
|
|
65
|
+
keys.forEach((k) => lru?.set(k, `${k}${k}${k}`));
|
|
66
|
+
|
|
67
|
+
expect(lru?.entries()).toEqual([['5', '555'], ['4', '444'], ['3', '333'], ['2', '222']]);
|
|
68
|
+
|
|
69
|
+
setTimeout((): void => {
|
|
70
|
+
lru?.get('3');
|
|
71
|
+
expect(lru?.entries()).toEqual([['3', '333']]);
|
|
72
|
+
}, 800);
|
|
73
|
+
});
|
|
74
|
+
});
|