libshv-js 3.4.2 → 3.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.
@@ -16,7 +16,7 @@ jobs:
16
16
  name: CPON/Chainpack conversions
17
17
  runs-on: ubuntu-22.04
18
18
  steps:
19
- - uses: actions/checkout@v3
19
+ - uses: actions/checkout@v4
20
20
 
21
21
  - name: Install dependencies
22
22
  run: npm install
@@ -25,4 +25,6 @@ jobs:
25
25
  run: npx tsc
26
26
 
27
27
  - name: Run tests
28
- run: node test.js
28
+ run: |
29
+ node test.js
30
+ node test_json.js
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libshv-js",
3
- "version": "3.4.2",
3
+ "version": "3.6.0",
4
4
  "description": "Typescript implementation of libshv",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",
@@ -21,6 +21,7 @@
21
21
  "./chainpack.ts": "./src/chainpack.ts",
22
22
  "./cpcontext.ts": "./src/cpcontext.ts",
23
23
  "./cpon.ts": "./src/cpon.ts",
24
+ "./json.ts": "./src/json.ts",
24
25
  "./rpcmessage.ts": "./src/rpcmessage.ts",
25
26
  "./rpcvalue.ts": "./src/rpcvalue.ts",
26
27
  "./ws-client.ts": "./src/ws-client.ts",
package/src/cpon.ts CHANGED
@@ -2,6 +2,33 @@
2
2
  import {type RpcValue, type RpcValueType, type DateTime, type List, Decimal, Double, type IMap, type Int, type MetaMap, RpcValueWithMetaData, type ShvMap, UInt, withOffset, shvMapType, isShvMap, isIMap} from './rpcvalue';
3
3
  import {PackContext, UnpackContext} from './cpcontext';
4
4
 
