libshv-js 3.11.0 → 3.12.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libshv-js",
3
- "version": "3.11.0",
3
+ "version": "3.12.0",
4
4
  "description": "Typescript implementation of libshv",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -31,12 +31,12 @@
31
31
  "zod": "^3.25.67"
32
32
  },
33
33
  "devDependencies": {
34
- "@typescript-eslint/parser": "^8.27.0",
35
- "eslint": "^9.22.0",
36
- "eslint-config-xo": "^0.46.0",
34
+ "@typescript-eslint/parser": "^8.35.0",
35
+ "eslint": "^9.29.0",
36
+ "eslint-config-xo": "^0.47.0",
37
37
  "eslint-formatter-unix": "^8.40.0",
38
- "eslint-plugin-unicorn": "^57.0.0",
39
- "typescript": "^5.8.2",
40
- "typescript-eslint": "^8.27.0"
38
+ "eslint-plugin-unicorn": "^59.0.1",
39
+ "typescript": "^5.8.3",
40
+ "typescript-eslint": "^8.35.0"
41
41
  }
42
42
  }
package/src/rpcmessage.ts CHANGED
@@ -9,6 +9,7 @@ export const RPC_MESSAGE_PARAMS = 1;
9
9
  export const RPC_MESSAGE_RESULT = 2;
10
10
  export const RPC_MESSAGE_ERROR = 3;
11
11
  export const RPC_MESSAGE_DELAY = 4;
12
+ export const RPC_MESSAGE_ABORT = 5;
12
13
 
13
14
  export const ERROR_CODE = 1;
14
15
  export const ERROR_MESSAGE = 2;
@@ -46,7 +47,9 @@ const RpcRequestMetaZod = z.metamap({
46
47
 
47
48
  const RpcRequestValueZod = z.imap({
48
49
  [RPC_MESSAGE_PARAMS]: z.rpcvalue().optional(),
49
- });
50
+ }).or(z.imap({
51
+ [RPC_MESSAGE_ABORT]: z.boolean(),
52
+ }));
50
53
 
51
54
  const RpcResponseMetaZod = z.metamap({
52
55
  [RPC_MESSAGE_CALLER_IDS]: z.int().or(z.array(z.int())).optional(),
package/src/rpcvalue.ts CHANGED
@@ -137,7 +137,9 @@ const makeMap = <T extends Record<string, RpcValue> = Record<string, RpcValue>,
137
137
  });
138
138
 
139
139
  class RpcValueWithMetaData<MetaSchema extends MetaMap = MetaMap, ValueSchema extends RpcValueType = RpcValueType> {
140
- constructor(public meta: MetaSchema, public value: ValueSchema) {}
140
+ constructor(public meta: MetaSchema, public value: ValueSchema) {
141
+ /* nothing */
142
+ }
141
143
  }
142
144
 
143
145
  export type RpcValue = RpcValueType | RpcValueWithMetaData;
package/src/ws-client.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import {ChainPackReader, CHAINPACK_PROTOCOL_TYPE, ChainPackWriter, toChainPack} from './chainpack';
2
2
  import {type CponReader, CPON_PROTOCOL_TYPE, toCpon} from './cpon';
3
- import {ERROR_MESSAGE, ErrorCode, ERROR_CODE, RpcMessageZod, type RpcMessage, isSignal, isRequest, type RpcRequest, isResponse, ERROR_DATA, type ErrorMap, RPC_MESSAGE_METHOD, RPC_MESSAGE_SHV_PATH, RPC_MESSAGE_REQUEST_ID, RPC_MESSAGE_PARAMS, RPC_MESSAGE_ERROR, RPC_MESSAGE_RESULT, RPC_MESSAGE_CALLER_IDS, RpcResponseValue, RPC_MESSAGE_DELAY} from './rpcmessage';
3
+ import {ERROR_MESSAGE, ErrorCode, ERROR_CODE, RpcMessageZod, type RpcMessage, isSignal, isRequest, type RpcRequest, isResponse, ERROR_DATA, type ErrorMap, RPC_MESSAGE_METHOD, RPC_MESSAGE_SHV_PATH, RPC_MESSAGE_REQUEST_ID, RPC_MESSAGE_PARAMS, RPC_MESSAGE_ERROR, RPC_MESSAGE_RESULT, RPC_MESSAGE_CALLER_IDS, RpcResponseValue, RPC_MESSAGE_DELAY, RPC_MESSAGE_ABORT, RpcSignal, RpcResponse} from './rpcmessage';
4
4
  import {type RpcValue, type Null, type Int, type IMap, type ShvMap, makeMap, makeIMap, RpcValueWithMetaData, makeMetaMap, Double} from './rpcvalue';
