statewire 0.8.1 → 0.9.1
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 +6 -5
- 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 +13 -16
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useStatewireCommands,
|
|
1
|
+
import { useStatewireCommands, useStatewireState } from "../host/host.js";
|
|
2
2
|
import "../host.js";
|
|
3
3
|
import { tick } from "./testing.js";
|
|
4
4
|
import { resource } from "@assistant-ui/tap";
|
|
@@ -10,21 +10,19 @@ const settle = async () => {
|
|
|
10
10
|
/** A minimal statewire host element: `{ count }` state with an `increment` command. */
|
|
11
11
|
const createCounterElement = () => resource(() => {
|
|
12
12
|
const [state] = useStatewireState(() => ({ count: 0 }));
|
|
13
|
-
const emit = useStatewireEmit();
|
|
14
13
|
return {
|
|
15
14
|
state,
|
|
16
15
|
commands: useStatewireCommands({ increment: () => {
|
|
17
16
|
state.count += 1;
|
|
18
|
-
} })
|
|
19
|
-
emit
|
|
17
|
+
} })
|
|
20
18
|
};
|
|
21
19
|
})();
|
|
22
|
-
/** Serialize a
|
|
23
|
-
const commandFrame = (method, seq, params = []) => JSON.stringify({
|
|
20
|
+
/** Serialize a one-statement cmd-lane frame as a client would send it. */
|
|
21
|
+
const commandFrame = (method, seq, params = []) => JSON.stringify({ cmd: [{
|
|
24
22
|
method,
|
|
25
23
|
params,
|
|
26
24
|
seq
|
|
27
|
-
});
|
|
25
|
+
}] });
|
|
28
26
|
/** Record sent frames and close calls; `last()` parses the latest frame. */
|
|
29
27
|
const createSocketRecorder = () => {
|
|
30
28
|
const sent = [];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixtures.js","names":["resource","useStatewireCommands","
|
|
1
|
+
{"version":3,"file":"fixtures.js","names":["resource","useStatewireCommands","useStatewireState","tick","settle","i","createCounterElement","state","count","commands","increment","commandFrame","method","seq","params","JSON","stringify","cmd","SocketRecorder","sent","closes","code","reason","last","send","data","close","createSocketRecorder","parse","at","push","undefined"],"sources":["../../src/transports/fixtures.ts"],"sourcesContent":["import { resource } from \"@assistant-ui/tap\";\nimport { useStatewireCommands, useStatewireState } from \"../host\";\nimport { tick } from \"./testing\";\n\n/** Let several rounds of queued microtasks and op batches drain. */\nexport const settle = async () => {\n for (let i = 0; i < 3; i++) await tick();\n};\n\n/** A minimal statewire host element: `{ count }` state with an `increment` command. */\nexport const createCounterElement = () =>\n resource(() => {\n const [state] = useStatewireState(() => ({ count: 0 }));\n const commands = useStatewireCommands({\n increment: () => {\n state.count += 1;\n },\n });\n return { state, commands };\n })();\n\n/** Serialize a one-statement cmd-lane frame as a client would send it. */\nexport const commandFrame = (\n method: string,\n seq: number,\n params: unknown[] = [],\n) => JSON.stringify({ cmd: [{ method, params, seq }] });\n\nexport type SocketRecorder = {\n sent: string[];\n closes: { code?: number; reason?: string }[];\n last: () => any;\n send: (data: string) => void;\n close: (code?: number, reason?: string) => void;\n};\n\n/** Record sent frames and close calls; `last()` parses the latest frame. */\nexport const createSocketRecorder = (): SocketRecorder => {\n const sent: string[] = [];\n const closes: { code?: number; reason?: string }[] = [];\n return {\n sent,\n closes,\n last: () => JSON.parse(sent.at(-1)!),\n send: (data) => {\n sent.push(data);\n },\n close: (code, reason) => {\n closes.push({\n ...(code !== undefined && { code }),\n ...(reason !== undefined && { reason }),\n });\n },\n };\n};\n"],"mappings":";;;;;;AAKA,MAAaI,SAAS,YAAY;CAChC,KAAK,IAAIC,IAAI,GAAGA,IAAI,GAAGA,KAAK,MAAMF,KAAK;AACzC;;AAGA,MAAaG,6BACXN,eAAe;CACb,MAAM,CAACO,SAASL,yBAAyB,EAAEM,OAAO,EAAE,EAAE;CAMtD,OAAO;EAAED;EAAOE,UALCR,qBAAqB,EACpCS,iBAAiB;GACfH,MAAMC,SAAS;EACjB,EACF,CACgBC;CAAS;AAC3B,CAAC,CAAC,CAAC;;AAGL,MAAaE,gBACXC,QACAC,KACAC,SAAoB,CAAA,MACjBC,KAAKC,UAAU,EAAEC,KAAK,CAAC;CAAEL;CAAQE;CAAQD;AAAI,CAAC,EAAE,CAAC;;AAWtD,MAAac,6BAA6C;CACxD,MAAMR,OAAiB,CAAA;CACvB,MAAMC,SAA+C,CAAA;CACrD,OAAO;EACLD;EACAC;EACAG,YAAYR,KAAKa,MAAMT,KAAKU,GAAG,EAAE,CAAE;EACnCL,OAAOC,SAAS;GACdN,KAAKW,KAAKL,IAAI;EAChB;EACAC,QAAQL,MAAMC,WAAW;GACvBF,OAAOU,KAAK;IACV,GAAIT,SAASU,KAAAA,KAAa,EAAEV,KAAK;IACjC,GAAIC,WAAWS,KAAAA,KAAa,EAAET,OAAO;GACvC,CAAC;EACH;CACF;AACF"}
|
|
@@ -6,7 +6,8 @@ import { StatewireSessionOptions } from "../client/session.js";
|
|
|
6
6
|
declare const useStatewireRemote: <TOptions extends {
|
|
7
7
|
url: string;
|
|
8
8
|
onChange?: (connection: StatewireClient.Connection) => void;
|
|
9
|
-
|
|
9
|
+
onDone?: () => void;
|
|
10
|
+
} & StatewireSessionOptions>(options: TOptions, useLink: (options: TOptions, ctx: StatewireRemoteProps) => RemoteLink) => Statewire<any, any>;
|
|
10
11
|
//#endregion
|
|
11
12
|
export { useStatewireRemote };
|
|
12
13
|
//# sourceMappingURL=remote.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote.d.ts","names":[],"sources":["../../src/transports/remote.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"remote.d.ts","names":[],"sources":["../../src/transports/remote.ts"],"mappings":";;;;;cAWa,qBACX;EACE;EACA,YAAY,YAAY,gBAAgB;EACxC;IACE,yBAEJ,SAAS,UACT,UAAU,SAAS,UAAU,KAAK,yBAAyB,eAC1D"}
|
|
@@ -1,18 +1,24 @@
|
|
|
1
|
-
import { useStatewireEmit } from "../host/host.js";
|
|
2
1
|
import { setContext, setTransport, useStatewireSession } from "../client/session.js";
|
|
3
2
|
import { useEffect } from "@assistant-ui/tap/react-shim";
|
|
4
3
|
//#region src/transports/remote.ts
|
|
5
4
|
const useStatewireRemote = (options, useLink) => {
|
|
6
|
-
const
|
|
7
|
-
|
|
5
|
+
const s = useStatewireSession({
|
|
6
|
+
sessionStore: options.sessionStore,
|
|
7
|
+
onDone: options.onDone
|
|
8
|
+
});
|
|
8
9
|
const conn = useLink(options, s.ctx);
|
|
9
10
|
useEffect(() => setTransport(s, conn), [s, conn]);
|
|
10
11
|
return {
|
|
11
12
|
state: s.canonical,
|
|
12
13
|
commands: s.commands,
|
|
13
|
-
emit,
|
|
14
14
|
connection: conn.connection,
|
|
15
15
|
source: s.source,
|
|
16
|
+
get durableSeq() {
|
|
17
|
+
return s.durableSeq;
|
|
18
|
+
},
|
|
19
|
+
get isBusy() {
|
|
20
|
+
return !s.idle;
|
|
21
|
+
},
|
|
16
22
|
setContext: (value) => setContext(s, value)
|
|
17
23
|
};
|
|
18
24
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote.js","names":["useEffect","
|
|
1
|
+
{"version":3,"file":"remote.js","names":["useEffect","Statewire","setContext","setTransport","useStatewireSession","StatewireSessionOptions","StatewireClient","RemoteLink","StatewireRemoteProps","useStatewireRemote","url","onChange","connection","Connection","onDone","options","TOptions","useLink","ctx","s","sessionStore","conn","state","canonical","commands","source","durableSeq","isBusy","idle","value"],"sources":["../../src/transports/remote.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport type { Statewire } from \"../types\";\nimport {\n setContext,\n setTransport,\n useStatewireSession,\n} from \"../client/session\";\nimport type { StatewireSessionOptions } from \"../client/session\";\nimport type { StatewireClient } from \"../statewire\";\nimport type { RemoteLink, StatewireRemoteProps } from \"./sse\";\n\nexport const useStatewireRemote = <\n TOptions extends {\n url: string;\n onChange?: (connection: StatewireClient.Connection) => void;\n onDone?: () => void;\n } & StatewireSessionOptions,\n>(\n options: TOptions,\n useLink: (options: TOptions, ctx: StatewireRemoteProps) => RemoteLink,\n): Statewire<any, any> => {\n const s = useStatewireSession({\n sessionStore: options.sessionStore,\n onDone: options.onDone,\n });\n const conn = useLink(options, s.ctx);\n useEffect(() => setTransport(s, conn), [s, conn]);\n\n return {\n state: s.canonical,\n commands: s.commands,\n connection: conn.connection,\n source: s.source,\n get durableSeq() {\n return s.durableSeq;\n },\n get isBusy() {\n return !s.idle;\n },\n setContext: (value) => setContext(s, value),\n };\n};\n"],"mappings":";;;AAWA,MAAaS,sBAOXM,SACAE,YACwB;CACxB,MAAME,IAAIf,oBAAoB;EAC5BgB,cAAcL,QAAQK;EACtBN,QAAQC,QAAQD;CAClB,CAAC;CACD,MAAMO,OAAOJ,QAAQF,SAASI,EAAED,GAAG;CACnClB,gBAAgBG,aAAagB,GAAGE,IAAI,GAAG,CAACF,GAAGE,IAAI,CAAC;CAEhD,OAAO;EACLC,OAAOH,EAAEI;EACTC,UAAUL,EAAEK;EACZZ,YAAYS,KAAKT;EACjBa,QAAQN,EAAEM;EACV,IAAIC,aAAa;GACf,OAAOP,EAAEO;EACX;EACA,IAAIC,SAAS;GACX,OAAO,CAACR,EAAES;EACZ;EACA1B,aAAa2B,UAAU3B,WAAWiB,GAAGU,KAAK;CAC5C;AACF"}
|
package/dist/transports/sse.d.ts
CHANGED
|
@@ -1,74 +1,60 @@
|
|
|
1
1
|
import { StatewireClient } from "../statewire.js";
|
|
2
2
|
import { Statewire } from "../types.js";
|
|
3
|
-
import { StatewireMetaEntry } from "../wire/meta.js";
|
|
4
3
|
import { StatewireHost } from "../host.js";
|
|
5
|
-
import {
|
|
4
|
+
import { StatewireCmd, StatewireSyn } from "../client/connection.js";
|
|
6
5
|
import { ResourceElement } from "@assistant-ui/tap";
|
|
7
6
|
//#region src/transports/sse.d.ts
|
|
7
|
+
/** A client frame's lanes: stamped statements, strictly consecutive per lane. */
|
|
8
|
+
type StatewireFrame = {
|
|
9
|
+
ctx?: {
|
|
10
|
+
ops: unknown[];
|
|
11
|
+
seq: number;
|
|
12
|
+
}[];
|
|
13
|
+
cmd?: {
|
|
14
|
+
method: string;
|
|
15
|
+
params: unknown[];
|
|
16
|
+
meta?: unknown;
|
|
17
|
+
seq: number;
|
|
18
|
+
}[];
|
|
19
|
+
};
|
|
8
20
|
type StatewireRemoteProps = {
|
|
9
21
|
clientId: string;
|
|
10
|
-
|
|
22
|
+
/** Lanes to ride the attach frame (no-lease supersession only); records the included seqs. */
|
|
23
|
+
takeAttachLanes: () => StatewireFrame | null;
|
|
24
|
+
onHello: (args: {
|
|
11
25
|
ops: unknown[];
|
|
12
|
-
|
|
26
|
+
syn: StatewireSyn;
|
|
13
27
|
}) => void;
|
|
14
28
|
onOps: (args: {
|
|
15
29
|
ops: unknown[];
|
|
16
|
-
|
|
17
|
-
ack?: number;
|
|
30
|
+
cmds: StatewireCmd[];
|
|
18
31
|
}) => void;
|
|
32
|
+
onCmds: (cmds: StatewireCmd[]) => void;
|
|
33
|
+
onRefusal: (syn: StatewireSyn) => void;
|
|
34
|
+
onIdle: (level: boolean) => void;
|
|
35
|
+
onDetach: () => void;
|
|
19
36
|
onFinish: (args: StatewireHost.Fin) => void;
|
|
20
37
|
};
|
|
21
|
-
type StatewireWireCommand = {
|
|
22
|
-
method: string;
|
|
23
|
-
params: readonly unknown[];
|
|
24
|
-
context?: never;
|
|
25
|
-
} | {
|
|
26
|
-
method?: never;
|
|
27
|
-
params?: never;
|
|
28
|
-
context: Record<string, unknown>;
|
|
29
|
-
};
|
|
30
|
-
/** The wire shape of a client context submission: one root replace op. */
|
|
31
|
-
declare const contextOps: (value: Record<string, unknown>) => {
|
|
32
|
-
op: string;
|
|
33
|
-
path: never[];
|
|
34
|
-
value: Record<string, unknown>;
|
|
35
|
-
}[];
|
|
36
|
-
declare const encodeWireCommand: (command: StatewireWireCommand) => {
|
|
37
|
-
context?: {
|
|
38
|
-
op: string;
|
|
39
|
-
path: never[];
|
|
40
|
-
value: Record<string, unknown>;
|
|
41
|
-
}[];
|
|
42
|
-
params?: unknown[];
|
|
43
|
-
meta?: StatewireMetaEntry[];
|
|
44
|
-
method?: string;
|
|
45
|
-
};
|
|
46
38
|
type StatewireSubmitResult = {
|
|
47
|
-
type: "
|
|
48
|
-
response?: StatewireResponse;
|
|
49
|
-
} | {
|
|
50
|
-
type: "rejected";
|
|
51
|
-
message: string;
|
|
39
|
+
type: "ok";
|
|
52
40
|
} | {
|
|
53
|
-
type: "
|
|
54
|
-
|
|
55
|
-
type: "unknown-client";
|
|
41
|
+
type: "refusal";
|
|
42
|
+
syn: StatewireSyn;
|
|
56
43
|
} | {
|
|
57
44
|
type: "stale-lease";
|
|
45
|
+
} | {
|
|
46
|
+
type: "no-lease";
|
|
58
47
|
};
|
|
59
48
|
type ConnectionState = StatewireClient.ConnectionState;
|
|
60
|
-
/** The live wire a remote session drives: connection status plus
|
|
49
|
+
/** The live wire a remote session drives: connection status plus frame submission. */
|
|
61
50
|
type RemoteLink = {
|
|
62
51
|
connection: StatewireClient.Connection;
|
|
63
|
-
send: (
|
|
64
|
-
seq: number;
|
|
65
|
-
command: StatewireWireCommand;
|
|
66
|
-
}) => Promise<StatewireSubmitResult>;
|
|
52
|
+
send: (frame: StatewireFrame) => Promise<StatewireSubmitResult>;
|
|
67
53
|
};
|
|
68
54
|
type AuthState = {
|
|
69
55
|
stale: boolean;
|
|
70
56
|
};
|
|
71
|
-
declare const StatewireSSE: <TState = any, TCommands extends Statewire.Commands = Statewire.Commands
|
|
57
|
+
declare const StatewireSSE: <TState = any, TCommands extends Statewire.Commands = Statewire.Commands>(options: StatewireClient.SSEOptions) => ResourceElement<Statewire<TState | undefined, TCommands>>;
|
|
72
58
|
//#endregion
|
|
73
|
-
export { AuthState, ConnectionState, RemoteLink, StatewireRemoteProps, StatewireSSE, StatewireSubmitResult
|
|
59
|
+
export { AuthState, ConnectionState, RemoteLink, StatewireFrame, StatewireRemoteProps, StatewireSSE, StatewireSubmitResult };
|
|
74
60
|
//# sourceMappingURL=sse.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.d.ts","names":[],"sources":["../../src/transports/sse.ts"],"mappings":";;;;;;;KAuBY;EACV;
|
|
1
|
+
{"version":3,"file":"sse.d.ts","names":[],"sources":["../../src/transports/sse.ts"],"mappings":";;;;;;;KAuBY;EACV;IAAQ;IAAgB;;EACxB;IAAQ;IAAgB;IAAmB;IAAgB;;;KAGjD;EACV;;EAEA,uBAAuB;EACvB,UAAU;IAAQ;IAAgB,KAAK;;EACvC,QAAQ;IAAQ;IAAgB,MAAM;;EACtC,SAAS,MAAM;EACf,YAAY,KAAK;EACjB,SAAS;EACT;EACA,WAAW,MAAM,cAAc;;KAGrB;EACN;;EACA;EAAiB,KAAK;;EACtB;;EACA;;KAEM,kBAAkB,gBAAgB;;KAGlC;EACV,YAAY,gBAAgB;EAC5B,OAAO,OAAO,mBAAmB,QAAQ;;KAQ/B;EAAc;;cAqOb,eACX,cACA,kBAAkB,UAAU,WAAW,UAAU,UAEjD,SAAS,gBAAgB,eACtB,gBAAgB,UAAU,oBAAoB"}
|
package/dist/transports/sse.js
CHANGED
|
@@ -1,28 +1,18 @@
|
|
|
1
|
-
import { encodeParams, reviveResponse } from "../wire/meta.js";
|
|
2
1
|
import { createSseParser } from "./sse-parser.js";
|
|
3
2
|
import { timing } from "../client/timing.js";
|
|
4
|
-
import { classify, handleEnvelope,
|
|
3
|
+
import { classify, handleEnvelope, useStatewireConn } from "../client/connection.js";
|
|
5
4
|
import { useStatewireRemote } from "./remote.js";
|
|
6
5
|
import { useRef, useState } from "@assistant-ui/tap/react-shim";
|
|
7
6
|
import { flushTapSync, resource } from "@assistant-ui/tap";
|
|
8
7
|
//#region src/transports/sse.ts
|
|
9
|
-
|
|
10
|
-
const contextOps = (value) => [{
|
|
11
|
-
op: "replace",
|
|
12
|
-
path: [],
|
|
13
|
-
value
|
|
14
|
-
}];
|
|
15
|
-
const encodeWireCommand = (command) => ({
|
|
16
|
-
...command.method !== void 0 && {
|
|
17
|
-
method: command.method,
|
|
18
|
-
...encodeParams(command.params)
|
|
19
|
-
},
|
|
20
|
-
...command.context !== void 0 && { context: contextOps(command.context) }
|
|
21
|
-
});
|
|
8
|
+
const LEASE_HEADER = "Statewire-Lease";
|
|
22
9
|
const baseUrl = (options) => options.url.replace(/\/$/, "");
|
|
23
|
-
const
|
|
10
|
+
const resolveHeaders = async (options, ctx) => {
|
|
24
11
|
const { headers } = options;
|
|
25
|
-
|
|
12
|
+
return new Headers(typeof headers === "function" ? await headers(ctx) : headers);
|
|
13
|
+
};
|
|
14
|
+
const withExtra = (headers, extra) => {
|
|
15
|
+
const merged = new Headers(headers);
|
|
26
16
|
for (const [name, value] of Object.entries(extra)) merged.set(name, value);
|
|
27
17
|
return merged;
|
|
28
18
|
};
|
|
@@ -58,29 +48,38 @@ const consume = async (c, reader, heartbeat, dead) => {
|
|
|
58
48
|
const attemptStream = async (c, options, auth, abort) => {
|
|
59
49
|
const stale = auth.stale;
|
|
60
50
|
auth.stale = false;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
51
|
+
c.lease = null;
|
|
52
|
+
const userHeaders = await resolveHeaders(options, { staleAuth: stale });
|
|
53
|
+
const lanes = flushTapSync(() => c.ctx.takeAttachLanes());
|
|
54
|
+
const frame = {
|
|
55
|
+
headers: Object.fromEntries(userHeaders),
|
|
56
|
+
...lanes ?? {}
|
|
57
|
+
};
|
|
65
58
|
const heartbeat = new AbortController();
|
|
66
59
|
const headerTimer = setTimeout(() => heartbeat.abort(), timing.heartbeatTimeoutMs);
|
|
67
60
|
let res;
|
|
68
61
|
try {
|
|
69
62
|
res = await timedFetch(options, `${baseUrl(options)}/stream`, {
|
|
70
|
-
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: withExtra(userHeaders, {
|
|
65
|
+
"content-type": "application/json",
|
|
66
|
+
"Statewire-Client-Id": c.ctx.clientId,
|
|
67
|
+
accept: "text/event-stream"
|
|
68
|
+
}),
|
|
69
|
+
body: JSON.stringify(frame),
|
|
71
70
|
cache: "no-store",
|
|
72
71
|
signal: AbortSignal.any([abort.signal, heartbeat.signal])
|
|
73
72
|
}, heartbeat.signal);
|
|
74
73
|
} finally {
|
|
75
74
|
clearTimeout(headerTimer);
|
|
76
75
|
}
|
|
77
|
-
if (res.status === 429 || res.status === 503) throw overloaded(res);
|
|
78
76
|
if (res.status === 401) {
|
|
79
77
|
if (stale) c.episode = { cause: "unauthorized" };
|
|
80
78
|
auth.stale = true;
|
|
81
79
|
throw new Error("unauthorized");
|
|
82
80
|
}
|
|
83
81
|
if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);
|
|
82
|
+
c.lease = res.headers.get(LEASE_HEADER);
|
|
84
83
|
const reader = res.body.getReader();
|
|
85
84
|
try {
|
|
86
85
|
return await consume(c, reader, heartbeat, () => abort.signal.aborted);
|
|
@@ -88,53 +87,51 @@ const attemptStream = async (c, options, auth, abort) => {
|
|
|
88
87
|
reader.cancel().catch(() => {});
|
|
89
88
|
}
|
|
90
89
|
};
|
|
91
|
-
const
|
|
92
|
-
const postCommand = async (c, options, { seq, command }, timeout, stale) => timedFetch(options, `${baseUrl(options)}/commands`, {
|
|
90
|
+
const postFrame = async (c, options, lease, frame, timeout, stale) => timedFetch(options, `${baseUrl(options)}/frames`, {
|
|
93
91
|
method: "POST",
|
|
94
|
-
headers: await
|
|
92
|
+
headers: withExtra(await resolveHeaders(options, { staleAuth: stale }), {
|
|
95
93
|
"content-type": "application/json",
|
|
96
94
|
"Statewire-Client-Id": c.ctx.clientId,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
body: JSON.stringify(encodeWireCommand(command)),
|
|
95
|
+
[LEASE_HEADER]: lease
|
|
96
|
+
}),
|
|
97
|
+
body: JSON.stringify(frame),
|
|
101
98
|
signal: AbortSignal.any([c.lifetime?.signal ?? AbortSignal.abort(), timeout])
|
|
102
99
|
}, timeout);
|
|
103
|
-
const
|
|
100
|
+
const retryAfterMs = (res) => {
|
|
101
|
+
const header = res.headers.get("retry-after")?.trim();
|
|
102
|
+
return header !== void 0 && /^\d+$/.test(header) ? Number(header) * 1e3 : null;
|
|
103
|
+
};
|
|
104
|
+
const refusalSyn = async (res) => {
|
|
105
|
+
const syn = (await res.json().catch(() => null))?.syn;
|
|
106
|
+
if (typeof syn !== "object" || syn === null) throw new Error(`HTTP ${res.status}`);
|
|
107
|
+
return syn;
|
|
108
|
+
};
|
|
109
|
+
const sendFrame = async (c, options, auth, frame) => {
|
|
110
|
+
const lease = c.lease;
|
|
111
|
+
if (lease === null) return { type: "no-lease" };
|
|
104
112
|
const timeout = AbortSignal.timeout(timing.heartbeatTimeoutMs);
|
|
105
|
-
let res = await
|
|
106
|
-
if (res.status === 401 && typeof options.headers === "function") res = await
|
|
107
|
-
if (res.ok) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
response: reviveResponse(body.res)
|
|
120
|
-
} : { type: "receipt" };
|
|
113
|
+
let res = await postFrame(c, options, lease, frame, timeout, false);
|
|
114
|
+
if (res.status === 401 && typeof options.headers === "function") res = await postFrame(c, options, lease, frame, timeout, true);
|
|
115
|
+
if (res.ok) return { type: "ok" };
|
|
116
|
+
if (res.status === 429 || res.status === 409) {
|
|
117
|
+
const headerRetry = res.status === 429 ? retryAfterMs(res) : null;
|
|
118
|
+
const syn = await refusalSyn(res).catch((error) => {
|
|
119
|
+
if (headerRetry === null) throw error;
|
|
120
|
+
return { retry: headerRetry };
|
|
121
|
+
});
|
|
122
|
+
if (syn.retry === void 0 && headerRetry !== null) syn.retry = headerRetry;
|
|
123
|
+
return {
|
|
124
|
+
type: "refusal",
|
|
125
|
+
syn
|
|
126
|
+
};
|
|
121
127
|
}
|
|
122
|
-
if (res.status ===
|
|
128
|
+
if (res.status === 423) return { type: "stale-lease" };
|
|
123
129
|
if (res.status === 401) {
|
|
124
130
|
auth.stale = true;
|
|
125
131
|
c.episode = { cause: "unauthorized" };
|
|
126
132
|
c.connection.reconnect();
|
|
127
133
|
throw new Error("unauthorized");
|
|
128
134
|
}
|
|
129
|
-
const body = await res.json().catch(() => null);
|
|
130
|
-
if (body?.error === "rejected") return {
|
|
131
|
-
type: "rejected",
|
|
132
|
-
message: body.rejections?.[0]?.message ?? `HTTP ${res.status}`
|
|
133
|
-
};
|
|
134
|
-
if (res.status === 409 || res.status === 412 || res.status === 423) {
|
|
135
|
-
const verdict = res.status === 409 ? "seq-gap" : res.status === 412 ? "unknown-client" : "stale-lease";
|
|
136
|
-
if (body?.error === verdict) return { type: verdict };
|
|
137
|
-
}
|
|
138
135
|
throw new Error(`HTTP ${res.status}`);
|
|
139
136
|
};
|
|
140
137
|
const useSseLink = (options, ctx) => {
|
|
@@ -151,13 +148,13 @@ const useSseLink = (options, ctx) => {
|
|
|
151
148
|
get connection() {
|
|
152
149
|
return c_0.connection;
|
|
153
150
|
},
|
|
154
|
-
send: (
|
|
151
|
+
send: (frame) => sendFrame(c_0, latest.current, auth, frame)
|
|
155
152
|
}));
|
|
156
153
|
return link;
|
|
157
154
|
};
|
|
158
155
|
const useStatewireSSE = (options) => useStatewireRemote(options, useSseLink);
|
|
159
156
|
const StatewireSSE = resource(useStatewireSSE);
|
|
160
157
|
//#endregion
|
|
161
|
-
export { StatewireSSE
|
|
158
|
+
export { StatewireSSE };
|
|
162
159
|
|
|
163
160
|
//# sourceMappingURL=sse.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.js","names":["useRef","useState","flushTapSync","resource","ResourceElement","encodeParams","reviveResponse","createSseParser","timing","classify","handleEnvelope","overloadedError","parseRetryAfter","useStatewireConn","Conn","StatewireEnvelope","StatewireResponse","Statewire","StatewireClient","StatewireHost","useStatewireRemote","StatewireRemoteProps","clientId","onSnapshot","args","ops","lastSeq","onOps","res","ack","onFinish","Fin","StatewireWireCommand","method","params","context","Record","contextOps","value","op","path","encodeWireCommand","command","undefined","StatewireSubmitResult","type","response","message","ConnectionState","RemoteLink","connection","Connection","send","seq","Promise","baseUrl","options","SSEOptions","url","replace","AuthState","stale","requestHeaders","extra","ctx","staleAuth","headers","merged","Headers","name","Object","entries","set","timedFetch","input","init","RequestInit","signal","AbortSignal","timeout","Response","fetch","globalThis","consume","c","reader","ReadableStreamDefaultReader","Uint8Array","heartbeat","AbortController","dead","decoder","TextDecoder","parse","timer","setTimeout","abort","heartbeatTimeoutMs","result","ReadableStreamReadResult","read","clearTimeout","done","data","decode","stream","env","JSON","attemptStream","auth","accept","headerTimer","cache","any","status","overloaded","episode","cause","Error","ok","body","getReader","aborted","cancel","catch","get","postCommand","String","lease","stringify","lifetime","sendCommand","json","reconnect","error","rejections","verdict","const","useSseLink","latest","current","onChange","attemptOnce","link","useStatewireSSE","StatewireSSE","Commands","TState","TCommands","TEvent"],"sources":["../../src/transports/sse.ts"],"sourcesContent":["import { useRef, useState } from \"react\";\nimport { flushTapSync, resource } from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { encodeParams, reviveResponse } from \"../wire/meta\";\nimport { createSseParser } from \"./sse-parser\";\nimport { timing } from \"../client/timing\";\nimport {\n classify,\n handleEnvelope,\n overloadedError,\n parseRetryAfter,\n useStatewireConn,\n} from \"../client/connection\";\nimport type {\n Conn,\n StatewireEnvelope,\n StatewireResponse,\n} from \"../client/connection\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireClient } from \"../statewire\";\nimport type { StatewireHost } from \"../host\";\nimport { useStatewireRemote } from \"./remote\";\n\nexport type StatewireRemoteProps = {\n clientId: string;\n onSnapshot: (args: { ops: unknown[]; lastSeq: number }) => void;\n onOps: (args: {\n ops: unknown[];\n res?: StatewireResponse[];\n ack?: number;\n }) => void;\n onFinish: (args: StatewireHost.Fin) => void;\n};\n\nexport type StatewireWireCommand =\n | { method: string; params: readonly unknown[]; context?: never }\n | { method?: never; params?: never; context: Record<string, unknown> };\n\n/** The wire shape of a client context submission: one root replace op. */\nexport const contextOps = (value: Record<string, unknown>) => [\n { op: \"replace\", path: [], value },\n];\n\nexport const encodeWireCommand = (command: StatewireWireCommand) => ({\n ...(command.method !== undefined && {\n method: command.method,\n ...encodeParams(command.params),\n }),\n ...(command.context !== undefined && {\n context: contextOps(command.context),\n }),\n});\n\nexport type StatewireSubmitResult =\n | { type: \"receipt\"; response?: StatewireResponse }\n | { type: \"rejected\"; message: string }\n | { type: \"seq-gap\" }\n | { type: \"unknown-client\" }\n | { type: \"stale-lease\" };\n\nexport type ConnectionState = StatewireClient.ConnectionState;\n\n/** The live wire a remote session drives: connection status plus command POST. */\nexport type RemoteLink = {\n connection: StatewireClient.Connection;\n send: (args: {\n seq: number;\n command: StatewireWireCommand;\n }) => Promise<StatewireSubmitResult>;\n};\n\nconst baseUrl = (options: StatewireClient.SSEOptions) =>\n options.url.replace(/\\/$/, \"\");\n\nexport type AuthState = { stale: boolean };\n\nconst requestHeaders = async (\n options: StatewireClient.SSEOptions,\n extra: Record<string, string>,\n ctx: { staleAuth: boolean },\n) => {\n const { headers } = options;\n const merged = new Headers(\n typeof headers === \"function\" ? await headers(ctx) : headers,\n );\n for (const [name, value] of Object.entries(extra)) merged.set(name, value);\n return merged;\n};\n\nconst timedFetch = async (\n options: StatewireClient.SSEOptions,\n input: string,\n init: RequestInit & { signal: AbortSignal },\n timeout: AbortSignal,\n): Promise<Response> => {\n try {\n return await (options.fetch ?? globalThis.fetch)(input, init);\n } catch {\n throw classify(timeout);\n }\n};\n\nconst consume = async (\n c: Conn,\n reader: ReadableStreamDefaultReader<Uint8Array>,\n heartbeat: AbortController,\n dead: () => boolean,\n): Promise<\"stop\" | \"reattach\" | \"ended\"> => {\n const decoder = new TextDecoder();\n const parse = createSseParser();\n for (;;) {\n const timer = setTimeout(\n () => heartbeat.abort(),\n timing.heartbeatTimeoutMs,\n );\n let result: ReadableStreamReadResult<Uint8Array>;\n try {\n result = await reader.read();\n } catch {\n throw classify(heartbeat.signal);\n } finally {\n clearTimeout(timer);\n }\n if (result.done) return \"ended\";\n for (const data of parse(decoder.decode(result.value, { stream: true }))) {\n if (dead()) return \"ended\";\n const env = JSON.parse(data) as StatewireEnvelope;\n const done = flushTapSync(() => handleEnvelope(c, env));\n if (done) return done;\n }\n }\n};\n\nconst attemptStream = async (\n c: Conn,\n options: StatewireClient.SSEOptions,\n auth: AuthState,\n abort: AbortController,\n): Promise<\"stop\" | \"reattach\" | \"ended\"> => {\n const stale = auth.stale;\n auth.stale = false;\n const headers = await requestHeaders(\n options,\n {\n \"Statewire-Client-Id\": c.ctx.clientId,\n accept: \"text/event-stream\",\n },\n { staleAuth: stale },\n );\n const heartbeat = new AbortController();\n const headerTimer = setTimeout(\n () => heartbeat.abort(),\n timing.heartbeatTimeoutMs,\n );\n let res: Response;\n try {\n res = await timedFetch(\n options,\n `${baseUrl(options)}/stream`,\n {\n headers,\n cache: \"no-store\",\n signal: AbortSignal.any([abort.signal, heartbeat.signal]),\n },\n heartbeat.signal,\n );\n } finally {\n clearTimeout(headerTimer);\n }\n if (res.status === 429 || res.status === 503) throw overloaded(res);\n if (res.status === 401) {\n if (stale) c.episode = { cause: \"unauthorized\" };\n auth.stale = true;\n throw new Error(\"unauthorized\");\n }\n if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);\n const reader = res.body.getReader();\n try {\n return await consume(c, reader, heartbeat, () => abort.signal.aborted);\n } finally {\n reader.cancel().catch(() => {});\n }\n};\n\nconst overloaded = (res: Response) =>\n overloadedError(parseRetryAfter(res.headers.get(\"Retry-After\")));\n\nconst postCommand = async (\n c: Conn,\n options: StatewireClient.SSEOptions,\n { seq, command }: { seq: number; command: StatewireWireCommand },\n timeout: AbortSignal,\n stale: boolean,\n): Promise<Response> =>\n timedFetch(\n options,\n `${baseUrl(options)}/commands`,\n {\n method: \"POST\",\n headers: await requestHeaders(\n options,\n {\n \"content-type\": \"application/json\",\n \"Statewire-Client-Id\": c.ctx.clientId,\n \"Statewire-Command-Seq\": String(seq),\n ...(c.lease !== null && { \"Statewire-Lease\": c.lease }),\n },\n { staleAuth: stale },\n ),\n body: JSON.stringify(encodeWireCommand(command)),\n signal: AbortSignal.any([\n c.lifetime?.signal ?? AbortSignal.abort(),\n timeout,\n ]),\n },\n timeout,\n );\n\nconst sendCommand = async (\n c: Conn,\n options: StatewireClient.SSEOptions,\n auth: AuthState,\n args: { seq: number; command: StatewireWireCommand },\n): Promise<StatewireSubmitResult> => {\n const timeout = AbortSignal.timeout(timing.heartbeatTimeoutMs);\n let res = await postCommand(c, options, args, timeout, false);\n if (res.status === 401 && typeof options.headers === \"function\") {\n res = await postCommand(c, options, args, timeout, true);\n }\n if (res.ok) {\n let body: { res?: StatewireResponse } | null;\n try {\n body = (await res.json()) as { res?: StatewireResponse } | null;\n } catch {\n return {\n type: \"rejected\",\n message: \"statewire: protocol error: malformed /commands receipt body\",\n };\n }\n return body?.res !== undefined\n ? { type: \"receipt\", response: reviveResponse(body.res) }\n : { type: \"receipt\" };\n }\n if (res.status === 429 || res.status === 503) throw overloaded(res);\n if (res.status === 401) {\n // Fresh credentials did not clear the 401: signal auth-failed on the\n // connection and let the send retry as a transport fault.\n auth.stale = true;\n c.episode = { cause: \"unauthorized\" };\n c.connection.reconnect();\n throw new Error(\"unauthorized\");\n }\n // Bodies settle, statuses pace: only a statewire-shaped body carries a\n // verdict; any bare status is middleware noise — a transport fault, retried.\n const body = (await res.json().catch(() => null)) as {\n error?: string;\n rejections?: { message?: string }[];\n } | null;\n if (body?.error === \"rejected\") {\n return {\n type: \"rejected\",\n message: body.rejections?.[0]?.message ?? `HTTP ${res.status}`,\n };\n }\n if (res.status === 409 || res.status === 412 || res.status === 423) {\n const verdict =\n res.status === 409\n ? (\"seq-gap\" as const)\n : res.status === 412\n ? (\"unknown-client\" as const)\n : (\"stale-lease\" as const);\n if (body?.error === verdict) return { type: verdict };\n }\n throw new Error(`HTTP ${res.status}`);\n};\n\nconst useSseLink = (\n options: StatewireClient.SSEOptions,\n ctx: StatewireRemoteProps,\n): RemoteLink => {\n const latest = useRef(options);\n latest.current = options;\n const [auth] = useState((): AuthState => ({ stale: false }));\n const c = useStatewireConn({\n url: options.url,\n onChange: options.onChange,\n ctx,\n attemptOnce: (c, abort) => attemptStream(c, options, auth, abort),\n });\n const [link] = useState(\n (): RemoteLink => ({\n get connection() {\n return c.connection;\n },\n send: (args) => sendCommand(c, latest.current, auth, args),\n }),\n );\n return link;\n};\n\nconst useStatewireSSE = (\n options: StatewireClient.SSEOptions,\n): Statewire<any, any, any> => useStatewireRemote(options, useSseLink);\n\nexport const StatewireSSE = resource(useStatewireSSE) as <\n TState = any,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.SSEOptions,\n) => ResourceElement<Statewire<TState | undefined, TCommands, TEvent>>;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sse.js","names":["useRef","useState","flushTapSync","resource","ResourceElement","createSseParser","timing","classify","handleEnvelope","useStatewireConn","AttemptOutcome","Conn","StatewireCmd","StatewireEnvelope","StatewireSyn","Statewire","StatewireClient","StatewireHost","useStatewireRemote","StatewireFrame","ctx","ops","seq","cmd","method","params","meta","StatewireRemoteProps","clientId","takeAttachLanes","onHello","args","syn","onOps","cmds","onCmds","onRefusal","onIdle","level","onDetach","onFinish","Fin","StatewireSubmitResult","type","ConnectionState","RemoteLink","connection","Connection","send","frame","Promise","LEASE_HEADER","baseUrl","options","SSEOptions","url","replace","AuthState","stale","resolveHeaders","staleAuth","Headers","headers","withExtra","extra","Record","merged","name","value","Object","entries","set","timedFetch","input","init","RequestInit","signal","AbortSignal","timeout","Response","fetch","globalThis","consume","c","reader","ReadableStreamDefaultReader","Uint8Array","heartbeat","AbortController","dead","decoder","TextDecoder","parse","timer","setTimeout","abort","heartbeatTimeoutMs","result","ReadableStreamReadResult","read","clearTimeout","done","data","decode","stream","env","JSON","attemptStream","auth","lease","userHeaders","lanes","fromEntries","headerTimer","res","accept","body","stringify","cache","any","status","episode","cause","Error","ok","get","getReader","aborted","cancel","catch","postFrame","lifetime","retryAfterMs","header","trim","undefined","test","Number","refusalSyn","json","sendFrame","headerRetry","error","retry","reconnect","useSseLink","latest","current","onChange","attemptOnce","link","useStatewireSSE","StatewireSSE","Commands","TState","TCommands"],"sources":["../../src/transports/sse.ts"],"sourcesContent":["import { useRef, useState } from \"react\";\nimport { flushTapSync, resource } from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { createSseParser } from \"./sse-parser\";\nimport { timing } from \"../client/timing\";\nimport {\n classify,\n handleEnvelope,\n useStatewireConn,\n} from \"../client/connection\";\nimport type {\n AttemptOutcome,\n Conn,\n StatewireCmd,\n StatewireEnvelope,\n StatewireSyn,\n} from \"../client/connection\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireClient } from \"../statewire\";\nimport type { StatewireHost } from \"../host\";\nimport { useStatewireRemote } from \"./remote\";\n\n/** A client frame's lanes: stamped statements, strictly consecutive per lane. */\nexport type StatewireFrame = {\n ctx?: { ops: unknown[]; seq: number }[];\n cmd?: { method: string; params: unknown[]; meta?: unknown; seq: number }[];\n};\n\nexport type StatewireRemoteProps = {\n clientId: string;\n /** Lanes to ride the attach frame (no-lease supersession only); records the included seqs. */\n takeAttachLanes: () => StatewireFrame | null;\n onHello: (args: { ops: unknown[]; syn: StatewireSyn }) => void;\n onOps: (args: { ops: unknown[]; cmds: StatewireCmd[] }) => void;\n onCmds: (cmds: StatewireCmd[]) => void;\n onRefusal: (syn: StatewireSyn) => void;\n onIdle: (level: boolean) => void;\n onDetach: () => void;\n onFinish: (args: StatewireHost.Fin) => void;\n};\n\nexport type StatewireSubmitResult =\n | { type: \"ok\" }\n | { type: \"refusal\"; syn: StatewireSyn }\n | { type: \"stale-lease\" }\n | { type: \"no-lease\" };\n\nexport type ConnectionState = StatewireClient.ConnectionState;\n\n/** The live wire a remote session drives: connection status plus frame submission. */\nexport type RemoteLink = {\n connection: StatewireClient.Connection;\n send: (frame: StatewireFrame) => Promise<StatewireSubmitResult>;\n};\n\nconst LEASE_HEADER = \"Statewire-Lease\";\n\nconst baseUrl = (options: StatewireClient.SSEOptions) =>\n options.url.replace(/\\/$/, \"\");\n\nexport type AuthState = { stale: boolean };\n\nconst resolveHeaders = async (\n options: StatewireClient.SSEOptions,\n ctx: { staleAuth: boolean },\n): Promise<Headers> => {\n const { headers } = options;\n return new Headers(\n typeof headers === \"function\" ? await headers(ctx) : headers,\n );\n};\n\nconst withExtra = (headers: Headers, extra: Record<string, string>) => {\n const merged = new Headers(headers);\n for (const [name, value] of Object.entries(extra)) merged.set(name, value);\n return merged;\n};\n\nconst timedFetch = async (\n options: StatewireClient.SSEOptions,\n input: string,\n init: RequestInit & { signal: AbortSignal },\n timeout: AbortSignal,\n): Promise<Response> => {\n try {\n return await (options.fetch ?? globalThis.fetch)(input, init);\n } catch {\n throw classify(timeout);\n }\n};\n\nconst consume = async (\n c: Conn,\n reader: ReadableStreamDefaultReader<Uint8Array>,\n heartbeat: AbortController,\n dead: () => boolean,\n): Promise<AttemptOutcome> => {\n const decoder = new TextDecoder();\n const parse = createSseParser();\n for (;;) {\n const timer = setTimeout(\n () => heartbeat.abort(),\n timing.heartbeatTimeoutMs,\n );\n let result: ReadableStreamReadResult<Uint8Array>;\n try {\n result = await reader.read();\n } catch {\n throw classify(heartbeat.signal);\n } finally {\n clearTimeout(timer);\n }\n if (result.done) return \"ended\";\n for (const data of parse(decoder.decode(result.value, { stream: true }))) {\n if (dead()) return \"ended\";\n const env = JSON.parse(data) as StatewireEnvelope;\n const done = flushTapSync(() => handleEnvelope(c, env));\n if (done) return done;\n }\n }\n};\n\nconst attemptStream = async (\n c: Conn,\n options: StatewireClient.SSEOptions,\n auth: AuthState,\n abort: AbortController,\n): Promise<AttemptOutcome> => {\n const stale = auth.stale;\n auth.stale = false;\n c.lease = null;\n const userHeaders = await resolveHeaders(options, { staleAuth: stale });\n const lanes = flushTapSync(() => c.ctx.takeAttachLanes());\n const frame = {\n headers: Object.fromEntries(userHeaders),\n ...(lanes ?? {}),\n };\n const heartbeat = new AbortController();\n const headerTimer = setTimeout(\n () => heartbeat.abort(),\n timing.heartbeatTimeoutMs,\n );\n let res: Response;\n try {\n res = await timedFetch(\n options,\n `${baseUrl(options)}/stream`,\n {\n method: \"POST\",\n headers: withExtra(userHeaders, {\n \"content-type\": \"application/json\",\n \"Statewire-Client-Id\": c.ctx.clientId,\n accept: \"text/event-stream\",\n }),\n body: JSON.stringify(frame),\n cache: \"no-store\",\n signal: AbortSignal.any([abort.signal, heartbeat.signal]),\n },\n heartbeat.signal,\n );\n } finally {\n clearTimeout(headerTimer);\n }\n if (res.status === 401) {\n if (stale) c.episode = { cause: \"unauthorized\" };\n auth.stale = true;\n throw new Error(\"unauthorized\");\n }\n if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);\n c.lease = res.headers.get(LEASE_HEADER);\n const reader = res.body.getReader();\n try {\n return await consume(c, reader, heartbeat, () => abort.signal.aborted);\n } finally {\n reader.cancel().catch(() => {});\n }\n};\n\nconst postFrame = async (\n c: Conn,\n options: StatewireClient.SSEOptions,\n lease: string,\n frame: StatewireFrame,\n timeout: AbortSignal,\n stale: boolean,\n): Promise<Response> =>\n timedFetch(\n options,\n `${baseUrl(options)}/frames`,\n {\n method: \"POST\",\n headers: withExtra(await resolveHeaders(options, { staleAuth: stale }), {\n \"content-type\": \"application/json\",\n \"Statewire-Client-Id\": c.ctx.clientId,\n [LEASE_HEADER]: lease,\n }),\n body: JSON.stringify(frame),\n signal: AbortSignal.any([\n c.lifetime?.signal ?? AbortSignal.abort(),\n timeout,\n ]),\n },\n timeout,\n );\n\n// Delta-seconds only; anything else reads as absent.\nconst retryAfterMs = (res: Response): number | null => {\n const header = res.headers.get(\"retry-after\")?.trim();\n return header !== undefined && /^\\d+$/.test(header)\n ? Number(header) * 1000\n : null;\n};\n\nconst refusalSyn = async (res: Response): Promise<StatewireSyn> => {\n const body = (await res.json().catch(() => null)) as {\n syn?: StatewireSyn;\n } | null;\n const syn = body?.syn;\n if (typeof syn !== \"object\" || syn === null) {\n throw new Error(`HTTP ${res.status}`);\n }\n return syn;\n};\n\nconst sendFrame = async (\n c: Conn,\n options: StatewireClient.SSEOptions,\n auth: AuthState,\n frame: StatewireFrame,\n): Promise<StatewireSubmitResult> => {\n const lease = c.lease;\n if (lease === null) return { type: \"no-lease\" };\n const timeout = AbortSignal.timeout(timing.heartbeatTimeoutMs);\n let res = await postFrame(c, options, lease, frame, timeout, false);\n if (res.status === 401 && typeof options.headers === \"function\") {\n res = await postFrame(c, options, lease, frame, timeout, true);\n }\n if (res.ok) return { type: \"ok\" };\n if (res.status === 429 || res.status === 409) {\n // The syn body retry wins; Retry-After covers 429s minted below the host.\n const headerRetry = res.status === 429 ? retryAfterMs(res) : null;\n const syn: StatewireSyn = await refusalSyn(res).catch((error: unknown) => {\n if (headerRetry === null) throw error;\n return { retry: headerRetry };\n });\n if (syn.retry === undefined && headerRetry !== null)\n syn.retry = headerRetry;\n return { type: \"refusal\", syn };\n }\n if (res.status === 423) return { type: \"stale-lease\" };\n if (res.status === 401) {\n // Fresh credentials did not clear the 401: signal auth-failed on the\n // connection and let the send retry as a transport fault.\n auth.stale = true;\n c.episode = { cause: \"unauthorized\" };\n c.connection.reconnect();\n throw new Error(\"unauthorized\");\n }\n throw new Error(`HTTP ${res.status}`);\n};\n\nconst useSseLink = (\n options: StatewireClient.SSEOptions,\n ctx: StatewireRemoteProps,\n): RemoteLink => {\n const latest = useRef(options);\n latest.current = options;\n const [auth] = useState((): AuthState => ({ stale: false }));\n const c = useStatewireConn({\n url: options.url,\n onChange: options.onChange,\n ctx,\n attemptOnce: (c, abort) => attemptStream(c, options, auth, abort),\n });\n const [link] = useState(\n (): RemoteLink => ({\n get connection() {\n return c.connection;\n },\n send: (frame) => sendFrame(c, latest.current, auth, frame),\n }),\n );\n return link;\n};\n\nconst useStatewireSSE = (\n options: StatewireClient.SSEOptions,\n): Statewire<any, any> => useStatewireRemote(options, useSseLink);\n\nexport const StatewireSSE = resource(useStatewireSSE) as <\n TState = any,\n TCommands extends Statewire.Commands = Statewire.Commands,\n>(\n options: StatewireClient.SSEOptions,\n) => ResourceElement<Statewire<TState | undefined, TCommands>>;\n"],"mappings":";;;;;;;AAuDA,MAAMmD,eAAe;AAErB,MAAMC,WAAWC,YACfA,QAAQE,IAAIC,QAAQ,OAAO,EAAE;AAI/B,MAAMG,iBAAiB,OACrBN,SACAjC,QACqB;CACrB,MAAM,EAAE0C,YAAYT;CACpB,OAAO,IAAIQ,QACT,OAAOC,YAAY,aAAa,MAAMA,QAAQ1C,GAAG,IAAI0C,OACvD;AACF;AAEA,MAAMC,aAAaD,SAAkBE,UAAkC;CACrE,MAAME,SAAS,IAAIL,QAAQC,OAAO;CAClC,KAAK,MAAM,CAACK,MAAMC,UAAUC,OAAOC,QAAQN,KAAK,GAAGE,OAAOK,IAAIJ,MAAMC,KAAK;CACzE,OAAOF;AACT;AAEA,MAAMM,aAAa,OACjBnB,SACAoB,OACAC,MACAI,YACsB;CACtB,IAAI;EACF,OAAO,OAAOzB,QAAQ2B,SAASC,WAAWD,MAAAA,CAAOP,OAAOC,IAAI;CAC9D,QAAQ;EACN,MAAMnE,SAASuE,OAAO;CACxB;AACF;AAEA,MAAMI,UAAU,OACdC,GACAC,QACAG,WACAE,SAC4B;CAC5B,MAAMC,UAAU,IAAIC,YAAY;CAChC,MAAMC,QAAQvF,gBAAgB;CAC9B,SAAS;EACP,MAAMwF,QAAQC,iBACNP,UAAUQ,MAAM,GACtBzF,OAAO0F,kBACT;EACA,IAAIC;EACJ,IAAI;GACFA,SAAS,MAAMb,OAAOe,KAAK;EAC7B,QAAQ;GACN,MAAM5F,SAASgF,UAAUX,MAAM;EACjC,UAAU;GACRwB,aAAaP,KAAK;EACpB;EACA,IAAII,OAAOI,MAAM,OAAO;EACxB,KAAK,MAAMC,QAAQV,MAAMF,QAAQa,OAAON,OAAO7B,OAAO,EAAEoC,QAAQ,KAAK,CAAC,CAAC,GAAG;GACxE,IAAIf,KAAK,GAAG,OAAO;GACnB,MAAMgB,MAAMC,KAAKd,MAAMU,IAAI;GAC3B,MAAMD,OAAOnG,mBAAmBM,eAAe2E,GAAGsB,GAAG,CAAC;GACtD,IAAIJ,MAAM,OAAOA;EACnB;CACF;AACF;AAEA,MAAMM,gBAAgB,OACpBxB,GACA9B,SACAuD,MACAb,UAC4B;CAC5B,MAAMrC,QAAQkD,KAAKlD;CACnBkD,KAAKlD,QAAQ;CACbyB,EAAE0B,QAAQ;CACV,MAAMC,cAAc,MAAMnD,eAAeN,SAAS,EAAEO,WAAWF,MAAM,CAAC;CACtE,MAAMqD,QAAQ7G,mBAAmBiF,EAAE/D,IAAIS,gBAAgB,CAAC;CACxD,MAAMoB,QAAQ;EACZa,SAASO,OAAO2C,YAAYF,WAAW;EACvC,GAAIC,SAAS,CAAC;CAChB;CACA,MAAMxB,YAAY,IAAIC,gBAAgB;CACtC,MAAMyB,cAAcnB,iBACZP,UAAUQ,MAAM,GACtBzF,OAAO0F,kBACT;CACA,IAAIkB;CACJ,IAAI;EACFA,MAAM,MAAM1C,WACVnB,SACA,GAAGD,QAAQC,OAAO,EAAC,UACnB;GACE7B,QAAQ;GACRsC,SAASC,UAAU+C,aAAa;IAC9B,gBAAgB;IAChB,uBAAuB3B,EAAE/D,IAAIQ;IAC7BuF,QAAQ;GACV,CAAC;GACDC,MAAMV,KAAKW,UAAUpE,KAAK;GAC1BqE,OAAO;GACP1C,QAAQC,YAAY0C,IAAI,CAACxB,MAAMnB,QAAQW,UAAUX,MAAM,CAAC;EAC1D,GACAW,UAAUX,MACZ;CACF,UAAU;EACRwB,aAAaa,WAAW;CAC1B;CACA,IAAIC,IAAIM,WAAW,KAAK;EACtB,IAAI9D,OAAOyB,EAAEsC,UAAU,EAAEC,OAAO,eAAe;EAC/Cd,KAAKlD,QAAQ;EACb,MAAM,IAAIiE,MAAM,cAAc;CAChC;CACA,IAAI,CAACT,IAAIU,MAAM,CAACV,IAAIE,MAAM,MAAM,IAAIO,MAAM,QAAQT,IAAIM,QAAQ;CAC9DrC,EAAE0B,QAAQK,IAAIpD,QAAQ+D,IAAI1E,YAAY;CACtC,MAAMiC,SAAS8B,IAAIE,KAAKU,UAAU;CAClC,IAAI;EACF,OAAO,MAAM5C,QAAQC,GAAGC,QAAQG,iBAAiBQ,MAAMnB,OAAOmD,OAAO;CACvE,UAAU;EACR3C,OAAO4C,OAAO,CAAC,CAACC,YAAY,CAAC,CAAC;CAChC;AACF;AAEA,MAAMC,YAAY,OAChB/C,GACA9B,SACAwD,OACA5D,OACA6B,SACApB,UAEAc,WACEnB,SACA,GAAGD,QAAQC,OAAO,EAAC,UACnB;CACE7B,QAAQ;CACRsC,SAASC,UAAU,MAAMJ,eAAeN,SAAS,EAAEO,WAAWF,MAAM,CAAC,GAAG;EACtE,gBAAgB;EAChB,uBAAuByB,EAAE/D,IAAIQ;GAC5BuB,eAAe0D;CAClB,CAAC;CACDO,MAAMV,KAAKW,UAAUpE,KAAK;CAC1B2B,QAAQC,YAAY0C,IAAI,CACtBpC,EAAEgD,UAAUvD,UAAUC,YAAYkB,MAAM,GACxCjB,OAAO,CACR;AACH,GACAA,OACF;AAGF,MAAMsD,gBAAgBlB,QAAiC;CACrD,MAAMmB,SAASnB,IAAIpD,QAAQ+D,IAAI,aAAa,CAAC,EAAES,KAAK;CACpD,OAAOD,WAAWE,KAAAA,KAAa,QAAQC,KAAKH,MAAM,IAC9CI,OAAOJ,MAAM,IAAI,MACjB;AACN;AAEA,MAAMK,aAAa,OAAOxB,QAAyC;CAIjE,MAAMlF,OAAMoF,MAHQF,IAAIyB,KAAK,CAAC,CAACV,YAAY,IAAI,EAAA,EAG7BjG;CAClB,IAAI,OAAOA,QAAQ,YAAYA,QAAQ,MACrC,MAAM,IAAI2F,MAAM,QAAQT,IAAIM,QAAQ;CAEtC,OAAOxF;AACT;AAEA,MAAM4G,YAAY,OAChBzD,GACA9B,SACAuD,MACA3D,UACmC;CACnC,MAAM4D,QAAQ1B,EAAE0B;CAChB,IAAIA,UAAU,MAAM,OAAO,EAAElE,MAAM,WAAW;CAC9C,MAAMmC,UAAUD,YAAYC,QAAQxE,OAAO0F,kBAAkB;CAC7D,IAAIkB,MAAM,MAAMgB,UAAU/C,GAAG9B,SAASwD,OAAO5D,OAAO6B,SAAS,KAAK;CAClE,IAAIoC,IAAIM,WAAW,OAAO,OAAOnE,QAAQS,YAAY,YACnDoD,MAAM,MAAMgB,UAAU/C,GAAG9B,SAASwD,OAAO5D,OAAO6B,SAAS,IAAI;CAE/D,IAAIoC,IAAIU,IAAI,OAAO,EAAEjF,MAAM,KAAK;CAChC,IAAIuE,IAAIM,WAAW,OAAON,IAAIM,WAAW,KAAK;EAE5C,MAAMqB,cAAc3B,IAAIM,WAAW,MAAMY,aAAalB,GAAG,IAAI;EAC7D,MAAMlF,MAAoB,MAAM0G,WAAWxB,GAAG,CAAC,CAACe,OAAOa,UAAmB;GACxE,IAAID,gBAAgB,MAAM,MAAMC;GAChC,OAAO,EAAEC,OAAOF,YAAY;EAC9B,CAAC;EACD,IAAI7G,IAAI+G,UAAUR,KAAAA,KAAaM,gBAAgB,MAC7C7G,IAAI+G,QAAQF;EACd,OAAO;GAAElG,MAAM;GAAWX;EAAI;CAChC;CACA,IAAIkF,IAAIM,WAAW,KAAK,OAAO,EAAE7E,MAAM,cAAc;CACrD,IAAIuE,IAAIM,WAAW,KAAK;EAGtBZ,KAAKlD,QAAQ;EACbyB,EAAEsC,UAAU,EAAEC,OAAO,eAAe;EACpCvC,EAAErC,WAAWkG,UAAU;EACvB,MAAM,IAAIrB,MAAM,cAAc;CAChC;CACA,MAAM,IAAIA,MAAM,QAAQT,IAAIM,QAAQ;AACtC;AAEA,MAAMyB,cACJ5F,SACAjC,QACe;CACf,MAAM8H,SAASlJ,OAAOqD,OAAO;CAC7B6F,OAAOC,UAAU9F;CACjB,MAAM,CAACuD,QAAQ3G,gBAA2B,EAAEyD,OAAO,MAAM,EAAE;CAC3D,MAAMyB,MAAI1E,iBAAiB;EACzB8C,KAAKF,QAAQE;EACb6F,UAAU/F,QAAQ+F;EAClBhI;EACAiI,cAAclE,GAAGY,UAAUY,cAAcxB,GAAG9B,SAASuD,MAAMb,KAAK;CAClE,CAAC;CACD,MAAM,CAACuD,QAAQrJ,gBACM;EACjB,IAAI6C,aAAa;GACf,OAAOqC,IAAErC;EACX;EACAE,OAAOC,UAAU2F,UAAUzD,KAAG+D,OAAOC,SAASvC,MAAM3D,KAAK;CAC3D,EACF;CACA,OAAOqG;AACT;AAEA,MAAMC,mBACJlG,YACwBnC,mBAAmBmC,SAAS4F,UAAU;AAEhE,MAAaO,eAAerJ,SAASoJ,eAAe"}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { Statewire } from "../types.js";
|
|
2
2
|
import { StatewireEnvelope } from "../client/connection.js";
|
|
3
|
-
import { applyOptimistic } from "../util/recordMutations.js";
|
|
4
3
|
import { SocketRecorder, commandFrame, createCounterElement, createSocketRecorder, settle } from "./fixtures.js";
|
|
4
|
+
import { applyOptimistic } from "../util/recordMutations.js";
|
|
5
5
|
import { ResourceElement } from "@assistant-ui/tap";
|
|
6
6
|
//#region src/transports/testing.d.ts
|
|
7
7
|
/** Let queued microtasks and op batches drain through the statewire client. */
|
|
8
8
|
declare const tick: () => Promise<void>;
|
|
9
9
|
type MockTransport<TState> = {
|
|
10
10
|
/** Pass as the `transport:` option of useStatewire / Harness. */
|
|
11
|
-
transport: ResourceElement<Statewire<TState | undefined, any
|
|
12
|
-
/** Captured
|
|
11
|
+
transport: ResourceElement<Statewire<TState | undefined, any>>;
|
|
12
|
+
/** Captured statements in arrival order: cmd as `{method, params}`, ctx as `{context}`. */
|
|
13
13
|
commands: {
|
|
14
14
|
method?: string;
|
|
15
15
|
params?: unknown[];
|
|
@@ -17,8 +17,10 @@ type MockTransport<TState> = {
|
|
|
17
17
|
}[];
|
|
18
18
|
/** Replace the replicated state and broadcast it as a root snapshot op. */
|
|
19
19
|
setState: (state: TState) => Promise<void>;
|
|
20
|
-
/** Deliver
|
|
21
|
-
|
|
20
|
+
/** Deliver a bare ack cmd packet on the stream. */
|
|
21
|
+
applied: (seq: number) => Promise<void>;
|
|
22
|
+
/** Deliver a terminal result cmd packet on the stream. */
|
|
23
|
+
settle: (seq: number, payload?: unknown) => Promise<void>;
|
|
22
24
|
/** Deliver an arbitrary envelope on the stream. */
|
|
23
25
|
sendEnvelope: (envelope: StatewireEnvelope) => Promise<void>;
|
|
24
26
|
/** End the stream with a fin; further attaches fail like a dead server. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","names":[],"sources":["../../src/transports/testing.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"testing.d.ts","names":[],"sources":["../../src/transports/testing.ts"],"mappings":";;;;;;;cAWa,YAAI;KAIL,cAAc;;EAExB,WAAW,gBAAgB,UAAU;;EAErC;IAAY;IAAiB;IAAoB;;;EAEjD,WAAW,OAAO,WAAW;;EAE7B,UAAU,gBAAgB;;EAE1B,SAAS,aAAa,sBAAsB;;EAE5C,eAAe,UAAU,sBAAsB;;EAE/C,SAAS,gBAAgB,qBAAqB;WACrC;WACA;;cAUE,sBAAuB,QAClC,cAAc,WACb,cAAc"}
|
|
@@ -7,8 +7,8 @@ import { commandFrame, createCounterElement, createSocketRecorder, settle } from
|
|
|
7
7
|
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
8
8
|
/**
|
|
9
9
|
* A mock statewire backend: serves `initialState` as the attach snapshot,
|
|
10
|
-
* records
|
|
11
|
-
* test code), and exposes the wire as a transport ResourceElement.
|
|
10
|
+
* records submitted frames (receipt only — acks and results are delivered
|
|
11
|
+
* from test code), and exposes the wire as a transport ResourceElement.
|
|
12
12
|
*/
|
|
13
13
|
let mockCount = 0;
|
|
14
14
|
const createMockTransport = (initialState) => {
|
|
@@ -22,15 +22,35 @@ const createMockTransport = (initialState) => {
|
|
|
22
22
|
for (const stream of streams) stream.send(sseFrame(envelope));
|
|
23
23
|
return tick();
|
|
24
24
|
};
|
|
25
|
+
const admit = (id, frame) => {
|
|
26
|
+
const rec = clients.get(id);
|
|
27
|
+
for (const statement of frame.ctx ?? []) {
|
|
28
|
+
rec.ctxSeq = Math.max(rec.ctxSeq, statement.seq);
|
|
29
|
+
const root = statement.ops[0];
|
|
30
|
+
commands.push({ context: root?.value });
|
|
31
|
+
}
|
|
32
|
+
for (const statement of frame.cmd ?? []) {
|
|
33
|
+
rec.seq = Math.max(rec.seq, statement.seq);
|
|
34
|
+
commands.push({
|
|
35
|
+
method: statement.method,
|
|
36
|
+
params: statement.params
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
25
40
|
const fakeFetch = (async (input, init) => {
|
|
26
41
|
if (finished) throw new TypeError("fetch failed (finished)");
|
|
27
42
|
const url = String(input);
|
|
28
|
-
const
|
|
29
|
-
const id = headers.get("Statewire-Client-Id") ?? "";
|
|
43
|
+
const id = new Headers(init?.headers).get("Statewire-Client-Id") ?? "";
|
|
30
44
|
if (url.endsWith("/stream")) {
|
|
31
45
|
clientId = id;
|
|
32
|
-
const
|
|
33
|
-
clients.set(id,
|
|
46
|
+
const known = clients.has(id);
|
|
47
|
+
if (!known) clients.set(id, {
|
|
48
|
+
seq: 0,
|
|
49
|
+
ctxSeq: 0
|
|
50
|
+
});
|
|
51
|
+
const rec = clients.get(id);
|
|
52
|
+
const frame = JSON.parse(String(init?.body));
|
|
53
|
+
admit(id, frame);
|
|
34
54
|
return openSSEStream(streams, id, (handle) => {
|
|
35
55
|
handle.send(sseFrame({
|
|
36
56
|
ops: [{
|
|
@@ -39,16 +59,15 @@ const createMockTransport = (initialState) => {
|
|
|
39
59
|
value: state
|
|
40
60
|
}],
|
|
41
61
|
syn: {
|
|
42
|
-
|
|
43
|
-
|
|
62
|
+
seq: rec.seq,
|
|
63
|
+
ctxSeq: rec.ctxSeq,
|
|
64
|
+
record: known ? "resumed" : "fresh"
|
|
44
65
|
}
|
|
45
66
|
}));
|
|
46
|
-
}, init?.signal ?? void 0);
|
|
67
|
+
}, init?.signal ?? void 0, { "Statewire-Lease": "mock-lease" });
|
|
47
68
|
}
|
|
48
|
-
if (url.endsWith("/
|
|
49
|
-
|
|
50
|
-
clients.set(id, Math.max(clients.get(id) ?? 0, seq));
|
|
51
|
-
commands.push(JSON.parse(String(init?.body)));
|
|
69
|
+
if (url.endsWith("/frames")) {
|
|
70
|
+
admit(id, JSON.parse(String(init?.body)));
|
|
52
71
|
return Response.json({});
|
|
53
72
|
}
|
|
54
73
|
return new Response(null, { status: 404 });
|
|
@@ -67,7 +86,15 @@ const createMockTransport = (initialState) => {
|
|
|
67
86
|
value: next
|
|
68
87
|
}] });
|
|
69
88
|
},
|
|
70
|
-
|
|
89
|
+
applied: (seq) => sendEnvelope({ cmd: [{
|
|
90
|
+
seq,
|
|
91
|
+
type: "applied"
|
|
92
|
+
}] }),
|
|
93
|
+
settle: (seq, payload) => sendEnvelope({ cmd: [{
|
|
94
|
+
seq,
|
|
95
|
+
type: "result",
|
|
96
|
+
...payload !== void 0 && { payload }
|
|
97
|
+
}] }),
|
|
71
98
|
sendEnvelope,
|
|
72
99
|
finish: async (reason, message) => {
|
|
73
100
|
await sendEnvelope({ fin: {
|