5
+ export const stringifyDate = (dt: DateTime) => {
6
+ const epochMsec = dt.getTime();
7
+ let utcOffset = dt.utc_offset;
8
+ const localMsec = epochMsec + (60_000 * (utcOffset ?? 0));
9
+ let res = new Date(localMsec).toISOString();
10
+ const rtrim = (localMsec % 1000) ? 1 : 5;
11
+ res = res.slice(0, Math.max(0, res.length - rtrim));
12
+
13
+ if (utcOffset === undefined || utcOffset === 0) {
14
+ res += 'Z';
15
+ } else {
16
+ if (utcOffset < 0) {
17
+ res += '-';
18
+ utcOffset = -utcOffset;
19
+ } else {
20
+ res += '+';
21
+ }
22
+
23
+ res += (Math.trunc(utcOffset / 60)).toString().padStart(2, '0');
24
+ if (utcOffset % 60) {
25
+ res += (utcOffset % 60).toString().padStart(2, '0');
26
+ }
27
+ }
28
+
29
+ return res;
30
+ };
31
+
5
32
  const hexify = (byte: number) => {
6
33
  if (byte < 10) {
7
34
  return 48 + byte;
@@ -222,6 +249,25 @@ class CponReader {
222
249
  }
223
250
 
224
251
  readDateTime() {
252
+ this.ctx.getByte(); // eat '"'
253
+ let b = this.ctx.peekByte();
254
+ if (b === '"'.codePointAt(0)) {
255
+ // d"" invalid data time
256
+ this.ctx.getByte();
257
+ throw new TypeError('Malformed empty date separator in DateTime');
258
+ }
259
+
260
+ const date = this.readDateTimeInner();
261
+
262
+ b = this.ctx.getByte();
263
+ if (b !== '"'.codePointAt(0)) {
264
+ throw new TypeError('DateTime literal should be terminated by \'"\'.');
265
+ }
266
+
267
+ return date;
268
+ }
269
+
270
+ readDateTimeInner() {
225
271
  let year = 0;
226
272
  let month = 0;
227
273
  let day = 1;
@@ -231,17 +277,9 @@ class CponReader {
231
277
  let msec = 0;
232
278
  let utcOffset = 0;
233
279
 
234
- this.ctx.getByte(); // eat '"'
235
- let b = this.ctx.peekByte();
236
- if (b === '"'.codePointAt(0)) {
237
- // d"" invalid data time
238
- this.ctx.getByte();
239
- throw new TypeError('Malformed empty date separator in DateTime');
240
- }
241
-
242
280
  year = Number(this.readInt());
243
281
 
244
- b = this.ctx.getByte();
282
+ let b = this.ctx.getByte();
245
283
  if (b !== '-'.codePointAt(0)) {
246
284
  throw new TypeError('Malformed year-month separator in DateTime');
247
285
  }
@@ -307,11 +345,6 @@ class CponReader {
307
345
  }
308
346
  }
309
347
 
310
- b = this.ctx.getByte();
311
- if (b !== '"'.codePointAt(0)) {
312
- throw new TypeError('DateTime literal should be terminated by \'"\'.');
313
- }
314
-
315
348
  // let epoch_sec = CponReader.timegm(year, month, mday, hour, min, sec);
316
349
  let epochMsec = Date.UTC(year, month - 1, day, hour, min, sec, msec);
317
350
  epochMsec -= utcOffset * 60_000;
@@ -775,41 +808,8 @@ class CponWriter {
775
808
  }
776
809
 
777
810
  writeDateTime(dt: DateTime) {
778
- if (!dt) {
779
- this.ctx.writeStringUtf8('d""');
780
- return;
781
- }
782
-
783
- const epochMsec = dt.getTime();
784
- let utcOffset = dt.utc_offset;
785
- const localMsec = epochMsec + (60_000 * (utcOffset ?? 0));
786
- let s = new Date(localMsec).toISOString();
787
- const rtrim = (localMsec % 1000) ? 1 : 5;
788
811
  this.ctx.writeStringUtf8('d"');
789
- for (let i = 0; i < s.length - rtrim; i++) {
790
- this.ctx.putByte(s.codePointAt(i)!);
791
- }
792
-
793
- if (utcOffset === undefined || utcOffset === 0) {
794
- this.ctx.writeStringUtf8('Z');
795
- } else {
796
- if (utcOffset < 0) {
797
- this.ctx.writeStringUtf8('-');
798
- utcOffset = -utcOffset;
799
- } else {
800
- this.ctx.writeStringUtf8('+');
801
- }
802
-
803
- s = (Math.trunc(utcOffset / 60)).toString().padStart(2, '0');
804
- if (utcOffset % 60) {
805
- s += (utcOffset % 60).toString().padStart(2, '0');
806
- }
807
-
808
- for (let i = 0; i < s.length; i++) {
809
- this.ctx.putByte(s.codePointAt(i)!);
810
- }
811
- }
812
-
812
+ this.ctx.writeStringUtf8(stringifyDate(dt));
813
813
  this.ctx.writeStringUtf8('"');
814
814
  }
815
815
 
@@ -835,10 +835,6 @@ class CponWriter {
835
835
  this.doIndentIfNotOneliner(map);
836
836
  let first = true;
837
837
  for (const [key, value] of Object.entries<RpcValue>(map)) {
838
- if (value === undefined) {
839
- continue;
840
- }
841
-
842
838
  if (!first) {
843
839
  this.ctx.putByte(','.codePointAt(0)!);
844
840
  this.doIndentIfNotOneliner(map);
package/src/json.ts ADDED
@@ -0,0 +1,136 @@
1
+ import {UnpackContext} from './cpcontext';
2
+ import {CponReader, stringifyDate} from './cpon';
3
+ import {Decimal, Double, isIMap, isShvMap, makeIMap, makeMap, makeMetaMap, RpcValueWithMetaData, UInt, type RpcValue} from './rpcvalue';
4
+
5
+ // eslint-disable-next-line @typescript-eslint/ban-types -- Zod needs the default argument, otherwise it'll infer as UInt<unknown>
6
+ const decodeMap = (x: object) => makeMap(Object.fromEntries(Object.entries(x).map(([key, value]) => [key, decodeValueJson(value)])));
7
+
8
+ const decodeValueJson = (x: unknown): RpcValue => {
9
+ switch (true) {
10
+ case Array.isArray(x): {
11
+ const list = x as unknown[];
12
+ const handleType = (typeName: unknown, typeValue: unknown) => {
13
+ switch (true) {
14
+ case typeName === 'Blob': {
15
+ if (typeof typeValue !== 'string' || typeValue.length % 2 !== 0) {
16
+ throw new TypeError(`Invalid JSON-encoded Blob: '${JSON.stringify(typeValue)}'`);
17
+ }
18
+
19
+ const buffer = new ArrayBuffer(typeValue.length / 2);
20
+ const array = new Uint8Array(buffer);
21
+
22
+ // I can assume even length because of the previous check.
23
+ for (let i = 0; i < typeValue.length / 2; i++) {
24
+ array[i] = Number.parseInt(typeValue.slice(i * 2, (i * 2) + 2), 16);
25
+ }
26
+
27
+ return buffer;
28
+ }
29
+
30
+ case typeName === 'IMap':
31
+ if (typeof typeValue !== 'object' || typeValue === null) {
32
+ throw new TypeError(`Invalid JSON-encoded IMap: '${JSON.stringify(typeValue)}'`);
33
+ }
34
+
35
+ if (Object.keys(typeValue).some(key => !/^-?\d+$/.test(key))) {
36
+ throw new TypeError(`Invalid JSON-encoded IMap (non-Int key): '${JSON.stringify(typeValue)}'`);
37
+ }
38
+
39
+ return makeIMap(decodeMap(typeValue));
40
+ case typeName === 'DateTime':
41
+ if (typeof typeValue !== 'string') {
42
+ throw new TypeError(`Invalid JSON-encoded DateTime: '${JSON.stringify(typeValue)}'`);
43
+ }
44
+
45
+ // Kinda lame that I'm encoding the datetime string, but this is the easiest way right now.
46
+ return new CponReader(new UnpackContext(new TextEncoder().encode(typeValue).buffer)).readDateTimeInner();
47
+ default:
48
+ throw new TypeError(`Invalid JSON-encoded type name: '${JSON.stringify(typeName)}'`);
49
+ }
50
+ };
51
+
52
+ switch (true) {
53
+ case list.length === 3 && list[0] === JSON_TAG_TYPE:
54
+ return handleType(list[1], list[2]);
55
+ case list.length === 3 && list[0] === JSON_TAG_META:
56
+ case list.length === 5 && list[0] === JSON_TAG_META && list[2] === JSON_TAG_TYPE: {
57
+ if (typeof list[1] !== 'object' || list[1] === null) {
58
+ throw new TypeError(`Invalid JSON-encoded MetaData: '${JSON.stringify(list[1])}'`);
59
+ }
60
+
61
+ const value = (() => {
62
+ if (list.length === 3) {
63
+ const decoded = decodeValueJson(list[2]);
64
+ if (decoded instanceof RpcValueWithMetaData) {
65
+ throw new TypeError('Logic error: got RpcValueWithMetaData as the value in JSON-encoded RpcValueWithMetaData, please report this bug to the maintainer.');
66
+ }
67
+
68
+ return decoded;
69
+ }
70
+
71
+ return handleType(list[3], list[4]);
72
+ })();
73
+
74
+ return new RpcValueWithMetaData(makeMetaMap(decodeMap(list[1])), value);
75
+ }
76
+
77
+ default:
78
+ return list.map(value => decodeValueJson(value));
79
+ }
80
+ }
81
+
82
+ case x === null:
83
+ return undefined;
84
+ case typeof x === 'object':
85
+ return decodeMap(x);
86
+ case typeof x === 'number':
87
+ case typeof x === 'boolean':
88
+ case typeof x === 'string':
89
+ return x;
90
+ default:
91
+ throw new Error(`Unknown JSON-encoded value: ${JSON.stringify(x)}`);
92
+ }
93
+ };
94
+
95
+ export const fromJson = (str: string): RpcValue | Error => decodeValueJson(JSON.parse(str));
96
+
97
+ const JSON_TAG_META = '!shvMeta';
98
+ const JSON_TAG_TYPE = '!shvType';
99
+
100
+ const encodeValueJson = (x: RpcValue): unknown => {
101
+ switch (true) {
102
+ case x instanceof RpcValueWithMetaData: {
103
+ const res = [JSON_TAG_META, encodeValueJson(makeMap(x.meta))] as const;
104
+ const encoded = encodeValueJson(x.value);
105
+ if (Array.isArray(encoded) && encoded[0] === JSON_TAG_TYPE) {
106
+ return [...res, ...encoded as unknown[]];
107
+ }
108
+
109
+ return [...res, encoded];
110
+ }
111
+
112
+ case Array.isArray(x):
113
+ return x.map(value => encodeValueJson(value));
114
+ case isShvMap(x): {
115
+ const map = x as Record<string, RpcValue>;
116
+ return Object.fromEntries(Object.entries(map).map(([key, value]) => [key, encodeValueJson(value)] as const));
117
+ }
118
+
119
+ case isIMap(x):
120
+ return [JSON_TAG_TYPE, 'IMap', encodeValueJson(makeMap(x))];
121
+ case x instanceof ArrayBuffer:
122
+ return [JSON_TAG_TYPE, 'Blob', new Uint8Array(x).reduce((acc, v) => acc + v.toString(16).padStart(2, '0'), '')];
123
+ case x instanceof Date:
124
+ return [JSON_TAG_TYPE, 'DateTime', stringifyDate(x)];
125
+ case x === undefined:
126
+ return null;
127
+ case x instanceof Decimal:
128
+ case x instanceof UInt:
129
+ case x instanceof Double:
130
+ throw new TypeError('Decimal/Double/UInt not supported in JSON');
131
+ default:
132
+ return x;
133
+ }
134
+ };
135
+
136
+ export const toJson = (value: RpcValue): string | Error => JSON.stringify(encodeValueJson(value));
package/src/ws-client.ts CHANGED
@@ -36,21 +36,38 @@ type Subscription = {
36
36
  callback: SubscriptionCallback;
37
37
  };
38
38
 
39
- type WsClientOptions = {
39
+ type Login = {
40
+ type: 'PLAIN';
41
+ user: string;
42
+ password: string;
43
+ } | {
44
+ type: 'TOKEN';
45
+ token: string;
46
+ };
47
+
48
+ type WsClientOptionsCommon = {
40
49
  logDebug: (...args: string[]) => void;
50
+ wsUri: string;
51
+ };
52
+
53
+ type WsClientOptionsLogin = WsClientOptionsCommon & {
41
54
  mountPoint?: string;
42
- user?: string;
43
- password: string;
44
- loginType?: 'PLAIN' | 'AZURE';
55
+ login: Login;
45
56
  timeout?: number;
46
57
  pingInterval?: number;
47
- wsUri: string;
48
58
  onConnected: () => void;
49
59
  onConnectionFailure: (error: Error) => void;
50
60
  onDisconnected: () => void;
51
61
  onRequest: (rpc_msg: RpcRequest) => void;
52
62
  };
53
63
 
64
+ type WsClientOptionsWorkflows = WsClientOptionsCommon & {
65
+ onWorkflows: (workflows: RpcValue) => void;
66
+ onWorkflowsFailed: () => void;
67
+ };
68
+
69
+ type WsClientOptions = WsClientOptionsLogin | WsClientOptionsWorkflows;
70
+
54
71
  type LsResult = string[];
55
72
  export enum DirFlags {
56
73
  Reserved = 1,
@@ -103,99 +120,31 @@ class UserIDRequired extends RpcError {}
103
120
  class NotImplemented extends RpcError {}
104
121
 
105
122
  class WsClient {
106
- requestId = 1;
107
- pingTimerId = -1;
123
+ private requestId = 1;
124
+ private pingTimerId = -1;
108
125
 
109
- rpcHandlers: Array<{
126
+ private rpcHandlers: Array<{
110
127
  resolve: RpcResponseResolver;
111
128
  timeout_handle: number;
112
129
  }> = [];
113
130
 
114
- subscriptions: Subscription[] = [];
115
- websocket: WebSocket;
116
-
117
- logDebug: WsClientOptions['logDebug'];
118
- mountPoint: WsClientOptions['mountPoint'];
119
- user?: WsClientOptions['user'];
120
- password: WsClientOptions['password'];
121
- loginType: WsClientOptions['loginType'];
122
- onConnected: WsClientOptions['onConnected'];
123
- onConnectionFailure: WsClientOptions['onConnectionFailure'];
124
- onDisconnected: WsClientOptions['onDisconnected'];
125
- onRequest: WsClientOptions['onRequest'];
126
- timeout: WsClientOptions['timeout'];
127
- pingInterval: WsClientOptions['pingInterval'];
131
+ private readonly subscriptions: Subscription[] = [];
132
+ private readonly websocket: WebSocket;
133
+
134
+ private readonly options: WsClientOptions;
135
+ private readonly timeout: number;
128
136
 
129
137
  constructor(options: WsClientOptions) {
130
138
  if (typeof options !== 'object') {
131
139
  throw new TypeError('No options object supplied');
132
140
  }
133
141
 
134
- this.logDebug = options.logDebug ?? (() => {/* nothing */});
135
- this.mountPoint = options.mountPoint;
136
-
137
- this.user = options.user ?? '';
138
- this.password = options.password;
139
- this.loginType = options.loginType ?? 'PLAIN';
142
+ this.options = options;
143
+ this.timeout = 'timeout' in options && options.timeout !== undefined ? options.timeout : DEFAULT_TIMEOUT;
140
144
 
141
145
  this.websocket = new WebSocket(options.wsUri);
142
146
  this.websocket.binaryType = 'arraybuffer';
143
147
 
144
- this.pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
145
- this.onConnected = options.onConnected ?? (() => {/* nothing */});
146
- this.onConnectionFailure = options.onConnectionFailure ?? (() => {/* nothing */});
147
- this.onDisconnected = options.onDisconnected ?? (() => {/* nothing */});
148
- this.onRequest = options.onRequest ?? (() => {/* nothing */});
149
-
150
- this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
151
-
152
- this.websocket.addEventListener('open', () => {
153
- this.logDebug('CONNECTED');
154
- const handleConnectionError = (error: Error) => {
155
- this.logDebug('FAILURE: couldn\'t perform initial handshake', error.message);
156
- this.onConnectionFailure(error);
157
- };
158
-
159
- this.callRpcMethod(undefined, 'hello').then(response => {
160
- if (response instanceof Error) {
161
- handleConnectionError(response);
162
- return;
163
- }
164
-
165
- const params = makeMap({
166
- login: makeMap({
167
- password: this.password,
168
- type: this.loginType,
169
- user: this.user,
170
- }),
171
- options: makeMap({
172
- device: this.mountPoint === 'string' ? makeMap({mountPoint: this.mountPoint}) : undefined,
173
- }),
174
- });
175
- return this.callRpcMethod(undefined, 'login', params);
176
- }).then(response => {
177
- if (response instanceof Error) {
178
- handleConnectionError(response);
179
- return;
180
- }
181
-
182
- this.logDebug('SUCCESS: connected to shv broker');
183
- this.onConnected();
184
- this.pingTimerId = window.setInterval(() => {
185
- this.sendPing();
186
- }, this.pingInterval);
187
- }).catch(() => {
188
- this.logDebug('FAILURE: couldn\' connected to shv broker');
189
- });
190
- });
191
-
192
- this.websocket.addEventListener('close', () => {
193
- this.logDebug('DISCONNECTED');
194
- this.subscriptions.length = 0;
195
- this.onDisconnected();
196
- window.clearInterval(this.pingTimerId);
197
- });
198
-
199
148
  this.websocket.addEventListener('message', (evt: MessageEvent<ArrayBuffer>) => {
200
149
  const rpcVal = dataToRpcValue(evt.data);
201
150
  const rpcMsg = RpcMessageZod.parse(rpcVal);
@@ -211,7 +160,9 @@ class WsClient {
211
160
  }
212
161
  }
213
162
  } else if (isRequest(rpcMsg)) {
214
- this.onRequest(rpcMsg);
163
+ if ('onRequest' in this.options) {
164
+ this.options.onRequest(rpcMsg);
165
+ }
215
166
  } else if (isResponse(rpcMsg)) {
216
167
  const requestId = rpcMsg.meta[RPC_MESSAGE_REQUEST_ID];
217
168
 
@@ -258,6 +209,12 @@ class WsClient {
258
209
  console.log('WebSocket ERROR', evt);
259
210
  this.logDebug('WebSocket ERROR');
260
211
  });
212
+
213
+ if ('onWorkflows' in this.options) {
214
+ this.workflowsProcedure(this.options);
215
+ } else {
216
+ this.loginProcedure(this.options);
217
+ }
261
218
  }
262
219
 
263
220
  callRpcMethod(shv_path: '.broker/currentClient', method: 'accessGrantForMethodCall', params: [string, string]): Promise<ResultOrError<string>>;
@@ -272,8 +229,7 @@ class WsClient {
272
229
  [RPC_MESSAGE_SHV_PATH]: shv_path ?? '',
273
230
  }), makeIMap({
274
231
  [RPC_MESSAGE_PARAMS]: params,
275
- }),
276
- );
232
+ }));
277
233
 
278
234
  this.sendRpcMessage(rq);
279
235
 
@@ -370,6 +326,110 @@ class WsClient {
370
326
  close() {
371
327
  this.websocket.close();
372
328
  }
329
+
330
+ private logDebug(...args: string[]) {
331
+ if (this.options.logDebug === undefined) {
332
+ return;
333
+ }
334
+
335
+ this.options.logDebug(...args);
336
+ }
337
+
338
+ private workflowsProcedure(options: WsClientOptionsWorkflows) {
339
+ let workflowsObtained = false;
340
+ this.websocket.addEventListener('open', () => {
341
+ this.options.logDebug('CONNECTED');
342
+ const handleConnectionError = (error: Error) => {
343
+ this.logDebug('FAILURE: couldn\'t retrieve workflows', error.message);
344
+ options.onWorkflowsFailed();
345
+ };
346
+
347
+ this.callRpcMethod(undefined, 'workflows').then(response => {
348
+ if (response instanceof Error) {
349
+ handleConnectionError(response);
350
+ return;
351
+ }
352
+
353
+ workflowsObtained = true;
354
+ options.onWorkflows(response);
355
+
356
+ this.close();
357
+ }).catch((_: unknown) => {
358
+ options.onWorkflowsFailed();
359
+ });
360
+ });
361
+
362
+ this.websocket.addEventListener('close', () => {
363
+ this.logDebug('DISCONNECTED');
364
+ if (!workflowsObtained) {
365
+ options.onWorkflowsFailed();
366
+ }
367
+ });
368
+ }
369
+
370
+ private loginProcedure(options: WsClientOptionsLogin) {
371
+ this.websocket.addEventListener('open', () => {
372
+ this.options.logDebug('CONNECTED');
373
+ const handleConnectionError = (error: Error) => {
374
+ this.logDebug('FAILURE: couldn\'t perform initial handshake', error.message);
375
+ options.onConnectionFailure(error);
376
+ };
377
+
378
+ const handleLoginResponse = (response: any) => {
379
+ if (response instanceof Error) {
380
+ handleConnectionError(response);
381
+ return;
382
+ }
383
+
384
+ this.logDebug('SUCCESS: connected to shv broker');
385
+ options.onConnected();
386
+ this.pingTimerId = window.setInterval(() => {
387
+ this.sendPing();
388
+ }, options.pingInterval ?? DEFAULT_PING_INTERVAL);
389
+ };
390
+
391
+ const makeLoginParams = (loginMap: ShvMap) => makeMap({
392
+ login: loginMap,
393
+ options: makeMap({
394
+ device: options.mountPoint === 'string' ? makeMap({mountPoint: options.mountPoint}) : undefined,
395
+ }),
396
+ });
397
+
398
+ this.callRpcMethod(undefined, 'hello').then(response => {
399
+ if (response instanceof Error) {
400
+ handleConnectionError(response);
401
+ return;
402
+ }
403
+
404
+ const makeLoginMap = () => {
405
+ switch (options.login.type) {
406
+ case 'PLAIN':
407
+ return makeMap({
408
+ password: options.login.password,
409
+ type: options.login.type,
410
+ user: options.login.user,
411
+ });
412
+ case 'TOKEN':
413
+ return makeMap({
414
+ token: options.login.token,
415
+ type: options.login.type,
416
+ });
417
+ }
418
+ };
419
+
420
+ return this.callRpcMethod(undefined, 'login', makeLoginParams(makeLoginMap()));
421
+ }).then(handleLoginResponse).catch(() => {
422
+ this.logDebug('FAILURE: couldn\' connected to shv broker');
423
+ });
424
+ });
425
+
426
+ this.websocket.addEventListener('close', () => {
427
+ this.logDebug('DISCONNECTED');
428
+ this.subscriptions.length = 0;
429
+ options.onDisconnected();
430
+ window.clearInterval(this.pingTimerId);
431
+ });
432
+ }
373
433
  }
374
434
 
375
435
  export {WsClient};
package/src/zod.ts CHANGED
@@ -11,9 +11,10 @@ export const uint = () => z.instanceof(UInt<number>);
11
11
  export const double = () => z.instanceof(Double);
12
12
  export const decimal = () => z.instanceof(Decimal);
13
13
  export const blob = () => z.instanceof(ArrayBuffer);
14
+ export const list = () => z.array(z.lazy(rpcvalue));
14
15
 
15
16
  const withMetaInstanceParser = z.instanceof(RpcValueWithMetaData);
16
- export const rpcvalue = () => z.union([
17
+ export const rpcvalue: () => ZodType<RpcValue, any, any> = () => z.union([
17
18
  z.undefined(),
18
19
  z.boolean(),
19
20
  z.number(),
@@ -23,7 +24,7 @@ export const rpcvalue = () => z.union([
23
24
  blob(),
24
25
  z.string(),
25
26
  z.date(),
26
- z.array(z.any()),
27
+ list(),
27
28
  map(),
28
29
  imap(),
29
30
  withMetaInstanceParser,
package/test_json.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { toCpon } from "./src/cpon";
2
+ import { fromJson, toJson } from "./src/json";
3
+ import {makeIMap, makeMap, makeMetaMap, RpcValue, RpcValueWithMetaData, withOffset} from "./src/rpcvalue";
4
+
5
+ const testJsonRoundTrip = (json: string, expected: RpcValue) => {
6
+ const parsed = fromJson(json);
7
+ if (parsed instanceof Error) {
8
+ throw parsed;
9
+ }
10
+ const parsedCpon = toCpon(parsed);
11
+ const expectedCpon = toCpon(expected);
12
+ if (parsedCpon !== expectedCpon) {
13
+ throw new Error(`Assertion error:\nExpected: '${expectedCpon}'\nActual: '${parsedCpon}'`);
14
+ }
15
+
16
+ const serializedJson = toJson(expected);
17
+ if (serializedJson !== json.replaceAll(' ', '')) {
18
+ throw new Error(`Assertion error:\nExpected: '${json}'\nActual: '${serializedJson}'`);
19
+ }
20
+
21
+ };
22
+
23
+ const MINUTE = 1;
24
+ const HOUR = 60 * MINUTE;
25
+
26
+ const tests = [
27
+ ['{}', makeMap({})],
28
+ ['{"null-field": null}', makeMap({"null-field": undefined})],
29
+ ['["!shvType", "Blob", ""]', new Uint8Array().buffer],
30
+ ['["!shvType", "Blob", "6162a1"]', new Uint8Array([0x61, 0x62, 0xa1]).buffer],
31
+ ['["!shvType", "IMap", {}]', makeIMap({})],
32
+ ['["!shvType", "IMap", {"1": 2}]', makeIMap({1: 2})],
33
+ ['["!shvType", "IMap", {"1": 2, "2": "foo"}]', makeIMap({1: 2, 2: "foo"})],
34
+ ['["!shvType", "DateTime", "2021-11-08T01:02:03+05"]', withOffset(new Date("2021-11-08T01:02:03+05:00"), 5 * HOUR)],
35
+ ['["!shvType", "DateTime", "2021-11-08T01:02:03-0815"]', withOffset(new Date("2021-11-08T01:02:03-08:15"), -8 * HOUR - 15 * MINUTE)],
36
+ ['["!shvType", "DateTime", "2021-11-08T01:02:03.456-0815"]', withOffset(new Date("2021-11-08T01:02:03.456-08:15"), -8 * HOUR - 15 * MINUTE)],
37
+ ['["!shvMeta", {"1":2}, 42]', new RpcValueWithMetaData(makeMetaMap({1: 2}), 42)],
38
+ ['["!shvMeta", {"1":2}, "!shvType", "IMap", {"42": 7}]', new RpcValueWithMetaData(makeMetaMap({1: 2}), makeIMap({42: 7}))],
39
+ ['["!shvMeta", {"1": 2, "foo": "bar"}, [1,2,3]]', new RpcValueWithMetaData(makeMetaMap({1: 2, "foo": "bar"}), [1, 2, 3])],
40
+ ] as const;
41
+
42
+ tests.forEach(([inputJson, expectedRpcValue]) => testJsonRoundTrip(inputJson, expectedRpcValue));
43
+
44
+ console.log('OK');