5
5
 
6
6
  const DEFAULT_TIMEOUT = 5000;
@@ -50,6 +50,16 @@ type WsClientOptionsCommon = {
50
50
  wsUri: string;
51
51
  };
52
52
 
53
+ export class RequestHandler {
54
+ constructor(public options: {
55
+ result: Promise<RpcValue>;
56
+ onAbort?: () => void;
57
+ onProgressQuery?: () => number | Promise<number>;
58
+ }) {
59
+ /* nothing */
60
+ }
61
+ }
62
+
53
63
  type WsClientOptionsLogin = WsClientOptionsCommon & {
54
64
  mountPoint?: string;
55
65
  login: Login;
@@ -58,7 +68,7 @@ type WsClientOptionsLogin = WsClientOptionsCommon & {
58
68
  onConnected: () => void;
59
69
  onConnectionFailure: (error: Error) => void;
60
70
  onDisconnected: () => void;
61
- onRequest: (shvPath: string, method: string, param: RpcValue, delay: (progress: number) => void) => RpcValue | Promise<RpcValue>;
71
+ onRequest: (shvPath: string, method: string, param: RpcValue, delay: (progress: number) => void) => RpcValue | Promise<RpcValue> | RequestHandler;
62
72
  };
63
73
 
64
74
  type WsClientOptionsWorkflows = WsClientOptionsCommon & {
@@ -137,13 +147,30 @@ export const UserIDRequired = createErrorClass(ErrorCode.UserIDRequired);
137
147
  export const NotImplemented = createErrorClass(ErrorCode.NotImplemented);
138
148
  /* eslint-enable @typescript-eslint/naming-convention */
139
149
 
150
+ const makeTimeout = (ms: number, resolve: RpcResponseResolver) => globalThis.setTimeout(() => {
151
+ resolve(new MethodCallTimeout(makeIMap({
152
+ [ERROR_CODE]: ErrorCode.MethodCallTimeout,
153
+ [ERROR_MESSAGE]: `Shv call timeout after: ${ms} msec.`,
154
+ [ERROR_DATA]: undefined,
155
+ })));
156
+ }, ms);
157
+
158
+ class RpcRequestPromise<Result> extends Promise<Result> {
159
+ constructor(executor: (resolve: (value: Result | PromiseLike<Result>) => void, reject: (reason?: unknown) => void) => void, public readonly abort: () => void, public readonly progressQuery: () => void) {
160
+ super(executor);
161
+ }
162
+ }
163
+
140
164
  class WsClient {
141
165
  private requestId = 1;
142
166
  private pingTimerId: ReturnType<typeof globalThis.setInterval> | undefined = undefined;
143
167
  private rpcHandlers: Array<{
144
168
  resolve: RpcResponseResolver;
169
+ delayCallback?: (progress: number) => void;
145
170
  timeout_handle: ReturnType<typeof globalThis.setTimeout>;
146
171
  }> = [];
172
+ private requestHandlers: RequestHandler[] = [];
173
+ private abortedRequests = new Set<number>();
147
174
  private readonly subscriptions: Subscription[] = [];
148
175
  private readonly websocket: WebSocket;
149
176
  private readonly options: WsClientOptions;
@@ -160,105 +187,166 @@ class WsClient {
160
187
  this.websocket = new WebSocket(options.wsUri);
161
188
  this.websocket.binaryType = 'arraybuffer';
162
189
 
163
- this.websocket.addEventListener('message', async (evt: MessageEvent<ArrayBuffer>) => {
164
- const rpcVal = dataToRpcValue(evt.data);
190
+ const handleSignal = (signal: RpcSignal) => {
191
+ for (const sub of this.subscriptions) {
192
+ const shvPath = signal.meta[RPC_MESSAGE_SHV_PATH];
193
+ const method = signal.meta[RPC_MESSAGE_METHOD];
165
194
 
166
- const rpcMsg = RpcMessageZod.parse(rpcVal);
167
- this.logDebug(`message received: ${toCpon(rpcMsg)}`);
195
+ if (shvPath.startsWith(sub.path) && method === sub.method) {
196
+ sub.callback(shvPath, method, signal.value[RPC_MESSAGE_PARAMS]);
197
+ }
198
+ }
199
+ };
200
+
201
+ const handleRequest = async (request: RpcRequest) => {
202
+ if ('onRequest' in this.options) {
203
+ const requestId = request.meta[RPC_MESSAGE_REQUEST_ID];
204
+ const respond = (value: RpcResponseValue) => {
205
+ this.sendRpcMessage(new RpcValueWithMetaData(
206
+ makeMetaMap({
207
+ [RPC_MESSAGE_CALLER_IDS]: request.meta[RPC_MESSAGE_CALLER_IDS],
208
+ [RPC_MESSAGE_REQUEST_ID]: requestId,
209
+ }),
210
+ value,
211
+ ));
212
+ };
168
213
 
169
- if (isSignal(rpcMsg)) {
170
- for (const sub of this.subscriptions) {
171
- const shvPath = rpcMsg.meta[RPC_MESSAGE_SHV_PATH];
172
- const method = rpcMsg.meta[RPC_MESSAGE_METHOD];
214
+ const sendDelay = (progress: number) => {
215
+ respond(makeIMap({
216
+ [RPC_MESSAGE_DELAY]: new Double(progress),
217
+ }));
218
+ };
219
+
220
+ if (RPC_MESSAGE_ABORT in request.value) {
221
+ const requestHandler = this.requestHandlers[requestId];
173
222
 
174
- if (shvPath.startsWith(sub.path) && method === sub.method) {
175
- sub.callback(shvPath, method, rpcMsg.value[RPC_MESSAGE_PARAMS]);
223
+ if (request.value[RPC_MESSAGE_ABORT] === true && requestHandler?.options.onAbort !== undefined) {
224
+ this.abortedRequests.add(requestId);
225
+ requestHandler.options.onAbort();
176
226
  }
227
+
228
+ if (request.value[RPC_MESSAGE_ABORT] === false && requestHandler?.options.onProgressQuery !== undefined) {
229
+ const progress = requestHandler.options.onProgressQuery();
230
+ sendDelay(progress instanceof Promise ? await progress : progress);
231
+ }
232
+
233
+ return;
177
234
  }
178
- } else if (isRequest(rpcMsg)) {
179
- if ('onRequest' in this.options) {
180
- const respond = (value: RpcResponseValue) => {
181
- this.sendRpcMessage(new RpcValueWithMetaData(
182
- makeMetaMap({
183
- [RPC_MESSAGE_CALLER_IDS]: rpcMsg.meta[RPC_MESSAGE_CALLER_IDS],
184
- [RPC_MESSAGE_REQUEST_ID]: rpcMsg.meta[RPC_MESSAGE_REQUEST_ID],
185
- }),
186
- value,
187
- ));
188
- };
189
235
 
190
- try {
191
- const sendDelay = (progress: number) => {
192
- respond(makeIMap({
193
- [RPC_MESSAGE_DELAY]: new Double(progress),
194
- }));
195
- };
236
+ try {
237
+ let result = this.options.onRequest(request.meta[RPC_MESSAGE_SHV_PATH], request.meta[RPC_MESSAGE_METHOD], request.value[RPC_MESSAGE_PARAMS], sendDelay);
238
+ if (result instanceof RequestHandler) {
239
+ this.requestHandlers[requestId] = result;
240
+ result = result.options.result;
241
+ }
242
+
243
+ const resultOrError = makeIMap({
244
+ [RPC_MESSAGE_RESULT]: result instanceof Promise ? await result : result,
245
+ });
246
+
247
+ if (!this.abortedRequests.has(requestId)) {
248
+ respond(resultOrError);
249
+ } else {
250
+ this.abortedRequests.delete(requestId);
251
+ }
196
252
 
197
- const result = this.options.onRequest(rpcMsg.meta[RPC_MESSAGE_SHV_PATH], rpcMsg.meta[RPC_MESSAGE_METHOD], rpcMsg.value[RPC_MESSAGE_PARAMS], sendDelay);
253
+ if (requestId in this.requestHandlers) {
254
+ // eslint-disable-next-line @typescript-eslint/no-array-delete
255
+ delete this.requestHandlers[requestId];
256
+ }
257
+ } catch (error: unknown) {
258
+ const sendError = (error: RpcError) => {
198
259
  respond(makeIMap({
199
- [RPC_MESSAGE_RESULT]: result instanceof Promise ? await result : result,
260
+ [RPC_MESSAGE_ERROR]: error.err_info,
200
261
  }));
201
- } catch (error: unknown) {
202
- const sendError = (error: RpcError) => {
203
- respond(makeIMap({
204
- [RPC_MESSAGE_ERROR]: error.err_info,
205
- }));
206
- };
207
-
208
- switch (true) {
209
- case error instanceof RpcError:
210
- sendError(error);
211
- break;
212
- case error instanceof Error:
213
- sendError(new InternalError(error.message));
214
- break;
215
- default:
216
- sendError(new InternalError('Unknown error'));
217
- break;
218
- }
262
+ };
263
+
264
+ switch (true) {
265
+ case error instanceof RpcError:
266
+ sendError(error);
267
+ break;
268
+ case error instanceof Error:
269
+ sendError(new InternalError(error.message));
270
+ break;
271
+ default:
272
+ sendError(new InternalError('Unknown error'));
273
+ break;
219
274
  }
220
275
  }
221
- } else if (isResponse(rpcMsg)) {
222
- const requestId = rpcMsg.meta[RPC_MESSAGE_REQUEST_ID];
223
-
224
- if (this.rpcHandlers[Number(requestId)] !== undefined) {
225
- const handler = this.rpcHandlers[Number(requestId)];
226
- clearTimeout(handler.timeout_handle);
227
- handler.resolve((() => {
228
- if (RPC_MESSAGE_ERROR in rpcMsg.value) {
229
- const code = rpcMsg.value[RPC_MESSAGE_ERROR][ERROR_CODE] as unknown;
230
- // eslint-disable-next-line @typescript-eslint/naming-convention
231
- const ErrorTypeCtor = (() => {
232
- switch (code) {
233
- case ErrorCode.InvalidRequest: return InvalidRequest;
234
- case ErrorCode.MethodNotFound: return MethodNotFound;
235
- case ErrorCode.InvalidParams: return InvalidParams;
236
- case ErrorCode.InternalError: return InternalError;
237
- case ErrorCode.ParseError: return ParseError;
238
- case ErrorCode.MethodCallTimeout: return MethodCallTimeout;
239
- case ErrorCode.MethodCallCancelled: return MethodCallCancelled;
240
- case ErrorCode.MethodCallException: return MethodCallException;
241
- case ErrorCode.Unknown: return Unknown;
242
- case ErrorCode.LoginRequired: return LoginRequired;
243
- case ErrorCode.UserIDRequired: return UserIDRequired;
244
- case ErrorCode.NotImplemented: return NotImplemented;
245
- default: return Unknown;
246
- }
247
- })();
248
-
249
- return new ErrorTypeCtor(rpcMsg.value[RPC_MESSAGE_ERROR]);
250
- }
251
-
252
- if (RPC_MESSAGE_RESULT in rpcMsg.value) {
253
- return rpcMsg.value[RPC_MESSAGE_RESULT];
254
- }
255
-
256
- // If both result and error are missing, the result is implicitly Null.
257
- // https://silicon-heaven.github.io/shv-doc/rpcmessage.html#response
258
- })());
259
- // eslint-disable-next-line @typescript-eslint/no-array-delete
260
- delete this.rpcHandlers[Number(requestId)];
276
+ }
277
+ };
278
+
279
+ const handleResponse = (response: RpcResponse) => {
280
+ const requestId = response.meta[RPC_MESSAGE_REQUEST_ID];
281
+
282
+ if (this.rpcHandlers[Number(requestId)] !== undefined) {
283
+ const handler = this.rpcHandlers[Number(requestId)];
284
+ clearTimeout(handler.timeout_handle);
285
+
286
+ if (RPC_MESSAGE_DELAY in response.value) {
287
+ if (handler.delayCallback !== undefined) {
288
+ handler.delayCallback(response.value[RPC_MESSAGE_DELAY].value);
289
+ }
290
+
291
+ handler.timeout_handle = makeTimeout(this.timeout, handler.resolve);
292
+ return;
261
293
  }
294
+
295
+ handler.resolve((() => {
296
+ if (RPC_MESSAGE_ERROR in response.value) {
297
+ const code = response.value[RPC_MESSAGE_ERROR][ERROR_CODE] as unknown;
298
+ // eslint-disable-next-line @typescript-eslint/naming-convention
299
+ const ErrorTypeCtor = (() => {
300
+ switch (code) {
301
+ case ErrorCode.InvalidRequest: return InvalidRequest;
302
+ case ErrorCode.MethodNotFound: return MethodNotFound;
303
+ case ErrorCode.InvalidParams: return InvalidParams;
304
+ case ErrorCode.InternalError: return InternalError;
305
+ case ErrorCode.ParseError: return ParseError;
306
+ case ErrorCode.MethodCallTimeout: return MethodCallTimeout;
307
+ case ErrorCode.MethodCallCancelled: return MethodCallCancelled;
308
+ case ErrorCode.MethodCallException: return MethodCallException;
309
+ case ErrorCode.Unknown: return Unknown;
310
+ case ErrorCode.LoginRequired: return LoginRequired;
311
+ case ErrorCode.UserIDRequired: return UserIDRequired;
312
+ case ErrorCode.NotImplemented: return NotImplemented;
313
+ default: return Unknown;
314
+ }
315
+ })();
316
+
317
+ return new ErrorTypeCtor(response.value[RPC_MESSAGE_ERROR]);
318
+ }
319
+
320
+ if (RPC_MESSAGE_RESULT in response.value) {
321
+ return response.value[RPC_MESSAGE_RESULT];
322
+ }
323
+
324
+ // At this point, the RpcResponse either has:
325
+ // - no keys, which means that the response has an implicit Null result.
326
+ // - has some unknown keys, in which case we will produce an error for the client.
327
+ // https://silicon-heaven.github.io/shv-doc/rpcmessage.html#response
328
+ const rpcMessageKeys = Object.keys(response.value);
329
+ if (rpcMessageKeys.length !== 0) {
330
+ return new NotImplemented(`Got an RpcResponse with unsupported keys: ${rpcMessageKeys.join(' ')}`);
331
+ }
332
+ })());
333
+ // eslint-disable-next-line @typescript-eslint/no-array-delete
334
+ delete this.rpcHandlers[Number(requestId)];
335
+ }
336
+ };
337
+
338
+ this.websocket.addEventListener('message', async (evt: MessageEvent<ArrayBuffer>) => {
339
+ const rpcVal = dataToRpcValue(evt.data);
340
+
341
+ const rpcMsg = RpcMessageZod.parse(rpcVal);
342
+ this.logDebug(`message received: ${toCpon(rpcMsg)}`);
343
+
344
+ if (isSignal(rpcMsg)) {
345
+ handleSignal(rpcMsg);
346
+ } else if (isRequest(rpcMsg)) {
347
+ await handleRequest(rpcMsg);
348
+ } else if (isResponse(rpcMsg)) {
349
+ handleResponse(rpcMsg);
262
350
  }
263
351
  });
264
352
 
@@ -274,31 +362,34 @@ class WsClient {
274
362
  }
275
363
  }
276
364
 
277
- callRpcMethod(shvPath: '.broker/currentClient', method: 'accessGrantForMethodCall', params: [string, string]): Promise<ResultOrError<string>>;
278
- callRpcMethod(shvPath: string | undefined, method: 'dir', params?: RpcValue): Promise<ResultOrError<DirResult>>;
279
- callRpcMethod(shvPath: string | undefined, method: 'ls', params?: RpcValue): Promise<ResultOrError<LsResult>>;
280
- callRpcMethod(shvPath: string | undefined, method: string, params?: RpcValue): Promise<ResultOrError>;
281
- callRpcMethod(shvPath: string | undefined, method: string, params?: RpcValue): Promise<ResultOrError> {
365
+ callRpcMethod(shvPath: '.broker/currentClient', method: 'accessGrantForMethodCall', params: [string, string], delayCallback?: (progress: number) => void): RpcRequestPromise<ResultOrError<string>>;
366
+ callRpcMethod(shvPath: string | undefined, method: 'dir', params?: RpcValue, delayCallback?: (progress: number) => void): RpcRequestPromise<ResultOrError<DirResult>>;
367
+ callRpcMethod(shvPath: string | undefined, method: 'ls', params?: RpcValue, delayCallback?: (progress: number) => void): RpcRequestPromise<ResultOrError<LsResult>>;
368
+ callRpcMethod(shvPath: string | undefined, method: string, params?: RpcValue, delayCallback?: (progress: number) => void): RpcRequestPromise<ResultOrError>;
369
+ callRpcMethod(shvPath: string | undefined, method: string, params?: RpcValue, delayCallback?: (progress: number) => void): RpcRequestPromise<ResultOrError> {
282
370
  const rqId = this.requestId++;
283
- const rq: RpcRequest = new RpcValueWithMetaData(makeMetaMap({
371
+ const makeRq = (value: IMap) => new RpcValueWithMetaData(makeMetaMap({
284
372
  [RPC_MESSAGE_CALLER_IDS]: undefined,
285
373
  [RPC_MESSAGE_REQUEST_ID]: rqId,
286
374
  [RPC_MESSAGE_METHOD]: method ?? '',
287
375
  [RPC_MESSAGE_SHV_PATH]: shvPath ?? '',
288
- }), makeIMap({
376
+ }), value);
377
+ const rq: RpcRequest = makeRq(makeIMap({
289
378
  [RPC_MESSAGE_PARAMS]: params,
290
379
  }));
291
380
 
292
381
  this.sendRpcMessage(rq);
293
382
 
294
- const promise = new Promise<ResultOrError>(resolve => {
295
- this.rpcHandlers[rqId] = {resolve, timeout_handle: globalThis.setTimeout(() => {
296
- resolve(new MethodCallTimeout(makeIMap({
297
- [ERROR_CODE]: ErrorCode.MethodCallTimeout,
298
- [ERROR_MESSAGE]: `Shv call timeout after: ${this.timeout} msec.`,
299
- [ERROR_DATA]: undefined,
300
- })));
301
- }, this.timeout)};
383
+ const promise = new RpcRequestPromise<ResultOrError>(resolve => {
384
+ this.rpcHandlers[rqId] = {resolve, timeout_handle: makeTimeout(this.timeout, resolve), delayCallback};
385
+ }, () => {
386
+ this.sendRpcMessage(makeRq(makeIMap({
387
+ [RPC_MESSAGE_ABORT]: true,
388
+ })));
389
+ }, () => {
390
+ this.sendRpcMessage(makeRq(makeIMap({
391
+ [RPC_MESSAGE_ABORT]: false,
392
+ })));
302
393
  });
303
394
 
304
395
  return promise;