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.
- package/.github/workflows/test.yml +4 -2
- package/package.json +2 -1
- package/src/cpon.ts +48 -52
- package/src/json.ts +136 -0
- package/src/ws-client.ts +146 -86
- package/src/zod.ts +3 -2
- package/test_json.ts +44 -0
|
@@ -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@
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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.
|
|
135
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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');
|