jsonrpc-client-websocket 1.5.5 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![📦 Build](https://github.com/fabriciobastian/jsonrpc-client-websocket/actions/workflows/build.yml/badge.svg)](https://github.com/fabriciobastian/jsonrpc-client-websocket/actions/workflows/build.yml) [![License](https://img.shields.io/github/license/fabriciobastian/jsonrpc-client-websocket)](https://choosealicense.com/licenses/mit/) [![NPM downloads](https://img.shields.io/npm/dt/jsonrpc-client-websocket.svg)](https://www.npmjs.com/package/jsonrpc-client-websocket)
1
+ [![✅ Continuous Integration](https://github.com/fabriciobastian/jsonrpc-client-websocket/actions/workflows/ci.yml/badge.svg)](https://github.com/fabriciobastian/jsonrpc-client-websocket/actions/workflows/ci.yml) [![License](https://img.shields.io/github/license/fabriciobastian/jsonrpc-client-websocket)](https://choosealicense.com/licenses/mit/) [![NPM downloads](https://img.shields.io/npm/dt/jsonrpc-client-websocket.svg)](https://www.npmjs.com/package/jsonrpc-client-websocket)
2
2
 
3
3
  ![jsonrpc-client-websocket](https://raw.githubusercontent.com/fabriciobastian/jsonrpc-client-websocket/main/assets/jsonrpc-client-websocket-banner-light-small.png)
4
4
 
@@ -74,33 +74,66 @@ The defined RPC methods can also be called with both positional and named parame
74
74
 
75
75
  ### Minification/Obfuscation
76
76
 
77
- When using code minification/obfuscation the parameter names may change, which can
78
- cause calls using named parameters to fail. To mitigate this you can do one or a
79
- combination of the following:
80
- - do not minify/obfuscate the code where `on` is used, so the function registration does not get modified
81
- - use positional parameters
82
- - register functions as following:
77
+ For code that will be minified or obfuscated, pass the public parameter names as
78
+ the optional third argument to `on`:
83
79
 
84
80
  ```typescript
85
- websocket.on('sum', (params: { a: number; b: number }) => {
86
- return params.a + params.b;
87
- });
81
+ websocket.on('subtract', (x: number, y: number) => x - y, ['a', 'b']);
88
82
  ```
89
83
 
90
- and then call the function using positional parameters using the object:
84
+ The list follows the callback's argument order: `a` maps to `x`, and `b` maps to `y`.
85
+ The order of keys in the request object does not matter:
91
86
 
92
87
  ```typescript
93
- websocket.call('sum', [{ a: 1, b: 2 }])
88
+ websocket.call('subtract', { b: 2, a: 5 })
94
89
  .then((response) => {
95
- // handle response
90
+ // response.result is 3
96
91
  })
97
92
  .catch((error) => {
98
93
  // handle error
99
94
  });
100
95
  ```
101
96
 
97
+ Positional calls such as `websocket.call('subtract', [5, 2])` also work. The explicit
98
+ list defines both the parameter order and the required argument count, even if
99
+ minification renames or removes unused callback parameters. Names must be unique;
100
+ use an empty list for a method that takes no arguments. The list is copied when
101
+ the method is registered.
102
+
103
+ Without the third argument, existing registrations continue to infer named
104
+ parameters from the callback's source. Those names may change during minification
105
+ or obfuscation, so use explicit names for named calls in production bundles.
106
+ If property mangling is enabled, preserve the public keys in request objects too.
107
+
102
108
  ## Remove RPC method
103
109
 
104
110
  ```typescript
105
111
  websocket.off('sum');
106
112
  ```
113
+
114
+ ## Development
115
+
116
+ Use the Node.js version in `.nvmrc` and its bundled npm, then run:
117
+
118
+ ```sh
119
+ npm ci
120
+ npm test
121
+ npm run lint
122
+ npm run format:check
123
+ npm run build
124
+ ```
125
+
126
+ TypeScript checks both the library and tests. ESLint uses native flat configuration
127
+ with TypeScript and Jest rules. TypeScript stays on the latest 6.0 patch supported
128
+ by `typescript-eslint`; upgrading to TypeScript 7 requires its official support.
129
+ Tests live alongside the source and are excluded from build output.
130
+ `npm run update-all` respects peer dependency requirements when selecting updates.
131
+
132
+ These tooling requirements apply to development only. The published library still
133
+ targets ES2020 and has no runtime dependencies.
134
+
135
+ ## Publishing
136
+
137
+ Releases are staged through GitHub Actions using
138
+ [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/) (OIDC).
139
+ A maintainer approves each staged package on npm before it becomes publicly available.
@@ -0,0 +1 @@
1
+ export declare function getParameterNames(callback: (...args: never[]) => unknown): string[];
@@ -0,0 +1,5 @@
1
+ export function getParameterNames(callback) {
2
+ const source = Function.prototype.toString.call(callback).replace(/\/\*[\s\S]*?\*\/|\/\/.*$/gm, ' ');
3
+ const parameters = source.match(/^(?:async\s+)?([^\s()=]+)\s*=>/)?.[1] ?? source.slice(source.indexOf('(') + 1, source.indexOf(')'));
4
+ return parameters.replace(/=[^,]+/g, '').match(/[^\s,]+/g) ?? [];
5
+ }
@@ -20,7 +20,11 @@ export declare class JsonRpcWebsocket {
20
20
  constructor(url: string, requestTimeoutMs: number, onError?: ErrorCallback | undefined);
21
21
  open(): Promise<Event>;
22
22
  close(): Promise<CloseEvent>;
23
- on(methodName: string, callback: (...args: any[]) => any): void;
23
+ /**
24
+ * Registers a callback, optionally declaring its public parameter names in argument order.
25
+ * Explicit names also determine the expected argument count, independently of the callback's signature.
26
+ */
27
+ on(methodName: string, callback: (...args: any[]) => any, parameterNames?: readonly string[]): void;
24
28
  off(methodName: string): void;
25
29
  call(method: string, params?: unknown): Promise<JsonRpcResponse>;
26
30
  notify(method: string, params?: unknown): void;
@@ -1,4 +1,4 @@
1
- import getParameterNames from 'get-parameter-names';
1
+ import { getParameterNames } from './get-parameter-names';
2
2
  import { DeferredPromise } from './deferred-promise';
3
3
  import { JsonRpcErrorCodes } from './jsonrpc.model';
4
4
  export var WebsocketReadyStates;
@@ -35,9 +35,20 @@ export class JsonRpcWebsocket {
35
35
  this.websocket = undefined;
36
36
  return this.closeDeferredPromise.asPromise();
37
37
  }
38
+ /**
39
+ * Registers a callback, optionally declaring its public parameter names in argument order.
40
+ * Explicit names also determine the expected argument count, independently of the callback's signature.
41
+ */
42
+ on(methodName,
38
43
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
- on(methodName, callback) {
40
- this.rpcMethods[methodName.toLowerCase()] = callback; // case-insensitive!
44
+ callback, parameterNames) {
45
+ if (parameterNames && new Set(parameterNames).size !== parameterNames.length) {
46
+ throw new Error('Parameter names must be unique');
47
+ }
48
+ this.rpcMethods[methodName.toLowerCase()] = {
49
+ callback,
50
+ parameterNames: parameterNames?.slice(),
51
+ }; // case-insensitive!
41
52
  }
42
53
  off(methodName) {
43
54
  delete this.rpcMethods[methodName.toLowerCase()]; // case-insensitive!
@@ -77,8 +88,7 @@ export class JsonRpcWebsocket {
77
88
  this.websocket.send(JSON.stringify(request));
78
89
  }
79
90
  catch (e) {
80
- // istanbul ignore next
81
- throw Error(this.getErrorMessage(e));
91
+ throw Error(this.getErrorMessage(e), { cause: e });
82
92
  }
83
93
  }
84
94
  createWebsocket() {
@@ -134,12 +144,11 @@ export class JsonRpcWebsocket {
134
144
  this.websocket.send(JSON.stringify(response));
135
145
  }
136
146
  catch (e) {
137
- // istanbul ignore next
138
- throw Error(this.getErrorMessage(e));
147
+ throw Error(this.getErrorMessage(e), { cause: e });
139
148
  }
140
149
  }
141
150
  handleMessage(msg) {
142
- let data = null;
151
+ let data;
143
152
  try {
144
153
  data = JSON.parse(msg);
145
154
  }
@@ -170,7 +179,7 @@ export class JsonRpcWebsocket {
170
179
  this.handleError(JsonRpcErrorCodes.METHOD_NOT_FOUND, `Method '${request.method}' was not found`, request.id);
171
180
  return;
172
181
  }
173
- let requestParams = [];
182
+ let requestParams;
174
183
  try {
175
184
  requestParams = this.getRequestParams(method, request);
176
185
  }
@@ -179,7 +188,8 @@ export class JsonRpcWebsocket {
179
188
  return;
180
189
  }
181
190
  try {
182
- const result = method(...requestParams);
191
+ const { callback } = method;
192
+ const result = callback(...requestParams);
183
193
  if (request.id) {
184
194
  this.respondOk(request.id, result);
185
195
  }
@@ -190,32 +200,32 @@ export class JsonRpcWebsocket {
190
200
  }
191
201
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
192
202
  getRequestParams(method, request) {
193
- let requestParams = [];
194
- if (request.params) {
195
- if (request.params instanceof Array) {
196
- if (method.length !== request.params.length) {
197
- throw new Error(`Invalid parameters. Method '${request.method}' expects ${method.length} parameters, but got ${request.params.length}`);
198
- }
199
- requestParams = request.params;
200
- }
201
- else if (request.params instanceof Object) {
202
- const parameterNames = getParameterNames(method);
203
- if (method.length !== Object.keys(request.params).length) {
204
- throw new Error(`Invalid parameters. Method '${request.method}' expects parameters [${parameterNames}], but got [${Object.keys(request.params)}]`);
205
- }
206
- parameterNames.forEach((paramName) => {
207
- const paramValue = request.params[paramName];
208
- if (paramValue === undefined) {
209
- throw new Error(`Invalid parameters. Method '${request.method}' expects parameters [${parameterNames}], but got [${Object.keys(request.params)}]`);
210
- }
211
- requestParams.push(paramValue);
212
- });
203
+ if (!request.params && method.parameterNames === undefined) {
204
+ return [];
205
+ }
206
+ const params = request.params === undefined ? [] : request.params;
207
+ const parameterCount = method.parameterNames?.length ?? method.callback.length;
208
+ if (params instanceof Array) {
209
+ if (parameterCount !== params.length) {
210
+ throw new Error(`Invalid parameters. Method '${request.method}' expects ${parameterCount} parameters, but got ${params.length}`);
213
211
  }
214
- else {
215
- throw new Error(`Invalid parameters. Expected array or object, but got ${typeof request.params}`);
212
+ return params;
213
+ }
214
+ if (params instanceof Object) {
215
+ const parameterNames = method.parameterNames ?? getParameterNames(method.callback);
216
+ const requestParameterNames = Object.keys(params);
217
+ const invalidParametersMessage = `Invalid parameters. Method '${request.method}' expects parameters [${parameterNames}], but got [${requestParameterNames}]`;
218
+ if (parameterCount !== requestParameterNames.length) {
219
+ throw new Error(invalidParametersMessage);
216
220
  }
221
+ return parameterNames.map((paramName) => {
222
+ if (!this.hasProperty(params, paramName)) {
223
+ throw new Error(invalidParametersMessage);
224
+ }
225
+ return params[paramName];
226
+ });
217
227
  }
218
- return requestParams;
228
+ throw new Error(`Invalid parameters. Expected array or object, but got ${typeof params}`);
219
229
  }
220
230
  handleResponse(response) {
221
231
  const activeRequest = this.pendingRequests[response.id];
@@ -293,7 +303,22 @@ export class JsonRpcWebsocket {
293
303
  return !!Object.prototype.hasOwnProperty.call(object, propertyName);
294
304
  }
295
305
  getErrorMessage(error) {
296
- // istanbul ignore next
297
- return error instanceof Error ? error?.message : JSON.stringify(error);
306
+ try {
307
+ if (typeof error === 'object' && error !== null && 'message' in error) {
308
+ const message = error.message;
309
+ if (typeof message === 'string') {
310
+ return message;
311
+ }
312
+ }
313
+ return JSON.stringify(error) ?? String(error);
314
+ }
315
+ catch {
316
+ try {
317
+ return String(error);
318
+ }
319
+ catch {
320
+ return 'Unknown error';
321
+ }
322
+ }
298
323
  }
299
324
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsonrpc-client-websocket",
3
- "version": "1.5.5",
3
+ "version": "1.6.0",
4
4
  "description": "A simple JSON RPC 2.0 websocket client",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -8,17 +8,18 @@
8
8
  "lib/**/*"
9
9
  ],
10
10
  "scripts": {
11
- "test": "jest",
12
- "coverage": "jest --config jest.config.ts --coverage",
11
+ "test": "tsc --project tsconfig.spec.json && jest",
12
+ "coverage": "npm test -- --coverage",
13
13
  "build": "tsc",
14
14
  "format": "prettier --write \"src/**/*.ts\"",
15
- "lint": "tsc --noEmit && eslint . --fix",
15
+ "format:check": "prettier --check \"src/**/*.ts\"",
16
+ "lint": "tsc --noEmit && eslint .",
16
17
  "prepublishOnly": "npm test && npm run lint",
17
18
  "prepare": "npm run build",
18
19
  "preversion": "npm run lint",
19
20
  "version": "npm run format && git add -A src",
20
21
  "postversion": "git push && git push --tags",
21
- "update-all": "npm-check-updates --upgrade"
22
+ "update-all": "npm-check-updates --upgrade --peer"
22
23
  },
23
24
  "repository": {
24
25
  "type": "git",
@@ -38,25 +39,19 @@
38
39
  "author": "Fabricio Bastian",
39
40
  "license": "MIT",
40
41
  "devDependencies": {
41
- "@eslint/eslintrc": "^3.1.0",
42
- "@eslint/js": "^9.9.0",
43
- "@types/jest": "^29.5.12",
44
- "@typescript-eslint/eslint-plugin": "^8.1.0",
45
- "@typescript-eslint/parser": "^8.1.0",
46
- "eslint": "^9.9.0",
47
- "eslint-config-prettier": "^9.1.0",
48
- "eslint-plugin-jest": "^28.8.0",
49
- "jest": "^29.7.0",
50
- "jest-environment-jsdom": "^29.7.0",
42
+ "@eslint/js": "^10.0.1",
43
+ "@types/jest": "^30.0.0",
44
+ "eslint": "^10.14.0",
45
+ "eslint-config-prettier": "^10.1.8",
46
+ "eslint-plugin-jest": "^29.17.0",
47
+ "jest": "^30.5.2",
48
+ "jest-environment-jsdom": "^30.5.2",
51
49
  "jest-websocket-mock": "^2.5.0",
52
50
  "mock-socket": "^9.3.1",
53
- "npm-check-updates": "^17.0.6",
54
- "prettier": "^3.3.3",
55
- "ts-jest": "^29.2.4",
56
- "ts-node": "^10.9.2",
57
- "typescript": "^5.5.4"
58
- },
59
- "dependencies": {
60
- "get-parameter-names": "^0.3.0"
51
+ "npm-check-updates": "^23.1.1",
52
+ "prettier": "^3.9.10",
53
+ "ts-jest": "^29.4.15",
54
+ "typescript": "~6.0.3",
55
+ "typescript-eslint": "^8.72.1"
61
56
  }
62
57
  }