statewire 0.17.0 → 0.17.1
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/dist/client/connection.js +3 -2
- package/dist/client/connection.js.map +1 -1
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +6 -5
- package/dist/client/session.js.map +1 -1
- package/dist/document.d.ts +25 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +88 -0
- package/dist/document.js.map +1 -0
- package/dist/host/draft.js +1 -1
- package/dist/host/socket.d.ts +2 -2
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +5 -5
- package/dist/host/socket.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/wire/documents.js +3 -3
- package/dist/wire/documents.js.map +1 -1
- package/dist/wire/meta.d.ts +3 -18
- package/dist/wire/meta.d.ts.map +1 -1
- package/dist/wire/meta.js +8 -57
- package/dist/wire/meta.js.map +1 -1
- package/dist/wire/ops.js +1 -1
- package/package.json +1 -1
- package/src/client/session.ts +4 -8
- package/src/document.ts +185 -0
- package/src/host/socket.ts +8 -7
- package/src/index.ts +2 -0
- package/src/wire/meta.ts +11 -94
package/dist/wire/meta.js
CHANGED
|
@@ -1,36 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { encodeDocument, reviveDocumentValue } from "../document.js";
|
|
2
2
|
//#region src/wire/meta.ts
|
|
3
|
-
const fault = (message) => {
|
|
4
|
-
throw new Error(`statewire: protocol error: ${message}`);
|
|
5
|
-
};
|
|
6
|
-
const isPath = (path) => Array.isArray(path) && path.every((seg) => typeof seg === "string" || typeof seg === "number" && Number.isInteger(seg));
|
|
7
|
-
/** Revive meta-typed locations inside a wire value (in place). Clients skip
|
|
8
|
-
* unknown types; hosts pass `rejectUnknown` to fault on them instead. */
|
|
9
|
-
const reviveValue = (value, meta, { rejectUnknown = false } = {}) => {
|
|
10
|
-
if (!Array.isArray(meta)) return fault("meta must be an array");
|
|
11
|
-
for (const entry of meta) {
|
|
12
|
-
const path = typeof entry === "object" && entry !== null ? entry.path : null;
|
|
13
|
-
if (!isPath(path)) return fault("meta entry must carry a path array");
|
|
14
|
-
if (entry.type !== "date") {
|
|
15
|
-
if (rejectUnknown) return fault(`unknown meta type: ${String(entry.type)}`);
|
|
16
|
-
continue;
|
|
17
|
-
}
|
|
18
|
-
const walked = walkStatePath(value, path, "resolve");
|
|
19
|
-
if (walked === void 0) return fault(`meta path ${path.join(".")} does not resolve`);
|
|
20
|
-
if (typeof walked.leaf !== "string") return fault(`meta path ${path.join(".")} must name a string`);
|
|
21
|
-
const date = new Date(walked.leaf);
|
|
22
|
-
if (!/(?:T| )\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})$/i.test(walked.leaf) || Number.isNaN(date.getTime())) return fault(`invalid date at meta path ${path.join(".")}`);
|
|
23
|
-
if (walked.parent === void 0) return date;
|
|
24
|
-
walked.parent[walked.key] = date;
|
|
25
|
-
}
|
|
26
|
-
return value;
|
|
27
|
-
};
|
|
28
3
|
const reviveOps = (ops) => ops.map((op) => {
|
|
29
4
|
if (typeof op !== "object" || op === null || !("meta" in op)) return op;
|
|
30
5
|
const { meta, ...rest } = op;
|
|
31
6
|
return {
|
|
32
7
|
...rest,
|
|
33
|
-
value:
|
|
8
|
+
value: reviveDocumentValue(rest.value, meta)
|
|
34
9
|
};
|
|
35
10
|
});
|
|
36
11
|
const reviveResponse = (response) => {
|
|
@@ -38,41 +13,17 @@ const reviveResponse = (response) => {
|
|
|
38
13
|
const { meta, ...rest } = response;
|
|
39
14
|
return {
|
|
40
15
|
...rest,
|
|
41
|
-
payload:
|
|
16
|
+
payload: reviveDocumentValue(rest.payload, meta)
|
|
42
17
|
};
|
|
43
18
|
};
|
|
44
|
-
const walkDates = (node, path, meta) => {
|
|
45
|
-
if (node instanceof Date) {
|
|
46
|
-
meta.push({
|
|
47
|
-
path,
|
|
48
|
-
type: "date"
|
|
49
|
-
});
|
|
50
|
-
return node.toISOString();
|
|
51
|
-
}
|
|
52
|
-
if (Array.isArray(node)) return node.map((item, i) => walkDates(item, [...path, i], meta));
|
|
53
|
-
if (typeof node === "object" && node !== null) return Object.fromEntries(Object.keys(node).map((key) => [key, walkDates(node[key], [...path, key], meta)]));
|
|
54
|
-
return node;
|
|
55
|
-
};
|
|
56
|
-
/** Encode Date instances inside command params as canonical ISO strings,
|
|
57
|
-
* recording their paths (starting at the param index) in meta. */
|
|
58
19
|
const encodeParams = (params) => {
|
|
59
|
-
const meta =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
params: encoded,
|
|
63
|
-
meta
|
|
64
|
-
} : { params: encoded };
|
|
65
|
-
};
|
|
66
|
-
/** Encode Date instances inside a wire value, recording their paths in meta. */
|
|
67
|
-
const encodeValue = (value) => {
|
|
68
|
-
const meta = [];
|
|
69
|
-
const encoded = walkDates(value, [], meta);
|
|
70
|
-
return meta.length > 0 ? {
|
|
71
|
-
value: encoded,
|
|
20
|
+
const { value, meta } = encodeDocument(params);
|
|
21
|
+
return meta ? {
|
|
22
|
+
params: value,
|
|
72
23
|
meta
|
|
73
|
-
} : {
|
|
24
|
+
} : { params: value };
|
|
74
25
|
};
|
|
75
26
|
//#endregion
|
|
76
|
-
export { encodeParams,
|
|
27
|
+
export { encodeParams, reviveOps, reviveResponse, reviveDocumentValue as reviveValue };
|
|
77
28
|
|
|
78
29
|
//# sourceMappingURL=meta.js.map
|
package/dist/wire/meta.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"meta.js","names":["
|
|
1
|
+
{"version":3,"file":"meta.js","names":["encodeDocument","reviveDocumentValue","reviveValue","StatewireDocument","reviveOps","ops","map","op","meta","rest","value","reviveResponse","payload","response","T","undefined","encodeParams","params","MetaEntry"],"sources":["../../src/wire/meta.ts"],"sourcesContent":["import {\n encodeDocument,\n reviveDocumentValue as reviveValue,\n} from \"../document\";\nimport type { StatewireDocument } from \"../document\";\n\nexport { reviveDocumentValue as reviveValue } from \"../document\";\n\nexport const reviveOps = (ops: unknown[]): unknown[] =>\n ops.map((op) => {\n if (typeof op !== \"object\" || op === null || !(\"meta\" in op)) return op;\n const { meta, ...rest } = op as { meta: unknown; value?: unknown };\n return { ...rest, value: reviveValue(rest.value, meta) };\n });\n\nexport const reviveResponse = <T extends { payload?: unknown; meta?: unknown }>(\n response: T,\n): T => {\n if (response.meta === undefined) return response;\n const { meta, ...rest } = response;\n return { ...rest, payload: reviveValue(rest.payload, meta) } as T;\n};\n\nexport const encodeParams = (\n params: readonly unknown[],\n): { params: unknown[]; meta?: StatewireDocument.MetaEntry[] } => {\n const { value, meta } = encodeDocument(params);\n return meta\n ? { params: value as unknown[], meta }\n : { params: value as unknown[] };\n};\n"],"mappings":";;AAQA,MAAaI,aAAaC,QACxBA,IAAIC,KAAKC,OAAO;CACd,IAAI,OAAOA,OAAO,YAAYA,OAAO,QAAQ,EAAE,UAAUA,KAAK,OAAOA;CACrE,MAAM,EAAEC,MAAM,GAAGC,SAASF;CAC1B,OAAO;EAAE,GAAGE;EAAMC,OAAOR,oBAAYO,KAAKC,OAAOF,IAAI;CAAE;AACzD,CAAC;AAEH,MAAaG,kBACXE,aACM;CACN,IAAIA,SAASL,SAASO,KAAAA,GAAW,OAAOF;CACxC,MAAM,EAAEL,MAAM,GAAGC,SAASI;CAC1B,OAAO;EAAE,GAAGJ;EAAMG,SAASV,oBAAYO,KAAKG,SAASJ,IAAI;CAAE;AAC7D;AAEA,MAAaQ,gBACXC,WACgE;CAChE,MAAM,EAAEP,OAAOF,SAASR,eAAeiB,MAAM;CAC7C,OAAOT,OACH;EAAES,QAAQP;EAAoBF;CAAK,IACnC,EAAES,QAAQP,MAAmB;AACnC"}
|
package/dist/wire/ops.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { recordMutations } from "../util/recordMutations.js";
|
|
2
1
|
import { isUnsafeKey, walkStatePath } from "../util/statePath.js";
|
|
2
|
+
import { recordMutations } from "../util/recordMutations.js";
|
|
3
3
|
//#region src/wire/ops.ts
|
|
4
4
|
const warnStripped = (key, parent) => {
|
|
5
5
|
if (typeof process === "undefined" || process.env["NODE_ENV"] !== "production") console.warn(`statewire: stripped unsafe key "${key}"` + (parent === void 0 ? "" : ` under "${parent}"`));
|
package/package.json
CHANGED
package/src/client/session.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { encodeDocument } from "../document";
|
|
1
2
|
import {
|
|
2
3
|
DEFAULT_PROTOCOL,
|
|
3
4
|
WIRE_VERSION,
|
|
@@ -18,12 +19,7 @@ import type { StatewirePath } from "../wire/ops";
|
|
|
18
19
|
import { MAX_COMMANDS_PER_FRAME } from "../wire/limits";
|
|
19
20
|
import { TERMINAL } from "./connection";
|
|
20
21
|
import type { StatewireCmd, StatewireSyn } from "./connection";
|
|
21
|
-
import {
|
|
22
|
-
encodeParams,
|
|
23
|
-
encodeValue,
|
|
24
|
-
reviveResponse,
|
|
25
|
-
reviveValue,
|
|
26
|
-
} from "../wire/meta";
|
|
22
|
+
import { encodeParams, reviveResponse, reviveValue } from "../wire/meta";
|
|
27
23
|
import { bindCommandsHost } from "../host/host";
|
|
28
24
|
import type { CommandsInvocation, CommandsListener } from "../host/host";
|
|
29
25
|
import type { Statewire } from "../types";
|
|
@@ -1165,7 +1161,7 @@ export const retryFailed = (
|
|
|
1165
1161
|
|
|
1166
1162
|
const contextSnapshot = (documents: StatewireDocuments.State) =>
|
|
1167
1163
|
[...documents.documents].map(([doc, d]) => {
|
|
1168
|
-
const encoded =
|
|
1164
|
+
const encoded = encodeDocument(d.value);
|
|
1169
1165
|
return {
|
|
1170
1166
|
op: "mount",
|
|
1171
1167
|
...(doc === 0 ? {} : { doc }),
|
|
@@ -1253,7 +1249,7 @@ const setContext = (
|
|
|
1253
1249
|
): void => {
|
|
1254
1250
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1255
1251
|
throw new Error("statewire: context must be a plain object");
|
|
1256
|
-
const encoded =
|
|
1252
|
+
const encoded = encodeDocument(value);
|
|
1257
1253
|
changeContext(s, (documents) => {
|
|
1258
1254
|
const found = [...documents.documents].find(
|
|
1259
1255
|
([, d]) => d.protocol === protocol && d.main,
|
package/src/document.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { isUnsafeKey, walkStatePath } from "./util/statePath";
|
|
2
|
+
|
|
3
|
+
export namespace StatewireDocument {
|
|
4
|
+
export type Value =
|
|
5
|
+
| null
|
|
6
|
+
| boolean
|
|
7
|
+
| number
|
|
8
|
+
| string
|
|
9
|
+
| Value[]
|
|
10
|
+
| { [key: string]: Value };
|
|
11
|
+
export type MetaEntry = { path: (string | number)[]; type: "date" };
|
|
12
|
+
export type Encoded = { value: Value; meta?: MetaEntry[] };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const invalid = (message: string): never => {
|
|
16
|
+
throw new Error(`statewire: protocol error: invalid document: ${message}`);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
20
|
+
typeof value === "object" &&
|
|
21
|
+
value !== null &&
|
|
22
|
+
(Object.getPrototypeOf(value) === Object.prototype ||
|
|
23
|
+
Object.getPrototypeOf(value) === null);
|
|
24
|
+
|
|
25
|
+
const copyValue = (
|
|
26
|
+
value: unknown,
|
|
27
|
+
dates: boolean,
|
|
28
|
+
meta: StatewireDocument.MetaEntry[],
|
|
29
|
+
path: (string | number)[] = [],
|
|
30
|
+
ancestors = new Set<object>(),
|
|
31
|
+
): StatewireDocument.Value => {
|
|
32
|
+
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
33
|
+
return value;
|
|
34
|
+
if (typeof value === "number") {
|
|
35
|
+
if (
|
|
36
|
+
!Number.isFinite(value) ||
|
|
37
|
+
(Number.isInteger(value) && !Number.isSafeInteger(value))
|
|
38
|
+
)
|
|
39
|
+
invalid(
|
|
40
|
+
`number at ${JSON.stringify(path)} must be finite and within the safe integer range`,
|
|
41
|
+
);
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
if (dates && value instanceof Date) {
|
|
45
|
+
if (
|
|
46
|
+
!Number.isFinite(value.getTime()) ||
|
|
47
|
+
value.getUTCFullYear() < 1 ||
|
|
48
|
+
value.getUTCFullYear() > 9999
|
|
49
|
+
)
|
|
50
|
+
invalid(
|
|
51
|
+
`date at ${JSON.stringify(path)} must have a year from 0001 to 9999`,
|
|
52
|
+
);
|
|
53
|
+
meta.push({ path, type: "date" });
|
|
54
|
+
return value.toISOString();
|
|
55
|
+
}
|
|
56
|
+
if (!Array.isArray(value) && !isObject(value))
|
|
57
|
+
return invalid(`unsupported value at ${JSON.stringify(path)}`);
|
|
58
|
+
if (ancestors.has(value)) invalid(`cycle at ${JSON.stringify(path)}`);
|
|
59
|
+
ancestors.add(value);
|
|
60
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
61
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
62
|
+
if (Array.isArray(value) && key === "length") continue;
|
|
63
|
+
if (typeof key !== "string")
|
|
64
|
+
invalid(`symbol key at ${JSON.stringify(path)}`);
|
|
65
|
+
const descriptor = descriptors[key as string]!;
|
|
66
|
+
if (!("value" in descriptor) || !descriptor.enumerable)
|
|
67
|
+
invalid(
|
|
68
|
+
`property at ${JSON.stringify([...path, key])} must be enumerable data`,
|
|
69
|
+
);
|
|
70
|
+
if (isUnsafeKey(key as string, path.at(-1)))
|
|
71
|
+
invalid(`unsafe key at ${JSON.stringify([...path, key])}`);
|
|
72
|
+
if (
|
|
73
|
+
Array.isArray(value) &&
|
|
74
|
+
(!/^(0|[1-9]\d*)$/.test(key as string) || Number(key) >= value.length)
|
|
75
|
+
)
|
|
76
|
+
invalid(
|
|
77
|
+
`unsupported array property at ${JSON.stringify([...path, key])}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
let result: StatewireDocument.Value;
|
|
81
|
+
if (Array.isArray(value)) {
|
|
82
|
+
result = Array.from({ length: value.length }, (_, index) => {
|
|
83
|
+
if (!Object.hasOwn(value, index))
|
|
84
|
+
invalid(`sparse array at ${JSON.stringify(path)}`);
|
|
85
|
+
return copyValue(
|
|
86
|
+
descriptors[index]!.value,
|
|
87
|
+
dates,
|
|
88
|
+
meta,
|
|
89
|
+
[...path, index],
|
|
90
|
+
ancestors,
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
result = Object.fromEntries(
|
|
95
|
+
Object.keys(descriptors).map((key) => [
|
|
96
|
+
key,
|
|
97
|
+
copyValue(
|
|
98
|
+
descriptors[key]!.value,
|
|
99
|
+
dates,
|
|
100
|
+
meta,
|
|
101
|
+
[...path, key],
|
|
102
|
+
ancestors,
|
|
103
|
+
),
|
|
104
|
+
]),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
ancestors.delete(value);
|
|
108
|
+
return result;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Encode a document value as detached JSON data with date metadata. */
|
|
112
|
+
export const encodeDocument = (value: unknown): StatewireDocument.Encoded => {
|
|
113
|
+
const meta: StatewireDocument.MetaEntry[] = [];
|
|
114
|
+
const encoded = copyValue(value, true, meta);
|
|
115
|
+
return meta.length ? { value: encoded, meta } : { value: encoded };
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** Validate and decode a document into detached values with native dates. */
|
|
119
|
+
export const decodeDocument = (encoded: unknown): unknown => {
|
|
120
|
+
const document = copyValue(encoded, false, []);
|
|
121
|
+
if (
|
|
122
|
+
!isObject(document) ||
|
|
123
|
+
!Object.hasOwn(document, "value") ||
|
|
124
|
+
Object.keys(document).some((key) => key !== "value" && key !== "meta")
|
|
125
|
+
)
|
|
126
|
+
return invalid("expected an object with value and optional meta");
|
|
127
|
+
const value: unknown = document.value;
|
|
128
|
+
if (!Object.hasOwn(document, "meta")) return value;
|
|
129
|
+
if (!Array.isArray(document.meta)) return invalid("meta must be an array");
|
|
130
|
+
return reviveDocumentValue(value, document.meta, {
|
|
131
|
+
rejectUnknown: true,
|
|
132
|
+
canonical: true,
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const reviveDocumentValue = (
|
|
137
|
+
value: unknown,
|
|
138
|
+
meta: unknown,
|
|
139
|
+
{ rejectUnknown = false, canonical = false } = {},
|
|
140
|
+
): unknown => {
|
|
141
|
+
if (!Array.isArray(meta)) return invalid("meta must be an array");
|
|
142
|
+
const seen = new Set<string>();
|
|
143
|
+
for (const entry of meta) {
|
|
144
|
+
if (
|
|
145
|
+
!isObject(entry) ||
|
|
146
|
+
!Array.isArray(entry.path) ||
|
|
147
|
+
(canonical &&
|
|
148
|
+
Object.keys(entry).some((key) => key !== "path" && key !== "type")) ||
|
|
149
|
+
!entry.path.every(
|
|
150
|
+
(segment) =>
|
|
151
|
+
typeof segment === "string" ||
|
|
152
|
+
(typeof segment === "number" &&
|
|
153
|
+
Number.isSafeInteger(segment) &&
|
|
154
|
+
segment >= 0),
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
return invalid("meta entry requires a path array and type date");
|
|
158
|
+
if (entry.type !== "date") {
|
|
159
|
+
if (rejectUnknown) invalid(`unknown meta type: ${String(entry.type)}`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const path = entry.path as (string | number)[];
|
|
163
|
+
const identity = JSON.stringify(path);
|
|
164
|
+
if (seen.has(identity)) invalid(`duplicate meta path ${identity}`);
|
|
165
|
+
seen.add(identity);
|
|
166
|
+
const walked = walkStatePath(value, path, "resolve");
|
|
167
|
+
if (!walked || typeof walked.leaf !== "string")
|
|
168
|
+
return invalid(`meta path ${identity} must resolve to a string`);
|
|
169
|
+
const date = new Date(walked.leaf);
|
|
170
|
+
if (
|
|
171
|
+
canonical
|
|
172
|
+
? !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(walked.leaf) ||
|
|
173
|
+
!Number.isFinite(date.getTime()) ||
|
|
174
|
+
date.getUTCFullYear() < 1 ||
|
|
175
|
+
date.toISOString() !== walked.leaf
|
|
176
|
+
: !/(?:T| )\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})$/i.test(
|
|
177
|
+
walked.leaf,
|
|
178
|
+
) || !Number.isFinite(date.getTime())
|
|
179
|
+
)
|
|
180
|
+
return invalid(`invalid date at meta path ${identity}`);
|
|
181
|
+
if (walked.parent === undefined) value = date;
|
|
182
|
+
else (walked.parent as Record<string | number, unknown>)[walked.key] = date;
|
|
183
|
+
}
|
|
184
|
+
return value;
|
|
185
|
+
};
|
package/src/host/socket.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { encodeDocument } from "../document";
|
|
1
2
|
import {
|
|
2
3
|
DEFAULT_PROTOCOL,
|
|
3
4
|
WIRE_VERSION,
|
|
@@ -15,7 +16,7 @@ import { StatewireSendError } from "../types";
|
|
|
15
16
|
import type { Statewire } from "../types";
|
|
16
17
|
import type { StatewireHost } from "../host";
|
|
17
18
|
import type { StatewireHostInternal } from "../host-internal";
|
|
18
|
-
import {
|
|
19
|
+
import { reviveValue } from "../wire/meta";
|
|
19
20
|
import {
|
|
20
21
|
COMMAND_CAPACITY_RETRY_MS,
|
|
21
22
|
MAX_COMMANDS_PER_FRAME,
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
MAX_INFLIGHT_COMMANDS_PER_HOST,
|
|
26
27
|
} from "../wire/limits";
|
|
27
28
|
import { createMacrotaskPoster } from "../util/macrotask";
|
|
28
|
-
import type {
|
|
29
|
+
import type { StatewireDocument } from "../document";
|
|
29
30
|
import type { StatewirePath } from "../wire/ops";
|
|
30
31
|
import type { StatewireCmd, StatewireEnvelope } from "../client/connection";
|
|
31
32
|
import { frames as serveFrames, stream as serveStream } from "./http";
|
|
@@ -79,7 +80,7 @@ type WireOp = {
|
|
|
79
80
|
op: string;
|
|
80
81
|
path?: StatewirePath;
|
|
81
82
|
value?: unknown;
|
|
82
|
-
meta?:
|
|
83
|
+
meta?: StatewireDocument.MetaEntry[];
|
|
83
84
|
};
|
|
84
85
|
|
|
85
86
|
type Admission = {
|
|
@@ -242,9 +243,9 @@ const frameIsOversized = (raw: string): boolean =>
|
|
|
242
243
|
|
|
243
244
|
const encodePayload = (
|
|
244
245
|
payload: unknown,
|
|
245
|
-
): { payload?: unknown; meta?:
|
|
246
|
+
): { payload?: unknown; meta?: StatewireDocument.MetaEntry[] } => {
|
|
246
247
|
if (payload === undefined) return {};
|
|
247
|
-
const { value, meta } =
|
|
248
|
+
const { value, meta } = encodeDocument(payload);
|
|
248
249
|
return { payload: value, ...(meta !== undefined && { meta }) };
|
|
249
250
|
};
|
|
250
251
|
|
|
@@ -374,7 +375,7 @@ const recordOp = (
|
|
|
374
375
|
},
|
|
375
376
|
) => {
|
|
376
377
|
if (op.op === "replace" || op.op === "add") {
|
|
377
|
-
const { value, meta } =
|
|
378
|
+
const { value, meta } = encodeDocument(op.value);
|
|
378
379
|
h.pendingOps.push({ ...op, value, ...(meta !== undefined && { meta }) });
|
|
379
380
|
} else {
|
|
380
381
|
h.pendingOps.push(op);
|
|
@@ -1290,7 +1291,7 @@ export const register = (
|
|
|
1290
1291
|
gcClients(h);
|
|
1291
1292
|
notifyClientRecords(h);
|
|
1292
1293
|
ensureHeartbeat(h);
|
|
1293
|
-
const { value, meta } =
|
|
1294
|
+
const { value, meta } = encodeDocument(h.stateHost!.snapshot());
|
|
1294
1295
|
const hello: StatewireEnvelope = {
|
|
1295
1296
|
ops: [
|
|
1296
1297
|
{
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type { Statewire } from "./types";
|
|
2
|
+
export { encodeDocument, decodeDocument } from "./document";
|
|
3
|
+
export type { StatewireDocument } from "./document";
|
|
2
4
|
export { StatewireClient, useStatewire, StatewireResource } from "./statewire";
|
|
3
5
|
export { StatewireHttp } from "./transports/sse";
|
|
4
6
|
export { StatewireWebsocket } from "./transports/ws";
|
package/src/wire/meta.ts
CHANGED
|
@@ -1,60 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
encodeDocument,
|
|
3
|
+
reviveDocumentValue as reviveValue,
|
|
4
|
+
} from "../document";
|
|
5
|
+
import type { StatewireDocument } from "../document";
|
|
2
6
|
|
|
3
|
-
export
|
|
4
|
-
path: (string | number)[];
|
|
5
|
-
type: string;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
const fault = (message: string): never => {
|
|
9
|
-
throw new Error(`statewire: protocol error: ${message}`);
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const isPath = (path: unknown): path is (string | number)[] =>
|
|
13
|
-
Array.isArray(path) &&
|
|
14
|
-
path.every(
|
|
15
|
-
(seg) =>
|
|
16
|
-
typeof seg === "string" ||
|
|
17
|
-
(typeof seg === "number" && Number.isInteger(seg)),
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
/** Revive meta-typed locations inside a wire value (in place). Clients skip
|
|
21
|
-
* unknown types; hosts pass `rejectUnknown` to fault on them instead. */
|
|
22
|
-
export const reviveValue = (
|
|
23
|
-
value: unknown,
|
|
24
|
-
meta: unknown,
|
|
25
|
-
{ rejectUnknown = false } = {},
|
|
26
|
-
): unknown => {
|
|
27
|
-
if (!Array.isArray(meta)) return fault("meta must be an array");
|
|
28
|
-
for (const entry of meta as { path?: unknown; type?: unknown }[]) {
|
|
29
|
-
const path =
|
|
30
|
-
typeof entry === "object" && entry !== null ? entry.path : null;
|
|
31
|
-
if (!isPath(path)) return fault("meta entry must carry a path array");
|
|
32
|
-
if (entry.type !== "date") {
|
|
33
|
-
if (rejectUnknown)
|
|
34
|
-
return fault(`unknown meta type: ${String(entry.type)}`);
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
const walked = walkStatePath(value, path, "resolve");
|
|
38
|
-
if (walked === undefined) {
|
|
39
|
-
return fault(`meta path ${path.join(".")} does not resolve`);
|
|
40
|
-
}
|
|
41
|
-
if (typeof walked.leaf !== "string") {
|
|
42
|
-
return fault(`meta path ${path.join(".")} must name a string`);
|
|
43
|
-
}
|
|
44
|
-
const date = new Date(walked.leaf);
|
|
45
|
-
if (
|
|
46
|
-
!/(?:T| )\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})$/i.test(
|
|
47
|
-
walked.leaf,
|
|
48
|
-
) ||
|
|
49
|
-
Number.isNaN(date.getTime())
|
|
50
|
-
) {
|
|
51
|
-
return fault(`invalid date at meta path ${path.join(".")}`);
|
|
52
|
-
}
|
|
53
|
-
if (walked.parent === undefined) return date;
|
|
54
|
-
(walked.parent as Record<string | number, unknown>)[walked.key] = date;
|
|
55
|
-
}
|
|
56
|
-
return value;
|
|
57
|
-
};
|
|
7
|
+
export { reviveDocumentValue as reviveValue } from "../document";
|
|
58
8
|
|
|
59
9
|
export const reviveOps = (ops: unknown[]): unknown[] =>
|
|
60
10
|
ops.map((op) => {
|
|
@@ -71,44 +21,11 @@ export const reviveResponse = <T extends { payload?: unknown; meta?: unknown }>(
|
|
|
71
21
|
return { ...rest, payload: reviveValue(rest.payload, meta) } as T;
|
|
72
22
|
};
|
|
73
23
|
|
|
74
|
-
const walkDates = (
|
|
75
|
-
node: unknown,
|
|
76
|
-
path: (string | number)[],
|
|
77
|
-
meta: StatewireMetaEntry[],
|
|
78
|
-
): unknown => {
|
|
79
|
-
if (node instanceof Date) {
|
|
80
|
-
meta.push({ path, type: "date" });
|
|
81
|
-
return node.toISOString();
|
|
82
|
-
}
|
|
83
|
-
if (Array.isArray(node)) {
|
|
84
|
-
return node.map((item, i) => walkDates(item, [...path, i], meta));
|
|
85
|
-
}
|
|
86
|
-
if (typeof node === "object" && node !== null) {
|
|
87
|
-
return Object.fromEntries(
|
|
88
|
-
Object.keys(node).map((key) => [
|
|
89
|
-
key,
|
|
90
|
-
walkDates((node as Record<string, unknown>)[key], [...path, key], meta),
|
|
91
|
-
]),
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
return node;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
/** Encode Date instances inside command params as canonical ISO strings,
|
|
98
|
-
* recording their paths (starting at the param index) in meta. */
|
|
99
24
|
export const encodeParams = (
|
|
100
25
|
params: readonly unknown[],
|
|
101
|
-
): { params: unknown[]; meta?:
|
|
102
|
-
const meta
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
/** Encode Date instances inside a wire value, recording their paths in meta. */
|
|
108
|
-
export const encodeValue = (
|
|
109
|
-
value: unknown,
|
|
110
|
-
): { value: unknown; meta?: StatewireMetaEntry[] } => {
|
|
111
|
-
const meta: StatewireMetaEntry[] = [];
|
|
112
|
-
const encoded = walkDates(value, [], meta);
|
|
113
|
-
return meta.length > 0 ? { value: encoded, meta } : { value: encoded };
|
|
26
|
+
): { params: unknown[]; meta?: StatewireDocument.MetaEntry[] } => {
|
|
27
|
+
const { value, meta } = encodeDocument(params);
|
|
28
|
+
return meta
|
|
29
|
+
? { params: value as unknown[], meta }
|
|
30
|
+
: { params: value as unknown[] };
|
|
114
31
|
};
|