libshv-js 3.9.1 → 3.11.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 +1 -0
- package/package.json +2 -2
- package/src/rpcvalue.ts +19 -0
- package/src/ws-client.ts +7 -3
- package/src/zod.ts +73 -17
- package/test_zod.ts +24 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libshv-js",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.11.0",
|
|
4
4
|
"description": "Typescript implementation of libshv",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"./zod.ts": "./src/zod.ts"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"zod": "^3.
|
|
31
|
+
"zod": "^3.25.67"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@typescript-eslint/parser": "^8.27.0",
|
package/src/rpcvalue.ts
CHANGED
|
@@ -102,6 +102,25 @@ const isIMap = (x: unknown): x is IMap => typeof x === 'object' && (x as IMap)[s
|
|
|
102
102
|
|
|
103
103
|
const isMetaMap = (x: unknown): x is IMap => typeof x === 'object' && (x as MetaMap)[shvMapType] === 'metamap';
|
|
104
104
|
|
|
105
|
+
export const typeName = (x: unknown): string => {
|
|
106
|
+
switch (true) {
|
|
107
|
+
case x === undefined: return 'Null';
|
|
108
|
+
case typeof x === 'boolean': return 'Bool';
|
|
109
|
+
case typeof x === 'string': return 'String';
|
|
110
|
+
case x instanceof ArrayBuffer: return 'Blob';
|
|
111
|
+
case x instanceof UInt: return 'UInt';
|
|
112
|
+
case typeof x === 'number': return 'Int';
|
|
113
|
+
case x instanceof Double: return 'Double';
|
|
114
|
+
case x instanceof Decimal: return 'Decimal';
|
|
115
|
+
case Array.isArray(x): return 'List';
|
|
116
|
+
case x instanceof Date: return 'Date';
|
|
117
|
+
case isShvMap(x): return 'ShvMap';
|
|
118
|
+
case isIMap(x): return 'IMap';
|
|
119
|
+
case isMetaMap(x): return 'MetaMap';
|
|
120
|
+
default: return '<unknown type>';
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
105
124
|
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> => ({
|
|
106
125
|
...x,
|
|
107
126
|
[shvMapType]: 'metamap',
|
package/src/ws-client.ts
CHANGED
|
@@ -58,7 +58,7 @@ type WsClientOptionsLogin = WsClientOptionsCommon & {
|
|
|
58
58
|
onConnected: () => void;
|
|
59
59
|
onConnectionFailure: (error: Error) => void;
|
|
60
60
|
onDisconnected: () => void;
|
|
61
|
-
onRequest: (shvPath: string, method: string, param: RpcValue, delay: (progress: number) => void) => Promise<RpcValue>;
|
|
61
|
+
onRequest: (shvPath: string, method: string, param: RpcValue, delay: (progress: number) => void) => RpcValue | Promise<RpcValue>;
|
|
62
62
|
};
|
|
63
63
|
|
|
64
64
|
type WsClientOptionsWorkflows = WsClientOptionsCommon & {
|
|
@@ -162,6 +162,7 @@ class WsClient {
|
|
|
162
162
|
|
|
163
163
|
this.websocket.addEventListener('message', async (evt: MessageEvent<ArrayBuffer>) => {
|
|
164
164
|
const rpcVal = dataToRpcValue(evt.data);
|
|
165
|
+
|
|
165
166
|
const rpcMsg = RpcMessageZod.parse(rpcVal);
|
|
166
167
|
this.logDebug(`message received: ${toCpon(rpcMsg)}`);
|
|
167
168
|
|
|
@@ -193,12 +194,15 @@ class WsClient {
|
|
|
193
194
|
}));
|
|
194
195
|
};
|
|
195
196
|
|
|
197
|
+
const result = this.options.onRequest(rpcMsg.meta[RPC_MESSAGE_SHV_PATH], rpcMsg.meta[RPC_MESSAGE_METHOD], rpcMsg.value[RPC_MESSAGE_PARAMS], sendDelay);
|
|
196
198
|
respond(makeIMap({
|
|
197
|
-
[RPC_MESSAGE_RESULT]: await
|
|
199
|
+
[RPC_MESSAGE_RESULT]: result instanceof Promise ? await result : result,
|
|
198
200
|
}));
|
|
199
201
|
} catch (error: unknown) {
|
|
200
202
|
const sendError = (error: RpcError) => {
|
|
201
|
-
respond(
|
|
203
|
+
respond(makeIMap({
|
|
204
|
+
[RPC_MESSAGE_ERROR]: error.err_info,
|
|
205
|
+
}));
|
|
202
206
|
};
|
|
203
207
|
|
|
204
208
|
switch (true) {
|
package/src/zod.ts
CHANGED
|
@@ -1,21 +1,44 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import {z, type ZodType} from 'zod';
|
|
3
|
-
import {Decimal, Double,
|
|
2
|
+
import {z, type ZodType} from 'zod/v4';
|
|
3
|
+
import {Decimal, Double, isIMap, isMetaMap, isShvMap, type MetaMap, RpcValue, type RpcValueType, RpcValueWithMetaData, shvMapType, typeName, UInt} from './rpcvalue';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
const implMakeMapParser = <MapBrand extends string, ObjectParser extends ZodType<object>>(mapValidator: (val: unknown) => boolean, _mapBrand: MapBrand, mapName: string, objectParser: ObjectParser) => z.custom<z.infer<ObjectParser> & {[shvMapType]: MapBrand}>().check(ctx => {
|
|
6
|
+
if (!mapValidator(ctx.value)) {
|
|
7
|
+
ctx.issues.push({
|
|
8
|
+
expected: 'map',
|
|
9
|
+
code: 'invalid_type',
|
|
10
|
+
input: ctx.value,
|
|
11
|
+
message: `Invalid input: expected ${mapName}, received ${typeName(ctx.value)}`,
|
|
12
|
+
});
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// For record, Zod finds out map brand, and calls the record's value parser with the value. There's no way to detect
|
|
17
|
+
// it, because Zod does not supply the parser with the key. To prevent this, remove the key before passing the value
|
|
18
|
+
// to Zod. This unfortunately means that we need to copy the whole object. :/
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
20
|
+
const {[shvMapType]: zodMustNotHaveThis, ...rest} = {...ctx.value};
|
|
21
|
+
const parsedObject = objectParser.safeParse(rest);
|
|
22
|
+
|
|
23
|
+
if (!parsedObject.success) {
|
|
24
|
+
ctx.issues = parsedObject.error.issues;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const map = <T extends Record<string, ZodType<any>>>(schema: T) => implMakeMapParser(isShvMap, 'map', 'ShvMap', z.object(schema));
|
|
29
|
+
export const imap = <T extends Record<number, ZodType<any>>>(schema: T) => implMakeMapParser(isIMap, 'imap', 'IMap', z.object(schema));
|
|
30
|
+
export const metamap = <T extends Record<string | number, ZodType<any>>>(schema: T) => implMakeMapParser(isMetaMap, 'metamap', 'MetaMap', z.object(schema));
|
|
31
|
+
export const recmap = <T extends ZodType<any>>(schema: T) => implMakeMapParser(val => isShvMap(val) || isIMap(val), 'map', 'ShvMap', z.record(z.any(), schema));
|
|
9
32
|
export const int = () => z.number();
|
|
10
33
|
|
|
11
34
|
export const uint = () => z.instanceof(UInt<number>);
|
|
12
35
|
export const double = () => z.instanceof(Double);
|
|
13
36
|
export const decimal = () => z.instanceof(Decimal);
|
|
14
37
|
export const blob = () => z.instanceof(ArrayBuffer);
|
|
15
|
-
export const list = () => z.array(z.lazy(rpcvalue));
|
|
38
|
+
export const list: () => z.ZodArray<z.ZodLazy<z.ZodType<RpcValue>>> = () => z.array(z.lazy<z.ZodType<RpcValue>>(rpcvalue));
|
|
16
39
|
|
|
17
40
|
const withMetaInstanceParser = z.instanceof(RpcValueWithMetaData);
|
|
18
|
-
export const rpcvalue: () => ZodType<RpcValue
|
|
41
|
+
export const rpcvalue: () => ZodType<RpcValue> = () => z.lazy(() => z.union([
|
|
19
42
|
z.undefined(),
|
|
20
43
|
z.boolean(),
|
|
21
44
|
z.number(),
|
|
@@ -26,15 +49,48 @@ export const rpcvalue: () => ZodType<RpcValue, any, any> = () => z.union([
|
|
|
26
49
|
z.string(),
|
|
27
50
|
z.date(),
|
|
28
51
|
list(),
|
|
29
|
-
|
|
30
|
-
imap(),
|
|
52
|
+
recmap(rpcvalue()),
|
|
31
53
|
withMetaInstanceParser,
|
|
32
|
-
]);
|
|
54
|
+
]) as z.ZodType<RpcValue>);
|
|
55
|
+
|
|
56
|
+
export const withMeta = <MetaSchema extends MetaMap, ValueSchema extends RpcValueType>(metaParser: ZodType<MetaSchema>, valueParser: ZodType<ValueSchema>) =>
|
|
57
|
+
z.custom<RpcValueWithMetaData<z.infer<typeof metaParser>, z.infer<typeof valueParser>>>().check(ctx => {
|
|
58
|
+
if (!withMetaInstanceParser.safeParse(ctx.value).success) {
|
|
59
|
+
ctx.issues.push({
|
|
60
|
+
expected: 'map',
|
|
61
|
+
code: 'invalid_type',
|
|
62
|
+
input: ctx.value,
|
|
63
|
+
message: 'Invalid input: expected a value with metadata, got a value with no metadata}',
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const parsedMeta = metaParser.safeParse(ctx.value.meta);
|
|
69
|
+
if (!parsedMeta.success) {
|
|
70
|
+
ctx.issues = [
|
|
71
|
+
{
|
|
72
|
+
expected: 'map',
|
|
73
|
+
code: 'invalid_type',
|
|
74
|
+
input: ctx.value,
|
|
75
|
+
message: 'Wrong RpcValueWithMetaData meta',
|
|
76
|
+
},
|
|
77
|
+
...parsedMeta.error.issues,
|
|
78
|
+
];
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
33
81
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
82
|
+
const parsedValue = valueParser.safeParse(ctx.value.value);
|
|
83
|
+
if (!parsedValue.success) {
|
|
84
|
+
ctx.issues = [
|
|
85
|
+
{
|
|
86
|
+
expected: 'map',
|
|
87
|
+
code: 'invalid_type',
|
|
88
|
+
input: ctx.value,
|
|
89
|
+
message: 'Wrong RpcValueWithMetaData value',
|
|
90
|
+
},
|
|
91
|
+
...parsedValue.error.issues,
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
});
|
|
39
95
|
|
|
40
|
-
export * from 'zod';
|
|
96
|
+
export * from 'zod/v4';
|
package/test_zod.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { fromCpon } from './src/cpon';
|
|
2
|
+
import { makeIMap, makeMap, makeMetaMap } from './src/rpcvalue';
|
|
3
|
+
import * as z from './src/zod';
|
|
4
|
+
|
|
5
|
+
z.map({ ok: z.boolean(), })
|
|
6
|
+
.parse(makeMap({ok: true}))
|
|
7
|
+
.ok;
|
|
8
|
+
z.imap({ 1: z.boolean(), })
|
|
9
|
+
.parse(makeIMap({1: true}))[1];
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
const x = z.metamap({1: z.boolean(), ok: z.boolean()})
|
|
13
|
+
.parse(makeMetaMap({1: true, ok: true}))
|
|
14
|
+
x.ok;
|
|
15
|
+
x[1];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const SiteInfoZod = z.recmap(z.map({
|
|
19
|
+
optKey: z.map({
|
|
20
|
+
}).optional(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
const lol = fromCpon(`{"key": {}}`);
|
|
24
|
+
SiteInfoZod.parse(lol);
|