statewire 0.8.1 → 0.9.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/README.md +2 -2
- package/dist/client/connection.d.ts +23 -19
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +143 -103
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts +30 -3
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +222 -260
- package/dist/client/core.js.map +1 -1
- package/dist/client/session.d.ts +34 -39
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +380 -373
- package/dist/client/session.js.map +1 -1
- package/dist/client/sessionStore.d.ts +1 -0
- package/dist/client/sessionStore.d.ts.map +1 -1
- package/dist/client/sessionStore.js.map +1 -1
- package/dist/client/timing.d.ts +1 -2
- package/dist/client/timing.d.ts.map +1 -1
- package/dist/client/timing.js +1 -5
- package/dist/client/timing.js.map +1 -1
- package/dist/host/draft.d.ts +2 -5
- package/dist/host/draft.d.ts.map +1 -1
- package/dist/host/draft.js.map +1 -1
- package/dist/host/host.d.ts +5 -8
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +23 -32
- package/dist/host/host.js.map +1 -1
- package/dist/host/socket.d.ts +3 -2
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +484 -424
- package/dist/host/socket.js.map +1 -1
- package/dist/host-internal.d.ts +4 -0
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host.d.ts +10 -10
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +2 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/statewire.d.ts +29 -24
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +8 -2
- package/dist/statewire.js.map +1 -1
- package/dist/transports/fake-sse.d.ts +1 -1
- package/dist/transports/fake-sse.d.ts.map +1 -1
- package/dist/transports/fake-sse.js +5 -2
- package/dist/transports/fake-sse.js.map +1 -1
- package/dist/transports/fixtures.d.ts +1 -2
- package/dist/transports/fixtures.d.ts.map +1 -1
- package/dist/transports/fixtures.js +5 -7
- package/dist/transports/fixtures.js.map +1 -1
- package/dist/transports/remote.d.ts +2 -1
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +10 -4
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +32 -46
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +56 -59
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +7 -5
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +41 -14
- package/dist/transports/testing.js.map +1 -1
- package/dist/transports/ws.d.ts +2 -3
- package/dist/transports/ws.d.ts.map +1 -1
- package/dist/transports/ws.js +25 -80
- package/dist/transports/ws.js.map +1 -1
- package/dist/types.d.ts +8 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -4
- package/dist/types.js.map +1 -1
- package/dist/wire/ops.d.ts +1 -5
- package/dist/wire/ops.d.ts.map +1 -1
- package/dist/wire/ops.js +1 -4
- package/dist/wire/ops.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +175 -128
- package/src/client/core.ts +287 -278
- package/src/client/session.ts +474 -480
- package/src/client/sessionStore.ts +1 -0
- package/src/client/timing.ts +0 -6
- package/src/host/draft.ts +1 -3
- package/src/host/host.ts +41 -58
- package/src/host/socket.ts +653 -448
- package/src/host-internal.ts +4 -0
- package/src/host.ts +13 -10
- package/src/index.ts +0 -1
- package/src/statewire.ts +38 -32
- package/src/transports/fake-sse.ts +2 -1
- package/src/transports/fixtures.ts +4 -9
- package/src/transports/remote.ts +12 -5
- package/src/transports/sse.ts +99 -115
- package/src/transports/testing.ts +47 -19
- package/src/transports/ws.ts +44 -115
- package/src/types.ts +15 -14
- package/src/wire/ops.ts +9 -16
package/src/transports/ws.ts
CHANGED
|
@@ -2,40 +2,32 @@ import { useState } from "react";
|
|
|
2
2
|
import { flushTapSync, resource } from "@assistant-ui/tap";
|
|
3
3
|
import type { ResourceElement } from "@assistant-ui/tap";
|
|
4
4
|
import { timing } from "../client/timing";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
import { handleEnvelope, useStatewireConn } from "../client/connection";
|
|
6
|
+
import type {
|
|
7
|
+
AttemptOutcome,
|
|
8
|
+
Conn,
|
|
9
|
+
StatewireEnvelope,
|
|
9
10
|
} from "../client/connection";
|
|
10
|
-
import type { Conn, StatewireEnvelope } from "../client/connection";
|
|
11
11
|
import type { Statewire } from "../types";
|
|
12
12
|
import type { StatewireClient } from "../statewire";
|
|
13
13
|
import { useStatewireRemote } from "./remote";
|
|
14
14
|
import { CLIENT_QUERY_PARAM } from "../host/adapter";
|
|
15
|
-
import { encodeWireCommand } from "./sse";
|
|
16
15
|
import type {
|
|
17
16
|
RemoteLink,
|
|
17
|
+
StatewireFrame,
|
|
18
18
|
StatewireSubmitResult,
|
|
19
19
|
StatewireRemoteProps,
|
|
20
|
-
StatewireWireCommand,
|
|
21
20
|
} from "./sse";
|
|
22
21
|
|
|
23
|
-
type PendingCommand = {
|
|
24
|
-
seq: number;
|
|
25
|
-
resolve: (result: StatewireSubmitResult) => void;
|
|
26
|
-
reject: (error: Error) => void;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
22
|
type WsChannel = {
|
|
30
23
|
socket: WebSocket | null;
|
|
31
|
-
pending: PendingCommand[];
|
|
32
24
|
authStale: boolean;
|
|
33
25
|
};
|
|
34
26
|
|
|
35
27
|
const OPEN = 1;
|
|
36
28
|
|
|
37
29
|
export const WS_SUBPROTOCOL = "statewire.v1";
|
|
38
|
-
|
|
30
|
+
const UNAUTHORIZED_CLOSE = 4401;
|
|
39
31
|
|
|
40
32
|
const socketUrl = async (
|
|
41
33
|
options: StatewireClient.WSOptions,
|
|
@@ -50,64 +42,35 @@ const socketUrl = async (
|
|
|
50
42
|
return `${base}/ws?${params.toString()}`;
|
|
51
43
|
};
|
|
52
44
|
|
|
53
|
-
const
|
|
45
|
+
const firstFrame = async (
|
|
54
46
|
options: StatewireClient.WSOptions,
|
|
55
47
|
stale: boolean,
|
|
56
|
-
|
|
48
|
+
lanes: StatewireFrame | null,
|
|
49
|
+
): Promise<string> => {
|
|
57
50
|
const { headers } = options;
|
|
58
|
-
if (headers === undefined) return null;
|
|
59
51
|
const resolved = new Headers(
|
|
60
52
|
typeof headers === "function"
|
|
61
53
|
? await headers({ staleAuth: stale })
|
|
62
54
|
: headers,
|
|
63
55
|
);
|
|
64
56
|
return JSON.stringify({
|
|
65
|
-
type: "headers",
|
|
66
57
|
headers: Object.fromEntries(resolved),
|
|
58
|
+
...(lanes ?? {}),
|
|
67
59
|
});
|
|
68
60
|
};
|
|
69
61
|
|
|
70
|
-
const settleOverloaded = (
|
|
71
|
-
w: WsChannel,
|
|
72
|
-
{ seq, retryAfterMs }: { seq?: number; retryAfterMs?: number },
|
|
73
|
-
) => {
|
|
74
|
-
if (seq === undefined) throw overloadedError(retryAfterMs);
|
|
75
|
-
const entry = w.pending.find((p) => p.seq === seq);
|
|
76
|
-
if (!entry) return;
|
|
77
|
-
w.pending = w.pending.filter((p) => p !== entry);
|
|
78
|
-
entry.reject(overloadedError(retryAfterMs));
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const resolvePending = (
|
|
82
|
-
w: WsChannel,
|
|
83
|
-
matches: (entry: PendingCommand) => boolean,
|
|
84
|
-
) => {
|
|
85
|
-
const remaining: PendingCommand[] = [];
|
|
86
|
-
for (const entry of w.pending) {
|
|
87
|
-
if (matches(entry)) entry.resolve({ type: "receipt" });
|
|
88
|
-
else remaining.push(entry);
|
|
89
|
-
}
|
|
90
|
-
w.pending = remaining;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
const failPending = (w: WsChannel, error: Error) => {
|
|
94
|
-
const pending = w.pending;
|
|
95
|
-
w.pending = [];
|
|
96
|
-
for (const entry of pending) entry.reject(error);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
62
|
const runSocket = (
|
|
100
63
|
c: Conn,
|
|
101
64
|
w: WsChannel,
|
|
102
65
|
ws: WebSocket,
|
|
103
|
-
frame: string
|
|
66
|
+
frame: string,
|
|
104
67
|
staleAuth: boolean,
|
|
105
68
|
abort: AbortController,
|
|
106
|
-
): Promise<
|
|
69
|
+
): Promise<AttemptOutcome> =>
|
|
107
70
|
new Promise((resolve, reject) => {
|
|
108
71
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
109
72
|
let timedOut = false;
|
|
110
|
-
let outcome:
|
|
73
|
+
let outcome: AttemptOutcome | null = null;
|
|
111
74
|
let closed = false;
|
|
112
75
|
const arm = () => {
|
|
113
76
|
clearTimeout(timer);
|
|
@@ -119,45 +82,36 @@ const runSocket = (
|
|
|
119
82
|
arm();
|
|
120
83
|
abort.signal.addEventListener("abort", () => ws.close());
|
|
121
84
|
ws.onerror = () => {};
|
|
122
|
-
|
|
85
|
+
ws.onopen = () => ws.send(frame);
|
|
123
86
|
const stale = () => Boolean(outcome) || closed || abort.signal.aborted;
|
|
124
87
|
const fail = (error: unknown) => {
|
|
125
88
|
clearTimeout(timer);
|
|
126
89
|
reject(error instanceof Error ? error : new Error("protocol"));
|
|
127
90
|
ws.close();
|
|
128
91
|
};
|
|
129
|
-
const processEnv = (env: StatewireEnvelope) => {
|
|
130
|
-
const done = flushTapSync(() => handleEnvelope(c, env));
|
|
131
|
-
if (env.ack !== undefined) {
|
|
132
|
-
const ack = env.ack;
|
|
133
|
-
resolvePending(w, (entry) => entry.seq <= ack);
|
|
134
|
-
}
|
|
135
|
-
if (env.res !== undefined) {
|
|
136
|
-
const seqs = new Set(env.res.map((r) => r.seq));
|
|
137
|
-
resolvePending(w, (entry) => seqs.has(entry.seq));
|
|
138
|
-
}
|
|
139
|
-
if (done) {
|
|
140
|
-
outcome = done;
|
|
141
|
-
ws.close();
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
92
|
ws.onmessage = (event) => {
|
|
145
93
|
if (stale()) return;
|
|
146
94
|
arm();
|
|
147
|
-
let
|
|
148
|
-
overloaded?: { seq?: number; retryAfterMs?: number };
|
|
149
|
-
};
|
|
95
|
+
let env: StatewireEnvelope;
|
|
150
96
|
try {
|
|
151
|
-
|
|
97
|
+
env = JSON.parse(String(event.data)) as StatewireEnvelope;
|
|
152
98
|
} catch {
|
|
153
99
|
fail(new Error("protocol"));
|
|
154
100
|
return;
|
|
155
101
|
}
|
|
102
|
+
// {} heartbeats live below the packet layer: invisible to handlers.
|
|
103
|
+
if (
|
|
104
|
+
typeof env === "object" &&
|
|
105
|
+
env !== null &&
|
|
106
|
+
Object.keys(env).length === 0
|
|
107
|
+
) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
156
110
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
111
|
+
const done = flushTapSync(() => handleEnvelope(c, env));
|
|
112
|
+
if (done) {
|
|
113
|
+
outcome = done;
|
|
114
|
+
ws.close();
|
|
161
115
|
}
|
|
162
116
|
} catch (error) {
|
|
163
117
|
fail(error);
|
|
@@ -166,7 +120,6 @@ const runSocket = (
|
|
|
166
120
|
ws.onclose = (event?: { code?: number }) => {
|
|
167
121
|
closed = true;
|
|
168
122
|
clearTimeout(timer);
|
|
169
|
-
if (w.socket === ws) failPending(w, new Error("network"));
|
|
170
123
|
if (outcome) resolve(outcome);
|
|
171
124
|
else if (event?.code === UNAUTHORIZED_CLOSE) {
|
|
172
125
|
if (staleAuth) c.episode = { cause: "unauthorized" };
|
|
@@ -181,13 +134,14 @@ const attemptSocket = async (
|
|
|
181
134
|
w: WsChannel,
|
|
182
135
|
options: StatewireClient.WSOptions,
|
|
183
136
|
abort: AbortController,
|
|
184
|
-
): Promise<
|
|
137
|
+
): Promise<AttemptOutcome> => {
|
|
185
138
|
let ws: WebSocket | null = null;
|
|
186
139
|
try {
|
|
187
140
|
const url = await socketUrl(options, c.ctx.clientId);
|
|
188
141
|
const stale = w.authStale;
|
|
189
142
|
w.authStale = false;
|
|
190
|
-
const
|
|
143
|
+
const lanes = flushTapSync(() => c.ctx.takeAttachLanes());
|
|
144
|
+
const frame = await firstFrame(options, stale, lanes);
|
|
191
145
|
abort.signal.throwIfAborted();
|
|
192
146
|
const { protocols, webSocket } = options;
|
|
193
147
|
const WS = webSocket ?? globalThis.WebSocket;
|
|
@@ -206,51 +160,27 @@ const attemptSocket = async (
|
|
|
206
160
|
}
|
|
207
161
|
};
|
|
208
162
|
|
|
209
|
-
const
|
|
163
|
+
const sendFrame = (
|
|
210
164
|
w: WsChannel,
|
|
211
|
-
|
|
165
|
+
frame: StatewireFrame,
|
|
212
166
|
): Promise<StatewireSubmitResult> => {
|
|
213
167
|
const ws = w.socket;
|
|
214
168
|
if (!ws || ws.readyState !== OPEN) {
|
|
215
169
|
return Promise.reject(new Error("network"));
|
|
216
170
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
},
|
|
224
|
-
reject: (error) => {
|
|
225
|
-
clearTimeout(timer);
|
|
226
|
-
reject(error);
|
|
227
|
-
},
|
|
228
|
-
};
|
|
229
|
-
// A timed-out command fails the socket: the retry then rides reconnect +
|
|
230
|
-
// attach-seq arbitration.
|
|
231
|
-
const timer = setTimeout(() => {
|
|
232
|
-
w.pending = w.pending.filter((p) => p !== entry);
|
|
233
|
-
reject(new Error("timeout"));
|
|
234
|
-
ws.close();
|
|
235
|
-
}, timing.heartbeatTimeoutMs);
|
|
236
|
-
w.pending.push(entry);
|
|
237
|
-
try {
|
|
238
|
-
ws.send(JSON.stringify({ ...encodeWireCommand(command), seq }));
|
|
239
|
-
} catch {
|
|
240
|
-
clearTimeout(timer);
|
|
241
|
-
w.pending = w.pending.filter((p) => p !== entry);
|
|
242
|
-
reject(new Error("network"));
|
|
243
|
-
}
|
|
244
|
-
});
|
|
171
|
+
try {
|
|
172
|
+
ws.send(JSON.stringify(frame));
|
|
173
|
+
} catch {
|
|
174
|
+
return Promise.reject(new Error("network"));
|
|
175
|
+
}
|
|
176
|
+
return Promise.resolve({ type: "ok" });
|
|
245
177
|
};
|
|
246
178
|
|
|
247
179
|
const useWsLink = (
|
|
248
180
|
options: StatewireClient.WSOptions,
|
|
249
181
|
ctx: StatewireRemoteProps,
|
|
250
182
|
): RemoteLink => {
|
|
251
|
-
const [w] = useState(
|
|
252
|
-
(): WsChannel => ({ socket: null, pending: [], authStale: false }),
|
|
253
|
-
);
|
|
183
|
+
const [w] = useState((): WsChannel => ({ socket: null, authStale: false }));
|
|
254
184
|
const c = useStatewireConn({
|
|
255
185
|
url: options.url,
|
|
256
186
|
onChange: options.onChange,
|
|
@@ -262,7 +192,7 @@ const useWsLink = (
|
|
|
262
192
|
get connection() {
|
|
263
193
|
return c.connection;
|
|
264
194
|
},
|
|
265
|
-
send: (
|
|
195
|
+
send: (frame) => sendFrame(w, frame),
|
|
266
196
|
}),
|
|
267
197
|
);
|
|
268
198
|
return link;
|
|
@@ -270,12 +200,11 @@ const useWsLink = (
|
|
|
270
200
|
|
|
271
201
|
const useStatewireWS = (
|
|
272
202
|
options: StatewireClient.WSOptions,
|
|
273
|
-
): Statewire<any, any
|
|
203
|
+
): Statewire<any, any> => useStatewireRemote(options, useWsLink);
|
|
274
204
|
|
|
275
205
|
export const StatewireWS = resource(useStatewireWS) as <
|
|
276
206
|
TState = any,
|
|
277
207
|
TCommands extends Statewire.Commands = Statewire.Commands,
|
|
278
|
-
TEvent = any,
|
|
279
208
|
>(
|
|
280
209
|
options: StatewireClient.WSOptions,
|
|
281
|
-
) => ResourceElement<Statewire<TState | undefined, TCommands
|
|
210
|
+
) => ResourceElement<Statewire<TState | undefined, TCommands>>;
|
package/src/types.ts
CHANGED
|
@@ -19,7 +19,7 @@ export namespace Statewire {
|
|
|
19
19
|
method: K;
|
|
20
20
|
params: Parameters<TCommands[K]>;
|
|
21
21
|
seq: number;
|
|
22
|
-
status: "queued" | "posted" | "
|
|
22
|
+
status: "queued" | "posted" | "applied" | "settled";
|
|
23
23
|
result?: unknown;
|
|
24
24
|
failure?: StatewireClient.SendFailure;
|
|
25
25
|
};
|
|
@@ -37,8 +37,7 @@ export namespace Statewire {
|
|
|
37
37
|
export type Op =
|
|
38
38
|
| { op: "replace"; path: StatewirePath; value: unknown }
|
|
39
39
|
| { op: "add"; path: StatewirePath; value: unknown }
|
|
40
|
-
| { op: "remove"; path: StatewirePath }
|
|
41
|
-
| { op: "event"; value: object };
|
|
40
|
+
| { op: "remove"; path: StatewirePath };
|
|
42
41
|
|
|
43
42
|
export type Changes = {
|
|
44
43
|
isChangedAt(path: readonly (string | number)[]): boolean;
|
|
@@ -49,33 +48,35 @@ export namespace Statewire {
|
|
|
49
48
|
export interface Statewire<
|
|
50
49
|
TState = any,
|
|
51
50
|
TCommands extends Statewire.Commands = Statewire.Commands,
|
|
52
|
-
TEvent = any,
|
|
53
51
|
> {
|
|
54
52
|
state: TState;
|
|
55
53
|
commands: Statewire.CommandsProxy<TCommands>;
|
|
56
|
-
emit?: (event: TEvent) => void;
|
|
57
54
|
connection?: StatewireClient.Connection;
|
|
58
55
|
source?: Statewire.StateSource;
|
|
59
|
-
/**
|
|
60
|
-
|
|
56
|
+
/** Highest command seq released from the resend queue by the server's dur flag. */
|
|
57
|
+
durableSeq?: number;
|
|
58
|
+
/** Negation of the server's broadcast idle level; absent means always busy. */
|
|
59
|
+
isBusy?: boolean;
|
|
60
|
+
/** Replace this client's ambient context on the server; it rides the ctx lane (no verdict). */
|
|
61
|
+
setContext?: (value: Record<string, unknown>) => void;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
const describe = (failure: StatewireClient.SendFailure): string => {
|
|
64
65
|
switch (failure.fate) {
|
|
65
66
|
case "rejected":
|
|
66
|
-
return `statewire: command rejected: ${failure.message}`;
|
|
67
|
-
case "unknown-command":
|
|
68
|
-
return "statewire: unknown command";
|
|
69
|
-
case "invalid-params":
|
|
70
|
-
return `statewire: invalid params: ${failure.message}`;
|
|
67
|
+
return `statewire: command rejected${failure.code ? ` (${failure.code})` : ""}${failure.message ? `: ${failure.message}` : ""}`;
|
|
71
68
|
case "aborted":
|
|
72
69
|
return `statewire: command aborted (${failure.cause})`;
|
|
73
70
|
case "lost":
|
|
74
71
|
return `statewire: command lost (${failure.cause})`;
|
|
72
|
+
case "failed":
|
|
73
|
+
return failure.message
|
|
74
|
+
? `statewire: command failed: ${failure.message}`
|
|
75
|
+
: "statewire: command failed";
|
|
75
76
|
case "crashed":
|
|
76
77
|
return "statewire: command crashed";
|
|
77
|
-
case "
|
|
78
|
-
return `statewire: command result
|
|
78
|
+
case "expired":
|
|
79
|
+
return `statewire: command result expired (${failure.cause})`;
|
|
79
80
|
}
|
|
80
81
|
};
|
|
81
82
|
|
package/src/wire/ops.ts
CHANGED
|
@@ -61,14 +61,12 @@ const sanitizeAt = (
|
|
|
61
61
|
export const sanitizeValue = (value: unknown, parent?: string): unknown =>
|
|
62
62
|
sanitizeAt(value, parent, parent === undefined ? [] : [parent]);
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const invalid = (op: MutableOp) =>
|
|
64
|
+
const invalid = (op: Statewire.Op) =>
|
|
67
65
|
new Error(`Statewire cannot apply ${op.op} at path: ${op.path.join(".")}`);
|
|
68
66
|
|
|
69
67
|
const insertedText = (
|
|
70
68
|
current: string,
|
|
71
|
-
op: Extract<
|
|
69
|
+
op: Extract<Statewire.Op, { op: "add" }>,
|
|
72
70
|
offset: number,
|
|
73
71
|
): string => {
|
|
74
72
|
if (typeof op.value !== "string" || offset > current.length) {
|
|
@@ -79,7 +77,7 @@ const insertedText = (
|
|
|
79
77
|
|
|
80
78
|
const applyAdd = (
|
|
81
79
|
box: { state: unknown },
|
|
82
|
-
op: Extract<
|
|
80
|
+
op: Extract<Statewire.Op, { op: "add" }>,
|
|
83
81
|
) => {
|
|
84
82
|
const offset = op.path[op.path.length - 1];
|
|
85
83
|
const walked = walkStatePath(box.state, op.path.slice(0, -1), "resolve");
|
|
@@ -107,7 +105,7 @@ const applyAdd = (
|
|
|
107
105
|
}
|
|
108
106
|
};
|
|
109
107
|
|
|
110
|
-
const applyInList = (list: unknown[], index: number, op:
|
|
108
|
+
const applyInList = (list: unknown[], index: number, op: Statewire.Op) => {
|
|
111
109
|
if (index >= list.length) throw invalid(op);
|
|
112
110
|
if (op.op === "replace") {
|
|
113
111
|
list[index] = sanitizeValue(op.value);
|
|
@@ -119,7 +117,7 @@ const applyInList = (list: unknown[], index: number, op: MutableOp) => {
|
|
|
119
117
|
const applyInObject = (
|
|
120
118
|
obj: Record<string, unknown>,
|
|
121
119
|
key: string,
|
|
122
|
-
op:
|
|
120
|
+
op: Statewire.Op,
|
|
123
121
|
) => {
|
|
124
122
|
if (op.op === "replace") {
|
|
125
123
|
obj[key] = sanitizeValue(op.value, key);
|
|
@@ -129,7 +127,7 @@ const applyInObject = (
|
|
|
129
127
|
}
|
|
130
128
|
};
|
|
131
129
|
|
|
132
|
-
const applyOpToDraft = (box: { state: unknown }, op:
|
|
130
|
+
const applyOpToDraft = (box: { state: unknown }, op: Statewire.Op): void => {
|
|
133
131
|
if (op.op === "add") {
|
|
134
132
|
if (op.path.length === 0) throw invalid(op);
|
|
135
133
|
applyAdd(box, op);
|
|
@@ -161,7 +159,7 @@ const unboxPaths = (paths: readonly StatewirePath[]): StatewirePath[] =>
|
|
|
161
159
|
|
|
162
160
|
export const applyOp = (
|
|
163
161
|
root: unknown,
|
|
164
|
-
op:
|
|
162
|
+
op: Statewire.Op,
|
|
165
163
|
): { state: unknown; changedPaths: StatewirePath[] } => {
|
|
166
164
|
const recorded = recordMutations({ state: root }, (draft) =>
|
|
167
165
|
applyOpToDraft(draft, op),
|
|
@@ -177,15 +175,11 @@ export const applyOps = (
|
|
|
177
175
|
ops: readonly unknown[],
|
|
178
176
|
): {
|
|
179
177
|
state: unknown;
|
|
180
|
-
events: unknown[];
|
|
181
178
|
changedPaths: StatewirePath[];
|
|
182
179
|
} => {
|
|
183
|
-
const
|
|
184
|
-
const mutable: MutableOp[] = [];
|
|
180
|
+
const mutable: Statewire.Op[] = [];
|
|
185
181
|
for (const op of ops as Statewire.Op[]) {
|
|
186
|
-
if (op.op === "
|
|
187
|
-
events.push(sanitizeValue(op.value));
|
|
188
|
-
} else if (op.op === "replace" || op.op === "add" || op.op === "remove") {
|
|
182
|
+
if (op.op === "replace" || op.op === "add" || op.op === "remove") {
|
|
189
183
|
mutable.push(op);
|
|
190
184
|
} else {
|
|
191
185
|
warnUnknownOp((op as { op: unknown }).op);
|
|
@@ -196,7 +190,6 @@ export const applyOps = (
|
|
|
196
190
|
});
|
|
197
191
|
return {
|
|
198
192
|
state: recorded.state.state,
|
|
199
|
-
events,
|
|
200
193
|
changedPaths: unboxPaths(recorded.changedPaths),
|
|
201
194
|
};
|
|
202
195
|
};
|