statewire 0.15.0 → 0.17.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 +14 -6
- package/dist/client/connection.d.ts +5 -1
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +19 -4
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +2 -0
- package/dist/client/core.js.map +1 -1
- package/dist/client/session.d.ts +15 -4
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +233 -52
- package/dist/client/session.js.map +1 -1
- package/dist/client/sessionStore.d.ts +5 -1
- package/dist/client/sessionStore.d.ts.map +1 -1
- package/dist/client/sessionStore.js +3 -1
- package/dist/client/sessionStore.js.map +1 -1
- package/dist/host/host.d.ts +1 -0
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js.map +1 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +27 -9
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +10 -3
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +103 -39
- package/dist/host/socket.js.map +1 -1
- package/dist/host-internal.d.ts +9 -1
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host-internal.js +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -3
- package/dist/statewire.d.ts +33 -2
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +34 -3
- package/dist/statewire.js.map +1 -1
- package/dist/transports/fixtures.js +1 -0
- package/dist/transports/fixtures.js.map +1 -1
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +15 -8
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +2 -2
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +11 -11
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +1 -1
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +14 -6
- package/dist/transports/testing.js.map +1 -1
- package/dist/transports/ws.d.ts +2 -2
- package/dist/transports/ws.d.ts.map +1 -1
- package/dist/transports/ws.js +5 -5
- package/dist/transports/ws.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/util/randomId.js +1 -1
- package/dist/util/randomId.js.map +1 -1
- package/dist/wire/documents.d.ts +25 -2
- package/dist/wire/documents.d.ts.map +1 -1
- package/dist/wire/documents.js +27 -7
- package/dist/wire/documents.js.map +1 -1
- package/dist/wire/protocol.d.ts +55 -0
- package/dist/wire/protocol.d.ts.map +1 -0
- package/dist/wire/protocol.js +221 -0
- package/dist/wire/protocol.js.map +1 -0
- package/dist/wire/version.d.ts +1 -3
- package/dist/wire/version.d.ts.map +1 -1
- package/dist/wire/version.js +1 -3
- package/dist/wire/version.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +43 -13
- package/src/client/core.ts +6 -0
- package/src/client/session.ts +339 -42
- package/src/client/sessionStore.ts +8 -0
- package/src/host/host.ts +1 -0
- package/src/host/http.ts +36 -7
- package/src/host/socket.ts +141 -50
- package/src/host-internal.ts +12 -1
- package/src/index.ts +5 -2
- package/src/statewire.ts +109 -6
- package/src/transports/fixtures.ts +1 -1
- package/src/transports/remote.ts +16 -7
- package/src/transports/sse.ts +28 -18
- package/src/transports/testing.ts +12 -4
- package/src/transports/ws.ts +8 -8
- package/src/types.ts +11 -0
- package/src/util/randomId.ts +3 -3
- package/src/wire/documents.ts +126 -8
- package/src/wire/protocol.ts +327 -0
- package/src/wire/version.ts +0 -2
package/src/statewire.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { StatewireDocuments } from "./wire/documents";
|
|
2
|
+
import type { StatewireProtocol } from "./wire/protocol";
|
|
1
3
|
import type { StatewireHost } from "./host";
|
|
2
4
|
import { useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
5
|
import {
|
|
@@ -13,18 +15,32 @@ import type { ResourceElement } from "@assistant-ui/tap";
|
|
|
13
15
|
import { CONNECTED_CONNECTION, useStatewireCore } from "./client/core";
|
|
14
16
|
import type { Statewire } from "./types";
|
|
15
17
|
import type { TERMINAL } from "./client/connection";
|
|
16
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
DocumentStorageContext,
|
|
20
|
+
ProtocolContext,
|
|
21
|
+
SessionStorageContext,
|
|
22
|
+
} from "./client/sessionStore";
|
|
17
23
|
|
|
18
24
|
const useSessionTransport = ({
|
|
19
25
|
transport,
|
|
20
26
|
session,
|
|
27
|
+
documents,
|
|
28
|
+
protocol,
|
|
21
29
|
}: {
|
|
22
30
|
transport: ResourceElement<Statewire<any, any>>;
|
|
23
31
|
session: StatewireClient.SessionStorage | undefined;
|
|
32
|
+
documents: StatewireDocuments.Storage | undefined;
|
|
33
|
+
protocol: StatewireProtocol.Options;
|
|
24
34
|
}): Statewire<unknown> =>
|
|
25
35
|
useContextProvider(SessionStorageContext, session, () =>
|
|
26
36
|
// oxlint-disable-next-line react-hooks/rules-of-hooks
|
|
27
|
-
|
|
37
|
+
useContextProvider(DocumentStorageContext, documents, () =>
|
|
38
|
+
// oxlint-disable-next-line react-hooks/rules-of-hooks
|
|
39
|
+
useContextProvider(ProtocolContext, protocol, () =>
|
|
40
|
+
// oxlint-disable-next-line react-hooks/rules-of-hooks
|
|
41
|
+
useResource(transport),
|
|
42
|
+
),
|
|
43
|
+
),
|
|
28
44
|
);
|
|
29
45
|
|
|
30
46
|
// The transport owns the session; it reads the storage off the context this
|
|
@@ -45,8 +61,30 @@ export const useStatewire = <
|
|
|
45
61
|
? [withKey("session", options.storage.session)]
|
|
46
62
|
: [],
|
|
47
63
|
)[0];
|
|
64
|
+
const documents = useResources(
|
|
65
|
+
options.storage?.documents === undefined
|
|
66
|
+
? []
|
|
67
|
+
: [withKey("documents", options.storage.documents)],
|
|
68
|
+
)[0];
|
|
69
|
+
const protocol: StatewireProtocol.Options = {
|
|
70
|
+
...(options.protocol !== undefined && { protocol: options.protocol }),
|
|
71
|
+
...(options.wireVersion !== undefined && {
|
|
72
|
+
wireVersion: options.wireVersion,
|
|
73
|
+
}),
|
|
74
|
+
...(options.applicationProtocols !== undefined && {
|
|
75
|
+
applicationProtocols: options.applicationProtocols,
|
|
76
|
+
}),
|
|
77
|
+
...(options.keyPrefixes !== undefined && {
|
|
78
|
+
keyPrefixes: options.keyPrefixes,
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
48
81
|
const value = useResource(
|
|
49
|
-
SessionTransport({
|
|
82
|
+
SessionTransport({
|
|
83
|
+
transport: options.transport,
|
|
84
|
+
session,
|
|
85
|
+
documents,
|
|
86
|
+
protocol,
|
|
87
|
+
}),
|
|
50
88
|
);
|
|
51
89
|
const core = useStatewireCore(() => latest.current, value);
|
|
52
90
|
const [subscribe] = useState(
|
|
@@ -76,6 +114,26 @@ export const useStatewire = <
|
|
|
76
114
|
};
|
|
77
115
|
return {
|
|
78
116
|
state,
|
|
117
|
+
documents: value.documents,
|
|
118
|
+
selection: value.selection,
|
|
119
|
+
invoke: (
|
|
120
|
+
protocol: string,
|
|
121
|
+
method: string,
|
|
122
|
+
params: readonly unknown[],
|
|
123
|
+
) => {
|
|
124
|
+
if (!value.invoke)
|
|
125
|
+
throw new Error(
|
|
126
|
+
"statewire: transport does not support protocol commands",
|
|
127
|
+
);
|
|
128
|
+
return value.invoke(protocol, method, params);
|
|
129
|
+
},
|
|
130
|
+
updateContext: (ops: readonly unknown[]) => {
|
|
131
|
+
if (!value.updateContext)
|
|
132
|
+
throw new Error(
|
|
133
|
+
"statewire: transport does not support context documents",
|
|
134
|
+
);
|
|
135
|
+
value.updateContext(ops);
|
|
136
|
+
},
|
|
79
137
|
getState: core.getState as () => TState,
|
|
80
138
|
connection,
|
|
81
139
|
commands: core.commands as Statewire.CommandsProxy<TCommands>,
|
|
@@ -129,6 +187,23 @@ export class StatewireClient<
|
|
|
129
187
|
return this.root.getValue().commands;
|
|
130
188
|
}
|
|
131
189
|
|
|
190
|
+
get documents(): StatewireDocuments.Source | undefined {
|
|
191
|
+
return this.root.getValue().documents;
|
|
192
|
+
}
|
|
193
|
+
get selection(): StatewireProtocol.Selection | undefined {
|
|
194
|
+
return this.root.getValue().selection;
|
|
195
|
+
}
|
|
196
|
+
invoke(
|
|
197
|
+
protocol: string,
|
|
198
|
+
method: string,
|
|
199
|
+
params: readonly unknown[],
|
|
200
|
+
): Promise<unknown> {
|
|
201
|
+
return this.root.getValue().invoke(protocol, method, params);
|
|
202
|
+
}
|
|
203
|
+
updateContext(ops: readonly unknown[]): void {
|
|
204
|
+
this.root.getValue().updateContext(ops);
|
|
205
|
+
}
|
|
206
|
+
|
|
132
207
|
get isBusy(): boolean {
|
|
133
208
|
return this.root.getValue().isBusy;
|
|
134
209
|
}
|
|
@@ -165,10 +240,17 @@ export namespace StatewireClient {
|
|
|
165
240
|
TState,
|
|
166
241
|
TCommands extends Statewire.Commands = Statewire.Commands,
|
|
167
242
|
> = {
|
|
243
|
+
protocol?: string;
|
|
244
|
+
keyPrefixes?: Readonly<Record<string, readonly string[]>>;
|
|
245
|
+
wireVersion?: StatewireProtocol.Range;
|
|
246
|
+
applicationProtocols?: readonly StatewireProtocol.Offer[] | "*";
|
|
168
247
|
transport: ResourceElement<Statewire<TState, TCommands>>;
|
|
169
248
|
/** Durable session: the mounted storage holds this client's lane identity and its un-durable commands, which resend on the next attach. */
|
|
170
249
|
storage?:
|
|
171
|
-
| {
|
|
250
|
+
| {
|
|
251
|
+
session?: ResourceElement<SessionStorage> | undefined;
|
|
252
|
+
documents?: ResourceElement<StatewireDocuments.Storage> | undefined;
|
|
253
|
+
}
|
|
172
254
|
| undefined;
|
|
173
255
|
/** Served as the canonical state until the attach snapshot; own commands fold over it. */
|
|
174
256
|
initialState?: TState;
|
|
@@ -197,6 +279,14 @@ export namespace StatewireClient {
|
|
|
197
279
|
> = {
|
|
198
280
|
/** The statewire's state type — remote statewires include `undefined` until the first attach snapshot applies. */
|
|
199
281
|
state: TState;
|
|
282
|
+
documents: StatewireDocuments.Source | undefined;
|
|
283
|
+
selection: StatewireProtocol.Selection | undefined;
|
|
284
|
+
invoke: (
|
|
285
|
+
protocol: string,
|
|
286
|
+
method: string,
|
|
287
|
+
params: readonly unknown[],
|
|
288
|
+
) => Promise<unknown>;
|
|
289
|
+
updateContext: (ops: readonly unknown[]) => void;
|
|
200
290
|
/** Identity-stable across renders; reads the current canonical state without subscribing the caller. */
|
|
201
291
|
getState: () => TState;
|
|
202
292
|
connection: Connection;
|
|
@@ -318,6 +408,8 @@ export namespace StatewireClient {
|
|
|
318
408
|
|
|
319
409
|
/** A non-accepted send retained for the user: the wire statement, its failure, and its original clocks. */
|
|
320
410
|
export type FailedCommand = {
|
|
411
|
+
readonly protocol: string;
|
|
412
|
+
readonly produced?: SessionCommand["produced"];
|
|
321
413
|
readonly command: string;
|
|
322
414
|
readonly params: readonly unknown[];
|
|
323
415
|
readonly meta?: unknown;
|
|
@@ -329,6 +421,11 @@ export namespace StatewireClient {
|
|
|
329
421
|
/** A command as persisted while un-durable: its lane seq plus the wire statement. */
|
|
330
422
|
export type SessionCommand = {
|
|
331
423
|
seq: number;
|
|
424
|
+
protocol: string;
|
|
425
|
+
produced?: {
|
|
426
|
+
wire: StatewireProtocol.Range;
|
|
427
|
+
protocols: readonly StatewireProtocol.Offer[] | "*";
|
|
428
|
+
};
|
|
332
429
|
method: string;
|
|
333
430
|
params: unknown[];
|
|
334
431
|
meta?: unknown;
|
|
@@ -339,6 +436,7 @@ export namespace StatewireClient {
|
|
|
339
436
|
clientId: string;
|
|
340
437
|
nextSeq: number;
|
|
341
438
|
ctxNextSeq: number;
|
|
439
|
+
context?: { ops: unknown[]; pending: { seq: number; ops: unknown[] }[] };
|
|
342
440
|
commands: readonly SessionCommand[];
|
|
343
441
|
/** Terminal verdicts parked for the sharer that minted the seq (shared storage only). */
|
|
344
442
|
verdicts?: readonly SessionVerdict[];
|
|
@@ -360,6 +458,11 @@ export namespace StatewireClient {
|
|
|
360
458
|
export type SessionStorage = {
|
|
361
459
|
load: () => SessionRecord | null;
|
|
362
460
|
save: (record: SessionRecord) => void;
|
|
461
|
+
/** Upgrade incompatible stored commands; null discards the old record and starts a new client identity. */
|
|
462
|
+
migrate?: (
|
|
463
|
+
record: SessionRecord,
|
|
464
|
+
current: NonNullable<SessionCommand["produced"]>,
|
|
465
|
+
) => SessionRecord | null;
|
|
363
466
|
/** Serializes record mutations across sharers; presence marks the storage shared. */
|
|
364
467
|
lock?: <T>(fn: () => T | Promise<T>) => Promise<T>;
|
|
365
468
|
/** Notifies of record saves by other sharers; returns unsubscribe. Required alongside `lock`. */
|
|
@@ -373,7 +476,7 @@ export namespace StatewireClient {
|
|
|
373
476
|
| HeadersInit
|
|
374
477
|
| ((ctx: { staleAuth: boolean }) => HeadersInit | Promise<HeadersInit>);
|
|
375
478
|
|
|
376
|
-
export type
|
|
479
|
+
export type HttpOptions = StatewireProtocol.Options & {
|
|
377
480
|
sessionStore?: SessionStorage | undefined;
|
|
378
481
|
url: string;
|
|
379
482
|
/** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */
|
|
@@ -387,7 +490,7 @@ export namespace StatewireClient {
|
|
|
387
490
|
onDone?: () => void;
|
|
388
491
|
};
|
|
389
492
|
|
|
390
|
-
export type
|
|
493
|
+
export type WebsocketOptions = StatewireProtocol.Options & {
|
|
391
494
|
sessionStore?: SessionStorage | undefined;
|
|
392
495
|
url: string;
|
|
393
496
|
/** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */
|
|
@@ -24,7 +24,7 @@ export const commandFrame = (
|
|
|
24
24
|
method: string,
|
|
25
25
|
seq: number,
|
|
26
26
|
params: unknown[] = [],
|
|
27
|
-
) => JSON.stringify({ cmd: [{ method, params, seq }] });
|
|
27
|
+
) => JSON.stringify({ cmd: [{ protocol: "default", method, params, seq }] });
|
|
28
28
|
|
|
29
29
|
export type SocketRecorder = {
|
|
30
30
|
sent: string[];
|
package/src/transports/remote.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
1
|
+
import { useContext, useEffect } from "react";
|
|
2
|
+
import { ProtocolContext } from "../client/sessionStore";
|
|
2
3
|
import type { Statewire } from "../types";
|
|
3
4
|
import {
|
|
5
|
+
documentSource,
|
|
6
|
+
invokeProtocol,
|
|
7
|
+
updateContext,
|
|
4
8
|
discardFailed,
|
|
5
9
|
failedCommands,
|
|
6
10
|
retryFailed,
|
|
@@ -22,16 +26,21 @@ export const useStatewireRemote = <
|
|
|
22
26
|
options: TOptions,
|
|
23
27
|
useLink: (options: TOptions, ctx: StatewireRemoteProps) => RemoteLink,
|
|
24
28
|
): Statewire<any, any> => {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
});
|
|
30
|
-
const conn = useLink(options, s.ctx);
|
|
29
|
+
const inherited = useContext(ProtocolContext);
|
|
30
|
+
const configured = { ...inherited, ...options };
|
|
31
|
+
const s = useStatewireSession(configured);
|
|
32
|
+
const conn = useLink(configured, s.ctx);
|
|
31
33
|
useEffect(() => setTransport(s, conn), [s, conn]);
|
|
32
34
|
|
|
33
35
|
return {
|
|
34
36
|
state: s.canonical,
|
|
37
|
+
documents: documentSource(s),
|
|
38
|
+
get selection() {
|
|
39
|
+
return s.selection;
|
|
40
|
+
},
|
|
41
|
+
invoke: (protocol, method, params) =>
|
|
42
|
+
invokeProtocol(s, protocol, method, params),
|
|
43
|
+
updateContext: (ops) => updateContext(s, ops),
|
|
35
44
|
commands: s.commands,
|
|
36
45
|
connection: conn.connection,
|
|
37
46
|
source: s.source,
|
package/src/transports/sse.ts
CHANGED
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
classify,
|
|
8
8
|
handleEnvelope,
|
|
9
9
|
resolveHeaders,
|
|
10
|
-
unsupportedVersion,
|
|
11
10
|
useLink,
|
|
12
11
|
validateRefusalSyn,
|
|
13
12
|
} from "../client/connection";
|
|
@@ -22,7 +21,7 @@ import type { Statewire } from "../types";
|
|
|
22
21
|
import type { StatewireClient } from "../statewire";
|
|
23
22
|
import type { StatewireHost } from "../host";
|
|
24
23
|
import { useStatewireRemote } from "./remote";
|
|
25
|
-
import {
|
|
24
|
+
import { negotiationHeaders, parseNegotiation } from "../wire/protocol";
|
|
26
25
|
|
|
27
26
|
/** A client frame's lanes: stamped statements, strictly consecutive per lane. */
|
|
28
27
|
export type StatewireFrame = {
|
|
@@ -61,7 +60,7 @@ export type RemoteLink = {
|
|
|
61
60
|
|
|
62
61
|
const LEASE_HEADER = "Statewire-Lease";
|
|
63
62
|
|
|
64
|
-
const baseUrl = (options: StatewireClient.
|
|
63
|
+
const baseUrl = (options: StatewireClient.HttpOptions) =>
|
|
65
64
|
options.url.replace(/\/$/, "");
|
|
66
65
|
|
|
67
66
|
const withExtra = (headers: Headers, extra: Record<string, string>) => {
|
|
@@ -71,7 +70,7 @@ const withExtra = (headers: Headers, extra: Record<string, string>) => {
|
|
|
71
70
|
};
|
|
72
71
|
|
|
73
72
|
const timedFetch = async (
|
|
74
|
-
options: StatewireClient.
|
|
73
|
+
options: StatewireClient.HttpOptions,
|
|
75
74
|
input: string,
|
|
76
75
|
init: RequestInit & { signal: AbortSignal },
|
|
77
76
|
timeout: AbortSignal,
|
|
@@ -116,11 +115,15 @@ const consume = async (
|
|
|
116
115
|
|
|
117
116
|
const attemptStream = async (
|
|
118
117
|
c: Conn,
|
|
119
|
-
options: StatewireClient.
|
|
118
|
+
options: StatewireClient.HttpOptions,
|
|
120
119
|
abort: AbortController,
|
|
121
120
|
): Promise<AttemptOutcome> => {
|
|
122
121
|
c.lease = null;
|
|
123
122
|
const headers = await resolveHeaders(options.headers, c.authStale);
|
|
123
|
+
for (const [name, value] of Object.entries(negotiationHeaders(options))) {
|
|
124
|
+
if (!headers.has(name)) headers.set(name, value);
|
|
125
|
+
}
|
|
126
|
+
c.negotiation = parseNegotiation(headers);
|
|
124
127
|
c.authStale = false;
|
|
125
128
|
const lanes = flushTapSync(() => c.ctx.takeAttachLanes());
|
|
126
129
|
const body = lanes === null ? undefined : JSON.stringify(lanes);
|
|
@@ -150,14 +153,21 @@ const attemptStream = async (
|
|
|
150
153
|
} finally {
|
|
151
154
|
clearTimeout(headerTimer);
|
|
152
155
|
}
|
|
153
|
-
if (!res.ok)
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
const body = await res
|
|
158
|
+
.clone()
|
|
159
|
+
.json()
|
|
160
|
+
.catch(() => null);
|
|
161
|
+
return (
|
|
162
|
+
flushTapSync(() =>
|
|
163
|
+
body?.fin === undefined
|
|
164
|
+
? settleFin(c, httpFin(res))
|
|
165
|
+
: handleEnvelope(c, body),
|
|
166
|
+
) ?? "stop"
|
|
167
|
+
);
|
|
168
|
+
}
|
|
154
169
|
if (!res.body)
|
|
155
170
|
throw new Error("statewire: protocol error: stream response has no body");
|
|
156
|
-
const version = res.headers.get(VERSION_HEADER)?.trim();
|
|
157
|
-
if (version !== undefined && version !== PROTOCOL_VERSION) {
|
|
158
|
-
res.body.cancel().catch(() => {});
|
|
159
|
-
return unsupportedVersion(c, version);
|
|
160
|
-
}
|
|
161
171
|
c.lease = res.headers.get(LEASE_HEADER);
|
|
162
172
|
const reader = res.body.getReader();
|
|
163
173
|
try {
|
|
@@ -169,7 +179,7 @@ const attemptStream = async (
|
|
|
169
179
|
|
|
170
180
|
const postFrame = async (
|
|
171
181
|
c: Conn,
|
|
172
|
-
options: StatewireClient.
|
|
182
|
+
options: StatewireClient.HttpOptions,
|
|
173
183
|
lease: string,
|
|
174
184
|
frame: StatewireFrame,
|
|
175
185
|
timeout: AbortSignal,
|
|
@@ -230,7 +240,7 @@ const refusalSyn = async (res: Response): Promise<StatewireSyn> => {
|
|
|
230
240
|
|
|
231
241
|
const sendFrame = async (
|
|
232
242
|
c: Conn,
|
|
233
|
-
options: StatewireClient.
|
|
243
|
+
options: StatewireClient.HttpOptions,
|
|
234
244
|
frame: StatewireFrame,
|
|
235
245
|
): Promise<StatewireSubmitResult> => {
|
|
236
246
|
const lease = c.lease;
|
|
@@ -256,7 +266,7 @@ const sendFrame = async (
|
|
|
256
266
|
};
|
|
257
267
|
|
|
258
268
|
const useSseLink = (
|
|
259
|
-
options: StatewireClient.
|
|
269
|
+
options: StatewireClient.HttpOptions,
|
|
260
270
|
ctx: StatewireRemoteProps,
|
|
261
271
|
): RemoteLink =>
|
|
262
272
|
useLink({
|
|
@@ -282,13 +292,13 @@ const useSseLink = (
|
|
|
282
292
|
},
|
|
283
293
|
});
|
|
284
294
|
|
|
285
|
-
const
|
|
286
|
-
options: StatewireClient.
|
|
295
|
+
const useStatewireHttp = (
|
|
296
|
+
options: StatewireClient.HttpOptions,
|
|
287
297
|
): Statewire<any, any> => useStatewireRemote(options, useSseLink);
|
|
288
298
|
|
|
289
|
-
export const
|
|
299
|
+
export const StatewireHttp = resource(useStatewireHttp) as <
|
|
290
300
|
TState = any,
|
|
291
301
|
TCommands extends Statewire.Commands = Statewire.Commands,
|
|
292
302
|
>(
|
|
293
|
-
options: StatewireClient.
|
|
303
|
+
options: StatewireClient.HttpOptions,
|
|
294
304
|
) => ResourceElement<Statewire<TState | undefined, TCommands>>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ResourceElement } from "@assistant-ui/tap";
|
|
2
2
|
import type { StatewireEnvelope } from "../client/connection";
|
|
3
|
-
import {
|
|
3
|
+
import { StatewireHttp } from "./sse";
|
|
4
4
|
import type { StatewireFrame } from "./sse";
|
|
5
5
|
import type { Statewire } from "../types";
|
|
6
6
|
import { openSSEStream, sseFrame } from "./fake-sse";
|
|
@@ -41,6 +41,7 @@ let mockCount = 0;
|
|
|
41
41
|
|
|
42
42
|
export const createMockTransport = <TState>(
|
|
43
43
|
initialState: TState,
|
|
44
|
+
protocol = "default",
|
|
44
45
|
): MockTransport<TState> => {
|
|
45
46
|
let state: unknown = initialState;
|
|
46
47
|
let clientId = "";
|
|
@@ -57,11 +58,13 @@ export const createMockTransport = <TState>(
|
|
|
57
58
|
const admit = (id: string, frame: StatewireFrame) => {
|
|
58
59
|
const rec = clients.get(id)!;
|
|
59
60
|
for (const statement of frame.ctx ?? []) {
|
|
61
|
+
if (statement.seq <= rec.ctxSeq) continue;
|
|
60
62
|
rec.ctxSeq = Math.max(rec.ctxSeq, statement.seq);
|
|
61
63
|
const root = statement.ops[0] as { value?: unknown } | undefined;
|
|
62
64
|
commands.push({ context: root?.value });
|
|
63
65
|
}
|
|
64
66
|
for (const statement of frame.cmd ?? []) {
|
|
67
|
+
if (statement.seq <= rec.seq) continue;
|
|
65
68
|
rec.seq = Math.max(rec.seq, statement.seq);
|
|
66
69
|
commands.push({ method: statement.method, params: statement.params });
|
|
67
70
|
}
|
|
@@ -87,8 +90,13 @@ export const createMockTransport = <TState>(
|
|
|
87
90
|
(handle) => {
|
|
88
91
|
handle.send(
|
|
89
92
|
sseFrame({
|
|
90
|
-
ops: [{ op: "
|
|
91
|
-
syn: {
|
|
93
|
+
ops: [{ op: "mount", protocol, main: true, value: state }],
|
|
94
|
+
syn: {
|
|
95
|
+
seq: rec.seq,
|
|
96
|
+
ctxSeq: rec.ctxSeq,
|
|
97
|
+
version: "2026-09-07",
|
|
98
|
+
protocols: [{ name: protocol, version: "2026-09-07" }],
|
|
99
|
+
},
|
|
92
100
|
}),
|
|
93
101
|
);
|
|
94
102
|
},
|
|
@@ -104,7 +112,7 @@ export const createMockTransport = <TState>(
|
|
|
104
112
|
}) as typeof fetch;
|
|
105
113
|
|
|
106
114
|
return {
|
|
107
|
-
transport:
|
|
115
|
+
transport: StatewireHttp({
|
|
108
116
|
url: `http://mock-${++mockCount}/api`,
|
|
109
117
|
fetch: fakeFetch,
|
|
110
118
|
}),
|
package/src/transports/ws.ts
CHANGED
|
@@ -34,7 +34,7 @@ const OPEN = 1;
|
|
|
34
34
|
export { WS_SUBPROTOCOL };
|
|
35
35
|
|
|
36
36
|
const socketUrl = async (
|
|
37
|
-
options: StatewireClient.
|
|
37
|
+
options: StatewireClient.WebsocketOptions,
|
|
38
38
|
clientId: string,
|
|
39
39
|
) => {
|
|
40
40
|
const { url, query } = options;
|
|
@@ -72,7 +72,7 @@ const runSocket = (
|
|
|
72
72
|
ws.onerror = () => {};
|
|
73
73
|
ws.onopen = () => {
|
|
74
74
|
const negotiated = ws.protocol.trim();
|
|
75
|
-
if (negotiated !==
|
|
75
|
+
if (negotiated !== WS_SUBPROTOCOL) {
|
|
76
76
|
outcome = unsupportedVersion(c, negotiated);
|
|
77
77
|
ws.close();
|
|
78
78
|
return;
|
|
@@ -124,7 +124,7 @@ const runSocket = (
|
|
|
124
124
|
const attemptSocket = async (
|
|
125
125
|
c: Conn,
|
|
126
126
|
w: WsChannel,
|
|
127
|
-
options: StatewireClient.
|
|
127
|
+
options: StatewireClient.WebsocketOptions,
|
|
128
128
|
abort: AbortController,
|
|
129
129
|
): Promise<AttemptOutcome> => {
|
|
130
130
|
let ws: WebSocket | null = null;
|
|
@@ -166,7 +166,7 @@ const sendFrame = (
|
|
|
166
166
|
};
|
|
167
167
|
|
|
168
168
|
const useWsLink = (
|
|
169
|
-
options: StatewireClient.
|
|
169
|
+
options: StatewireClient.WebsocketOptions,
|
|
170
170
|
ctx: StatewireRemoteProps,
|
|
171
171
|
): RemoteLink => {
|
|
172
172
|
const [w] = useState((): WsChannel => ({ socket: null }));
|
|
@@ -178,13 +178,13 @@ const useWsLink = (
|
|
|
178
178
|
});
|
|
179
179
|
};
|
|
180
180
|
|
|
181
|
-
const
|
|
182
|
-
options: StatewireClient.
|
|
181
|
+
const useStatewireWebsocket = (
|
|
182
|
+
options: StatewireClient.WebsocketOptions,
|
|
183
183
|
): Statewire<any, any> => useStatewireRemote(options, useWsLink);
|
|
184
184
|
|
|
185
|
-
export const
|
|
185
|
+
export const StatewireWebsocket = resource(useStatewireWebsocket) as <
|
|
186
186
|
TState = any,
|
|
187
187
|
TCommands extends Statewire.Commands = Statewire.Commands,
|
|
188
188
|
>(
|
|
189
|
-
options: StatewireClient.
|
|
189
|
+
options: StatewireClient.WebsocketOptions,
|
|
190
190
|
) => ResourceElement<Statewire<TState | undefined, TCommands>>;
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { StatewireDocuments } from "./wire/documents";
|
|
2
|
+
import type { StatewireProtocol } from "./wire/protocol";
|
|
1
3
|
import type { StatewireClient } from "./statewire";
|
|
2
4
|
import type { StatewirePath } from "./wire/ops";
|
|
3
5
|
|
|
@@ -16,6 +18,7 @@ export namespace Statewire {
|
|
|
16
18
|
|
|
17
19
|
export type CommandUpdate<TCommands extends Commands = Commands> = {
|
|
18
20
|
[K in keyof TCommands]: {
|
|
21
|
+
readonly protocol?: string;
|
|
19
22
|
method: K;
|
|
20
23
|
params: Parameters<TCommands[K]>;
|
|
21
24
|
seq: number;
|
|
@@ -50,6 +53,14 @@ export interface Statewire<
|
|
|
50
53
|
TCommands extends Statewire.Commands = Statewire.Commands,
|
|
51
54
|
> {
|
|
52
55
|
state: TState;
|
|
56
|
+
documents?: StatewireDocuments.Source;
|
|
57
|
+
selection?: StatewireProtocol.Selection | undefined;
|
|
58
|
+
invoke?: (
|
|
59
|
+
protocol: string,
|
|
60
|
+
method: string,
|
|
61
|
+
params: readonly unknown[],
|
|
62
|
+
) => Promise<unknown>;
|
|
63
|
+
updateContext?: (ops: readonly unknown[]) => void;
|
|
53
64
|
commands: Statewire.CommandsProxy<TCommands>;
|
|
54
65
|
connection?: StatewireClient.Connection;
|
|
55
66
|
source?: Statewire.StateSource;
|
package/src/util/randomId.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export const randomId = (): string =>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
Array.from(crypto.getRandomValues(new Uint8Array(16)), (byte) =>
|
|
3
|
+
byte.toString(16).padStart(2, "0"),
|
|
4
|
+
).join("");
|