libshv-js 3.0.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/lint.yml +33 -0
- package/.github/workflows/test.yml +25 -0
- package/.xo-config.json +33 -0
- package/LICENSE +21 -0
- package/README.md +2 -0
- package/deno.json +8 -0
- package/examples/websocketosaurus.html +44 -0
- package/examples/websocketosaurus.js +94 -0
- package/package.json +30 -0
- package/src/chainpack.ts +590 -0
- package/src/cpcontext.ts +83 -0
- package/src/cpon.ts +920 -0
- package/src/rpcmessage.ts +195 -0
- package/src/rpcvalue.ts +111 -0
- package/src/ws-client.ts +249 -0
- package/test.ts +107 -0
- package/tsconfig.json +109 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import {IMap, Int, MetaMap, type RpcValue, RpcValueWithMetaData} from './rpcvalue.ts';
|
|
2
|
+
import {toCpon} from './cpon.ts';
|
|
3
|
+
import {toChainPack} from './chainpack.ts';
|
|
4
|
+
|
|
5
|
+
const TagRequestId = 8;
|
|
6
|
+
const TagShvPath = 9;
|
|
7
|
+
const TagMethod = 10;
|
|
8
|
+
const TagCallerIds = 11;
|
|
9
|
+
|
|
10
|
+
const KeyParams = 1;
|
|
11
|
+
const KeyResult = 2;
|
|
12
|
+
const KeyError = 3;
|
|
13
|
+
|
|
14
|
+
export const ERROR_CODE = 1;
|
|
15
|
+
export const ERROR_MESSAGE = 2;
|
|
16
|
+
export const ERROR_DATA = 3;
|
|
17
|
+
|
|
18
|
+
enum ErrorCode {
|
|
19
|
+
InvalidRequest = 1,
|
|
20
|
+
MethodNotFound = 2,
|
|
21
|
+
InvalidParams = 3,
|
|
22
|
+
InternalError = 4,
|
|
23
|
+
ParseError = 5,
|
|
24
|
+
MethodCallTimeout = 6,
|
|
25
|
+
MethodCallCancelled = 7,
|
|
26
|
+
MethodCallException = 8,
|
|
27
|
+
Unknown = 9,
|
|
28
|
+
LoginRequired = 10,
|
|
29
|
+
UserIDRequired = 11,
|
|
30
|
+
NotImplemented = 12,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type ErrorMap = IMap<{
|
|
34
|
+
[ERROR_CODE]: Int<ErrorCode>;
|
|
35
|
+
[ERROR_MESSAGE]?: string;
|
|
36
|
+
[ERROR_DATA]?: RpcValue;
|
|
37
|
+
}>;
|
|
38
|
+
|
|
39
|
+
class RpcError extends Error {
|
|
40
|
+
constructor(private readonly err_info: ErrorMap) {
|
|
41
|
+
super(err_info.value[ERROR_MESSAGE]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
data() {
|
|
45
|
+
return this.err_info.value[ERROR_DATA];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class ProtocolError extends Error {}
|
|
50
|
+
|
|
51
|
+
export class InvalidRequest extends RpcError {}
|
|
52
|
+
export class MethodNotFound extends RpcError {}
|
|
53
|
+
export class InvalidParams extends RpcError {}
|
|
54
|
+
export class InternalError extends RpcError {}
|
|
55
|
+
export class ParseError extends RpcError {}
|
|
56
|
+
export class MethodCallTimeout extends RpcError {}
|
|
57
|
+
export class MethodCallCancelled extends RpcError {}
|
|
58
|
+
export class MethodCallException extends RpcError {}
|
|
59
|
+
export class Unknown extends RpcError {}
|
|
60
|
+
export class LoginRequired extends RpcError {}
|
|
61
|
+
export class UserIDRequired extends RpcError {}
|
|
62
|
+
export class NotImplemented extends RpcError {}
|
|
63
|
+
|
|
64
|
+
class RpcMessage {
|
|
65
|
+
value: IMap;
|
|
66
|
+
meta: MetaMap;
|
|
67
|
+
constructor(rpc_val?: RpcValue) {
|
|
68
|
+
if (rpc_val === undefined) {
|
|
69
|
+
this.value = new IMap();
|
|
70
|
+
this.meta = new MetaMap();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!(rpc_val instanceof RpcValueWithMetaData && rpc_val.value instanceof IMap)) {
|
|
75
|
+
throw new TypeError(`RpcMessage initialized with a non-IMap: ${toCpon(rpc_val)}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.value = rpc_val.value;
|
|
79
|
+
this.meta = rpc_val.meta;
|
|
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.value[TagRequestId] as Int);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setRequestId(id: number | Int) {
|
|
103
|
+
this.meta.value[TagRequestId] = new Int(id);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
callerIds(): RpcValue[] | undefined {
|
|
107
|
+
return this.meta.value[TagCallerIds] as RpcValue[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setCallerIds(ids: RpcValue[]) {
|
|
111
|
+
this.meta.value[TagCallerIds] = ids;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
shvPath(): string | undefined {
|
|
115
|
+
return (this.meta.value[TagShvPath] as string);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
setShvPath(val: string) {
|
|
119
|
+
this.meta.value[TagShvPath] = val;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
method(): string | undefined {
|
|
123
|
+
return (this.meta.value[TagMethod] as string);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
setMethod(val: string) {
|
|
127
|
+
this.meta.value[TagMethod] = val;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
params() {
|
|
131
|
+
return this.value.value[KeyParams];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
setParams(params: RpcValue) {
|
|
135
|
+
this.value.value[KeyParams] = params;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
resultOrError() {
|
|
139
|
+
if (this.value.value[KeyError] !== undefined) {
|
|
140
|
+
if (!(this.value.value[KeyError] instanceof IMap)) {
|
|
141
|
+
return new ProtocolError('Response had an error, but this error was not a map');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const error_map = this.value.value[KeyError];
|
|
145
|
+
if (error_map.value[ERROR_CODE] === undefined) {
|
|
146
|
+
return new ProtocolError('Response had an error, but this error did not contain at least an error code');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const ErrorType = (() => {
|
|
150
|
+
switch ((this.value.value[KeyError] as ErrorMap).value[ERROR_CODE].value) {
|
|
151
|
+
case ErrorCode.InvalidRequest: return InvalidRequest;
|
|
152
|
+
case ErrorCode.MethodNotFound: return MethodNotFound;
|
|
153
|
+
case ErrorCode.InvalidParams: return InvalidParams;
|
|
154
|
+
case ErrorCode.InternalError: return InternalError;
|
|
155
|
+
case ErrorCode.ParseError: return ParseError;
|
|
156
|
+
case ErrorCode.MethodCallTimeout: return MethodCallTimeout;
|
|
157
|
+
case ErrorCode.MethodCallCancelled: return MethodCallCancelled;
|
|
158
|
+
case ErrorCode.MethodCallException: return MethodCallException;
|
|
159
|
+
case ErrorCode.Unknown: return Unknown;
|
|
160
|
+
case ErrorCode.LoginRequired: return LoginRequired;
|
|
161
|
+
case ErrorCode.UserIDRequired: return UserIDRequired;
|
|
162
|
+
case ErrorCode.NotImplemented: return NotImplemented;
|
|
163
|
+
}
|
|
164
|
+
})();
|
|
165
|
+
|
|
166
|
+
return new ErrorType(this.value.value[KeyError] as ErrorMap);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (this.value.value[KeyResult] !== undefined) {
|
|
170
|
+
return this.value.value[KeyResult];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return new ProtocolError('Response included neither result nor error');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setResult(result: RpcValue) {
|
|
177
|
+
this.value.value[KeyResult] = result;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
setError(error: string) {
|
|
181
|
+
this.value.value[KeyError] = error;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
toCpon() {
|
|
185
|
+
return toCpon(new RpcValueWithMetaData(this.value, this.meta));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
toChainPack() {
|
|
189
|
+
return toChainPack(new RpcValueWithMetaData(this.value, this.meta));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export type RpcResponse<T = RpcValue> = T | Error;
|
|
194
|
+
|
|
195
|
+
export {RpcMessage, RpcError, ErrorCode};
|
package/src/rpcvalue.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
class Int<T = number> {
|
|
2
|
+
readonly value: T;
|
|
3
|
+
|
|
4
|
+
constructor(u: T | Int<T>) {
|
|
5
|
+
if (u instanceof Int) {
|
|
6
|
+
this.value = u.value;
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (!Number.isInteger(u)) {
|
|
10
|
+
throw new TypeError('Value for Int must a positive integral number');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
this.value = u;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
[Symbol.toPrimitive](_hint: string) {
|
|
17
|
+
return this.value;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class UInt {
|
|
22
|
+
readonly value: number;
|
|
23
|
+
|
|
24
|
+
constructor(u: number) {
|
|
25
|
+
if (u < 0 || !Number.isInteger(u)) {
|
|
26
|
+
throw new Error(`Invalid value '${u}' for UInt must a positive integral number`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.value = u;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
[Symbol.toPrimitive](_hint: string) {
|
|
33
|
+
return this.value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
toString() {
|
|
37
|
+
return `${this.value.toString()}`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class Double {
|
|
42
|
+
readonly value: number;
|
|
43
|
+
|
|
44
|
+
constructor(u: number) {
|
|
45
|
+
this.value = u;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
class Decimal {
|
|
50
|
+
mantisa: number;
|
|
51
|
+
exponent: number;
|
|
52
|
+
constructor(mantisa: number, exponent: number) {
|
|
53
|
+
if (!Number.isInteger(exponent)) {
|
|
54
|
+
throw new TypeError(`Decimal: exponent must be integral (${exponent})`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.mantisa = mantisa;
|
|
58
|
+
this.exponent = exponent;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type Null = undefined;
|
|
63
|
+
export type Bool = boolean;
|
|
64
|
+
export type Blob = ArrayBuffer;
|
|
65
|
+
export type ShvString = string;
|
|
66
|
+
export type DateTime = Date & {utc_offset?: number};
|
|
67
|
+
const withOffset = (date: Date, utc_offset?: number) => {
|
|
68
|
+
const cloned_date: DateTime = new Date(date.getTime());
|
|
69
|
+
cloned_date.utc_offset = utc_offset;
|
|
70
|
+
return cloned_date;
|
|
71
|
+
};
|
|
72
|
+
export type List = RpcValue[];
|
|
73
|
+
|
|
74
|
+
type ShvMapDefaultType = Record<string, RpcValue | undefined>;
|
|
75
|
+
class ShvMap<T extends ShvMapDefaultType = ShvMapDefaultType> {
|
|
76
|
+
constructor(public value: T = {} as T) {}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type IMapDefaultType = Record<number, RpcValue | undefined>;
|
|
80
|
+
class IMap<T extends IMapDefaultType = IMapDefaultType> {
|
|
81
|
+
constructor(public value: T = {} as T) {}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type RpcValueType =
|
|
85
|
+
Null |
|
|
86
|
+
Bool |
|
|
87
|
+
Int |
|
|
88
|
+
UInt |
|
|
89
|
+
Double |
|
|
90
|
+
Decimal |
|
|
91
|
+
Blob |
|
|
92
|
+
ShvString |
|
|
93
|
+
DateTime |
|
|
94
|
+
List |
|
|
95
|
+
ShvMap |
|
|
96
|
+
IMap;
|
|
97
|
+
|
|
98
|
+
class MetaMap {
|
|
99
|
+
value: Record<number | string, RpcValue | undefined>;
|
|
100
|
+
constructor(obj?: Record<number | string, RpcValue | undefined>) {
|
|
101
|
+
this.value = obj ?? {};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
class RpcValueWithMetaData {
|
|
106
|
+
constructor(public value: RpcValueType, public meta: MetaMap) {}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export type RpcValue = RpcValueType | RpcValueWithMetaData;
|
|
110
|
+
|
|
111
|
+
export {Decimal, Double, Int, IMap, MetaMap, RpcValueWithMetaData, ShvMap, UInt, withOffset};
|
package/src/ws-client.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import {ChainPackReader, ChainpackProtocolType, ChainPackWriter} from './chainpack.ts';
|
|
2
|
+
import {type CponReader, CponProtocolType, toCpon} from './cpon.ts';
|
|
3
|
+
import {ERROR_MESSAGE, ErrorCode, ERROR_CODE, RpcError, RpcMessage, type RpcResponse, MethodCallTimeout} from './rpcmessage.ts';
|
|
4
|
+
import {type RpcValue, type Null, Int, IMap, ShvMap} from './rpcvalue.ts';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TIMEOUT = 5000;
|
|
7
|
+
|
|
8
|
+
const dataToRpcValue = (buff: ArrayBuffer) => {
|
|
9
|
+
const rd: ChainPackReader | CponReader = new ChainPackReader(buff);
|
|
10
|
+
rd.readUIntData();
|
|
11
|
+
const proto = rd.ctx.getByte();
|
|
12
|
+
switch (proto) {
|
|
13
|
+
case ChainpackProtocolType:
|
|
14
|
+
break;
|
|
15
|
+
case CponProtocolType:
|
|
16
|
+
throw new Error('CPON protocol type is not supported anymore');
|
|
17
|
+
default:
|
|
18
|
+
throw new Error(`Unsupported protocol type ${proto}`);
|
|
19
|
+
}
|
|
20
|
+
const rpc_val = rd.read();
|
|
21
|
+
return rpc_val;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type SubscriptionCallback = (path: string, method: string, param?: RpcValue) => void;
|
|
25
|
+
|
|
26
|
+
type RpcResponseResolver = (rpc_msg: RpcResponse) => void;
|
|
27
|
+
|
|
28
|
+
type Subscription = {
|
|
29
|
+
path: string;
|
|
30
|
+
method: string;
|
|
31
|
+
callback: SubscriptionCallback;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type WsClientOptions = {
|
|
35
|
+
logDebug: (...args: string[]) => void;
|
|
36
|
+
mountPoint?: string;
|
|
37
|
+
user?: string;
|
|
38
|
+
password: string;
|
|
39
|
+
loginType?: 'PLAIN' | 'AZURE';
|
|
40
|
+
timeout?: number;
|
|
41
|
+
wsUri: string;
|
|
42
|
+
onConnected: () => void;
|
|
43
|
+
onRequest: (rpc_msg: RpcMessage) => void;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type LsResult = string[];
|
|
47
|
+
export enum DirFlags {
|
|
48
|
+
Reserved = 1,
|
|
49
|
+
Getter = 2,
|
|
50
|
+
Setter = 4,
|
|
51
|
+
LargeResultHint = 8,
|
|
52
|
+
NotIdempotent = 16,
|
|
53
|
+
RequiresClientId = 32,
|
|
54
|
+
}
|
|
55
|
+
export const DIR_NAME = 1;
|
|
56
|
+
export const DIR_FLAGS = 2;
|
|
57
|
+
export const DIR_PARAM = 3;
|
|
58
|
+
export const DIR_RESULT = 4;
|
|
59
|
+
export const DIR_ACCESS = 5;
|
|
60
|
+
export const DIR_SIGNALS = 6;
|
|
61
|
+
export const DIR_EXTRA = 63;
|
|
62
|
+
type DirResult = Array<IMap<{
|
|
63
|
+
[DIR_NAME]: string;
|
|
64
|
+
[DIR_FLAGS]: Int<DirFlags>;
|
|
65
|
+
[DIR_PARAM]: string | Null;
|
|
66
|
+
[DIR_RESULT]: string | Null;
|
|
67
|
+
[DIR_ACCESS]: Int;
|
|
68
|
+
[DIR_SIGNALS]: ShvMap<Record<string, string | Null>>;
|
|
69
|
+
[DIR_EXTRA]: ShvMap;
|
|
70
|
+
}>>;
|
|
71
|
+
|
|
72
|
+
class WsClient {
|
|
73
|
+
requestId = 1;
|
|
74
|
+
rpcHandlers: Array<{
|
|
75
|
+
resolve: RpcResponseResolver;
|
|
76
|
+
timeout_handle: number;
|
|
77
|
+
}> = [];
|
|
78
|
+
|
|
79
|
+
subscriptions: Subscription[] = [];
|
|
80
|
+
websocket: WebSocket;
|
|
81
|
+
|
|
82
|
+
logDebug: WsClientOptions['logDebug'];
|
|
83
|
+
mountPoint: WsClientOptions['mountPoint'];
|
|
84
|
+
user?: WsClientOptions['user'];
|
|
85
|
+
password: WsClientOptions['password'];
|
|
86
|
+
loginType: WsClientOptions['loginType'];
|
|
87
|
+
onConnected: WsClientOptions['onConnected'];
|
|
88
|
+
onRequest: WsClientOptions['onRequest'];
|
|
89
|
+
timeout: WsClientOptions['timeout'];
|
|
90
|
+
|
|
91
|
+
constructor(options: WsClientOptions) {
|
|
92
|
+
if (typeof options !== 'object') {
|
|
93
|
+
throw new TypeError('No options object supplied');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.logDebug = options.logDebug ?? (() => {/* nothing */});
|
|
97
|
+
this.mountPoint = options.mountPoint;
|
|
98
|
+
|
|
99
|
+
this.user = options.user ?? '';
|
|
100
|
+
this.password = options.password;
|
|
101
|
+
this.loginType = options.loginType ?? 'PLAIN';
|
|
102
|
+
|
|
103
|
+
this.websocket = new WebSocket(options.wsUri);
|
|
104
|
+
this.websocket.binaryType = 'arraybuffer';
|
|
105
|
+
|
|
106
|
+
this.onConnected = options.onConnected ?? (() => {/* nothing */});
|
|
107
|
+
this.onRequest = options.onRequest ?? (() => {/* nothing */});
|
|
108
|
+
|
|
109
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
110
|
+
|
|
111
|
+
this.websocket.addEventListener('open', () => {
|
|
112
|
+
this.logDebug('CONNECTED');
|
|
113
|
+
this.callRpcMethod(undefined, 'hello').then(() => {
|
|
114
|
+
const params = new ShvMap({
|
|
115
|
+
login: new ShvMap({
|
|
116
|
+
password: this.password,
|
|
117
|
+
type: this.loginType,
|
|
118
|
+
user: this.user,
|
|
119
|
+
}),
|
|
120
|
+
options: new ShvMap({
|
|
121
|
+
device: this.mountPoint === 'string' ? new ShvMap({mountPoint: this.mountPoint}) : undefined,
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
return this.callRpcMethod(undefined, 'login', params);
|
|
125
|
+
}).then(() => {
|
|
126
|
+
this.logDebug('SUCCESS: connected to shv broker');
|
|
127
|
+
this.onConnected();
|
|
128
|
+
}).catch(() => {
|
|
129
|
+
this.logDebug('FAILURE: couldn\' connected to shv broker');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
this.websocket.addEventListener('close', () => {
|
|
134
|
+
this.logDebug('DISCONNECTED');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
this.websocket.addEventListener('message', evt => {
|
|
138
|
+
const rpc_val = dataToRpcValue(evt.data);
|
|
139
|
+
const rpc_msg = new RpcMessage(rpc_val);
|
|
140
|
+
this.logDebug(`message received: ${rpc_msg.toCpon()}`);
|
|
141
|
+
|
|
142
|
+
if (rpc_msg.isSignal()) {
|
|
143
|
+
for (const sub of this.subscriptions) {
|
|
144
|
+
const shv_path = rpc_msg.shvPath();
|
|
145
|
+
const method = rpc_msg.method();
|
|
146
|
+
|
|
147
|
+
if (shv_path?.startsWith(sub.path) && method === sub.method) {
|
|
148
|
+
sub.callback(shv_path, method, rpc_msg.params());
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} else if (rpc_msg.isRequest()) {
|
|
152
|
+
this.onRequest(rpc_msg);
|
|
153
|
+
} else if (rpc_msg.isResponse()) {
|
|
154
|
+
const requestId = rpc_msg.requestId();
|
|
155
|
+
if (requestId === undefined) {
|
|
156
|
+
throw new Error('got RpcResponse without requestId');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (this.rpcHandlers[Number(requestId)] !== undefined) {
|
|
160
|
+
const handler = this.rpcHandlers[Number(requestId)];
|
|
161
|
+
clearTimeout(handler.timeout_handle);
|
|
162
|
+
handler.resolve(rpc_msg.resultOrError());
|
|
163
|
+
// eslint-disable-next-line @typescript-eslint/no-array-delete, @typescript-eslint/no-dynamic-delete
|
|
164
|
+
delete this.rpcHandlers[Number(requestId)];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
this.websocket.addEventListener('error', evt => {
|
|
170
|
+
console.log('WebSocket ERROR', evt);
|
|
171
|
+
this.logDebug('WebSocket ERROR');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
this.websocket.addEventListener('close', evt => {
|
|
175
|
+
this.logDebug(`socket close code: ${evt.code}`);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
callRpcMethod(shv_path: string | undefined, method: 'dir', params?: RpcValue): Promise<RpcResponse<DirResult>>;
|
|
180
|
+
callRpcMethod(shv_path: string | undefined, method: 'ls', params?: RpcValue): Promise<RpcResponse<LsResult>>;
|
|
181
|
+
callRpcMethod(shv_path: string | undefined, method: string, params?: RpcValue): Promise<RpcResponse>;
|
|
182
|
+
callRpcMethod(shv_path: string | undefined, method: string, params?: RpcValue) {
|
|
183
|
+
const rq = new RpcMessage();
|
|
184
|
+
const rq_id = this.requestId++;
|
|
185
|
+
rq.setRequestId(rq_id);
|
|
186
|
+
if (shv_path !== undefined) {
|
|
187
|
+
rq.setShvPath(shv_path);
|
|
188
|
+
}
|
|
189
|
+
rq.setMethod(method);
|
|
190
|
+
if (params !== undefined) {
|
|
191
|
+
rq.setParams(params);
|
|
192
|
+
}
|
|
193
|
+
this.sendRpcMessage(rq);
|
|
194
|
+
|
|
195
|
+
const promise = new Promise<RpcResponse>(resolve => {
|
|
196
|
+
this.rpcHandlers[rq_id] = {resolve, timeout_handle: self.setTimeout(() => {
|
|
197
|
+
resolve(new MethodCallTimeout(new IMap({
|
|
198
|
+
[ERROR_CODE]: new Int(ErrorCode.MethodCallTimeout),
|
|
199
|
+
[ERROR_MESSAGE]: `Shv call timeout after: ${this.timeout} msec.`,
|
|
200
|
+
})));
|
|
201
|
+
}, this.timeout)};
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
return promise;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
sendRpcMessage(rpc_msg: RpcMessage) {
|
|
208
|
+
if (this.websocket && this.websocket.readyState === 1) {
|
|
209
|
+
this.logDebug('sending rpc message:', rpc_msg.toCpon());
|
|
210
|
+
const msg_data = new Uint8Array(rpc_msg.toChainPack());
|
|
211
|
+
|
|
212
|
+
const wr = new ChainPackWriter();
|
|
213
|
+
wr.writeUIntData(msg_data.length + 1);
|
|
214
|
+
const dgram = new Uint8Array(wr.ctx.length + 1 + msg_data.length);
|
|
215
|
+
let ix = 0;
|
|
216
|
+
for (let i = 0; i < wr.ctx.length; i++) {
|
|
217
|
+
dgram[ix++] = wr.ctx.data[i];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
dgram[ix++] = ChainpackProtocolType;
|
|
221
|
+
|
|
222
|
+
for (const msg_datum of msg_data) {
|
|
223
|
+
dgram[ix++] = msg_datum;
|
|
224
|
+
}
|
|
225
|
+
this.logDebug(`sending ${dgram.length} bytes of data`);
|
|
226
|
+
this.websocket.send(dgram.buffer);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
subscribe(path: string, method: string, callback: SubscriptionCallback) {
|
|
231
|
+
this.callRpcMethod('.broker/app', 'subscribe', new ShvMap({
|
|
232
|
+
method, path,
|
|
233
|
+
})).catch(() => {
|
|
234
|
+
this.logDebug(`Couldn't subscribe to ${path}, ${method}`);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
this.subscriptions.push({
|
|
238
|
+
path,
|
|
239
|
+
method,
|
|
240
|
+
callback,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
close() {
|
|
245
|
+
this.websocket.close();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export {WsClient};
|
package/test.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {toChainPack, fromChainPack} from './src/chainpack.ts';
|
|
2
|
+
import {toCpon, fromCpon} from './src/cpon.ts';
|
|
3
|
+
import {type DateTime} from './src/rpcvalue.ts';
|
|
4
|
+
|
|
5
|
+
const checkEq = (e1: string | number, e2: string | number) => {
|
|
6
|
+
if (e1 !== e2) {
|
|
7
|
+
throw new Error(`test check error: ${e1} === ${e2}`);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
for (const lst of [
|
|
11
|
+
[((2 ** 31) - 1) + 'u', null],
|
|
12
|
+
[((2 ** 32) - 1) + 'u', null], // too big for JS bitwise operations
|
|
13
|
+
[String((2 ** 31) - 1), null],
|
|
14
|
+
[String(-((2 ** 30) - 1)), null],
|
|
15
|
+
[String((2 ** 53) - 1), null], // Number.MAX_SAFE_INTEGER
|
|
16
|
+
[String(-((2 ** 53) - 1)), null], // Number.MIN_SAFE_INTEGER
|
|
17
|
+
[String((2 ** 32) - 1), null], // too big for JS bitwise operations
|
|
18
|
+
['true', null],
|
|
19
|
+
['false', null],
|
|
20
|
+
['null', null],
|
|
21
|
+
['1u', null],
|
|
22
|
+
['134', null],
|
|
23
|
+
['7', null],
|
|
24
|
+
['-2', null],
|
|
25
|
+
['0xab', '171'],
|
|
26
|
+
['-0xCD', '-205'],
|
|
27
|
+
['0x1a2b3c4d', '439041101'],
|
|
28
|
+
['223.', null],
|
|
29
|
+
['2.30', null],
|
|
30
|
+
['12.3e-10', '123e-11'],
|
|
31
|
+
['-0.00012', '-12e-5'],
|
|
32
|
+
['-1234567890.', '-1234567890.'],
|
|
33
|
+
['"foo"', null],
|
|
34
|
+
['b"a1\\d2"', null],
|
|
35
|
+
['x"6131d2"', 'b"a1\\d2"'],
|
|
36
|
+
['[]', null],
|
|
37
|
+
['[1]', null],
|
|
38
|
+
['[1,]', '[1]'],
|
|
39
|
+
['[1,2,3]', null],
|
|
40
|
+
['[[]]', null],
|
|
41
|
+
['{"foo":"bar"}', null],
|
|
42
|
+
['{"login":{"password":"lautr","type":"PLAIN","user":"revitest"}}', null],
|
|
43
|
+
['i{1:2}', null],
|
|
44
|
+
['i{\n\t1: "bar",\n\t345u : "foo",\n}', 'i{1:"bar",345:"foo"}'],
|
|
45
|
+
['[1u,{"a":1},2.30]', null],
|
|
46
|
+
['<1:2>3', null],
|
|
47
|
+
['[1,<7:8>9]', null],
|
|
48
|
+
['<>1', null],
|
|
49
|
+
['<8:3u>i{2:[[".broker",<1:2>true]]}', null],
|
|
50
|
+
['<1:2,"foo":"bar">i{1:<7:8>9}', null],
|
|
51
|
+
['<1:2,"foo":<5:6>"bar">[1u,{"a":1},2.30]', null],
|
|
52
|
+
['i{1:2 // comment to end of line\n}', 'i{1:2}'],
|
|
53
|
+
[`/*comment 1*/{ /*comment 2*/
|
|
54
|
+
\t"foo"/*comment "3"*/: "bar", //comment to end of line
|
|
55
|
+
\t"baz" : 1,
|
|
56
|
+
/*
|
|
57
|
+
\tmultiline comment
|
|
58
|
+
\t"baz" : 1,
|
|
59
|
+
\t"baz" : 1, // single inside multi
|
|
60
|
+
*/
|
|
61
|
+
}`, '{"foo":"bar","baz":1}'],
|
|
62
|
+
['<1:2>[3,<4:5>6]', null],
|
|
63
|
+
['<4:"svete">i{2:<4:"svete">[0,1]}', null],
|
|
64
|
+
['d"2019-05-03T11:30:00-0700"', 'd"2019-05-03T11:30:00-07"'],
|
|
65
|
+
['d"2018-02-02T00:00:00Z"', null],
|
|
66
|
+
['d"2027-05-03T11:30:12.345+01"', null],
|
|
67
|
+
]) {
|
|
68
|
+
const cpon1 = lst[0];
|
|
69
|
+
const cpon2 = lst[1] ?? cpon1!;
|
|
70
|
+
|
|
71
|
+
console.log('testing', JSON.stringify(cpon1), '\t-------->\t', cpon2);
|
|
72
|
+
const rv1 = fromCpon(cpon1!);
|
|
73
|
+
const cpn1 = toCpon(rv1);
|
|
74
|
+
checkEq(cpn1, cpon2);
|
|
75
|
+
|
|
76
|
+
const cpk1 = toChainPack(rv1);
|
|
77
|
+
const rv2 = fromChainPack(cpk1);
|
|
78
|
+
const cpn2 = toCpon(rv2);
|
|
79
|
+
checkEq(cpn1, cpn2);
|
|
80
|
+
}
|
|
81
|
+
// same points in time
|
|
82
|
+
const v1 = fromCpon('d"2017-05-03T18:30:00Z"') as DateTime;
|
|
83
|
+
const v2 = fromCpon('d"2017-05-03T22:30:00+04"') as DateTime;
|
|
84
|
+
const v3 = fromCpon('d"2017-05-03T11:30:00-0700"') as DateTime;
|
|
85
|
+
const v4 = fromCpon('d"2017-05-03T15:00:00-0330"') as DateTime;
|
|
86
|
+
checkEq(v1.getTime(), v2.getTime());
|
|
87
|
+
checkEq(v2.getTime(), v3.getTime());
|
|
88
|
+
checkEq(v3.getTime(), v4.getTime());
|
|
89
|
+
checkEq(v4.getTime(), v1.getTime());
|
|
90
|
+
|
|
91
|
+
{
|
|
92
|
+
const c1 = '{"1":"a"}';
|
|
93
|
+
const v1 = fromCpon(c1);
|
|
94
|
+
const c2 = toCpon(v1);
|
|
95
|
+
console.log(c1, 'vs.', c2);
|
|
96
|
+
checkEq(c1, c2);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
{
|
|
100
|
+
const c1 = 'i{1:"a"}';
|
|
101
|
+
const v1 = fromCpon(c1);
|
|
102
|
+
const c2 = toCpon(v1);
|
|
103
|
+
console.log(c1, 'vs.', c2);
|
|
104
|
+
checkEq(c1, c2);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log('PASSED');
|