statewire 0.0.1 → 0.0.3
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 +14 -13
- package/dist/client/changes.d.ts +2 -2
- package/dist/client/changes.d.ts.map +1 -1
- package/dist/client/changes.js +3 -10
- package/dist/client/changes.js.map +1 -1
- package/dist/client/connection.d.ts +18 -39
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +61 -54
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts +7 -11
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +58 -55
- package/dist/client/core.js.map +1 -1
- package/dist/client/session.d.ts +12 -26
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +70 -43
- package/dist/client/session.js.map +1 -1
- package/dist/client/sessionStore.d.ts +1 -1
- package/dist/client/sessionStore.js.map +1 -1
- package/dist/client/timing.d.ts +2 -1
- package/dist/client/timing.d.ts.map +1 -1
- package/dist/client/timing.js +5 -1
- package/dist/client/timing.js.map +1 -1
- 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 +21 -7
- package/dist/host/draft.d.ts.map +1 -1
- package/dist/host/draft.js +190 -103
- package/dist/host/draft.js.map +1 -1
- package/dist/host/host.d.ts +19 -35
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +86 -54
- package/dist/host/host.js.map +1 -1
- package/dist/host/socket.d.ts +14 -0
- package/dist/host/socket.d.ts.map +1 -0
- package/dist/host/socket.js +511 -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 +54 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +4 -0
- package/dist/index.d.ts +5 -28
- package/dist/index.js +2 -3
- package/dist/statewire.d.ts +97 -10
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +32 -36
- package/dist/statewire.js.map +1 -1
- 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 +6 -13
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +7 -25
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +32 -20
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +57 -46
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +8 -6
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +19 -46
- package/dist/transports/testing.js.map +1 -1
- package/dist/transports/ws.d.ts +4 -13
- package/dist/transports/ws.d.ts.map +1 -1
- package/dist/transports/ws.js +59 -63
- package/dist/transports/ws.js.map +1 -1
- package/dist/types.d.ts +55 -100
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- 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/{client/optimistic.d.ts → util/recordMutations.d.ts} +2 -2
- package/dist/util/recordMutations.d.ts.map +1 -0
- package/dist/{client/optimistic.js → util/recordMutations.js} +5 -24
- 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 +6 -1
- package/dist/wire/meta.d.ts.map +1 -1
- package/dist/wire/meta.js +30 -28
- package/dist/wire/meta.js.map +1 -1
- package/dist/wire/ops.d.ts +5 -26
- package/dist/wire/ops.d.ts.map +1 -1
- package/dist/wire/ops.js +67 -80
- package/dist/wire/ops.js.map +1 -1
- package/package.json +15 -11
- package/src/client/changes.ts +5 -15
- package/src/client/connection.ts +87 -113
- package/src/client/core.ts +57 -91
- package/src/client/session.ts +129 -90
- package/src/client/sessionStore.ts +1 -1
- package/src/client/timing.ts +6 -0
- package/src/host/adapter.ts +33 -0
- package/src/host/draft.ts +293 -125
- package/src/host/host.ts +122 -110
- package/src/host/socket.ts +618 -0
- package/src/host-internal.ts +28 -0
- package/src/host.ts +72 -0
- package/src/index.ts +2 -62
- package/src/statewire.ts +168 -61
- package/src/transports/fake-sse.ts +51 -0
- package/src/transports/fixtures.ts +60 -0
- package/src/transports/remote.ts +14 -51
- package/src/transports/sse.ts +87 -89
- package/src/transports/testing.ts +33 -63
- package/src/transports/ws.ts +78 -100
- package/src/types.ts +52 -131
- 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/{client/optimistic.ts → util/recordMutations.ts} +0 -0
- package/src/util/statePath.ts +61 -0
- package/src/wire/meta.ts +42 -47
- package/src/wire/ops.ts +107 -106
- package/LICENSE +0 -21
- package/dist/client/optimistic.d.ts.map +0 -1
- package/dist/client/optimistic.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/server.d.ts +0 -15
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -3
- package/src/server.ts +0 -32
package/src/transports/sse.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
1
2
|
import { flushTapSync, resource } from "@assistant-ui/tap";
|
|
2
3
|
import type { ResourceElement } from "@assistant-ui/tap";
|
|
3
4
|
import { encodeParams, reviveResponse } from "../wire/meta";
|
|
@@ -5,24 +6,22 @@ import { createSseParser } from "./sse-parser";
|
|
|
5
6
|
import { timing } from "../client/timing";
|
|
6
7
|
import {
|
|
7
8
|
classify,
|
|
8
|
-
createGate,
|
|
9
9
|
handleEnvelope,
|
|
10
10
|
overloadedError,
|
|
11
11
|
parseRetryAfter,
|
|
12
|
-
|
|
12
|
+
useStatewireConn,
|
|
13
13
|
} from "../client/connection";
|
|
14
14
|
import type {
|
|
15
|
-
|
|
15
|
+
Conn,
|
|
16
16
|
StatewireEnvelope,
|
|
17
|
-
StatewireFinish,
|
|
18
17
|
StatewireResponse,
|
|
19
18
|
} from "../client/connection";
|
|
20
|
-
import type {
|
|
19
|
+
import type { Statewire } from "../types";
|
|
20
|
+
import type { StatewireClient } from "../statewire";
|
|
21
|
+
import type { StatewireHost } from "../host";
|
|
21
22
|
import { useStatewireRemote } from "./remote";
|
|
22
|
-
import type { StatewireSessionStore } from "../client/sessionStore";
|
|
23
|
-
import type { StatewireCommands } from "../types";
|
|
24
23
|
|
|
25
|
-
export type
|
|
24
|
+
export type StatewireRemoteProps = {
|
|
26
25
|
clientId: string;
|
|
27
26
|
onSnapshot: (args: { ops: unknown[]; lastSeq: number }) => void;
|
|
28
27
|
onOps: (args: {
|
|
@@ -30,17 +29,31 @@ export type StatewireTransportProps = {
|
|
|
30
29
|
res?: StatewireResponse[];
|
|
31
30
|
ack?: number;
|
|
32
31
|
}) => void;
|
|
33
|
-
onFinish: (args:
|
|
32
|
+
onFinish: (args: StatewireHost.Fin) => void;
|
|
34
33
|
};
|
|
35
34
|
|
|
36
|
-
export type StatewireWireCommand =
|
|
37
|
-
method: string;
|
|
38
|
-
params:
|
|
39
|
-
|
|
35
|
+
export type StatewireWireCommand =
|
|
36
|
+
| { method: string; params: readonly unknown[]; context?: never }
|
|
37
|
+
| { method?: never; params?: never; context: Record<string, unknown> };
|
|
38
|
+
|
|
39
|
+
/** The wire shape of a client context submission: one root replace op. */
|
|
40
|
+
export const contextOps = (value: Record<string, unknown>) => [
|
|
41
|
+
{ op: "replace", path: [], value },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export const encodeWireCommand = (command: StatewireWireCommand) => ({
|
|
45
|
+
...(command.method !== undefined && {
|
|
46
|
+
method: command.method,
|
|
47
|
+
...encodeParams(command.params),
|
|
48
|
+
}),
|
|
49
|
+
...(command.context !== undefined && {
|
|
50
|
+
context: contextOps(command.context),
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
40
53
|
|
|
41
54
|
export type StatewireSubmitResult =
|
|
42
55
|
| { type: "receipt"; response?: StatewireResponse }
|
|
43
|
-
| { type: "rejected"; message: string
|
|
56
|
+
| { type: "rejected"; message: string }
|
|
44
57
|
| { type: "seq-gap" }
|
|
45
58
|
| { type: "unknown-client" }
|
|
46
59
|
| { type: "stale-lease" };
|
|
@@ -50,7 +63,7 @@ export type ConnectionState =
|
|
|
50
63
|
| { status: "live" }
|
|
51
64
|
| {
|
|
52
65
|
status: "reconnecting";
|
|
53
|
-
cause: "dropped" | "
|
|
66
|
+
cause: "dropped" | "evicted" | "error";
|
|
54
67
|
attempt: number;
|
|
55
68
|
nextRetryAt: number;
|
|
56
69
|
lastError?: string;
|
|
@@ -58,61 +71,23 @@ export type ConnectionState =
|
|
|
58
71
|
}
|
|
59
72
|
| { status: "gone"; message?: string; payload?: unknown };
|
|
60
73
|
|
|
61
|
-
export type StatewireConnection = ConnectionState & { reconnect: () => void };
|
|
62
|
-
|
|
63
74
|
/** The live wire a remote session drives: connection status plus command POST. */
|
|
64
75
|
export type RemoteLink = {
|
|
65
|
-
connection:
|
|
76
|
+
connection: StatewireClient.Connection;
|
|
66
77
|
send: (args: {
|
|
67
78
|
seq: number;
|
|
68
79
|
command: StatewireWireCommand;
|
|
69
80
|
}) => Promise<StatewireSubmitResult>;
|
|
70
81
|
};
|
|
71
82
|
|
|
72
|
-
|
|
73
|
-
url
|
|
74
|
-
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
75
|
-
fetch?: typeof globalThis.fetch;
|
|
76
|
-
onChange?: (connection: StatewireConnection) => void;
|
|
77
|
-
sessionStore?: StatewireSessionStore;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
type Latest = {
|
|
81
|
-
options: StatewireSSEOptions;
|
|
82
|
-
ctx: StatewireTransportProps;
|
|
83
|
-
};
|
|
83
|
+
const baseUrl = (options: StatewireClient.SSEOptions) =>
|
|
84
|
+
options.url.replace(/\/$/, "");
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const createConn = (
|
|
91
|
-
latest: { readonly current: Latest },
|
|
92
|
-
notify: (connection: StatewireConnection) => void,
|
|
93
|
-
): Conn => {
|
|
94
|
-
const c: Conn = {
|
|
95
|
-
hasSnapshot: false,
|
|
96
|
-
lease: null,
|
|
97
|
-
attempt: 0,
|
|
98
|
-
episode: null,
|
|
99
|
-
abort: null,
|
|
100
|
-
gate: createGate(),
|
|
101
|
-
lifetime: null,
|
|
102
|
-
latest,
|
|
103
|
-
notify,
|
|
104
|
-
reconnect: () => reconnectConn(c),
|
|
105
|
-
attemptOnce: (abort) => attemptStream(c, abort),
|
|
106
|
-
send: (args) => sendCommand(c, args),
|
|
107
|
-
connection: { status: "connecting", reconnect: () => c.reconnect() },
|
|
108
|
-
};
|
|
109
|
-
return c;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const baseUrl = (c: Conn) => c.latest.current.options.url.replace(/\/$/, "");
|
|
113
|
-
|
|
114
|
-
const requestHeaders = async (c: Conn, extra: Record<string, string>) => {
|
|
115
|
-
const { headers } = c.latest.current.options;
|
|
86
|
+
const requestHeaders = async (
|
|
87
|
+
options: StatewireClient.SSEOptions,
|
|
88
|
+
extra: Record<string, string>,
|
|
89
|
+
) => {
|
|
90
|
+
const { headers } = options;
|
|
116
91
|
const merged = new Headers(
|
|
117
92
|
typeof headers === "function" ? await headers() : headers,
|
|
118
93
|
);
|
|
@@ -121,16 +96,13 @@ const requestHeaders = async (c: Conn, extra: Record<string, string>) => {
|
|
|
121
96
|
};
|
|
122
97
|
|
|
123
98
|
const timedFetch = async (
|
|
124
|
-
|
|
99
|
+
options: StatewireClient.SSEOptions,
|
|
125
100
|
input: string,
|
|
126
101
|
init: RequestInit & { signal: AbortSignal },
|
|
127
102
|
timeout: AbortSignal,
|
|
128
103
|
): Promise<Response> => {
|
|
129
104
|
try {
|
|
130
|
-
return await (
|
|
131
|
-
input,
|
|
132
|
-
init,
|
|
133
|
-
);
|
|
105
|
+
return await (options.fetch ?? globalThis.fetch)(input, init);
|
|
134
106
|
} catch {
|
|
135
107
|
throw classify(timeout);
|
|
136
108
|
}
|
|
@@ -144,7 +116,6 @@ const consume = async (
|
|
|
144
116
|
): Promise<"stop" | "reattach" | "ended"> => {
|
|
145
117
|
const decoder = new TextDecoder();
|
|
146
118
|
const parse = createSseParser();
|
|
147
|
-
let attached = false;
|
|
148
119
|
for (;;) {
|
|
149
120
|
const timer = setTimeout(
|
|
150
121
|
() => heartbeat.abort(),
|
|
@@ -162,8 +133,7 @@ const consume = async (
|
|
|
162
133
|
for (const data of parse(decoder.decode(result.value, { stream: true }))) {
|
|
163
134
|
if (dead()) return "ended";
|
|
164
135
|
const env = JSON.parse(data) as StatewireEnvelope;
|
|
165
|
-
const done = flushTapSync(() => handleEnvelope(c, env
|
|
166
|
-
attached = true;
|
|
136
|
+
const done = flushTapSync(() => handleEnvelope(c, env));
|
|
167
137
|
if (done) return done;
|
|
168
138
|
}
|
|
169
139
|
}
|
|
@@ -171,10 +141,11 @@ const consume = async (
|
|
|
171
141
|
|
|
172
142
|
const attemptStream = async (
|
|
173
143
|
c: Conn,
|
|
144
|
+
options: StatewireClient.SSEOptions,
|
|
174
145
|
abort: AbortController,
|
|
175
146
|
): Promise<"stop" | "reattach" | "ended"> => {
|
|
176
|
-
const headers = await requestHeaders(
|
|
177
|
-
"Statewire-Client-Id": c.
|
|
147
|
+
const headers = await requestHeaders(options, {
|
|
148
|
+
"Statewire-Client-Id": c.ctx.clientId,
|
|
178
149
|
accept: "text/event-stream",
|
|
179
150
|
});
|
|
180
151
|
const heartbeat = new AbortController();
|
|
@@ -185,8 +156,8 @@ const attemptStream = async (
|
|
|
185
156
|
let res: Response;
|
|
186
157
|
try {
|
|
187
158
|
res = await timedFetch(
|
|
188
|
-
|
|
189
|
-
`${baseUrl(
|
|
159
|
+
options,
|
|
160
|
+
`${baseUrl(options)}/stream`,
|
|
190
161
|
{
|
|
191
162
|
headers,
|
|
192
163
|
cache: "no-store",
|
|
@@ -212,24 +183,22 @@ const overloaded = (res: Response) =>
|
|
|
212
183
|
|
|
213
184
|
const sendCommand = async (
|
|
214
185
|
c: Conn,
|
|
186
|
+
options: StatewireClient.SSEOptions,
|
|
215
187
|
{ seq, command }: { seq: number; command: StatewireWireCommand },
|
|
216
188
|
): Promise<StatewireSubmitResult> => {
|
|
217
189
|
const timeout = AbortSignal.timeout(timing.heartbeatTimeoutMs);
|
|
218
190
|
const res = await timedFetch(
|
|
219
|
-
|
|
220
|
-
`${baseUrl(
|
|
191
|
+
options,
|
|
192
|
+
`${baseUrl(options)}/commands`,
|
|
221
193
|
{
|
|
222
194
|
method: "POST",
|
|
223
|
-
headers: await requestHeaders(
|
|
195
|
+
headers: await requestHeaders(options, {
|
|
224
196
|
"content-type": "application/json",
|
|
225
|
-
"Statewire-Client-Id": c.
|
|
197
|
+
"Statewire-Client-Id": c.ctx.clientId,
|
|
226
198
|
"Statewire-Command-Seq": String(seq),
|
|
227
199
|
...(c.lease !== null && { "Statewire-Lease": c.lease }),
|
|
228
200
|
}),
|
|
229
|
-
body: JSON.stringify(
|
|
230
|
-
method: command.method,
|
|
231
|
-
...encodeParams(command.params),
|
|
232
|
-
}),
|
|
201
|
+
body: JSON.stringify(encodeWireCommand(command)),
|
|
233
202
|
signal: AbortSignal.any([
|
|
234
203
|
c.lifetime?.signal ?? AbortSignal.abort(),
|
|
235
204
|
timeout,
|
|
@@ -238,9 +207,15 @@ const sendCommand = async (
|
|
|
238
207
|
timeout,
|
|
239
208
|
);
|
|
240
209
|
if (res.ok) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
210
|
+
let body: { res?: StatewireResponse } | null;
|
|
211
|
+
try {
|
|
212
|
+
body = (await res.json()) as { res?: StatewireResponse } | null;
|
|
213
|
+
} catch {
|
|
214
|
+
return {
|
|
215
|
+
type: "rejected",
|
|
216
|
+
message: "statewire: protocol error: malformed /commands receipt body",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
244
219
|
return body?.res !== undefined
|
|
245
220
|
? { type: "receipt", response: reviveResponse(body.res) }
|
|
246
221
|
: { type: "receipt" };
|
|
@@ -267,14 +242,37 @@ const sendCommand = async (
|
|
|
267
242
|
throw new Error(`HTTP ${res.status}`);
|
|
268
243
|
};
|
|
269
244
|
|
|
245
|
+
const useSseLink = (
|
|
246
|
+
options: StatewireClient.SSEOptions,
|
|
247
|
+
ctx: StatewireRemoteProps,
|
|
248
|
+
): RemoteLink => {
|
|
249
|
+
const latest = useRef(options);
|
|
250
|
+
latest.current = options;
|
|
251
|
+
const c = useStatewireConn({
|
|
252
|
+
url: options.url,
|
|
253
|
+
onChange: options.onChange,
|
|
254
|
+
ctx,
|
|
255
|
+
attemptOnce: (c, abort) => attemptStream(c, options, abort),
|
|
256
|
+
});
|
|
257
|
+
const [link] = useState(
|
|
258
|
+
(): RemoteLink => ({
|
|
259
|
+
get connection() {
|
|
260
|
+
return c.connection;
|
|
261
|
+
},
|
|
262
|
+
send: (args) => sendCommand(c, latest.current, args),
|
|
263
|
+
}),
|
|
264
|
+
);
|
|
265
|
+
return link;
|
|
266
|
+
};
|
|
267
|
+
|
|
270
268
|
const useStatewireSSE = (
|
|
271
|
-
options:
|
|
272
|
-
):
|
|
269
|
+
options: StatewireClient.SSEOptions,
|
|
270
|
+
): Statewire<any, any, any> => useStatewireRemote(options, useSseLink);
|
|
273
271
|
|
|
274
272
|
export const StatewireSSE = resource(useStatewireSSE) as <
|
|
275
273
|
TState = any,
|
|
276
|
-
TCommands extends
|
|
274
|
+
TCommands extends Statewire.Commands = Statewire.Commands,
|
|
277
275
|
TEvent = any,
|
|
278
276
|
>(
|
|
279
|
-
options:
|
|
280
|
-
) => ResourceElement<
|
|
277
|
+
options: StatewireClient.SSEOptions,
|
|
278
|
+
) => ResourceElement<Statewire<TState | undefined, TCommands, TEvent>>;
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { ResourceElement } from "@assistant-ui/tap";
|
|
2
2
|
import type { StatewireEnvelope } from "../client/connection";
|
|
3
|
-
import { recordMutations } from "../
|
|
3
|
+
import { recordMutations } from "../util/recordMutations";
|
|
4
4
|
import { StatewireSSE } from "./sse";
|
|
5
|
-
import type {
|
|
5
|
+
import type { Statewire } from "../types";
|
|
6
|
+
import { openSSEStream, sseFrame } from "./fake-sse";
|
|
7
|
+
import type { FakeStreamHandle } from "./fake-sse";
|
|
8
|
+
|
|
9
|
+
export * from "./fixtures";
|
|
6
10
|
|
|
7
11
|
/** Let queued microtasks and op batches drain through the statewire client. */
|
|
8
12
|
export const tick = () => new Promise<void>((r) => setTimeout(r, 0));
|
|
@@ -13,17 +17,11 @@ export const applyOptimistic = <TState>(
|
|
|
13
17
|
mutate: (draft: TState) => void,
|
|
14
18
|
): TState => recordMutations(state, mutate).state;
|
|
15
19
|
|
|
16
|
-
type StreamHandle = {
|
|
17
|
-
clientId: string;
|
|
18
|
-
send: (text: string) => void;
|
|
19
|
-
close: () => void;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
20
|
export type MockTransport<TState> = {
|
|
23
21
|
/** Pass as the `transport:` option of useStatewire / Harness. */
|
|
24
|
-
transport: ResourceElement<
|
|
25
|
-
/** Captured `
|
|
26
|
-
commands: { method
|
|
22
|
+
transport: ResourceElement<Statewire<TState | undefined, any, any>>;
|
|
23
|
+
/** Captured `/commands` POST bodies, in POST order. */
|
|
24
|
+
commands: { method?: string; params?: unknown[]; context?: unknown }[];
|
|
27
25
|
/** Replace the replicated state and broadcast it as a root snapshot op. */
|
|
28
26
|
setState: (state: TState) => Promise<void>;
|
|
29
27
|
/** Deliver an ack watermark on the stream. */
|
|
@@ -50,60 +48,14 @@ export const createMockTransport = <TState>(
|
|
|
50
48
|
let clientId = "";
|
|
51
49
|
let finished = false;
|
|
52
50
|
const clients = new Map<string, number>();
|
|
53
|
-
const streams = new Set<
|
|
54
|
-
const commands:
|
|
51
|
+
const streams = new Set<FakeStreamHandle>();
|
|
52
|
+
const commands: MockTransport<unknown>["commands"] = [];
|
|
55
53
|
|
|
56
54
|
const sendEnvelope = (envelope: StatewireEnvelope) => {
|
|
57
|
-
for (const stream of streams)
|
|
58
|
-
stream.send(`data: ${JSON.stringify(envelope)}\n\n`);
|
|
59
|
-
}
|
|
55
|
+
for (const stream of streams) stream.send(sseFrame(envelope));
|
|
60
56
|
return tick();
|
|
61
57
|
};
|
|
62
58
|
|
|
63
|
-
const handleStream = (id: string, signal: AbortSignal | undefined) => {
|
|
64
|
-
clientId = id;
|
|
65
|
-
const lastSeq = clients.get(id) ?? 0;
|
|
66
|
-
clients.set(id, lastSeq);
|
|
67
|
-
const encoder = new TextEncoder();
|
|
68
|
-
let handle: StreamHandle;
|
|
69
|
-
const body = new ReadableStream<Uint8Array>({
|
|
70
|
-
start: (controller) => {
|
|
71
|
-
let open = true;
|
|
72
|
-
handle = {
|
|
73
|
-
clientId: id,
|
|
74
|
-
send: (text) => {
|
|
75
|
-
if (open) controller.enqueue(encoder.encode(text));
|
|
76
|
-
},
|
|
77
|
-
close: () => {
|
|
78
|
-
if (!open) return;
|
|
79
|
-
open = false;
|
|
80
|
-
streams.delete(handle);
|
|
81
|
-
controller.close();
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
streams.add(handle);
|
|
85
|
-
handle.send(
|
|
86
|
-
`data: ${JSON.stringify({
|
|
87
|
-
ops: [{ op: "replace", path: [], value: state }],
|
|
88
|
-
syn: { lastSeq, lease: "mock-lease" },
|
|
89
|
-
})}\n\n`,
|
|
90
|
-
);
|
|
91
|
-
},
|
|
92
|
-
cancel: () => {
|
|
93
|
-
streams.delete(handle);
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
|
-
signal?.addEventListener("abort", () => {
|
|
97
|
-
for (const stream of [...streams]) {
|
|
98
|
-
if (stream.clientId === id) stream.close();
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
return new Response(body, {
|
|
102
|
-
status: 200,
|
|
103
|
-
headers: { "content-type": "text/event-stream" },
|
|
104
|
-
});
|
|
105
|
-
};
|
|
106
|
-
|
|
107
59
|
const fakeFetch = (async (
|
|
108
60
|
input: RequestInfo | URL,
|
|
109
61
|
init?: RequestInit,
|
|
@@ -112,13 +64,31 @@ export const createMockTransport = <TState>(
|
|
|
112
64
|
const url = String(input);
|
|
113
65
|
const headers = new Headers(init?.headers);
|
|
114
66
|
const id = headers.get("Statewire-Client-Id") ?? "";
|
|
115
|
-
if (url.endsWith("/stream"))
|
|
116
|
-
|
|
67
|
+
if (url.endsWith("/stream")) {
|
|
68
|
+
clientId = id;
|
|
69
|
+
const lastSeq = clients.get(id) ?? 0;
|
|
70
|
+
clients.set(id, lastSeq);
|
|
71
|
+
return openSSEStream(
|
|
72
|
+
streams,
|
|
73
|
+
id,
|
|
74
|
+
(handle) => {
|
|
75
|
+
handle.send(
|
|
76
|
+
sseFrame({
|
|
77
|
+
ops: [{ op: "replace", path: [], value: state }],
|
|
78
|
+
syn: { lastSeq, lease: "mock-lease" },
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
},
|
|
82
|
+
init?.signal ?? undefined,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
117
85
|
if (url.endsWith("/commands")) {
|
|
118
86
|
const seq = Number(headers.get("Statewire-Command-Seq"));
|
|
119
87
|
clients.set(id, Math.max(clients.get(id) ?? 0, seq));
|
|
120
88
|
commands.push(
|
|
121
|
-
JSON.parse(
|
|
89
|
+
JSON.parse(
|
|
90
|
+
String(init?.body),
|
|
91
|
+
) as MockTransport<unknown>["commands"][number],
|
|
122
92
|
);
|
|
123
93
|
return Response.json({});
|
|
124
94
|
}
|