libshv-js 3.11.1 → 3.12.1

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.1",
3
+ "version": "3.12.1",
4
4
  "description": "Typescript implementation of libshv",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
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/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 & {
@@ -96,7 +106,25 @@ type DirResult = Array<IMap<{
96
106
 
97
107
  class RpcError extends Error {
98
108
  constructor(readonly err_info: ErrorMap) {
99
- super(err_info[ERROR_MESSAGE] ?? 'Unknown RpcError');
109
+ const errorCodeToMessage = (code: ErrorMap[typeof ERROR_CODE]) => {
110
+ switch (code) {
111
+ case ErrorCode.InvalidRequest: return 'InvalidRequest';
112
+ case ErrorCode.MethodNotFound: return 'MethodNotFound';
113
+ case ErrorCode.InvalidParams: return 'InvalidParams';
114
+ case ErrorCode.InternalError: return 'InternalError';
115
+ case ErrorCode.ParseError: return 'ParseError';
116
+ case ErrorCode.MethodCallTimeout: return 'MethodCallTimeout';
117
+ case ErrorCode.MethodCallCancelled: return 'MethodCallCancelled';
118
+ case ErrorCode.MethodCallException: return 'MethodCallException';
119
+ case ErrorCode.Unknown: return 'Unknown';
120
+ case ErrorCode.LoginRequired: return 'LoginRequired';
121
+ case ErrorCode.UserIDRequired: return 'UserIDRequired';
122
+ case ErrorCode.NotImplemented: return 'NotImplemented';
123
+ default: return 'Unknown RpcError';
124
+ }
125
+ };
126
+
127
+ super(err_info[ERROR_MESSAGE] ?? `${errorCodeToMessage(err_info[ERROR_CODE])} (${err_info[ERROR_CODE]})`);
100
128
  }
101
129
 
102
130
  data() {
@@ -137,13 +165,30 @@ export const UserIDRequired = createErrorClass(ErrorCode.UserIDRequired);
137
165
  export const NotImplemented = createErrorClass(ErrorCode.NotImplemented);
138
166
  /* eslint-enable @typescript-eslint/naming-convention */
139
167
 
168
+ const makeTimeout = (ms: number, resolve: RpcResponseResolver) => globalThis.setTimeout(() => {
169
+ resolve(new MethodCallTimeout(makeIMap({
170
+ [ERROR_CODE]: ErrorCode.MethodCallTimeout,
171
+ [ERROR_MESSAGE]: `Shv call timeout after: ${ms} msec.`,
172
+ [ERROR_DATA]: undefined,
173
+ })));
174
+ }, ms);
175
+
176
+ class RpcRequestPromise<Result> extends Promise<Result> {
177
+ constructor(executor: (resolve: (value: Result | PromiseLike<Result>) => void, reject: (reason?: unknown) => void) => void, public readonly abort: () => void, public readonly progressQuery: () => void) {
178
+ super(executor);
179
+ }
180
+ }
181
+
140
182
  class WsClient {
141
183
  private requestId = 1;
142
184
  private pingTimerId: ReturnType<typeof globalThis.setInterval> | undefined = undefined;
143
185
  private rpcHandlers: Array<{
144
186
  resolve: RpcResponseResolver;
187
+ delayCallback?: (progress: number) => void;
145
188
  timeout_handle: ReturnType<typeof globalThis.setTimeout>;
146
189
  }> = [];
190
+ private requestHandlers: RequestHandler[] = [];
191
+ private abortedRequests = new Set<number>();
147
192
  private readonly subscriptions: Subscription[] = [];
148
193
  private readonly websocket: WebSocket;
149
194
  private readonly options: WsClientOptions;
@@ -160,105 +205,166 @@ class WsClient {
160
205
  this.websocket = new WebSocket(options.wsUri);
161
206
  this.websocket.binaryType = 'arraybuffer';
162
207
 
163
- this.websocket.addEventListener('message', async (evt: MessageEvent<ArrayBuffer>) => {
164
- const rpcVal = dataToRpcValue(evt.data);
208
+ const handleSignal = (signal: RpcSignal) => {
209
+ for (const sub of this.subscriptions) {
210
+ const shvPath = signal.meta[RPC_MESSAGE_SHV_PATH];
211
+ const method = signal.meta[RPC_MESSAGE_METHOD];
165
212
 
166
- const rpcMsg = RpcMessageZod.parse(rpcVal);
167
- this.logDebug(`message received: ${toCpon(rpcMsg)}`);
213
+ if (shvPath.startsWith(sub.path) && method === sub.method) {
214
+ sub.callback(shvPath, method, signal.value[RPC_MESSAGE_PARAMS]);
215
+ }
216
+ }
217
+ };
218
+
219
+ const handleRequest = async (request: RpcRequest) => {
220
+ if ('onRequest' in this.options) {
221
+ const requestId = request.meta[RPC_MESSAGE_REQUEST_ID];
222
+ const respond = (value: RpcResponseValue) => {
223
+ this.sendRpcMessage(new RpcValueWithMetaData(
224
+ makeMetaMap({
225
+ [RPC_MESSAGE_CALLER_IDS]: request.meta[RPC_MESSAGE_CALLER_IDS],
226
+ [RPC_MESSAGE_REQUEST_ID]: requestId,
227
+ }),
228
+ value,
229
+ ));
230
+ };
168
231
 
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];
232
+ const sendDelay = (progress: number) => {
233
+ respond(makeIMap({
234
+ [RPC_MESSAGE_DELAY]: new Double(progress),
235
+ }));
236
+ };
237
+
238
+ if (RPC_MESSAGE_ABORT in request.value) {
239
+ const requestHandler = this.requestHandlers[requestId];
240
+
241
+ if (request.value[RPC_MESSAGE_ABORT] === true && requestHandler?.options.onAbort !== undefined) {
242
+ this.abortedRequests.add(requestId);
243
+ requestHandler.options.onAbort();
244
+ }
173
245
 
174
- if (shvPath.startsWith(sub.path) && method === sub.method) {
175
- sub.callback(shvPath, method, rpcMsg.value[RPC_MESSAGE_PARAMS]);
246
+ if (request.value[RPC_MESSAGE_ABORT] === false && requestHandler?.options.onProgressQuery !== undefined) {
247
+ const progress = requestHandler.options.onProgressQuery();
248
+ sendDelay(progress instanceof Promise ? await progress : progress);
176
249
  }
250
+
251
+ return;
177
252
  }
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
253
 
190
- try {
191
- const sendDelay = (progress: number) => {
192
- respond(makeIMap({
193
- [RPC_MESSAGE_DELAY]: new Double(progress),
194
- }));
195
- };
254
+ try {
255
+ let result = this.options.onRequest(request.meta[RPC_MESSAGE_SHV_PATH], request.meta[RPC_MESSAGE_METHOD], request.value[RPC_MESSAGE_PARAMS], sendDelay);
256
+ if (result instanceof RequestHandler) {
257
+ this.requestHandlers[requestId] = result;
258
+ result = result.options.result;
259
+ }
260
+
261
+ const resultOrError = makeIMap({
262
+ [RPC_MESSAGE_RESULT]: result instanceof Promise ? await result : result,
263
+ });
264
+
265
+ if (!this.abortedRequests.has(requestId)) {
266
+ respond(resultOrError);
267
+ } else {
268
+ this.abortedRequests.delete(requestId);
269
+ }
196
270
 
197
- const result = this.options.onRequest(rpcMsg.meta[RPC_MESSAGE_SHV_PATH], rpcMsg.meta[RPC_MESSAGE_METHOD], rpcMsg.value[RPC_MESSAGE_PARAMS], sendDelay);
271
+ if (requestId in this.requestHandlers) {
272
+ // eslint-disable-next-line @typescript-eslint/no-array-delete
273
+ delete this.requestHandlers[requestId];
274
+ }
275
+ } catch (error: unknown) {
276
+ const sendError = (error: RpcError) => {
198
277
  respond(makeIMap({
199
- [RPC_MESSAGE_RESULT]: result instanceof Promise ? await result : result,
278
+ [RPC_MESSAGE_ERROR]: error.err_info,
200
279
  }));
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
- }
280
+ };
281
+
282
+ switch (true) {
283
+ case error instanceof RpcError:
284
+ sendError(error);
285
+ break;
286
+ case error instanceof Error:
287
+ sendError(new InternalError(error.message));
288
+ break;
289
+ default:
290
+ sendError(new InternalError('Unknown error'));
291
+ break;
219
292
  }
220
293
  }
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)];
294
+ }
295
+ };
296
+
297
+ const handleResponse = (response: RpcResponse) => {
298
+ const requestId = response.meta[RPC_MESSAGE_REQUEST_ID];
299
+
300
+ if (this.rpcHandlers[Number(requestId)] !== undefined) {
301
+ const handler = this.rpcHandlers[Number(requestId)];
302
+ clearTimeout(handler.timeout_handle);
303
+
304
+ if (RPC_MESSAGE_DELAY in response.value) {
305
+ if (handler.delayCallback !== undefined) {
306
+ handler.delayCallback(response.value[RPC_MESSAGE_DELAY].value);
307
+ }
308
+
309
+ handler.timeout_handle = makeTimeout(this.timeout, handler.resolve);
310
+ return;
261
311
  }
312
+
313
+ handler.resolve((() => {
314
+ if (RPC_MESSAGE_ERROR in response.value) {
315
+ const code = response.value[RPC_MESSAGE_ERROR][ERROR_CODE] as unknown;
316
+ // eslint-disable-next-line @typescript-eslint/naming-convention
317
+ const ErrorTypeCtor = (() => {
318
+ switch (code) {
319
+ case ErrorCode.InvalidRequest: return InvalidRequest;
320
+ case ErrorCode.MethodNotFound: return MethodNotFound;
321
+ case ErrorCode.InvalidParams: return InvalidParams;
322
+ case ErrorCode.InternalError: return InternalError;
323
+ case ErrorCode.ParseError: return ParseError;
324
+ case ErrorCode.MethodCallTimeout: return MethodCallTimeout;
325
+ case ErrorCode.MethodCallCancelled: return MethodCallCancelled;
326
+ case ErrorCode.MethodCallException: return MethodCallException;
327
+ case ErrorCode.Unknown: return Unknown;
328
+ case ErrorCode.LoginRequired: return LoginRequired;
329
+ case ErrorCode.UserIDRequired: return UserIDRequired;
330
+ case ErrorCode.NotImplemented: return NotImplemented;
331
+ default: return Unknown;
332
+ }
333
+ })();
334
+
335
+ return new ErrorTypeCtor(response.value[RPC_MESSAGE_ERROR]);
336
+ }
337
+
338
+ if (RPC_MESSAGE_RESULT in response.value) {
339
+ return response.value[RPC_MESSAGE_RESULT];
340
+ }
341
+
342
+ // At this point, the RpcResponse either has:
343
+ // - no keys, which means that the response has an implicit Null result.
344
+ // - has some unknown keys, in which case we will produce an error for the client.
345
+ // https://silicon-heaven.github.io/shv-doc/rpcmessage.html#response
346
+ const rpcMessageKeys = Object.keys(response.value);
347
+ if (rpcMessageKeys.length !== 0) {
348
+ return new NotImplemented(`Got an RpcResponse with unsupported keys: ${rpcMessageKeys.join(' ')}`);
349
+ }
350
+ })());
351
+ // eslint-disable-next-line @typescript-eslint/no-array-delete
352
+ delete this.rpcHandlers[Number(requestId)];
353
+ }
354
+ };
355
+
356
+ this.websocket.addEventListener('message', async (evt: MessageEvent<ArrayBuffer>) => {
357
+ const rpcVal = dataToRpcValue(evt.data);
358
+
359
+ const rpcMsg = RpcMessageZod.parse(rpcVal);
360
+ this.logDebug(`message received: ${toCpon(rpcMsg)}`);
361
+
362
+ if (isSignal(rpcMsg)) {
363
+ handleSignal(rpcMsg);
364
+ } else if (isRequest(rpcMsg)) {
365
+ await handleRequest(rpcMsg);
366
+ } else if (isResponse(rpcMsg)) {
367
+ handleResponse(rpcMsg);
262
368
  }
263
369
  });
264
370
 
@@ -274,31 +380,34 @@ class WsClient {
274
380
  }
275
381
  }
276
382
 
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> {
383
+ callRpcMethod(shvPath: '.broker/currentClient', method: 'accessGrantForMethodCall', params: [string, string], delayCallback?: (progress: number) => void): RpcRequestPromise<ResultOrError<string>>;
384
+ callRpcMethod(shvPath: string | undefined, method: 'dir', params?: RpcValue, delayCallback?: (progress: number) => void): RpcRequestPromise<ResultOrError<DirResult>>;
385
+ callRpcMethod(shvPath: string | undefined, method: 'ls', params?: RpcValue, delayCallback?: (progress: number) => void): RpcRequestPromise<ResultOrError<LsResult>>;
386
+ callRpcMethod(shvPath: string | undefined, method: string, params?: RpcValue, delayCallback?: (progress: number) => void): RpcRequestPromise<ResultOrError>;
387
+ callRpcMethod(shvPath: string | undefined, method: string, params?: RpcValue, delayCallback?: (progress: number) => void): RpcRequestPromise<ResultOrError> {
282
388
  const rqId = this.requestId++;
283
- const rq: RpcRequest = new RpcValueWithMetaData(makeMetaMap({
389
+ const makeRq = (value: IMap) => new RpcValueWithMetaData(makeMetaMap({
284
390
  [RPC_MESSAGE_CALLER_IDS]: undefined,
285
391
  [RPC_MESSAGE_REQUEST_ID]: rqId,
286
392
  [RPC_MESSAGE_METHOD]: method ?? '',
287
393
  [RPC_MESSAGE_SHV_PATH]: shvPath ?? '',
288
- }), makeIMap({
394
+ }), value);
395
+ const rq: RpcRequest = makeRq(makeIMap({
289
396
  [RPC_MESSAGE_PARAMS]: params,
290
397
  }));
291
398
 
292
399
  this.sendRpcMessage(rq);
293
400
 
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)};
401
+ const promise = new RpcRequestPromise<ResultOrError>(resolve => {
402
+ this.rpcHandlers[rqId] = {resolve, timeout_handle: makeTimeout(this.timeout, resolve), delayCallback};
403
+ }, () => {
404
+ this.sendRpcMessage(makeRq(makeIMap({
405
+ [RPC_MESSAGE_ABORT]: true,
406
+ })));
407
+ }, () => {
408
+ this.sendRpcMessage(makeRq(makeIMap({
409
+ [RPC_MESSAGE_ABORT]: false,
410
+ })));
302
411
  });
303
412
 
304
413
  return promise;