libshv-js 3.3.5 → 3.4.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/check-version-bump.yml +22 -0
- package/.github/workflows/lint.yml +2 -2
- package/.xo-config.json +22 -14
- package/package.json +3 -3
- package/src/chainpack.ts +124 -76
- package/src/cpcontext.ts +9 -4
- package/src/cpon.ts +145 -72
- package/src/rpcmessage.ts +57 -176
- package/src/rpcvalue.ts +11 -8
- package/src/ws-client.ts +131 -52
- package/src/zod.ts +27 -3
- package/test.ts +2 -0
package/src/rpcmessage.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {toCpon} from './cpon';
|
|
3
|
-
import {toChainPack} from './chainpack';
|
|
1
|
+
import * as z from './zod';
|
|
4
2
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
3
|
+
export const RPC_MESSAGE_REQUEST_ID = 8;
|
|
4
|
+
export const RPC_MESSAGE_SHV_PATH = 9;
|
|
5
|
+
export const RPC_MESSAGE_METHOD = 10;
|
|
6
|
+
export const RPC_MESSAGE_CALLER_IDS = 11;
|
|
9
7
|
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
8
|
+
export const RPC_MESSAGE_PARAMS = 1;
|
|
9
|
+
export const RPC_MESSAGE_RESULT = 2;
|
|
10
|
+
export const RPC_MESSAGE_ERROR = 3;
|
|
13
11
|
|
|
14
12
|
export const ERROR_CODE = 1;
|
|
15
13
|
export const ERROR_MESSAGE = 2;
|
|
@@ -30,169 +28,52 @@ enum ErrorCode {
|
|
|
30
28
|
NotImplemented = 12,
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
[ERROR_CODE]:
|
|
35
|
-
[ERROR_MESSAGE]
|
|
36
|
-
[ERROR_DATA]
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
isValid() {
|
|
83
|
-
return this.shvPath() && (this.isRequest() || this.isResponse || this.isSignal());
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
isRequest(): boolean {
|
|
87
|
-
return this.requestId() !== undefined && this.method() !== undefined;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
isResponse(): boolean {
|
|
91
|
-
return this.requestId() !== undefined && this.method() === undefined;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
isSignal(): boolean {
|
|
95
|
-
return this.requestId() === undefined && this.method() !== undefined;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
requestId(): Int | undefined {
|
|
99
|
-
return (this.meta[TagRequestId] as Int);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
setRequestId(id: number) {
|
|
103
|
-
this.meta[TagRequestId] = id;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
callerIds(): RpcValue[] | undefined {
|
|
107
|
-
return this.meta[TagCallerIds] as RpcValue[];
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
setCallerIds(ids: RpcValue[]) {
|
|
111
|
-
this.meta[TagCallerIds] = ids;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
shvPath(): string | undefined {
|
|
115
|
-
return (this.meta[TagShvPath] as string);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
setShvPath(val: string) {
|
|
119
|
-
this.meta[TagShvPath] = val;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
method(): string | undefined {
|
|
123
|
-
return (this.meta[TagMethod] as string);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
setMethod(val: string) {
|
|
127
|
-
this.meta[TagMethod] = val;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
params() {
|
|
131
|
-
return this.value[KeyParams];
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
setParams(params: RpcValue) {
|
|
135
|
-
this.value[KeyParams] = params;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
resultOrError() {
|
|
139
|
-
if (Object.hasOwn(this.value, KeyError)) {
|
|
140
|
-
if (!isIMap(this.value[KeyError])) {
|
|
141
|
-
return new ProtocolError('Response had an error, but this error was not a map');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const error_map = this.value[KeyError];
|
|
145
|
-
if (typeof error_map[ERROR_CODE] !== 'number') {
|
|
146
|
-
return new ProtocolError('Response had an error, but this error did not contain at least an error code');
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const code = error_map[ERROR_CODE] as unknown;
|
|
150
|
-
|
|
151
|
-
const ErrorType = (() => {
|
|
152
|
-
switch (code) {
|
|
153
|
-
case ErrorCode.InvalidRequest: return InvalidRequest;
|
|
154
|
-
case ErrorCode.MethodNotFound: return MethodNotFound;
|
|
155
|
-
case ErrorCode.InvalidParams: return InvalidParams;
|
|
156
|
-
case ErrorCode.InternalError: return InternalError;
|
|
157
|
-
case ErrorCode.ParseError: return ParseError;
|
|
158
|
-
case ErrorCode.MethodCallTimeout: return MethodCallTimeout;
|
|
159
|
-
case ErrorCode.MethodCallCancelled: return MethodCallCancelled;
|
|
160
|
-
case ErrorCode.MethodCallException: return MethodCallException;
|
|
161
|
-
case ErrorCode.Unknown: return Unknown;
|
|
162
|
-
case ErrorCode.LoginRequired: return LoginRequired;
|
|
163
|
-
case ErrorCode.UserIDRequired: return UserIDRequired;
|
|
164
|
-
case ErrorCode.NotImplemented: return NotImplemented;
|
|
165
|
-
default: return Unknown;
|
|
166
|
-
}
|
|
167
|
-
})();
|
|
168
|
-
|
|
169
|
-
return new ErrorType(this.value[KeyError] as unknown as ErrorMap);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (Object.hasOwn(this.value, KeyResult)) {
|
|
173
|
-
return this.value[KeyResult];
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return new ProtocolError('Response included neither result nor error');
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
setResult(result: RpcValue) {
|
|
180
|
-
this.value[KeyResult] = result;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
setError(error: string) {
|
|
184
|
-
this.value[KeyError] = error;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
toCpon() {
|
|
188
|
-
return toCpon(new RpcValueWithMetaData(this.value, this.meta));
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
toChainPack() {
|
|
192
|
-
return toChainPack(new RpcValueWithMetaData(this.value, this.meta));
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export type RpcResponse<T = RpcValue> = T | Error;
|
|
197
|
-
|
|
198
|
-
export {RpcMessage, RpcError, ErrorCode};
|
|
31
|
+
const ErrorMapZod = z.imap({
|
|
32
|
+
[ERROR_CODE]: z.number(),
|
|
33
|
+
[ERROR_MESSAGE]: z.string().optional(),
|
|
34
|
+
[ERROR_DATA]: z.rpcvalue().optional(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export type ErrorMap = z.infer<typeof ErrorMapZod>;
|
|
38
|
+
|
|
39
|
+
const RpcRequestMetaZod = z.metamap({
|
|
40
|
+
[RPC_MESSAGE_REQUEST_ID]: z.number(),
|
|
41
|
+
[RPC_MESSAGE_METHOD]: z.string(),
|
|
42
|
+
[RPC_MESSAGE_SHV_PATH]: z.string(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const RpcRequestValueZod = z.imap({
|
|
46
|
+
[RPC_MESSAGE_PARAMS]: z.rpcvalue().optional(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const RpcResponseMetaZod = z.metamap({
|
|
50
|
+
[RPC_MESSAGE_REQUEST_ID]: z.number(),
|
|
51
|
+
});
|
|
52
|
+
const RpcResponseValueZod = z.imap({
|
|
53
|
+
[RPC_MESSAGE_RESULT]: z.rpcvalue(),
|
|
54
|
+
}).or(z.imap({
|
|
55
|
+
[RPC_MESSAGE_ERROR]: ErrorMapZod,
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
const RpcSignalMetaZod = z.metamap({
|
|
59
|
+
[RPC_MESSAGE_SHV_PATH]: z.string(),
|
|
60
|
+
[RPC_MESSAGE_METHOD]: z.string(),
|
|
61
|
+
});
|
|
62
|
+
const RpcSignalValueZod = z.imap({
|
|
63
|
+
[RPC_MESSAGE_PARAMS]: z.rpcvalue().optional(),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const RpcRequestZod = z.withMeta(RpcRequestMetaZod, RpcRequestValueZod);
|
|
67
|
+
const RpcResponseZod = z.withMeta(RpcResponseMetaZod, RpcResponseValueZod);
|
|
68
|
+
const RpcSignalZod = z.withMeta(RpcSignalMetaZod, RpcSignalValueZod);
|
|
69
|
+
const RpcMessageZod = z.union([RpcRequestZod, RpcResponseZod, RpcSignalZod]);
|
|
70
|
+
export type RpcRequest = z.infer<typeof RpcRequestZod>;
|
|
71
|
+
export type RpcResponse = z.infer<typeof RpcResponseZod>;
|
|
72
|
+
export type RpcSignal = z.infer<typeof RpcSignalZod>;
|
|
73
|
+
export type RpcMessage = z.infer<typeof RpcMessageZod>;
|
|
74
|
+
|
|
75
|
+
export const isSignal = (message: RpcMessage): message is RpcSignal => !(RPC_MESSAGE_REQUEST_ID in message.meta) && RPC_MESSAGE_METHOD in message.meta;
|
|
76
|
+
export const isRequest = (message: RpcMessage): message is RpcRequest => RPC_MESSAGE_REQUEST_ID in message.meta && RPC_MESSAGE_METHOD in message.meta;
|
|
77
|
+
export const isResponse = (message: RpcMessage): message is RpcResponse => RPC_MESSAGE_REQUEST_ID in message.meta && !(RPC_MESSAGE_METHOD in message.meta);
|
|
78
|
+
|
|
79
|
+
export {RpcMessageZod, ErrorCode};
|
package/src/rpcvalue.ts
CHANGED
|
@@ -50,10 +50,11 @@ export type Blob = ArrayBuffer;
|
|
|
50
50
|
export type ShvString = string;
|
|
51
51
|
export type DateTime = Date & {utc_offset?: number};
|
|
52
52
|
const withOffset = (date: Date, utc_offset?: number) => {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
return
|
|
53
|
+
const clonedDate: DateTime = new Date(date.getTime());
|
|
54
|
+
clonedDate.utc_offset = utc_offset;
|
|
55
|
+
return clonedDate;
|
|
56
56
|
};
|
|
57
|
+
|
|
57
58
|
export type List = RpcValue[];
|
|
58
59
|
|
|
59
60
|
const shvMapType = Symbol('shvMapType');
|
|
@@ -92,9 +93,11 @@ type MetaMap<T extends Record<string | number, RpcValue> = Record<string | numbe
|
|
|
92
93
|
[shvMapType]: 'metamap';
|
|
93
94
|
};
|
|
94
95
|
|
|
95
|
-
const isShvMap = (x:
|
|
96
|
+
const isShvMap = (x: unknown): x is ShvMap => typeof x === 'object' && (x as ShvMap)[shvMapType] === 'map';
|
|
97
|
+
|
|
98
|
+
const isIMap = (x: unknown): x is IMap => typeof x === 'object' && (x as IMap)[shvMapType] === 'imap';
|
|
96
99
|
|
|
97
|
-
const
|
|
100
|
+
const isMetaMap = (x: unknown): x is IMap => typeof x === 'object' && (x as MetaMap)[shvMapType] === 'metamap';
|
|
98
101
|
|
|
99
102
|
const makeMetaMap = <T extends Record<string | number, RpcValue> = Record<string | number, RpcValue>, U extends Record<number, RpcValue> = Omit<T, typeof shvMapType>>(x: U = {} as U): MetaMap<U> => ({
|
|
100
103
|
...x,
|
|
@@ -111,10 +114,10 @@ const makeMap = <T extends Record<string, RpcValue> = Record<string, RpcValue>,
|
|
|
111
114
|
[shvMapType]: 'map',
|
|
112
115
|
});
|
|
113
116
|
|
|
114
|
-
class RpcValueWithMetaData {
|
|
115
|
-
constructor(public
|
|
117
|
+
class RpcValueWithMetaData<MetaSchema extends MetaMap = MetaMap, ValueSchema extends RpcValueType = RpcValueType> {
|
|
118
|
+
constructor(public meta: MetaSchema, public value: ValueSchema) {}
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
export type RpcValue = RpcValueType | RpcValueWithMetaData;
|
|
119
122
|
|
|
120
|
-
export {shvMapType, Decimal, Double, type IMap, type MetaMap, RpcValueWithMetaData, type ShvMap, UInt, withOffset, makeMap, makeIMap, makeMetaMap, isIMap, isShvMap};
|
|
123
|
+
export {shvMapType, Decimal, Double, type IMap, type MetaMap, RpcValueWithMetaData, type ShvMap, UInt, withOffset, makeMap, makeIMap, makeMetaMap, isIMap, isMetaMap, isShvMap};
|
package/src/ws-client.ts
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
|
-
import {ChainPackReader,
|
|
2
|
-
import {type CponReader,
|
|
3
|
-
import {ERROR_MESSAGE, ErrorCode, ERROR_CODE, RpcMessage, type
|
|
4
|
-
import {type RpcValue, type Null, type Int, type IMap, type ShvMap, makeMap, makeIMap} from './rpcvalue';
|
|
1
|
+
import {ChainPackReader, CHAINPACK_PROTOCOL_TYPE, ChainPackWriter, toChainPack} from './chainpack';
|
|
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} from './rpcmessage';
|
|
4
|
+
import {type RpcValue, type Null, type Int, type IMap, type ShvMap, makeMap, makeIMap, RpcValueWithMetaData, makeMetaMap} from './rpcvalue';
|
|
5
5
|
|
|
6
6
|
const DEFAULT_TIMEOUT = 5000;
|
|
7
|
+
const DEFAULT_PING_INTERVAL = 30 * 1000;
|
|
7
8
|
|
|
8
9
|
const dataToRpcValue = (buff: ArrayBuffer) => {
|
|
9
10
|
const rd: ChainPackReader | CponReader = new ChainPackReader(buff);
|
|
10
11
|
rd.readUIntData();
|
|
11
12
|
const proto = rd.ctx.getByte();
|
|
12
13
|
switch (proto) {
|
|
13
|
-
case
|
|
14
|
+
case CHAINPACK_PROTOCOL_TYPE:
|
|
14
15
|
break;
|
|
15
|
-
case
|
|
16
|
+
case CPON_PROTOCOL_TYPE:
|
|
16
17
|
throw new Error('CPON protocol type is not supported anymore');
|
|
17
18
|
default:
|
|
18
19
|
throw new Error(`Unsupported protocol type ${proto}`);
|
|
19
20
|
}
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
|
|
22
|
+
const rpcVal = rd.read();
|
|
23
|
+
return rpcVal;
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
type SubscriptionCallback = (path: string, method: string, param?: RpcValue) => void;
|
|
25
27
|
|
|
26
|
-
type
|
|
28
|
+
type ResultOrError<T = RpcValue> = T | Error;
|
|
29
|
+
|
|
30
|
+
type RpcResponseResolver = (rpc_msg: ResultOrError) => void;
|
|
27
31
|
|
|
28
32
|
type Subscription = {
|
|
29
33
|
subscriber: string;
|
|
@@ -39,11 +43,12 @@ type WsClientOptions = {
|
|
|
39
43
|
password: string;
|
|
40
44
|
loginType?: 'PLAIN' | 'AZURE';
|
|
41
45
|
timeout?: number;
|
|
46
|
+
pingInterval?: number;
|
|
42
47
|
wsUri: string;
|
|
43
48
|
onConnected: () => void;
|
|
44
49
|
onConnectionFailure: (error: Error) => void;
|
|
45
50
|
onDisconnected: () => void;
|
|
46
|
-
onRequest: (rpc_msg:
|
|
51
|
+
onRequest: (rpc_msg: RpcRequest) => void;
|
|
47
52
|
};
|
|
48
53
|
|
|
49
54
|
type LsResult = string[];
|
|
@@ -72,8 +77,35 @@ type DirResult = Array<IMap<{
|
|
|
72
77
|
[DIR_EXTRA]: ShvMap;
|
|
73
78
|
}>>;
|
|
74
79
|
|
|
80
|
+
class RpcError extends Error {
|
|
81
|
+
constructor(private readonly err_info: ErrorMap) {
|
|
82
|
+
super(err_info[ERROR_MESSAGE] ?? 'Unknown RpcError');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
data() {
|
|
86
|
+
return this.err_info[ERROR_DATA];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class ProtocolError extends Error {}
|
|
91
|
+
|
|
92
|
+
class InvalidRequest extends RpcError {}
|
|
93
|
+
class MethodNotFound extends RpcError {}
|
|
94
|
+
class InvalidParams extends RpcError {}
|
|
95
|
+
class InternalError extends RpcError {}
|
|
96
|
+
class ParseError extends RpcError {}
|
|
97
|
+
class MethodCallTimeout extends RpcError {}
|
|
98
|
+
class MethodCallCancelled extends RpcError {}
|
|
99
|
+
class MethodCallException extends RpcError {}
|
|
100
|
+
class Unknown extends RpcError {}
|
|
101
|
+
class LoginRequired extends RpcError {}
|
|
102
|
+
class UserIDRequired extends RpcError {}
|
|
103
|
+
class NotImplemented extends RpcError {}
|
|
104
|
+
|
|
75
105
|
class WsClient {
|
|
76
106
|
requestId = 1;
|
|
107
|
+
pingTimerId = -1;
|
|
108
|
+
|
|
77
109
|
rpcHandlers: Array<{
|
|
78
110
|
resolve: RpcResponseResolver;
|
|
79
111
|
timeout_handle: number;
|
|
@@ -92,6 +124,7 @@ class WsClient {
|
|
|
92
124
|
onDisconnected: WsClientOptions['onDisconnected'];
|
|
93
125
|
onRequest: WsClientOptions['onRequest'];
|
|
94
126
|
timeout: WsClientOptions['timeout'];
|
|
127
|
+
pingInterval: WsClientOptions['pingInterval'];
|
|
95
128
|
|
|
96
129
|
constructor(options: WsClientOptions) {
|
|
97
130
|
if (typeof options !== 'object') {
|
|
@@ -108,6 +141,7 @@ class WsClient {
|
|
|
108
141
|
this.websocket = new WebSocket(options.wsUri);
|
|
109
142
|
this.websocket.binaryType = 'arraybuffer';
|
|
110
143
|
|
|
144
|
+
this.pingInterval = options.pingInterval ?? DEFAULT_PING_INTERVAL;
|
|
111
145
|
this.onConnected = options.onConnected ?? (() => {/* nothing */});
|
|
112
146
|
this.onConnectionFailure = options.onConnectionFailure ?? (() => {/* nothing */});
|
|
113
147
|
this.onDisconnected = options.onDisconnected ?? (() => {/* nothing */});
|
|
@@ -121,11 +155,13 @@ class WsClient {
|
|
|
121
155
|
this.logDebug('FAILURE: couldn\'t perform initial handshake', error.message);
|
|
122
156
|
this.onConnectionFailure(error);
|
|
123
157
|
};
|
|
158
|
+
|
|
124
159
|
this.callRpcMethod(undefined, 'hello').then(response => {
|
|
125
160
|
if (response instanceof Error) {
|
|
126
161
|
handleConnectionError(response);
|
|
127
162
|
return;
|
|
128
163
|
}
|
|
164
|
+
|
|
129
165
|
const params = makeMap({
|
|
130
166
|
login: makeMap({
|
|
131
167
|
password: this.password,
|
|
@@ -142,8 +178,12 @@ class WsClient {
|
|
|
142
178
|
handleConnectionError(response);
|
|
143
179
|
return;
|
|
144
180
|
}
|
|
181
|
+
|
|
145
182
|
this.logDebug('SUCCESS: connected to shv broker');
|
|
146
183
|
this.onConnected();
|
|
184
|
+
this.pingTimerId = window.setInterval(() => {
|
|
185
|
+
this.sendPing();
|
|
186
|
+
}, this.pingInterval);
|
|
147
187
|
}).catch(() => {
|
|
148
188
|
this.logDebug('FAILURE: couldn\' connected to shv broker');
|
|
149
189
|
});
|
|
@@ -153,34 +193,61 @@ class WsClient {
|
|
|
153
193
|
this.logDebug('DISCONNECTED');
|
|
154
194
|
this.subscriptions.length = 0;
|
|
155
195
|
this.onDisconnected();
|
|
196
|
+
window.clearInterval(this.pingTimerId);
|
|
156
197
|
});
|
|
157
198
|
|
|
158
|
-
this.websocket.addEventListener('message', evt => {
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
this.logDebug(`message received: ${
|
|
199
|
+
this.websocket.addEventListener('message', (evt: MessageEvent<ArrayBuffer>) => {
|
|
200
|
+
const rpcVal = dataToRpcValue(evt.data);
|
|
201
|
+
const rpcMsg = RpcMessageZod.parse(rpcVal);
|
|
202
|
+
this.logDebug(`message received: ${toCpon(rpcMsg)}`);
|
|
162
203
|
|
|
163
|
-
if (
|
|
204
|
+
if (isSignal(rpcMsg)) {
|
|
164
205
|
for (const sub of this.subscriptions) {
|
|
165
|
-
const
|
|
166
|
-
const method =
|
|
206
|
+
const shvPath = rpcMsg.meta[RPC_MESSAGE_SHV_PATH];
|
|
207
|
+
const method = rpcMsg.meta[RPC_MESSAGE_METHOD];
|
|
167
208
|
|
|
168
|
-
if (
|
|
169
|
-
sub.callback(
|
|
209
|
+
if (shvPath.startsWith(sub.path) && method === sub.method) {
|
|
210
|
+
sub.callback(shvPath, method, rpcMsg.value[RPC_MESSAGE_PARAMS]);
|
|
170
211
|
}
|
|
171
212
|
}
|
|
172
|
-
} else if (
|
|
173
|
-
this.onRequest(
|
|
174
|
-
} else if (
|
|
175
|
-
const requestId =
|
|
176
|
-
if (requestId === undefined) {
|
|
177
|
-
throw new Error('got RpcResponse without requestId');
|
|
178
|
-
}
|
|
213
|
+
} else if (isRequest(rpcMsg)) {
|
|
214
|
+
this.onRequest(rpcMsg);
|
|
215
|
+
} else if (isResponse(rpcMsg)) {
|
|
216
|
+
const requestId = rpcMsg.meta[RPC_MESSAGE_REQUEST_ID];
|
|
179
217
|
|
|
180
218
|
if (this.rpcHandlers[Number(requestId)] !== undefined) {
|
|
181
219
|
const handler = this.rpcHandlers[Number(requestId)];
|
|
182
220
|
clearTimeout(handler.timeout_handle);
|
|
183
|
-
handler.resolve(
|
|
221
|
+
handler.resolve((() => {
|
|
222
|
+
if (RPC_MESSAGE_ERROR in rpcMsg.value) {
|
|
223
|
+
const code = rpcMsg.value[RPC_MESSAGE_ERROR][ERROR_CODE] as unknown;
|
|
224
|
+
const ErrorTypeCtor = (() => {
|
|
225
|
+
switch (code) {
|
|
226
|
+
case ErrorCode.InvalidRequest: return InvalidRequest;
|
|
227
|
+
case ErrorCode.MethodNotFound: return MethodNotFound;
|
|
228
|
+
case ErrorCode.InvalidParams: return InvalidParams;
|
|
229
|
+
case ErrorCode.InternalError: return InternalError;
|
|
230
|
+
case ErrorCode.ParseError: return ParseError;
|
|
231
|
+
case ErrorCode.MethodCallTimeout: return MethodCallTimeout;
|
|
232
|
+
case ErrorCode.MethodCallCancelled: return MethodCallCancelled;
|
|
233
|
+
case ErrorCode.MethodCallException: return MethodCallException;
|
|
234
|
+
case ErrorCode.Unknown: return Unknown;
|
|
235
|
+
case ErrorCode.LoginRequired: return LoginRequired;
|
|
236
|
+
case ErrorCode.UserIDRequired: return UserIDRequired;
|
|
237
|
+
case ErrorCode.NotImplemented: return NotImplemented;
|
|
238
|
+
default: return Unknown;
|
|
239
|
+
}
|
|
240
|
+
})();
|
|
241
|
+
|
|
242
|
+
return new ErrorTypeCtor(rpcMsg.value[RPC_MESSAGE_ERROR]);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (RPC_MESSAGE_RESULT in rpcMsg.value) {
|
|
246
|
+
return rpcMsg.value[RPC_MESSAGE_RESULT];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return new ProtocolError('Response included neither result nor error');
|
|
250
|
+
})());
|
|
184
251
|
// eslint-disable-next-line @typescript-eslint/no-array-delete, @typescript-eslint/no-dynamic-delete
|
|
185
252
|
delete this.rpcHandlers[Number(requestId)];
|
|
186
253
|
}
|
|
@@ -193,28 +260,29 @@ class WsClient {
|
|
|
193
260
|
});
|
|
194
261
|
}
|
|
195
262
|
|
|
196
|
-
callRpcMethod(shv_path: '.broker/currentClient', method: 'accessGrantForMethodCall', params: [string, string]): Promise<
|
|
197
|
-
callRpcMethod(shv_path: string | undefined, method: 'dir', params?: RpcValue): Promise<
|
|
198
|
-
callRpcMethod(shv_path: string | undefined, method: 'ls', params?: RpcValue): Promise<
|
|
199
|
-
callRpcMethod(shv_path: string | undefined, method: string, params?: RpcValue): Promise<
|
|
200
|
-
callRpcMethod(shv_path: string | undefined, method: string, params?: RpcValue) {
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
263
|
+
callRpcMethod(shv_path: '.broker/currentClient', method: 'accessGrantForMethodCall', params: [string, string]): Promise<ResultOrError<string>>;
|
|
264
|
+
callRpcMethod(shv_path: string | undefined, method: 'dir', params?: RpcValue): Promise<ResultOrError<DirResult>>;
|
|
265
|
+
callRpcMethod(shv_path: string | undefined, method: 'ls', params?: RpcValue): Promise<ResultOrError<LsResult>>;
|
|
266
|
+
callRpcMethod(shv_path: string | undefined, method: string, params?: RpcValue): Promise<ResultOrError>;
|
|
267
|
+
callRpcMethod(shv_path: string | undefined, method: string, params?: RpcValue): Promise<ResultOrError> {
|
|
268
|
+
const rqId = this.requestId++;
|
|
269
|
+
const rq: RpcRequest = new RpcValueWithMetaData(makeMetaMap({
|
|
270
|
+
[RPC_MESSAGE_REQUEST_ID]: rqId,
|
|
271
|
+
[RPC_MESSAGE_METHOD]: method ?? '',
|
|
272
|
+
[RPC_MESSAGE_SHV_PATH]: shv_path ?? '',
|
|
273
|
+
}), makeIMap({
|
|
274
|
+
[RPC_MESSAGE_PARAMS]: params,
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
277
|
+
|
|
211
278
|
this.sendRpcMessage(rq);
|
|
212
279
|
|
|
213
|
-
const promise = new Promise<
|
|
214
|
-
this.rpcHandlers[
|
|
280
|
+
const promise = new Promise<ResultOrError>(resolve => {
|
|
281
|
+
this.rpcHandlers[rqId] = {resolve, timeout_handle: self.setTimeout(() => {
|
|
215
282
|
resolve(new MethodCallTimeout(makeIMap({
|
|
216
283
|
[ERROR_CODE]: ErrorCode.MethodCallTimeout,
|
|
217
284
|
[ERROR_MESSAGE]: `Shv call timeout after: ${this.timeout} msec.`,
|
|
285
|
+
[ERROR_DATA]: undefined,
|
|
218
286
|
})));
|
|
219
287
|
}, this.timeout)};
|
|
220
288
|
});
|
|
@@ -223,23 +291,24 @@ class WsClient {
|
|
|
223
291
|
}
|
|
224
292
|
|
|
225
293
|
sendRpcMessage(rpc_msg: RpcMessage) {
|
|
226
|
-
if (this.websocket && this.websocket.readyState ===
|
|
227
|
-
this.logDebug('sending rpc message:',
|
|
228
|
-
const
|
|
294
|
+
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
|
|
295
|
+
this.logDebug('sending rpc message:', toCpon(rpc_msg));
|
|
296
|
+
const msgData = new Uint8Array(toChainPack(rpc_msg));
|
|
229
297
|
|
|
230
298
|
const wr = new ChainPackWriter();
|
|
231
|
-
wr.writeUIntData(
|
|
232
|
-
const dgram = new Uint8Array(wr.ctx.length + 1 +
|
|
299
|
+
wr.writeUIntData(msgData.length + 1);
|
|
300
|
+
const dgram = new Uint8Array(wr.ctx.length + 1 + msgData.length);
|
|
233
301
|
let ix = 0;
|
|
234
302
|
for (let i = 0; i < wr.ctx.length; i++) {
|
|
235
303
|
dgram[ix++] = wr.ctx.data[i];
|
|
236
304
|
}
|
|
237
305
|
|
|
238
|
-
dgram[ix++] =
|
|
306
|
+
dgram[ix++] = CHAINPACK_PROTOCOL_TYPE;
|
|
239
307
|
|
|
240
|
-
for (const
|
|
241
|
-
dgram[ix++] =
|
|
308
|
+
for (const msgDatum of msgData) {
|
|
309
|
+
dgram[ix++] = msgDatum;
|
|
242
310
|
}
|
|
311
|
+
|
|
243
312
|
this.logDebug(`sending ${dgram.length} bytes of data`);
|
|
244
313
|
this.websocket.send(dgram.buffer);
|
|
245
314
|
}
|
|
@@ -250,6 +319,7 @@ class WsClient {
|
|
|
250
319
|
this.logDebug(`Already subscribed {$path}:${method} for subscriber ${subscriber}`);
|
|
251
320
|
return;
|
|
252
321
|
}
|
|
322
|
+
|
|
253
323
|
// If this path:method has not been subscribed on the broker, do it now
|
|
254
324
|
if (!this.subscriptions.some(val => val.path === path && val.method === method)) {
|
|
255
325
|
this.callRpcMethod('.broker/app', 'subscribe', makeMap({
|
|
@@ -258,6 +328,7 @@ class WsClient {
|
|
|
258
328
|
this.logDebug(`Couldn't subscribe to ${path}, ${method}`);
|
|
259
329
|
});
|
|
260
330
|
}
|
|
331
|
+
|
|
261
332
|
this.subscriptions.push({
|
|
262
333
|
subscriber,
|
|
263
334
|
path,
|
|
@@ -272,11 +343,13 @@ class WsClient {
|
|
|
272
343
|
this.logDebug(`No such subscription ${path}:${method} for subscriber ${subscriber}`);
|
|
273
344
|
return;
|
|
274
345
|
}
|
|
346
|
+
|
|
275
347
|
this.subscriptions.splice(idx, 1);
|
|
276
348
|
// Unsubscribe on the broker only if there are no other subscriptions of this path:method
|
|
277
349
|
if (this.subscriptions.some(val => val.path === path && val.method === method)) {
|
|
278
350
|
return;
|
|
279
351
|
}
|
|
352
|
+
|
|
280
353
|
this.callRpcMethod('.broker/app', 'unsubscribe', makeMap({
|
|
281
354
|
method, path,
|
|
282
355
|
})).catch(() => {
|
|
@@ -284,6 +357,12 @@ class WsClient {
|
|
|
284
357
|
});
|
|
285
358
|
}
|
|
286
359
|
|
|
360
|
+
sendPing() {
|
|
361
|
+
this.callRpcMethod('.broker/app', 'ping').catch((error: unknown) => {
|
|
362
|
+
console.log('Failed to send ping:', error);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
287
366
|
accessGrantForMethodCall(path: string, method: string) {
|
|
288
367
|
return this.callRpcMethod('.broker/currentClient', 'accessGrantForMethodCall', [path, method]);
|
|
289
368
|
}
|
package/src/zod.ts
CHANGED
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
import {z, type ZodType} from 'zod';
|
|
2
|
-
import {type IMap, isIMap, isShvMap, type RpcValue, type ShvMap, UInt} from './rpcvalue';
|
|
2
|
+
import {Decimal, Double, type IMap, isIMap, isMetaMap, isShvMap, type MetaMap, type RpcValue, type RpcValueType, RpcValueWithMetaData, type ShvMap, UInt} from './rpcvalue';
|
|
3
3
|
|
|
4
|
-
export const map = <T extends Record<string, ZodType<any, any, any>>>(schema
|
|
4
|
+
export const map = <T extends Record<string, ZodType<any, any, any>>>(schema?: T) => z.custom<ShvMap<{[K in keyof T]: z.infer<T[K]>}>>((data: RpcValue) => isShvMap(data) && (schema === undefined || z.object(schema).safeParse(data).success));
|
|
5
5
|
export const recmap = <T extends ZodType<any, any, any>>(schema: T) => z.custom<ShvMap<Record<string, z.infer<T>>>>((data: RpcValue) => isShvMap(data) && z.record(z.string(), schema).safeParse(data).success);
|
|
6
|
-
export const imap = <T extends Record<string, ZodType<any, any, any>>>(schema
|
|
6
|
+
export const imap = <T extends Record<string, ZodType<any, any, any>>>(schema?: T) => z.custom<IMap<{[K in keyof T]: z.infer<T[K]>}>>((data: RpcValue) => isIMap(data) && (schema === undefined || z.object(schema).safeParse(data).success));
|
|
7
|
+
export const metamap = <T extends Record<string | number, ZodType<any, any, any>>>(schema?: T) => z.custom<MetaMap<{[K in keyof T]: z.infer<T[K]>}>>((data: RpcValue) => isMetaMap(data) && (schema === undefined || z.object(schema).safeParse(data).success));
|
|
7
8
|
export const int = () => z.number();
|
|
8
9
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-arguments -- Zod needs the default argument, otherwise it'll infer as UInt<unknown>
|
|
9
10
|
export const uint = () => z.instanceof(UInt<number>);
|
|
10
11
|
|
|
12
|
+
const withMetaInstanceParser = z.instanceof(RpcValueWithMetaData);
|
|
13
|
+
export const rpcvalue = () => z.union([
|
|
14
|
+
z.undefined(),
|
|
15
|
+
z.boolean(),
|
|
16
|
+
z.number(),
|
|
17
|
+
uint(),
|
|
18
|
+
z.instanceof(Double),
|
|
19
|
+
z.instanceof(Decimal),
|
|
20
|
+
z.instanceof(ArrayBuffer),
|
|
21
|
+
z.string(),
|
|
22
|
+
z.date(),
|
|
23
|
+
z.array(z.any()),
|
|
24
|
+
map(),
|
|
25
|
+
imap(),
|
|
26
|
+
withMetaInstanceParser,
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export const withMeta = <MetaSchema extends MetaMap, ValueSchema extends RpcValueType>(metaParser: ZodType<MetaSchema, any, any>, valueParser: ZodType<ValueSchema, any, any>) =>
|
|
30
|
+
z.custom<RpcValueWithMetaData<z.infer<typeof metaParser>, z.infer<typeof valueParser>>>((data: any) => withMetaInstanceParser.and(z.object({
|
|
31
|
+
meta: metaParser,
|
|
32
|
+
value: valueParser,
|
|
33
|
+
})).safeParse(data).success);
|
|
34
|
+
|
|
11
35
|
export * from 'zod';
|