libshv-js 3.16.1 → 3.17.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/src/ws-client.ts DELETED
@@ -1,708 +0,0 @@
1
- import {accessLevelFromAccessString} from './access';
2
- import {ChainPackReader, CHAINPACK_PROTOCOL_TYPE, ChainPackWriter, toChainPack} from './chainpack';
3
- import {type CponReader, CPON_PROTOCOL_TYPE, toCpon} from './cpon';
4
- 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, RPC_MESSAGE_ACCESS_LEVEL} from './rpcmessage';
5
- import {type RpcValue, type Null, type Int, type IMap, type ShvMap, makeMap, makeIMap, RpcValueWithMetaData, makeMetaMap, Double} from './rpcvalue';
6
- import * as z from './zod';
7
-
8
- const DEFAULT_TIMEOUT = 5000;
9
- const DEFAULT_PING_INTERVAL = 30 * 1000;
10
-
11
- const dataToRpcValue = (buff: ArrayBuffer) => {
12
- const rd: ChainPackReader | CponReader = new ChainPackReader(buff);
13
- rd.readUIntData();
14
- const proto = rd.ctx.getByte();
15
- switch (proto) {
16
- case CHAINPACK_PROTOCOL_TYPE:
17
- break;
18
- case CPON_PROTOCOL_TYPE:
19
- throw new Error('CPON protocol type is not supported anymore');
20
- default:
21
- throw new Error(`Unsupported protocol type ${proto}`);
22
- }
23
-
24
- const rpcVal = rd.read();
25
- return rpcVal;
26
- };
27
-
28
- type SubscriptionCallback = (path: string, method: string, param?: RpcValue) => void;
29
-
30
- type ResultOrError<T = RpcValue> = T | Error;
31
-
32
- type RpcResponseResolver = (rpc_msg: ResultOrError) => void;
33
-
34
- type Subscription = {
35
- subscriber: string;
36
- path: string;
37
- method: string;
38
- callback: SubscriptionCallback;
39
- };
40
-
41
- type Login = {
42
- type: 'PLAIN';
43
- user: string;
44
- password: string;
45
- } | {
46
- type: 'TOKEN';
47
- token: string;
48
- };
49
-
50
- type WsClientOptionsCommon = {
51
- logDebug: (...args: string[]) => void;
52
- wsUri: string;
53
- };
54
-
55
- export class RequestHandler {
56
- constructor(public options: {
57
- result: Promise<RpcValue>;
58
- onAbort?: () => void;
59
- onProgressQuery?: () => number | Promise<number>;
60
- }) {
61
- /* nothing */
62
- }
63
- }
64
-
65
- export const LsParamZod = z.undefined().or(z.string());
66
- export const DirParamZod = z.undefined().or(z.boolean()).or(z.string());
67
-
68
- export type DirEntry = IMap<{
69
- [DIR_NAME]: string;
70
- [DIR_FLAGS]: number;
71
- [DIR_PARAM]: string | Null;
72
- [DIR_RESULT]: string | Null;
73
- [DIR_ACCESS]: Int;
74
- [DIR_SIGNALS]: ShvMap<Record<string, string | Null>>;
75
- [DIR_EXTRA]: ShvMap;
76
- }>;
77
-
78
- export type DirResult = DirEntry[];
79
-
80
- export type MethodHandler = {
81
- paramParser: z.ZodType<RpcValue>,
82
- handler: (shvPath: string, method: string, params: RpcValue, delay: (progress: number) => void) => Promise<RpcValue> | RpcValue | RequestHandler,
83
- };
84
-
85
- export type WsClientOptionsLogin = WsClientOptionsCommon & {
86
- mountPoint?: string;
87
- login: Login;
88
- timeout?: number;
89
- pingInterval?: number;
90
- onConnected: () => void;
91
- onConnectionFailure: (error: Error) => void;
92
- onDisconnected: () => void;
93
- onRequest: (shvPath: string) => {
94
- ls: (shvPath: string) => string[] | Promise<string[]>;
95
- dirEntries: Array<{entry: DirEntry} & MethodHandler>;
96
- } | undefined;
97
- };
98
-
99
- type WsClientOptionsWorkflows = WsClientOptionsCommon & {
100
- onWorkflows: (workflows: RpcValue) => void;
101
- onWorkflowsFailed: () => void;
102
- };
103
-
104
- type WsClientOptions = WsClientOptionsLogin | WsClientOptionsWorkflows;
105
-
106
- type LsResult = string[];
107
- export enum DirFlags {
108
- Reserved = 1,
109
- Getter = 2,
110
- Setter = 4,
111
- LargeResultHint = 8,
112
- NotIdempotent = 16,
113
- RequiresClientId = 32,
114
- }
115
- export const DIR_NAME = 1;
116
- export const DIR_FLAGS = 2;
117
- export const DIR_PARAM = 3;
118
- export const DIR_RESULT = 4;
119
- export const DIR_ACCESS = 5;
120
- export const DIR_SIGNALS = 6;
121
- export const DIR_EXTRA = 63;
122
-
123
- class RpcError extends Error {
124
- constructor(readonly err_info: ErrorMap) {
125
- const errorCodeToMessage = (code: ErrorMap[typeof ERROR_CODE]) => {
126
- switch (code) {
127
- case ErrorCode.InvalidRequest: return 'InvalidRequest';
128
- case ErrorCode.MethodNotFound: return 'MethodNotFound';
129
- case ErrorCode.InvalidParams: return 'InvalidParams';
130
- case ErrorCode.InternalError: return 'InternalError';
131
- case ErrorCode.ParseError: return 'ParseError';
132
- case ErrorCode.MethodCallTimeout: return 'MethodCallTimeout';
133
- case ErrorCode.MethodCallCancelled: return 'MethodCallCancelled';
134
- case ErrorCode.MethodCallException: return 'MethodCallException';
135
- case ErrorCode.Unknown: return 'Unknown';
136
- case ErrorCode.LoginRequired: return 'LoginRequired';
137
- case ErrorCode.UserIDRequired: return 'UserIDRequired';
138
- case ErrorCode.NotImplemented: return 'NotImplemented';
139
- default: return 'Unknown RpcError';
140
- }
141
- };
142
-
143
- super(err_info[ERROR_MESSAGE] ?? `${errorCodeToMessage(err_info[ERROR_CODE])} (${err_info[ERROR_CODE]})`);
144
- }
145
-
146
- data() {
147
- return this.err_info[ERROR_DATA];
148
- }
149
- }
150
-
151
- const makeErrInfo = (errInfoOrMsg: ErrorMap | string, code: ErrorCode) => {
152
- if (typeof errInfoOrMsg === 'object') {
153
- return errInfoOrMsg;
154
- }
155
-
156
- return makeIMap({
157
- [ERROR_CODE]: code,
158
- [ERROR_MESSAGE]: errInfoOrMsg,
159
- [ERROR_DATA]: undefined,
160
- });
161
- };
162
-
163
- const createErrorClass = (code: ErrorCode) => class extends RpcError {
164
- constructor(errInfoOrMsg: ErrorMap | string) {
165
- super(makeErrInfo(errInfoOrMsg, code));
166
- }
167
- };
168
-
169
- /* eslint-disable @typescript-eslint/naming-convention */
170
- export const InvalidRequest = createErrorClass(ErrorCode.InvalidRequest);
171
- export const MethodNotFound = createErrorClass(ErrorCode.MethodNotFound);
172
- export const InvalidParams = createErrorClass(ErrorCode.InvalidParams);
173
- export const InternalError = createErrorClass(ErrorCode.InternalError);
174
- export const ParseError = createErrorClass(ErrorCode.ParseError);
175
- export const MethodCallTimeout = createErrorClass(ErrorCode.MethodCallTimeout);
176
- export const MethodCallCancelled = createErrorClass(ErrorCode.MethodCallCancelled);
177
- export const MethodCallException = createErrorClass(ErrorCode.MethodCallException);
178
- export const Unknown = createErrorClass(ErrorCode.Unknown);
179
- export const LoginRequired = createErrorClass(ErrorCode.LoginRequired);
180
- export const UserIDRequired = createErrorClass(ErrorCode.UserIDRequired);
181
- export const NotImplemented = createErrorClass(ErrorCode.NotImplemented);
182
- /* eslint-enable @typescript-eslint/naming-convention */
183
-
184
- const makeTimeout = (ms: number, resolve: RpcResponseResolver) => globalThis.setTimeout(() => {
185
- resolve(new MethodCallTimeout(makeIMap({
186
- [ERROR_CODE]: ErrorCode.MethodCallTimeout,
187
- [ERROR_MESSAGE]: `Shv call timeout after: ${ms} msec.`,
188
- [ERROR_DATA]: undefined,
189
- })));
190
- }, ms);
191
-
192
- class RpcRequestPromise<Result> extends Promise<Result> {
193
- constructor(executor: (resolve: (value: Result | PromiseLike<Result>) => void, reject: (reason?: unknown) => void) => void, public readonly abort: () => void, public readonly progressQuery: () => void) {
194
- super(executor);
195
- }
196
- }
197
-
198
- class WsClient {
199
- private requestId = 1;
200
- private pingTimerId: ReturnType<typeof globalThis.setInterval> | undefined = undefined;
201
- private rpcHandlers: Array<{
202
- resolve: RpcResponseResolver;
203
- delayCallback?: (progress: number) => void;
204
- timeout_handle: ReturnType<typeof globalThis.setTimeout>;
205
- }> = [];
206
- private requestHandlers = new Map<number, RequestHandler>();
207
- private abortedRequests = new Set<number>();
208
- private readonly subscriptions: Subscription[] = [];
209
- private readonly websocket: WebSocket;
210
- private readonly options: WsClientOptions;
211
- private readonly timeout: number;
212
-
213
- constructor(options: WsClientOptions) {
214
- if (typeof options !== 'object') {
215
- throw new TypeError('No options object supplied');
216
- }
217
-
218
- this.options = options;
219
- this.timeout = 'timeout' in options && options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT;
220
-
221
- this.websocket = new WebSocket(options.wsUri);
222
- this.websocket.binaryType = 'arraybuffer';
223
-
224
- const handleSignal = (signal: RpcSignal) => {
225
- for (const sub of this.subscriptions) {
226
- const shvPath = signal.meta[RPC_MESSAGE_SHV_PATH];
227
- const method = signal.meta[RPC_MESSAGE_METHOD];
228
-
229
- if (shvPath.startsWith(sub.path) && method === sub.method) {
230
- sub.callback(shvPath, method, signal.value[RPC_MESSAGE_PARAMS]);
231
- }
232
- }
233
- };
234
-
235
- // eslint-disable-next-line complexity
236
- const handleRequest = async (request: RpcRequest) => {
237
- if ('onRequest' in this.options) {
238
- const requestId = request.meta[RPC_MESSAGE_REQUEST_ID];
239
- const respond = (value: RpcResponseValue) => {
240
- this.sendRpcMessage(new RpcValueWithMetaData(
241
- makeMetaMap({
242
- [RPC_MESSAGE_CALLER_IDS]: request.meta[RPC_MESSAGE_CALLER_IDS],
243
- [RPC_MESSAGE_REQUEST_ID]: requestId,
244
- }),
245
- value,
246
- ));
247
- };
248
-
249
- const sendDelay = (progress: number) => {
250
- respond(makeIMap({
251
- [RPC_MESSAGE_DELAY]: new Double(progress),
252
- }));
253
- };
254
-
255
- if (RPC_MESSAGE_ABORT in request.value) {
256
- const requestHandler = this.requestHandlers.get(requestId);
257
-
258
- if (request.value[RPC_MESSAGE_ABORT] === true) {
259
- this.abortedRequests.add(requestId);
260
-
261
- if (requestHandler?.options.onAbort !== undefined) {
262
- requestHandler.options.onAbort();
263
- }
264
- }
265
-
266
- if (request.value[RPC_MESSAGE_ABORT] === false && requestHandler?.options.onProgressQuery !== undefined) {
267
- const progress = requestHandler.options.onProgressQuery();
268
- sendDelay(progress instanceof Promise ? await progress : progress);
269
- }
270
-
271
- return;
272
- }
273
-
274
- try {
275
- const shvPath = request.meta[RPC_MESSAGE_SHV_PATH];
276
- const methodName = request.meta[RPC_MESSAGE_METHOD];
277
- const unhandledMethod = () => {
278
- throw new MethodNotFound(`No such path '${shvPath}' or method '${methodName}' or insufficient access rights.`);
279
- };
280
-
281
- const accessLevel = request.meta[RPC_MESSAGE_ACCESS_LEVEL];
282
- if (accessLevel === undefined) {
283
- unhandledMethod();
284
- return;
285
- }
286
-
287
- const methods = this.options.onRequest(shvPath);
288
- if (methods === undefined) {
289
- unhandledMethod();
290
- return;
291
- }
292
-
293
- const methodDefinition = ((): {entry: DirEntry} & MethodHandler | undefined => {
294
- const dirEntry = makeIMap({
295
- [DIR_NAME]: 'dir',
296
- [DIR_FLAGS]: 0,
297
- [DIR_PARAM]: 'DirParam',
298
- [DIR_RESULT]: 'DirResult',
299
- [DIR_ACCESS]: accessLevelFromAccessString('bws'),
300
- [DIR_SIGNALS]: makeMap({}),
301
- [DIR_EXTRA]: makeMap({}),
302
- });
303
- const lsEntry = makeIMap({
304
- [DIR_NAME]: 'ls',
305
- [DIR_FLAGS]: 0,
306
- [DIR_PARAM]: 'LsParam',
307
- [DIR_RESULT]: 'LsResult',
308
- [DIR_ACCESS]: accessLevelFromAccessString('bws'),
309
- [DIR_SIGNALS]: makeMap({}),
310
- [DIR_EXTRA]: makeMap({}),
311
- });
312
- switch (methodName) {
313
- case 'dir':
314
- return {
315
- entry: dirEntry,
316
- paramParser: DirParamZod,
317
- handler(_shvPath, _method, param) {
318
- const methodsResult = [dirEntry, lsEntry, ...methods.dirEntries.map(method => method.entry)];
319
- switch (true) {
320
- case typeof param === 'string':
321
- return methodsResult.find(method => method[DIR_NAME] === param) ?? false;
322
- case typeof param === 'boolean':
323
- case param === undefined:
324
- console.log('methodsResult', '=', methodsResult);
325
- return methodsResult;
326
- }
327
- },
328
- };
329
- case 'ls':
330
- return {
331
- entry: lsEntry,
332
- paramParser: LsParamZod,
333
- handler(shvPath) {
334
- return methods.ls(shvPath);
335
- },
336
- };
337
- default:
338
- return methods.dirEntries.find(method => method.entry[DIR_NAME] === methodName);
339
- }
340
- })();
341
-
342
- if (methodDefinition === undefined) {
343
- unhandledMethod();
344
- return;
345
- }
346
-
347
- if (accessLevel < methodDefinition.entry[DIR_ACCESS]) {
348
- unhandledMethod();
349
- return;
350
- }
351
-
352
- const requestParam = request.value[RPC_MESSAGE_PARAMS];
353
- const parsedParam = methodDefinition.paramParser.safeParse(requestParam);
354
- if (!parsedParam.success) {
355
- throw new InvalidParams(`Unexpected param for method ${methodName} on path '${shvPath}': ${toCpon(requestParam)}`);
356
- }
357
-
358
- let result = methodDefinition.handler(
359
- shvPath,
360
- methodName,
361
- parsedParam.data,
362
- sendDelay,
363
- );
364
- if (result instanceof RequestHandler) {
365
- this.requestHandlers.set(requestId, result);
366
- result = result.options.result;
367
- }
368
-
369
- const resultOrError = makeIMap({
370
- [RPC_MESSAGE_RESULT]: result instanceof Promise ? await result : result,
371
- });
372
-
373
- if (!this.abortedRequests.has(requestId)) {
374
- respond(resultOrError);
375
- } else {
376
- this.abortedRequests.delete(requestId);
377
- }
378
-
379
- if (requestId in this.requestHandlers) {
380
- this.requestHandlers.delete(requestId);
381
- }
382
- } catch (error: unknown) {
383
- const sendError = (error: RpcError) => {
384
- respond(makeIMap({
385
- [RPC_MESSAGE_ERROR]: error.err_info,
386
- }));
387
- };
388
-
389
- switch (true) {
390
- case error instanceof RpcError:
391
- sendError(error);
392
- break;
393
- case error instanceof Error:
394
- sendError(new InternalError(error.message));
395
- break;
396
- default:
397
- sendError(new InternalError('Unknown error'));
398
- break;
399
- }
400
- }
401
- }
402
- };
403
-
404
- const handleResponse = (response: RpcResponse) => {
405
- const requestId = response.meta[RPC_MESSAGE_REQUEST_ID];
406
-
407
- if (this.rpcHandlers[Number(requestId)] !== undefined) {
408
- const handler = this.rpcHandlers[Number(requestId)];
409
- clearTimeout(handler.timeout_handle);
410
-
411
- if (RPC_MESSAGE_DELAY in response.value) {
412
- if (handler.delayCallback !== undefined) {
413
- handler.delayCallback(response.value[RPC_MESSAGE_DELAY].value);
414
- }
415
-
416
- handler.timeout_handle = makeTimeout(this.timeout, handler.resolve);
417
- return;
418
- }
419
-
420
- handler.resolve((() => {
421
- if (RPC_MESSAGE_ERROR in response.value) {
422
- const code = response.value[RPC_MESSAGE_ERROR][ERROR_CODE] as unknown;
423
- // eslint-disable-next-line @typescript-eslint/naming-convention
424
- const ErrorTypeCtor = (() => {
425
- switch (code) {
426
- case ErrorCode.InvalidRequest: return InvalidRequest;
427
- case ErrorCode.MethodNotFound: return MethodNotFound;
428
- case ErrorCode.InvalidParams: return InvalidParams;
429
- case ErrorCode.InternalError: return InternalError;
430
- case ErrorCode.ParseError: return ParseError;
431
- case ErrorCode.MethodCallTimeout: return MethodCallTimeout;
432
- case ErrorCode.MethodCallCancelled: return MethodCallCancelled;
433
- case ErrorCode.MethodCallException: return MethodCallException;
434
- case ErrorCode.Unknown: return Unknown;
435
- case ErrorCode.LoginRequired: return LoginRequired;
436
- case ErrorCode.UserIDRequired: return UserIDRequired;
437
- case ErrorCode.NotImplemented: return NotImplemented;
438
- default: return Unknown;
439
- }
440
- })();
441
-
442
- return new ErrorTypeCtor(response.value[RPC_MESSAGE_ERROR]);
443
- }
444
-
445
- if (RPC_MESSAGE_RESULT in response.value) {
446
- return response.value[RPC_MESSAGE_RESULT];
447
- }
448
-
449
- // At this point, the RpcResponse either has:
450
- // - no keys, which means that the response has an implicit Null result.
451
- // - has some unknown keys, in which case we will produce an error for the client.
452
- // https://silicon-heaven.github.io/shv-doc/rpcmessage.html#response
453
- const rpcMessageKeys = Object.keys(response.value);
454
- if (rpcMessageKeys.length !== 0) {
455
- return new NotImplemented(`Got an RpcResponse with unsupported keys: ${rpcMessageKeys.join(' ')}`);
456
- }
457
- })());
458
- // eslint-disable-next-line @typescript-eslint/no-array-delete
459
- delete this.rpcHandlers[Number(requestId)];
460
- }
461
- };
462
-
463
- this.websocket.addEventListener('message', async (evt: MessageEvent<ArrayBuffer>) => {
464
- const rpcVal = dataToRpcValue(evt.data);
465
-
466
- const rpcMsg = RpcMessageZod.parse(rpcVal);
467
- this.logDebug(`message received: ${toCpon(rpcMsg)}`);
468
-
469
- if (isSignal(rpcMsg)) {
470
- handleSignal(rpcMsg);
471
- } else if (isRequest(rpcMsg)) {
472
- await handleRequest(rpcMsg);
473
- } else if (isResponse(rpcMsg)) {
474
- handleResponse(rpcMsg);
475
- }
476
- });
477
-
478
- this.websocket.addEventListener('error', evt => {
479
- console.log('WebSocket ERROR', evt);
480
- this.logDebug('WebSocket ERROR');
481
- });
482
-
483
- if ('onWorkflows' in this.options) {
484
- this.workflowsProcedure(this.options);
485
- } else {
486
- this.loginProcedure(this.options);
487
- }
488
- }
489
-
490
- callRpcMethod(shvPath: '.broker/currentClient', method: 'accessGrantForMethodCall', params: [string, string], delayCallback?: (progress: number) => void): RpcRequestPromise<ResultOrError<string>>;
491
- callRpcMethod(shvPath: string | undefined, method: 'dir', params?: RpcValue, delayCallback?: (progress: number) => void): RpcRequestPromise<ResultOrError<DirResult>>;
492
- callRpcMethod(shvPath: string | undefined, method: 'ls', params?: RpcValue, delayCallback?: (progress: number) => void): RpcRequestPromise<ResultOrError<LsResult>>;
493
- callRpcMethod(shvPath: string | undefined, method: string, params?: RpcValue, delayCallback?: (progress: number) => void): RpcRequestPromise<ResultOrError>;
494
- callRpcMethod(shvPath: string | undefined, method: string, params?: RpcValue, delayCallback?: (progress: number) => void): RpcRequestPromise<ResultOrError> {
495
- const rqId = this.requestId++;
496
- const makeRq = (value: IMap) => new RpcValueWithMetaData(makeMetaMap({
497
- [RPC_MESSAGE_CALLER_IDS]: undefined,
498
- [RPC_MESSAGE_REQUEST_ID]: rqId,
499
- [RPC_MESSAGE_METHOD]: method ?? '',
500
- [RPC_MESSAGE_SHV_PATH]: shvPath ?? '',
501
- }), value);
502
- const rq: RpcRequest = makeRq(makeIMap({
503
- [RPC_MESSAGE_PARAMS]: params,
504
- }));
505
-
506
- this.sendRpcMessage(rq);
507
-
508
- const promise = new RpcRequestPromise<ResultOrError>(resolve => {
509
- this.rpcHandlers[rqId] = {resolve, timeout_handle: makeTimeout(this.timeout, resolve), delayCallback};
510
- }, () => {
511
- this.sendRpcMessage(makeRq(makeIMap({
512
- [RPC_MESSAGE_ABORT]: true,
513
- })));
514
- }, () => {
515
- this.sendRpcMessage(makeRq(makeIMap({
516
- [RPC_MESSAGE_ABORT]: false,
517
- })));
518
- });
519
-
520
- return promise;
521
- }
522
-
523
- sendRpcMessage(rpcMsg: RpcMessage) {
524
- if (this.websocket.readyState === WebSocket.OPEN) {
525
- this.logDebug('sending rpc message:', toCpon(rpcMsg));
526
- const msgData = new Uint8Array(toChainPack(rpcMsg));
527
-
528
- const wr = new ChainPackWriter();
529
- wr.writeUIntData(msgData.length + 1);
530
- const dgram = new Uint8Array(wr.ctx.length + 1 + msgData.length);
531
- let ix = 0;
532
- for (let i = 0; i < wr.ctx.length; i++) {
533
- dgram[ix++] = wr.ctx.data[i];
534
- }
535
-
536
- dgram[ix++] = CHAINPACK_PROTOCOL_TYPE;
537
-
538
- for (const msgDatum of msgData) {
539
- dgram[ix++] = msgDatum;
540
- }
541
-
542
- this.websocket.send(dgram.buffer);
543
- }
544
- }
545
-
546
- subscribe(subscriber: string, path: string, method: string, callback: SubscriptionCallback) {
547
- if (this.subscriptions.some(val => val.subscriber === subscriber && val.path === path && val.method === method)) {
548
- this.logDebug(`Already subscribed {$path}:${method} for subscriber ${subscriber}`);
549
- return;
550
- }
551
-
552
- // If this path:method has not been subscribed on the broker, do it now
553
- if (!this.subscriptions.some(val => val.path === path && val.method === method)) {
554
- this.callRpcMethod('.broker/app', 'subscribe', makeMap({
555
- method, path,
556
- })).catch(() => {
557
- this.logDebug(`Couldn't subscribe to ${path}, ${method}`);
558
- });
559
- }
560
-
561
- this.subscriptions.push({
562
- subscriber,
563
- path,
564
- method,
565
- callback,
566
- });
567
- }
568
-
569
- unsubscribe(subscriber: string, path: string, method: string) {
570
- const idx = this.subscriptions.findIndex(val => val.subscriber === subscriber && val.path === path && val.method === method);
571
- if (idx === -1) {
572
- this.logDebug(`No such subscription ${path}:${method} for subscriber ${subscriber}`);
573
- return;
574
- }
575
-
576
- this.subscriptions.splice(idx, 1);
577
- // Unsubscribe on the broker only if there are no other subscriptions of this path:method
578
- if (this.subscriptions.some(val => val.path === path && val.method === method)) {
579
- return;
580
- }
581
-
582
- this.callRpcMethod('.broker/app', 'unsubscribe', makeMap({
583
- method, path,
584
- })).catch(() => {
585
- this.logDebug(`Couldn't unsubscribe ${path}, ${method}`);
586
- });
587
- }
588
-
589
- sendPing() {
590
- this.callRpcMethod('.broker/app', 'ping').catch((error: unknown) => {
591
- console.log('Failed to send ping:', error);
592
- });
593
- }
594
-
595
- accessGrantForMethodCall(path: string, method: string) {
596
- return this.callRpcMethod('.broker/currentClient', 'accessGrantForMethodCall', [path, method]);
597
- }
598
-
599
- close() {
600
- this.websocket.close();
601
- }
602
-
603
- private logDebug(...args: string[]) {
604
- if (this.options.logDebug === undefined) {
605
- return;
606
- }
607
-
608
- this.options.logDebug(...args);
609
- }
610
-
611
- private workflowsProcedure(options: WsClientOptionsWorkflows) {
612
- let workflowsObtained = false;
613
- this.websocket.addEventListener('open', () => {
614
- this.options.logDebug('CONNECTED');
615
- const handleConnectionError = (error: Error) => {
616
- this.logDebug('FAILURE: couldn\'t retrieve workflows', error.message);
617
- options.onWorkflowsFailed();
618
- };
619
-
620
- this.callRpcMethod(undefined, 'workflows').then(response => {
621
- if (response instanceof Error) {
622
- handleConnectionError(response);
623
- return;
624
- }
625
-
626
- workflowsObtained = true;
627
- options.onWorkflows(response);
628
-
629
- this.close();
630
- }).catch(() => {
631
- options.onWorkflowsFailed();
632
- });
633
- });
634
-
635
- this.websocket.addEventListener('close', () => {
636
- this.logDebug('DISCONNECTED');
637
- if (!workflowsObtained) {
638
- options.onWorkflowsFailed();
639
- }
640
- });
641
- }
642
-
643
- private loginProcedure(options: WsClientOptionsLogin) {
644
- this.websocket.addEventListener('open', () => {
645
- this.options.logDebug('CONNECTED');
646
- const handleConnectionError = (error: Error) => {
647
- this.logDebug('FAILURE: couldn\'t perform initial handshake', error.message);
648
- options.onConnectionFailure(error);
649
- };
650
-
651
- const handleLoginResponse = (response: unknown) => {
652
- if (response instanceof Error) {
653
- handleConnectionError(response);
654
- return;
655
- }
656
-
657
- this.logDebug('SUCCESS: connected to shv broker');
658
- options.onConnected();
659
- this.pingTimerId = globalThis.setInterval(() => {
660
- this.sendPing();
661
- }, options.pingInterval ?? DEFAULT_PING_INTERVAL);
662
- };
663
-
664
- const makeLoginParams = (loginMap: ShvMap) => makeMap({
665
- login: loginMap,
666
- options: makeMap({
667
- device: typeof options.mountPoint === 'string' ? makeMap({mountPoint: options.mountPoint}) : undefined,
668
- }),
669
- });
670
-
671
- this.callRpcMethod(undefined, 'hello').then(response => {
672
- if (response instanceof Error) {
673
- handleConnectionError(response);
674
- return;
675
- }
676
-
677
- const makeLoginMap = () => {
678
- switch (options.login.type) {
679
- case 'PLAIN':
680
- return makeMap({
681
- password: options.login.password,
682
- type: options.login.type,
683
- user: options.login.user,
684
- });
685
- case 'TOKEN':
686
- return makeMap({
687
- token: options.login.token,
688
- type: options.login.type,
689
- });
690
- }
691
- };
692
-
693
- return this.callRpcMethod(undefined, 'login', makeLoginParams(makeLoginMap()));
694
- }).then(handleLoginResponse).catch(() => {
695
- this.logDebug('FAILURE: couldn\' connected to shv broker');
696
- });
697
- });
698
-
699
- this.websocket.addEventListener('close', () => {
700
- this.logDebug('DISCONNECTED');
701
- this.subscriptions.length = 0;
702
- options.onDisconnected();
703
- globalThis.clearInterval(this.pingTimerId);
704
- });
705
- }
706
- }
707
-
708
- export {WsClient};