jsonrpc-client-websocket 1.5.4 → 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
+ }
@@ -17,10 +17,14 @@ export declare class JsonRpcWebsocket {
17
17
  private pendingRequests;
18
18
  private rpcMethods;
19
19
  get state(): WebsocketReadyStates;
20
- constructor(url: string, requestTimeoutMs: number, onError?: ErrorCallback);
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;
@@ -38,4 +42,5 @@ export declare class JsonRpcWebsocket {
38
42
  private callOnError;
39
43
  private getRequestId;
40
44
  private hasProperty;
45
+ private getErrorMessage;
41
46
  }
@@ -1,4 +1,4 @@
1
- import * as 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!
@@ -57,7 +68,10 @@ export class JsonRpcWebsocket {
57
68
  }
58
69
  catch (e) {
59
70
  // istanbul ignore next
60
- return Promise.reject({ code: JsonRpcErrorCodes.INTERNAL_ERROR, message: `Internal error. ${e}` });
71
+ return Promise.reject({
72
+ code: JsonRpcErrorCodes.INTERNAL_ERROR,
73
+ message: `Internal error. ${this.getErrorMessage(e)}`,
74
+ });
61
75
  }
62
76
  return this.createPendingRequest(request);
63
77
  }
@@ -74,8 +88,7 @@ export class JsonRpcWebsocket {
74
88
  this.websocket.send(JSON.stringify(request));
75
89
  }
76
90
  catch (e) {
77
- // istanbul ignore next
78
- throw Error(e);
91
+ throw Error(this.getErrorMessage(e), { cause: e });
79
92
  }
80
93
  }
81
94
  createWebsocket() {
@@ -131,12 +144,11 @@ export class JsonRpcWebsocket {
131
144
  this.websocket.send(JSON.stringify(response));
132
145
  }
133
146
  catch (e) {
134
- // istanbul ignore next
135
- throw Error(e);
147
+ throw Error(this.getErrorMessage(e), { cause: e });
136
148
  }
137
149
  }
138
150
  handleMessage(msg) {
139
- let data = null;
151
+ let data;
140
152
  try {
141
153
  data = JSON.parse(msg);
142
154
  }
@@ -167,52 +179,53 @@ export class JsonRpcWebsocket {
167
179
  this.handleError(JsonRpcErrorCodes.METHOD_NOT_FOUND, `Method '${request.method}' was not found`, request.id);
168
180
  return;
169
181
  }
170
- let requestParams = [];
182
+ let requestParams;
171
183
  try {
172
184
  requestParams = this.getRequestParams(method, request);
173
185
  }
174
186
  catch (error) {
175
- this.handleError(JsonRpcErrorCodes.INVALID_PARAMS, error.message, request.id);
187
+ this.handleError(JsonRpcErrorCodes.INVALID_PARAMS, this.getErrorMessage(error), request.id);
176
188
  return;
177
189
  }
178
190
  try {
179
- const result = method(...requestParams);
191
+ const { callback } = method;
192
+ const result = callback(...requestParams);
180
193
  if (request.id) {
181
194
  this.respondOk(request.id, result);
182
195
  }
183
196
  }
184
197
  catch (error) {
185
- this.handleError(JsonRpcErrorCodes.REQUEST_FAILED, `Method '${request.method}' has thrown: '${error.message}'`, request.id);
198
+ this.handleError(JsonRpcErrorCodes.REQUEST_FAILED, `Method '${request.method}' has thrown: '${this.getErrorMessage(error)}'`, request.id);
186
199
  }
187
200
  }
188
201
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
202
  getRequestParams(method, request) {
190
- let requestParams = [];
191
- if (request.params) {
192
- if (request.params instanceof Array) {
193
- if (method.length !== request.params.length) {
194
- throw new Error(`Invalid parameters. Method '${request.method}' expects ${method.length} parameters, but got ${request.params.length}`);
195
- }
196
- requestParams = request.params;
197
- }
198
- else if (request.params instanceof Object) {
199
- const parameterNames = getParameterNames(method);
200
- if (method.length !== Object.keys(request.params).length) {
201
- throw new Error(`Invalid parameters. Method '${request.method}' expects parameters [${parameterNames}], but got [${Object.keys(request.params)}]`);
202
- }
203
- parameterNames.forEach((paramName) => {
204
- const paramValue = request.params[paramName];
205
- if (paramValue === undefined) {
206
- throw new Error(`Invalid parameters. Method '${request.method}' expects parameters [${parameterNames}], but got [${Object.keys(request.params)}]`);
207
- }
208
- requestParams.push(paramValue);
209
- });
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}`);
210
211
  }
211
- else {
212
- 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);
213
220
  }
221
+ return parameterNames.map((paramName) => {
222
+ if (!this.hasProperty(params, paramName)) {
223
+ throw new Error(invalidParametersMessage);
224
+ }
225
+ return params[paramName];
226
+ });
214
227
  }
215
- return requestParams;
228
+ throw new Error(`Invalid parameters. Expected array or object, but got ${typeof params}`);
216
229
  }
217
230
  handleResponse(response) {
218
231
  const activeRequest = this.pendingRequests[response.id];
@@ -289,4 +302,23 @@ export class JsonRpcWebsocket {
289
302
  hasProperty(object, propertyName) {
290
303
  return !!Object.prototype.hasOwnProperty.call(object, propertyName);
291
304
  }
305
+ getErrorMessage(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
+ }
323
+ }
292
324
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jsonrpc-client-websocket",
3
- "version": "1.5.4",
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 . --ext .js,.jsx,.ts,.tsx --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
- "updateAll": "npm-check-updates --upgrade"
22
+ "update-all": "npm-check-updates --upgrade --peer"
22
23
  },
23
24
  "repository": {
24
25
  "type": "git",
@@ -38,23 +39,19 @@
38
39
  "author": "Fabricio Bastian",
39
40
  "license": "MIT",
40
41
  "devDependencies": {
41
- "@types/jest": "^29.5.1",
42
- "@typescript-eslint/eslint-plugin": "^5.59.2",
43
- "@typescript-eslint/parser": "^5.59.2",
44
- "eslint": "^8.39.0",
45
- "eslint-config-prettier": "^8.8.0",
46
- "eslint-plugin-jest": "^27.2.1",
47
- "jest": "^29.5.0",
48
- "jest-environment-jsdom": "^29.5.0",
49
- "jest-websocket-mock": "^2.4.0",
50
- "mock-socket": "^9.2.1",
51
- "npm-check-updates": "^16.10.9",
52
- "prettier": "^2.8.8",
53
- "ts-jest": "^29.1.0",
54
- "ts-node": "^10.9.1",
55
- "typescript": "^5.0.4"
56
- },
57
- "dependencies": {
58
- "get-parameter-names": "^0.3.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",
49
+ "jest-websocket-mock": "^2.5.0",
50
+ "mock-socket": "^9.3.1",
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"
59
56
  }
60
57
  }