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
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const timing = {
|
|
2
|
+
backoffBaseMs: 250,
|
|
3
|
+
backoffMaxMs: 30_000,
|
|
4
|
+
heartbeatTimeoutMs: 30_000,
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export const backoff = (attempt: number) =>
|
|
8
|
+
Math.min(timing.backoffBaseMs * 2 ** (attempt - 1), timing.backoffMaxMs);
|
|
9
|
+
|
|
10
|
+
export const retryDelay = (error: unknown, attempt: number): number => {
|
|
11
|
+
const retryAfterMs = (error as { retryAfterMs?: unknown } | undefined)
|
|
12
|
+
?.retryAfterMs;
|
|
13
|
+
return typeof retryAfterMs === "number" ? retryAfterMs : backoff(attempt);
|
|
14
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { StatewireHostInternal } from "../host-internal";
|
|
2
|
+
|
|
3
|
+
export const CLIENT_QUERY_PARAM = "client";
|
|
4
|
+
|
|
5
|
+
export const clientIdFromUrl = (url: string | null | undefined): string => {
|
|
6
|
+
if (!url) {
|
|
7
|
+
throw new Error("statewire: cannot resolve client id: missing url");
|
|
8
|
+
}
|
|
9
|
+
let parsed: URL;
|
|
10
|
+
try {
|
|
11
|
+
parsed = new URL(url, "http://statewire");
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`statewire: cannot resolve client id: unparseable url "${url}"`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
const clientId = parsed.searchParams.get(CLIENT_QUERY_PARAM);
|
|
18
|
+
if (!clientId) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`statewire: url is missing the "${CLIENT_QUERY_PARAM}" query parameter`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return clientId;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const textFrameOrClose = (
|
|
27
|
+
socket: Pick<StatewireHostInternal.Socket, "close">,
|
|
28
|
+
data: unknown,
|
|
29
|
+
): string | null => {
|
|
30
|
+
if (typeof data === "string") return data;
|
|
31
|
+
socket.close(1008, "text frames only");
|
|
32
|
+
return null;
|
|
33
|
+
};
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import type { Statewire } from "../types";
|
|
2
|
+
import type { StatewirePath } from "../wire/ops";
|
|
3
|
+
import { sanitizeValue } from "../wire/ops";
|
|
4
|
+
import { isUnsafeKey } from "../util/statePath";
|
|
5
|
+
import { sharedDraftTraps } from "../util/draftTraps";
|
|
6
|
+
|
|
7
|
+
export type ServerOp = Exclude<Statewire.Op, { op: "event" }>;
|
|
8
|
+
|
|
9
|
+
export type DraftHost = {
|
|
10
|
+
snapshot: () => unknown;
|
|
11
|
+
record: (op: ServerOp, changedPath: StatewirePath) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type StateDraft<T> = {
|
|
15
|
+
proxy: T;
|
|
16
|
+
/** Seal pending writes and return the committed state. */
|
|
17
|
+
flush: () => unknown;
|
|
18
|
+
/** Committed state was replaced externally; cached nodes re-resolve. */
|
|
19
|
+
invalidate: () => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const NODE = Symbol("statewire.draftNode");
|
|
23
|
+
|
|
24
|
+
type NodeHandle = {
|
|
25
|
+
append: (key: string | number, suffix: string) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const isIndex = (key: string) => /^(0|[1-9]\d*)$/.test(key);
|
|
29
|
+
|
|
30
|
+
const invalid = (path: StatewirePath, message: string) =>
|
|
31
|
+
new Error(`statewire: ${message} at path: ${path.join(".")}`);
|
|
32
|
+
|
|
33
|
+
type Container = Record<string | number, unknown>;
|
|
34
|
+
|
|
35
|
+
type Node = {
|
|
36
|
+
parent: Node | null;
|
|
37
|
+
key: string | number;
|
|
38
|
+
path: StatewirePath;
|
|
39
|
+
gen: number;
|
|
40
|
+
base: unknown;
|
|
41
|
+
copy: Container | null;
|
|
42
|
+
children: Map<string, Node>;
|
|
43
|
+
isArray: boolean;
|
|
44
|
+
proxy: object;
|
|
45
|
+
handle: NodeHandle;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const createStateDraft = <T>(host: DraftHost): StateDraft<T> => {
|
|
49
|
+
let gen = 0;
|
|
50
|
+
const dirty: Node[] = [];
|
|
51
|
+
|
|
52
|
+
const latest = (node: Node): unknown => node.copy ?? node.base;
|
|
53
|
+
|
|
54
|
+
const resolve = (node: Node): Node => {
|
|
55
|
+
if (node.gen === gen) return node;
|
|
56
|
+
node.gen = gen;
|
|
57
|
+
if (node.parent === null) {
|
|
58
|
+
if (node.copy === null) node.base = host.snapshot();
|
|
59
|
+
return node;
|
|
60
|
+
}
|
|
61
|
+
const value = (latest(resolve(node.parent)) as Container)[node.key];
|
|
62
|
+
if (value !== latest(node)) {
|
|
63
|
+
node.base = value;
|
|
64
|
+
node.copy = null;
|
|
65
|
+
}
|
|
66
|
+
return node;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const prepareForWrite = (node: Node): Container => {
|
|
70
|
+
resolve(node);
|
|
71
|
+
if (node.copy !== null) return node.copy;
|
|
72
|
+
const base = node.base;
|
|
73
|
+
node.copy = Array.isArray(base)
|
|
74
|
+
? (base.slice() as unknown as Container)
|
|
75
|
+
: { ...(base as Container) };
|
|
76
|
+
dirty.push(node);
|
|
77
|
+
if (node.parent !== null) {
|
|
78
|
+
prepareForWrite(node.parent)[node.key] = node.copy;
|
|
79
|
+
}
|
|
80
|
+
return node.copy;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const changedPathFor = (node: Node, key: string | number): StatewirePath =>
|
|
84
|
+
Array.isArray(latest(node)) ? node.path : [...node.path, key];
|
|
85
|
+
|
|
86
|
+
const applyAppend = (node: Node, rawKey: string | number, suffix: string) => {
|
|
87
|
+
const container = latest(resolve(node)) as Container;
|
|
88
|
+
const key =
|
|
89
|
+
Array.isArray(container) && typeof rawKey === "string" && isIndex(rawKey)
|
|
90
|
+
? Number(rawKey)
|
|
91
|
+
: rawKey;
|
|
92
|
+
const existing = Object.hasOwn(container as object, key)
|
|
93
|
+
? container[key]
|
|
94
|
+
: undefined;
|
|
95
|
+
if (typeof existing !== "string") {
|
|
96
|
+
throw invalid([...node.path, key], "cannot append to a non-string");
|
|
97
|
+
}
|
|
98
|
+
if (suffix === "") return;
|
|
99
|
+
prepareForWrite(node)[key] = existing + suffix;
|
|
100
|
+
host.record(
|
|
101
|
+
{ op: "add", path: [...node.path, key, existing.length], value: suffix },
|
|
102
|
+
changedPathFor(node, key),
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const createNode = (parent: Node | null, key: string | number): Node => {
|
|
107
|
+
const seed =
|
|
108
|
+
parent === null ? host.snapshot() : (latest(parent) as Container)[key];
|
|
109
|
+
const node: Node = {
|
|
110
|
+
parent,
|
|
111
|
+
key,
|
|
112
|
+
path: parent === null ? [] : [...parent.path, key],
|
|
113
|
+
gen,
|
|
114
|
+
base: seed,
|
|
115
|
+
copy: null,
|
|
116
|
+
children: new Map(),
|
|
117
|
+
isArray: Array.isArray(seed),
|
|
118
|
+
proxy: null!,
|
|
119
|
+
handle: null!,
|
|
120
|
+
};
|
|
121
|
+
node.handle = {
|
|
122
|
+
append: (childKey, suffix) => applyAppend(node, childKey, suffix),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const current = () => latest(resolve(node));
|
|
126
|
+
|
|
127
|
+
const childProxy = (
|
|
128
|
+
prop: string,
|
|
129
|
+
childKey: string | number,
|
|
130
|
+
value: object,
|
|
131
|
+
) => {
|
|
132
|
+
let child = node.children.get(prop);
|
|
133
|
+
if (child === undefined || child.isArray !== Array.isArray(value)) {
|
|
134
|
+
child = createNode(node, childKey);
|
|
135
|
+
node.children.set(prop, child);
|
|
136
|
+
}
|
|
137
|
+
return child.proxy;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const assign = (
|
|
141
|
+
container: Container,
|
|
142
|
+
key: string | number,
|
|
143
|
+
value: unknown,
|
|
144
|
+
) => {
|
|
145
|
+
const old = Object.hasOwn(container as object, key)
|
|
146
|
+
? container[key]
|
|
147
|
+
: undefined;
|
|
148
|
+
if (old === value) return;
|
|
149
|
+
const isObjectContainer = !Array.isArray(container);
|
|
150
|
+
if (
|
|
151
|
+
isObjectContainer &&
|
|
152
|
+
isUnsafeKey(key, node.path[node.path.length - 1])
|
|
153
|
+
) {
|
|
154
|
+
throw invalid([...node.path, key], `cannot set unsafe key "${key}"`);
|
|
155
|
+
}
|
|
156
|
+
if (value === undefined) {
|
|
157
|
+
if (!isObjectContainer) {
|
|
158
|
+
throw invalid([...node.path, key], "cannot serialize undefined");
|
|
159
|
+
}
|
|
160
|
+
delete prepareForWrite(node)[key];
|
|
161
|
+
host.record({ op: "remove", path: [...node.path, key] }, [
|
|
162
|
+
...node.path,
|
|
163
|
+
key,
|
|
164
|
+
]);
|
|
165
|
+
gen++;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
prepareForWrite(node)[key] = isObjectContainer
|
|
169
|
+
? sanitizeValue(value, key as string)
|
|
170
|
+
: sanitizeValue(value);
|
|
171
|
+
host.record(
|
|
172
|
+
{ op: "replace", path: [...node.path, key], value },
|
|
173
|
+
changedPathFor(node, key),
|
|
174
|
+
);
|
|
175
|
+
if (
|
|
176
|
+
(typeof old === "object" && old !== null) ||
|
|
177
|
+
(typeof value === "object" && value !== null)
|
|
178
|
+
) {
|
|
179
|
+
gen++;
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
node.proxy = new Proxy(seed as object, {
|
|
184
|
+
get: (_target, prop) => {
|
|
185
|
+
if (prop === NODE) return node.handle;
|
|
186
|
+
const container = current();
|
|
187
|
+
if (typeof prop === "symbol") {
|
|
188
|
+
return (container as Record<symbol, unknown>)[prop];
|
|
189
|
+
}
|
|
190
|
+
const c = container as Container;
|
|
191
|
+
const key = Array.isArray(c) && isIndex(prop) ? Number(prop) : prop;
|
|
192
|
+
const value = c[key];
|
|
193
|
+
if (typeof value === "object" && value !== null) {
|
|
194
|
+
return childProxy(prop, key, value);
|
|
195
|
+
}
|
|
196
|
+
return value;
|
|
197
|
+
},
|
|
198
|
+
set: (_target, prop, value) => {
|
|
199
|
+
if (typeof prop === "symbol") return false;
|
|
200
|
+
const container = current();
|
|
201
|
+
if (Array.isArray(container)) {
|
|
202
|
+
if (prop === "length") {
|
|
203
|
+
const next = Number(value);
|
|
204
|
+
if (
|
|
205
|
+
!Number.isInteger(next) ||
|
|
206
|
+
next < 0 ||
|
|
207
|
+
next > container.length
|
|
208
|
+
) {
|
|
209
|
+
throw invalid(
|
|
210
|
+
node.path,
|
|
211
|
+
`cannot set list length to ${String(value)}`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
if (next === container.length) return true;
|
|
215
|
+
const copy = prepareForWrite(node) as unknown as unknown[];
|
|
216
|
+
for (let i = container.length - 1; i >= next; i--) {
|
|
217
|
+
copy.splice(i, 1);
|
|
218
|
+
host.record({ op: "remove", path: [...node.path, i] }, node.path);
|
|
219
|
+
}
|
|
220
|
+
gen++;
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
if (!isIndex(prop)) {
|
|
224
|
+
throw invalid(node.path, `cannot set key "${prop}" on a list`);
|
|
225
|
+
}
|
|
226
|
+
const index = Number(prop);
|
|
227
|
+
if (index > container.length) {
|
|
228
|
+
throw invalid(node.path, `list index ${index} out of range`);
|
|
229
|
+
}
|
|
230
|
+
if (index === container.length) {
|
|
231
|
+
if (value === undefined) {
|
|
232
|
+
throw invalid(
|
|
233
|
+
[...node.path, index],
|
|
234
|
+
"cannot serialize undefined",
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
prepareForWrite(node)[index] = sanitizeValue(value);
|
|
238
|
+
host.record(
|
|
239
|
+
{ op: "add", path: [...node.path, index], value },
|
|
240
|
+
node.path,
|
|
241
|
+
);
|
|
242
|
+
gen++;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
assign(container as unknown as Container, index, value);
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
assign(container as Container, prop, value);
|
|
249
|
+
return true;
|
|
250
|
+
},
|
|
251
|
+
deleteProperty: (_target, prop) => {
|
|
252
|
+
if (typeof prop === "symbol") return false;
|
|
253
|
+
const container = current();
|
|
254
|
+
if (Array.isArray(container)) {
|
|
255
|
+
if (!isIndex(prop) || Number(prop) >= container.length) return true;
|
|
256
|
+
const index = Number(prop);
|
|
257
|
+
(prepareForWrite(node) as unknown as unknown[]).splice(index, 1);
|
|
258
|
+
host.record({ op: "remove", path: [...node.path, index] }, node.path);
|
|
259
|
+
gen++;
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
if (!Object.hasOwn(container as object, prop)) return true;
|
|
263
|
+
if (isUnsafeKey(prop, node.path[node.path.length - 1])) {
|
|
264
|
+
throw invalid(
|
|
265
|
+
[...node.path, prop],
|
|
266
|
+
`cannot remove unsafe key "${prop}"`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
delete prepareForWrite(node)[prop];
|
|
270
|
+
host.record({ op: "remove", path: [...node.path, prop] }, [
|
|
271
|
+
...node.path,
|
|
272
|
+
prop,
|
|
273
|
+
]);
|
|
274
|
+
gen++;
|
|
275
|
+
return true;
|
|
276
|
+
},
|
|
277
|
+
has: (_target, prop) => Reflect.has(current() as object, prop),
|
|
278
|
+
...sharedDraftTraps(() => current() as object, "on a state draft"),
|
|
279
|
+
});
|
|
280
|
+
return node;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const root = createNode(null, "");
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
proxy: root.proxy as T,
|
|
287
|
+
flush: () => {
|
|
288
|
+
if (dirty.length === 0) return host.snapshot();
|
|
289
|
+
for (const node of dirty) {
|
|
290
|
+
if (node.copy !== null) {
|
|
291
|
+
node.base = node.copy;
|
|
292
|
+
node.copy = null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
dirty.length = 0;
|
|
296
|
+
return root.base;
|
|
297
|
+
},
|
|
298
|
+
invalidate: () => {
|
|
299
|
+
gen++;
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Append `suffix` to the string at `parent[key]` on a statewire state draft,
|
|
306
|
+
* recording a single end-offset text-insert `add` op instead of a full
|
|
307
|
+
* `replace` — use it on token-streaming hot paths. Throws when the current
|
|
308
|
+
* value is not a string.
|
|
309
|
+
*/
|
|
310
|
+
export const append = <TKey extends string | number>(
|
|
311
|
+
parent: { readonly [P in TKey]: string },
|
|
312
|
+
key: TKey,
|
|
313
|
+
suffix: string,
|
|
314
|
+
): void => {
|
|
315
|
+
const handle = (parent as { [NODE]?: NodeHandle })[NODE];
|
|
316
|
+
if (handle === undefined) {
|
|
317
|
+
throw new Error("statewire: append expects a state draft object");
|
|
318
|
+
}
|
|
319
|
+
if (typeof suffix !== "string") {
|
|
320
|
+
throw new Error("statewire: append suffix must be a string");
|
|
321
|
+
}
|
|
322
|
+
handle.append(key, suffix);
|
|
323
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Definitive handler rejection: the command settles rejected and
|
|
3
|
+
* message/payload travel to the client's rejected promise.
|
|
4
|
+
*/
|
|
5
|
+
export class StatewireReject extends Error {
|
|
6
|
+
readonly payload?: unknown;
|
|
7
|
+
|
|
8
|
+
constructor(message: string, options?: { payload?: unknown }) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "StatewireReject";
|
|
11
|
+
if (options?.payload !== undefined) this.payload = options.payload;
|
|
12
|
+
}
|
|
13
|
+
}
|