statewire 0.0.1 → 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 +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 +8 -25
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +56 -36
- 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 +18 -35
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +81 -53
- 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 +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 -28
- package/dist/index.js +2 -3
- package/dist/statewire.d.ts +94 -10
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +23 -35
- 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 +5 -24
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +10 -20
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +42 -41
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +4 -3
- 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 +57 -60
- package/dist/transports/ws.js.map +1 -1
- package/dist/types.d.ts +53 -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 +87 -70
- 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 +107 -106
- package/src/host/socket.ts +542 -0
- package/src/host-internal.ts +28 -0
- package/src/host.ts +61 -0
- package/src/index.ts +2 -62
- package/src/statewire.ts +154 -61
- package/src/transports/fake-sse.ts +51 -0
- package/src/transports/fixtures.ts +60 -0
- package/src/transports/remote.ts +12 -54
- package/src/transports/sse.ts +68 -81
- package/src/transports/testing.ts +27 -59
- package/src/transports/ws.ts +76 -92
- package/src/types.ts +50 -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,7 +29,7 @@ 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
35
|
export type StatewireWireCommand = {
|
|
@@ -40,7 +39,7 @@ export type StatewireWireCommand = {
|
|
|
40
39
|
|
|
41
40
|
export type StatewireSubmitResult =
|
|
42
41
|
| { type: "receipt"; response?: StatewireResponse }
|
|
43
|
-
| { type: "rejected"; message: string
|
|
42
|
+
| { type: "rejected"; message: string }
|
|
44
43
|
| { type: "seq-gap" }
|
|
45
44
|
| { type: "unknown-client" }
|
|
46
45
|
| { type: "stale-lease" };
|
|
@@ -50,7 +49,7 @@ export type ConnectionState =
|
|
|
50
49
|
| { status: "live" }
|
|
51
50
|
| {
|
|
52
51
|
status: "reconnecting";
|
|
53
|
-
cause: "dropped" | "
|
|
52
|
+
cause: "dropped" | "evicted" | "error";
|
|
54
53
|
attempt: number;
|
|
55
54
|
nextRetryAt: number;
|
|
56
55
|
lastError?: string;
|
|
@@ -58,61 +57,23 @@ export type ConnectionState =
|
|
|
58
57
|
}
|
|
59
58
|
| { status: "gone"; message?: string; payload?: unknown };
|
|
60
59
|
|
|
61
|
-
export type StatewireConnection = ConnectionState & { reconnect: () => void };
|
|
62
|
-
|
|
63
60
|
/** The live wire a remote session drives: connection status plus command POST. */
|
|
64
61
|
export type RemoteLink = {
|
|
65
|
-
connection:
|
|
62
|
+
connection: StatewireClient.Connection;
|
|
66
63
|
send: (args: {
|
|
67
64
|
seq: number;
|
|
68
65
|
command: StatewireWireCommand;
|
|
69
66
|
}) => Promise<StatewireSubmitResult>;
|
|
70
67
|
};
|
|
71
68
|
|
|
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
|
-
};
|
|
84
|
-
|
|
85
|
-
type Conn = ConnBase & {
|
|
86
|
-
latest: { readonly current: Latest };
|
|
87
|
-
send: RemoteLink["send"];
|
|
88
|
-
};
|
|
69
|
+
const baseUrl = (options: StatewireClient.SSEOptions) =>
|
|
70
|
+
options.url.replace(/\/$/, "");
|
|
89
71
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
)
|
|
94
|
-
const
|
|
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;
|
|
72
|
+
const requestHeaders = async (
|
|
73
|
+
options: StatewireClient.SSEOptions,
|
|
74
|
+
extra: Record<string, string>,
|
|
75
|
+
) => {
|
|
76
|
+
const { headers } = options;
|
|
116
77
|
const merged = new Headers(
|
|
117
78
|
typeof headers === "function" ? await headers() : headers,
|
|
118
79
|
);
|
|
@@ -121,16 +82,13 @@ const requestHeaders = async (c: Conn, extra: Record<string, string>) => {
|
|
|
121
82
|
};
|
|
122
83
|
|
|
123
84
|
const timedFetch = async (
|
|
124
|
-
|
|
85
|
+
options: StatewireClient.SSEOptions,
|
|
125
86
|
input: string,
|
|
126
87
|
init: RequestInit & { signal: AbortSignal },
|
|
127
88
|
timeout: AbortSignal,
|
|
128
89
|
): Promise<Response> => {
|
|
129
90
|
try {
|
|
130
|
-
return await (
|
|
131
|
-
input,
|
|
132
|
-
init,
|
|
133
|
-
);
|
|
91
|
+
return await (options.fetch ?? globalThis.fetch)(input, init);
|
|
134
92
|
} catch {
|
|
135
93
|
throw classify(timeout);
|
|
136
94
|
}
|
|
@@ -144,7 +102,6 @@ const consume = async (
|
|
|
144
102
|
): Promise<"stop" | "reattach" | "ended"> => {
|
|
145
103
|
const decoder = new TextDecoder();
|
|
146
104
|
const parse = createSseParser();
|
|
147
|
-
let attached = false;
|
|
148
105
|
for (;;) {
|
|
149
106
|
const timer = setTimeout(
|
|
150
107
|
() => heartbeat.abort(),
|
|
@@ -162,8 +119,7 @@ const consume = async (
|
|
|
162
119
|
for (const data of parse(decoder.decode(result.value, { stream: true }))) {
|
|
163
120
|
if (dead()) return "ended";
|
|
164
121
|
const env = JSON.parse(data) as StatewireEnvelope;
|
|
165
|
-
const done = flushTapSync(() => handleEnvelope(c, env
|
|
166
|
-
attached = true;
|
|
122
|
+
const done = flushTapSync(() => handleEnvelope(c, env));
|
|
167
123
|
if (done) return done;
|
|
168
124
|
}
|
|
169
125
|
}
|
|
@@ -171,10 +127,11 @@ const consume = async (
|
|
|
171
127
|
|
|
172
128
|
const attemptStream = async (
|
|
173
129
|
c: Conn,
|
|
130
|
+
options: StatewireClient.SSEOptions,
|
|
174
131
|
abort: AbortController,
|
|
175
132
|
): Promise<"stop" | "reattach" | "ended"> => {
|
|
176
|
-
const headers = await requestHeaders(
|
|
177
|
-
"Statewire-Client-Id": c.
|
|
133
|
+
const headers = await requestHeaders(options, {
|
|
134
|
+
"Statewire-Client-Id": c.ctx.clientId,
|
|
178
135
|
accept: "text/event-stream",
|
|
179
136
|
});
|
|
180
137
|
const heartbeat = new AbortController();
|
|
@@ -185,8 +142,8 @@ const attemptStream = async (
|
|
|
185
142
|
let res: Response;
|
|
186
143
|
try {
|
|
187
144
|
res = await timedFetch(
|
|
188
|
-
|
|
189
|
-
`${baseUrl(
|
|
145
|
+
options,
|
|
146
|
+
`${baseUrl(options)}/stream`,
|
|
190
147
|
{
|
|
191
148
|
headers,
|
|
192
149
|
cache: "no-store",
|
|
@@ -212,17 +169,18 @@ const overloaded = (res: Response) =>
|
|
|
212
169
|
|
|
213
170
|
const sendCommand = async (
|
|
214
171
|
c: Conn,
|
|
172
|
+
options: StatewireClient.SSEOptions,
|
|
215
173
|
{ seq, command }: { seq: number; command: StatewireWireCommand },
|
|
216
174
|
): Promise<StatewireSubmitResult> => {
|
|
217
175
|
const timeout = AbortSignal.timeout(timing.heartbeatTimeoutMs);
|
|
218
176
|
const res = await timedFetch(
|
|
219
|
-
|
|
220
|
-
`${baseUrl(
|
|
177
|
+
options,
|
|
178
|
+
`${baseUrl(options)}/commands`,
|
|
221
179
|
{
|
|
222
180
|
method: "POST",
|
|
223
|
-
headers: await requestHeaders(
|
|
181
|
+
headers: await requestHeaders(options, {
|
|
224
182
|
"content-type": "application/json",
|
|
225
|
-
"Statewire-Client-Id": c.
|
|
183
|
+
"Statewire-Client-Id": c.ctx.clientId,
|
|
226
184
|
"Statewire-Command-Seq": String(seq),
|
|
227
185
|
...(c.lease !== null && { "Statewire-Lease": c.lease }),
|
|
228
186
|
}),
|
|
@@ -238,9 +196,15 @@ const sendCommand = async (
|
|
|
238
196
|
timeout,
|
|
239
197
|
);
|
|
240
198
|
if (res.ok) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
199
|
+
let body: { res?: StatewireResponse } | null;
|
|
200
|
+
try {
|
|
201
|
+
body = (await res.json()) as { res?: StatewireResponse } | null;
|
|
202
|
+
} catch {
|
|
203
|
+
return {
|
|
204
|
+
type: "rejected",
|
|
205
|
+
message: "statewire: protocol error: malformed /commands receipt body",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
244
208
|
return body?.res !== undefined
|
|
245
209
|
? { type: "receipt", response: reviveResponse(body.res) }
|
|
246
210
|
: { type: "receipt" };
|
|
@@ -267,14 +231,37 @@ const sendCommand = async (
|
|
|
267
231
|
throw new Error(`HTTP ${res.status}`);
|
|
268
232
|
};
|
|
269
233
|
|
|
234
|
+
const useSseLink = (
|
|
235
|
+
options: StatewireClient.SSEOptions,
|
|
236
|
+
ctx: StatewireRemoteProps,
|
|
237
|
+
): RemoteLink => {
|
|
238
|
+
const latest = useRef(options);
|
|
239
|
+
latest.current = options;
|
|
240
|
+
const c = useStatewireConn({
|
|
241
|
+
url: options.url,
|
|
242
|
+
onChange: options.onChange,
|
|
243
|
+
ctx,
|
|
244
|
+
attemptOnce: (c, abort) => attemptStream(c, options, abort),
|
|
245
|
+
});
|
|
246
|
+
const [link] = useState(
|
|
247
|
+
(): RemoteLink => ({
|
|
248
|
+
get connection() {
|
|
249
|
+
return c.connection;
|
|
250
|
+
},
|
|
251
|
+
send: (args) => sendCommand(c, latest.current, args),
|
|
252
|
+
}),
|
|
253
|
+
);
|
|
254
|
+
return link;
|
|
255
|
+
};
|
|
256
|
+
|
|
270
257
|
const useStatewireSSE = (
|
|
271
|
-
options:
|
|
272
|
-
):
|
|
258
|
+
options: StatewireClient.SSEOptions,
|
|
259
|
+
): Statewire<any, any, any> => useStatewireRemote(options, useSseLink);
|
|
273
260
|
|
|
274
261
|
export const StatewireSSE = resource(useStatewireSSE) as <
|
|
275
262
|
TState = any,
|
|
276
|
-
TCommands extends
|
|
263
|
+
TCommands extends Statewire.Commands = Statewire.Commands,
|
|
277
264
|
TEvent = any,
|
|
278
265
|
>(
|
|
279
|
-
options:
|
|
280
|
-
) => ResourceElement<
|
|
266
|
+
options: StatewireClient.SSEOptions,
|
|
267
|
+
) => 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,15 +17,9 @@ 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<
|
|
22
|
+
transport: ResourceElement<Statewire<TState | undefined, any, any>>;
|
|
25
23
|
/** Captured `{ method, params }` command bodies, in POST order. */
|
|
26
24
|
commands: { method: string; params: unknown[] }[];
|
|
27
25
|
/** Replace the replicated state and broadcast it as a root snapshot op. */
|
|
@@ -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<
|
|
51
|
+
const streams = new Set<FakeStreamHandle>();
|
|
54
52
|
const commands: { method: string; params: unknown[] }[] = [];
|
|
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,8 +64,24 @@ 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));
|