statewire 0.0.0-0 → 0.0.2
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/README.md +84 -1
- package/dist/client/changes.d.ts +7 -0
- package/dist/client/changes.d.ts.map +1 -0
- package/dist/client/changes.js +25 -0
- package/dist/client/changes.js.map +1 -0
- package/dist/client/connection.d.ts +63 -0
- package/dist/client/connection.d.ts.map +1 -0
- package/dist/client/connection.js +211 -0
- package/dist/client/connection.js.map +1 -0
- package/dist/client/core.d.ts +14 -0
- package/dist/client/core.d.ts.map +1 -0
- package/dist/client/core.js +272 -0
- package/dist/client/core.js.map +1 -0
- package/dist/client/session.d.ts +71 -0
- package/dist/client/session.d.ts.map +1 -0
- package/dist/client/session.js +529 -0
- package/dist/client/session.js.map +1 -0
- package/dist/client/sessionStore.d.ts +16 -0
- package/dist/client/sessionStore.d.ts.map +1 -0
- package/dist/client/sessionStore.js +14 -0
- package/dist/client/sessionStore.js.map +1 -0
- package/dist/client/timing.d.ts +11 -0
- package/dist/client/timing.d.ts.map +1 -0
- package/dist/client/timing.js +15 -0
- package/dist/client/timing.js.map +1 -0
- package/dist/host/adapter.d.ts +8 -0
- package/dist/host/adapter.d.ts.map +1 -0
- package/dist/host/adapter.js +23 -0
- package/dist/host/adapter.js.map +1 -0
- package/dist/host/draft.d.ts +28 -0
- package/dist/host/draft.d.ts.map +1 -0
- package/dist/host/draft.js +211 -0
- package/dist/host/draft.js.map +1 -0
- package/dist/host/engine.d.ts +14 -0
- package/dist/host/engine.d.ts.map +1 -0
- package/dist/host/engine.js +17 -0
- package/dist/host/engine.js.map +1 -0
- package/dist/host/host.d.ts +62 -0
- package/dist/host/host.d.ts.map +1 -0
- package/dist/host/host.js +331 -0
- package/dist/host/host.js.map +1 -0
- package/dist/host/socket.d.ts +14 -0
- package/dist/host/socket.d.ts.map +1 -0
- package/dist/host/socket.js +459 -0
- package/dist/host/socket.js.map +1 -0
- package/dist/host-internal.d.ts +27 -0
- package/dist/host-internal.d.ts.map +1 -0
- package/dist/host-internal.js +3 -0
- package/dist/host.d.ts +44 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +4 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/statewire.d.ts +104 -0
- package/dist/statewire.d.ts.map +1 -0
- package/dist/statewire.js +53 -0
- package/dist/statewire.js.map +1 -0
- package/dist/transports/fake-sse.d.ts +12 -0
- package/dist/transports/fake-sse.d.ts.map +1 -0
- package/dist/transports/fake-sse.js +39 -0
- package/dist/transports/fake-sse.js.map +1 -0
- package/dist/transports/fixtures.d.ts +33 -0
- package/dist/transports/fixtures.d.ts.map +1 -0
- package/dist/transports/fixtures.js +50 -0
- package/dist/transports/fixtures.js.map +1 -0
- package/dist/transports/remote.d.ts +12 -0
- package/dist/transports/remote.d.ts.map +1 -0
- package/dist/transports/remote.js +21 -0
- package/dist/transports/remote.js.map +1 -0
- package/dist/transports/sse-parser.d.ts +5 -0
- package/dist/transports/sse-parser.d.ts.map +1 -0
- package/dist/transports/sse-parser.js +26 -0
- package/dist/transports/sse-parser.js.map +1 -0
- package/dist/transports/sse.d.ts +64 -0
- package/dist/transports/sse.d.ts.map +1 -0
- package/dist/transports/sse.js +137 -0
- package/dist/transports/sse.js.map +1 -0
- package/dist/transports/testing.d.ts +32 -0
- package/dist/transports/testing.d.ts.map +1 -0
- package/dist/transports/testing.js +94 -0
- package/dist/transports/testing.js.map +1 -0
- package/dist/transports/ws.d.ts +9 -0
- package/dist/transports/ws.d.ts.map +1 -0
- package/dist/transports/ws.js +169 -0
- package/dist/transports/ws.js.map +1 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +24 -0
- package/dist/types.js.map +1 -0
- package/dist/util/draftTraps.d.ts +6 -0
- package/dist/util/draftTraps.d.ts.map +1 -0
- package/dist/util/draftTraps.js +25 -0
- package/dist/util/draftTraps.js.map +1 -0
- package/dist/util/macrotask.d.ts +5 -0
- package/dist/util/macrotask.d.ts.map +1 -0
- package/dist/util/macrotask.js +25 -0
- package/dist/util/macrotask.js.map +1 -0
- package/dist/util/methodProxy.d.ts +5 -0
- package/dist/util/methodProxy.d.ts.map +1 -0
- package/dist/util/methodProxy.js +17 -0
- package/dist/util/methodProxy.js.map +1 -0
- package/dist/util/randomId.d.ts +5 -0
- package/dist/util/randomId.d.ts.map +1 -0
- package/dist/util/randomId.js +6 -0
- package/dist/util/randomId.js.map +1 -0
- package/dist/util/recordMutations.d.ts +10 -0
- package/dist/util/recordMutations.d.ts.map +1 -0
- package/dist/util/recordMutations.js +109 -0
- package/dist/util/recordMutations.js.map +1 -0
- package/dist/util/statePath.d.ts +14 -0
- package/dist/util/statePath.d.ts.map +1 -0
- package/dist/util/statePath.js +31 -0
- package/dist/util/statePath.js.map +1 -0
- package/dist/wire/meta.d.ts +27 -0
- package/dist/wire/meta.d.ts.map +1 -0
- package/dist/wire/meta.js +75 -0
- package/dist/wire/meta.js.map +1 -0
- package/dist/wire/ops.d.ts +19 -0
- package/dist/wire/ops.d.ts.map +1 -0
- package/dist/wire/ops.js +112 -0
- package/dist/wire/ops.js.map +1 -0
- package/package.json +52 -5
- package/src/client/changes.ts +38 -0
- package/src/client/connection.ts +301 -0
- package/src/client/core.ts +312 -0
- package/src/client/session.ts +696 -0
- package/src/client/sessionStore.ts +23 -0
- package/src/client/timing.ts +14 -0
- package/src/host/adapter.ts +33 -0
- package/src/host/draft.ts +323 -0
- package/src/host/engine.ts +13 -0
- package/src/host/host.ts +482 -0
- package/src/host/socket.ts +542 -0
- package/src/host-internal.ts +28 -0
- package/src/host.ts +61 -0
- package/src/index.ts +5 -0
- package/src/statewire.ts +200 -0
- package/src/transports/fake-sse.ts +51 -0
- package/src/transports/fixtures.ts +60 -0
- package/src/transports/remote.ts +30 -0
- package/src/transports/sse-parser.ts +24 -0
- package/src/transports/sse.ts +267 -0
- package/src/transports/testing.ts +123 -0
- package/src/transports/ws.ts +256 -0
- package/src/types.ts +88 -0
- package/src/util/draftTraps.ts +32 -0
- package/src/util/macrotask.ts +25 -0
- package/src/util/methodProxy.ts +16 -0
- package/src/util/randomId.ts +4 -0
- package/src/util/recordMutations.ts +0 -0
- package/src/util/statePath.ts +61 -0
- package/src/wire/meta.ts +101 -0
- package/src/wire/ops.ts +202 -0
- package/index.js +0 -1
package/src/wire/meta.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { walkStatePath } from "../util/statePath";
|
|
2
|
+
|
|
3
|
+
export type StatewireMetaEntry = {
|
|
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). Unknown types
|
|
21
|
+
* are skipped; a path that does not resolve to a valid target faults. */
|
|
22
|
+
export const reviveValue = (value: unknown, meta: unknown): unknown => {
|
|
23
|
+
if (!Array.isArray(meta)) return fault("meta must be an array");
|
|
24
|
+
for (const entry of meta as { path?: unknown; type?: unknown }[]) {
|
|
25
|
+
const path =
|
|
26
|
+
typeof entry === "object" && entry !== null ? entry.path : null;
|
|
27
|
+
if (!isPath(path)) return fault("meta entry must carry a path array");
|
|
28
|
+
if (entry.type !== "date") continue;
|
|
29
|
+
const walked = walkStatePath(value, path, "resolve");
|
|
30
|
+
if (walked === undefined) {
|
|
31
|
+
return fault(`meta path ${path.join(".")} does not resolve`);
|
|
32
|
+
}
|
|
33
|
+
if (typeof walked.leaf !== "string") {
|
|
34
|
+
return fault(`meta path ${path.join(".")} must name a string`);
|
|
35
|
+
}
|
|
36
|
+
const date = new Date(walked.leaf);
|
|
37
|
+
if (Number.isNaN(date.getTime())) {
|
|
38
|
+
return fault(`invalid date at meta path ${path.join(".")}`);
|
|
39
|
+
}
|
|
40
|
+
if (walked.parent === undefined) return date;
|
|
41
|
+
(walked.parent as Record<string | number, unknown>)[walked.key] = date;
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const reviveOps = (ops: unknown[]): unknown[] =>
|
|
47
|
+
ops.map((op) => {
|
|
48
|
+
if (typeof op !== "object" || op === null || !("meta" in op)) return op;
|
|
49
|
+
const { meta, ...rest } = op as { meta: unknown; value?: unknown };
|
|
50
|
+
return { ...rest, value: reviveValue(rest.value, meta) };
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const reviveResponse = <T extends { payload?: unknown; meta?: unknown }>(
|
|
54
|
+
response: T,
|
|
55
|
+
): T => {
|
|
56
|
+
if (response.meta === undefined) return response;
|
|
57
|
+
const { meta, ...rest } = response;
|
|
58
|
+
return { ...rest, payload: reviveValue(rest.payload, meta) } as T;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const walkDates = (
|
|
62
|
+
node: unknown,
|
|
63
|
+
path: (string | number)[],
|
|
64
|
+
meta: StatewireMetaEntry[],
|
|
65
|
+
): unknown => {
|
|
66
|
+
if (node instanceof Date) {
|
|
67
|
+
meta.push({ path, type: "date" });
|
|
68
|
+
return node.toISOString();
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(node)) {
|
|
71
|
+
return node.map((item, i) => walkDates(item, [...path, i], meta));
|
|
72
|
+
}
|
|
73
|
+
if (typeof node === "object" && node !== null) {
|
|
74
|
+
return Object.fromEntries(
|
|
75
|
+
Object.keys(node).map((key) => [
|
|
76
|
+
key,
|
|
77
|
+
walkDates((node as Record<string, unknown>)[key], [...path, key], meta),
|
|
78
|
+
]),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return node;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** Encode Date instances inside command params as canonical ISO strings,
|
|
85
|
+
* recording their paths (starting at the param index) in meta. */
|
|
86
|
+
export const encodeParams = (
|
|
87
|
+
params: readonly unknown[],
|
|
88
|
+
): { params: unknown[]; meta?: StatewireMetaEntry[] } => {
|
|
89
|
+
const meta: StatewireMetaEntry[] = [];
|
|
90
|
+
const encoded = params.map((param, i) => walkDates(param, [i], meta));
|
|
91
|
+
return meta.length > 0 ? { params: encoded, meta } : { params: encoded };
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** Encode Date instances inside a wire value, recording their paths in meta. */
|
|
95
|
+
export const encodeValue = (
|
|
96
|
+
value: unknown,
|
|
97
|
+
): { value: unknown; meta?: StatewireMetaEntry[] } => {
|
|
98
|
+
const meta: StatewireMetaEntry[] = [];
|
|
99
|
+
const encoded = walkDates(value, [], meta);
|
|
100
|
+
return meta.length > 0 ? { value: encoded, meta } : { value: encoded };
|
|
101
|
+
};
|
package/src/wire/ops.ts
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { recordMutations } from "../util/recordMutations";
|
|
2
|
+
import { isUnsafeKey, walkStatePath } from "../util/statePath";
|
|
3
|
+
import type { Statewire } from "../types";
|
|
4
|
+
|
|
5
|
+
export type StatewirePath = readonly (string | number)[];
|
|
6
|
+
|
|
7
|
+
const warnStripped = (key: string, parent: string | undefined) => {
|
|
8
|
+
if (
|
|
9
|
+
typeof process === "undefined" ||
|
|
10
|
+
process.env["NODE_ENV"] !== "production"
|
|
11
|
+
) {
|
|
12
|
+
console.warn(
|
|
13
|
+
`statewire: stripped unsafe key "${key}"` +
|
|
14
|
+
(parent === undefined ? "" : ` under "${parent}"`),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const unserializable = (
|
|
20
|
+
kind: "function" | "undefined",
|
|
21
|
+
path: readonly (string | number)[],
|
|
22
|
+
): never => {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`statewire: cannot serialize ${kind}` +
|
|
25
|
+
(path.length === 0 ? "" : ` at ${path.join(".")}`),
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const sanitizeAt = (
|
|
30
|
+
value: unknown,
|
|
31
|
+
parent: string | undefined,
|
|
32
|
+
path: readonly (string | number)[],
|
|
33
|
+
): unknown => {
|
|
34
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
return value.map((item, index) => {
|
|
37
|
+
if (item === undefined) unserializable("undefined", [...path, index]);
|
|
38
|
+
return sanitizeAt(item, undefined, [...path, index]);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === "object" && value !== null) {
|
|
42
|
+
const out: Record<string, unknown> = {};
|
|
43
|
+
for (const key of Object.keys(value)) {
|
|
44
|
+
if (isUnsafeKey(key, parent)) {
|
|
45
|
+
warnStripped(key, parent);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const item = (value as Record<string, unknown>)[key];
|
|
49
|
+
if (item === undefined) continue;
|
|
50
|
+
out[key] = sanitizeAt(item, key, [...path, key]);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === "function") unserializable("function", path);
|
|
55
|
+
return value;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// One clone pass: strips unsafe keys (with a warning); explicit-undefined
|
|
59
|
+
// object entries are omitted (delete semantics), functions and undefined
|
|
60
|
+
// list items throw.
|
|
61
|
+
export const sanitizeValue = (value: unknown, parent?: string): unknown =>
|
|
62
|
+
sanitizeAt(value, parent, parent === undefined ? [] : [parent]);
|
|
63
|
+
|
|
64
|
+
type MutableOp = Extract<Statewire.Op, { op: "replace" | "add" | "remove" }>;
|
|
65
|
+
|
|
66
|
+
const invalid = (op: MutableOp) =>
|
|
67
|
+
new Error(`Statewire cannot apply ${op.op} at path: ${op.path.join(".")}`);
|
|
68
|
+
|
|
69
|
+
const insertedText = (
|
|
70
|
+
current: string,
|
|
71
|
+
op: Extract<MutableOp, { op: "add" }>,
|
|
72
|
+
offset: number,
|
|
73
|
+
): string => {
|
|
74
|
+
if (typeof op.value !== "string" || offset > current.length) {
|
|
75
|
+
throw invalid(op);
|
|
76
|
+
}
|
|
77
|
+
return current.slice(0, offset) + op.value + current.slice(offset);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const applyAdd = (
|
|
81
|
+
box: { state: unknown },
|
|
82
|
+
op: Extract<MutableOp, { op: "add" }>,
|
|
83
|
+
) => {
|
|
84
|
+
const offset = op.path[op.path.length - 1];
|
|
85
|
+
const walked = walkStatePath(box.state, op.path.slice(0, -1), "resolve");
|
|
86
|
+
if (
|
|
87
|
+
walked === undefined ||
|
|
88
|
+
typeof offset !== "number" ||
|
|
89
|
+
!Number.isInteger(offset) ||
|
|
90
|
+
offset < 0
|
|
91
|
+
) {
|
|
92
|
+
throw invalid(op);
|
|
93
|
+
}
|
|
94
|
+
const target = walked.leaf;
|
|
95
|
+
if (Array.isArray(target)) {
|
|
96
|
+
if (offset > target.length) throw invalid(op);
|
|
97
|
+
target.splice(offset, 0, sanitizeValue(op.value));
|
|
98
|
+
} else if (typeof target === "string") {
|
|
99
|
+
const next = insertedText(target, op, offset);
|
|
100
|
+
if (op.path.length === 1) {
|
|
101
|
+
box.state = next;
|
|
102
|
+
} else {
|
|
103
|
+
(walked.parent as Record<string | number, unknown>)[walked.key] = next;
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
throw invalid(op);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const applyInList = (list: unknown[], index: number, op: MutableOp) => {
|
|
111
|
+
if (index >= list.length) throw invalid(op);
|
|
112
|
+
if (op.op === "replace") {
|
|
113
|
+
list[index] = sanitizeValue(op.value);
|
|
114
|
+
} else {
|
|
115
|
+
list.splice(index, 1);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const applyInObject = (
|
|
120
|
+
obj: Record<string, unknown>,
|
|
121
|
+
key: string,
|
|
122
|
+
op: MutableOp,
|
|
123
|
+
) => {
|
|
124
|
+
if (op.op === "replace") {
|
|
125
|
+
obj[key] = sanitizeValue(op.value, key);
|
|
126
|
+
} else {
|
|
127
|
+
if (!Object.hasOwn(obj, key)) throw invalid(op);
|
|
128
|
+
delete obj[key];
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const applyOpToDraft = (box: { state: unknown }, op: MutableOp): void => {
|
|
133
|
+
if (op.op === "add") {
|
|
134
|
+
if (op.path.length === 0) throw invalid(op);
|
|
135
|
+
applyAdd(box, op);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (op.path.length === 0) {
|
|
139
|
+
if (op.op !== "replace") throw invalid(op);
|
|
140
|
+
box.state = sanitizeValue(op.value);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const walked = walkStatePath(box.state, op.path, "parent");
|
|
144
|
+
if (walked === undefined) throw invalid(op);
|
|
145
|
+
if (typeof walked.key === "number") {
|
|
146
|
+
applyInList(walked.parent as unknown[], walked.key, op);
|
|
147
|
+
} else {
|
|
148
|
+
applyInObject(walked.parent as Record<string, unknown>, walked.key, op);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const warnedOps = new Set<unknown>();
|
|
153
|
+
const warnUnknownOp = (type: unknown) => {
|
|
154
|
+
if (warnedOps.has(type)) return;
|
|
155
|
+
warnedOps.add(type);
|
|
156
|
+
console.warn(`statewire: ignoring unknown op type ${JSON.stringify(type)}`);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const unboxPaths = (paths: readonly StatewirePath[]): StatewirePath[] =>
|
|
160
|
+
paths.map((path) => path.slice(1));
|
|
161
|
+
|
|
162
|
+
export const applyOp = (
|
|
163
|
+
root: unknown,
|
|
164
|
+
op: MutableOp,
|
|
165
|
+
): { state: unknown; changedPaths: StatewirePath[] } => {
|
|
166
|
+
const recorded = recordMutations({ state: root }, (draft) =>
|
|
167
|
+
applyOpToDraft(draft, op),
|
|
168
|
+
);
|
|
169
|
+
return {
|
|
170
|
+
state: recorded.state.state,
|
|
171
|
+
changedPaths: unboxPaths(recorded.changedPaths),
|
|
172
|
+
};
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const applyOps = (
|
|
176
|
+
state: unknown,
|
|
177
|
+
ops: readonly unknown[],
|
|
178
|
+
): {
|
|
179
|
+
state: unknown;
|
|
180
|
+
events: unknown[];
|
|
181
|
+
changedPaths: StatewirePath[];
|
|
182
|
+
} => {
|
|
183
|
+
const events: unknown[] = [];
|
|
184
|
+
const mutable: MutableOp[] = [];
|
|
185
|
+
for (const op of ops as Statewire.Op[]) {
|
|
186
|
+
if (op.op === "event") {
|
|
187
|
+
events.push(sanitizeValue(op.value));
|
|
188
|
+
} else if (op.op === "replace" || op.op === "add" || op.op === "remove") {
|
|
189
|
+
mutable.push(op);
|
|
190
|
+
} else {
|
|
191
|
+
warnUnknownOp((op as { op: unknown }).op);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const recorded = recordMutations({ state }, (draft) => {
|
|
195
|
+
for (const op of mutable) applyOpToDraft(draft, op);
|
|
196
|
+
});
|
|
197
|
+
return {
|
|
198
|
+
state: recorded.state.state,
|
|
199
|
+
events,
|
|
200
|
+
changedPaths: unboxPaths(recorded.changedPaths),
|
|
201
|
+
};
|
|
202
|
+
};
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
throw new Error("statewire: placeholder release — not yet published");
|