statewire 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +2 -2
  2. package/dist/client/connection.d.ts +23 -19
  3. package/dist/client/connection.d.ts.map +1 -1
  4. package/dist/client/connection.js +143 -103
  5. package/dist/client/connection.js.map +1 -1
  6. package/dist/client/core.d.ts +30 -3
  7. package/dist/client/core.d.ts.map +1 -1
  8. package/dist/client/core.js +222 -260
  9. package/dist/client/core.js.map +1 -1
  10. package/dist/client/session.d.ts +34 -39
  11. package/dist/client/session.d.ts.map +1 -1
  12. package/dist/client/session.js +380 -373
  13. package/dist/client/session.js.map +1 -1
  14. package/dist/client/sessionStore.d.ts +1 -0
  15. package/dist/client/sessionStore.d.ts.map +1 -1
  16. package/dist/client/sessionStore.js.map +1 -1
  17. package/dist/client/timing.d.ts +1 -2
  18. package/dist/client/timing.d.ts.map +1 -1
  19. package/dist/client/timing.js +1 -5
  20. package/dist/client/timing.js.map +1 -1
  21. package/dist/host/draft.d.ts +2 -5
  22. package/dist/host/draft.d.ts.map +1 -1
  23. package/dist/host/draft.js.map +1 -1
  24. package/dist/host/host.d.ts +5 -8
  25. package/dist/host/host.d.ts.map +1 -1
  26. package/dist/host/host.js +23 -32
  27. package/dist/host/host.js.map +1 -1
  28. package/dist/host/socket.d.ts +3 -2
  29. package/dist/host/socket.d.ts.map +1 -1
  30. package/dist/host/socket.js +484 -424
  31. package/dist/host/socket.js.map +1 -1
  32. package/dist/host-internal.d.ts +4 -0
  33. package/dist/host-internal.d.ts.map +1 -1
  34. package/dist/host.d.ts +10 -10
  35. package/dist/host.d.ts.map +1 -1
  36. package/dist/host.js +2 -2
  37. package/dist/index.d.ts +1 -2
  38. package/dist/index.js +1 -2
  39. package/dist/statewire.d.ts +29 -24
  40. package/dist/statewire.d.ts.map +1 -1
  41. package/dist/statewire.js +8 -2
  42. package/dist/statewire.js.map +1 -1
  43. package/dist/transports/fake-sse.d.ts +1 -1
  44. package/dist/transports/fake-sse.d.ts.map +1 -1
  45. package/dist/transports/fake-sse.js +5 -2
  46. package/dist/transports/fake-sse.js.map +1 -1
  47. package/dist/transports/fixtures.d.ts +1 -2
  48. package/dist/transports/fixtures.d.ts.map +1 -1
  49. package/dist/transports/fixtures.js +5 -7
  50. package/dist/transports/fixtures.js.map +1 -1
  51. package/dist/transports/remote.d.ts +2 -1
  52. package/dist/transports/remote.d.ts.map +1 -1
  53. package/dist/transports/remote.js +10 -4
  54. package/dist/transports/remote.js.map +1 -1
  55. package/dist/transports/sse.d.ts +32 -46
  56. package/dist/transports/sse.d.ts.map +1 -1
  57. package/dist/transports/sse.js +56 -59
  58. package/dist/transports/sse.js.map +1 -1
  59. package/dist/transports/testing.d.ts +7 -5
  60. package/dist/transports/testing.d.ts.map +1 -1
  61. package/dist/transports/testing.js +41 -14
  62. package/dist/transports/testing.js.map +1 -1
  63. package/dist/transports/ws.d.ts +2 -3
  64. package/dist/transports/ws.d.ts.map +1 -1
  65. package/dist/transports/ws.js +25 -80
  66. package/dist/transports/ws.js.map +1 -1
  67. package/dist/types.d.ts +8 -8
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/types.js +3 -4
  70. package/dist/types.js.map +1 -1
  71. package/dist/wire/ops.d.ts +1 -5
  72. package/dist/wire/ops.d.ts.map +1 -1
  73. package/dist/wire/ops.js +1 -4
  74. package/dist/wire/ops.js.map +1 -1
  75. package/package.json +1 -1
  76. package/src/client/connection.ts +175 -128
  77. package/src/client/core.ts +287 -278
  78. package/src/client/session.ts +474 -480
  79. package/src/client/sessionStore.ts +1 -0
  80. package/src/client/timing.ts +0 -6
  81. package/src/host/draft.ts +1 -3
  82. package/src/host/host.ts +41 -58
  83. package/src/host/socket.ts +653 -448
  84. package/src/host-internal.ts +4 -0
  85. package/src/host.ts +13 -10
  86. package/src/index.ts +0 -1
  87. package/src/statewire.ts +38 -32
  88. package/src/transports/fake-sse.ts +2 -1
  89. package/src/transports/fixtures.ts +4 -9
  90. package/src/transports/remote.ts +12 -5
  91. package/src/transports/sse.ts +99 -115
  92. package/src/transports/testing.ts +47 -19
  93. package/src/transports/ws.ts +44 -115
  94. package/src/types.ts +15 -14
  95. package/src/wire/ops.ts +9 -16
@@ -1,4 +1,4 @@
1
- import { useStatewireCommands, useStatewireEmit, useStatewireState } from "../host/host.js";
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 command frame as a client would send it over a socket. */
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","useStatewireEmit","useStatewireState","tick","settle","i","createCounterElement","state","count","emit","commands","increment","commandFrame","method","seq","params","JSON","stringify","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 {\n useStatewireCommands,\n useStatewireEmit,\n useStatewireState,\n} 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 emit = useStatewireEmit();\n const commands = useStatewireCommands({\n increment: () => {\n state.count += 1;\n },\n });\n return { state, commands, emit };\n })();\n\n/** Serialize a command frame as a client would send it over a socket. */\nexport const commandFrame = (\n method: string,\n seq: number,\n params: unknown[] = [],\n) => JSON.stringify({ 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":";;;;;;AASA,MAAaK,SAAS,YAAY;CAChC,KAAK,IAAIC,IAAI,GAAGA,IAAI,GAAGA,KAAK,MAAMF,KAAK;AACzC;;AAGA,MAAaG,6BACXP,eAAe;CACb,MAAM,CAACQ,SAASL,yBAAyB,EAAEM,OAAO,EAAE,EAAE;CACtD,MAAMC,OAAOR,iBAAiB;CAM9B,OAAO;EAAEM;EAAOG,UALCV,qBAAqB,EACpCW,iBAAiB;GACfJ,MAAMC,SAAS;EACjB,EACF,CACgBE;EAAUD;CAAK;AACjC,CAAC,CAAC,CAAC;;AAGL,MAAaG,gBACXC,QACAC,KACAC,SAAoB,CAAA,MACjBC,KAAKC,UAAU;CAAEJ;CAAQE;CAAQD;AAAI,CAAC;;AAW3C,MAAaa,6BAA6C;CACxD,MAAMR,OAAiB,CAAA;CACvB,MAAMC,SAA+C,CAAA;CACrD,OAAO;EACLD;EACAC;EACAG,YAAYP,KAAKY,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"}
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
- } & StatewireSessionOptions>(options: TOptions, useLink: (options: TOptions, ctx: StatewireRemoteProps) => RemoteLink) => Statewire<any, any, any>;
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":";;;;;cAYa,qBACX;EACE;EACA,YAAY,YAAY,gBAAgB;IACtC,yBAEJ,SAAS,UACT,UAAU,SAAS,UAAU,KAAK,yBAAyB,eAC1D"}
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 emit = useStatewireEmit();
7
- const s = useStatewireSession({ sessionStore: options.sessionStore }, emit);
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","useStatewireEmit","Statewire","setContext","setTransport","useStatewireSession","StatewireSessionOptions","StatewireClient","RemoteLink","StatewireRemoteProps","useStatewireRemote","url","onChange","connection","Connection","options","TOptions","useLink","ctx","emit","s","sessionStore","conn","state","canonical","commands","source","value"],"sources":["../../src/transports/remote.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { useStatewireEmit } from \"../host/host\";\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 } & StatewireSessionOptions,\n>(\n options: TOptions,\n useLink: (options: TOptions, ctx: StatewireRemoteProps) => RemoteLink,\n): Statewire<any, any, any> => {\n const emit = useStatewireEmit<any>();\n const s = useStatewireSession({ sessionStore: options.sessionStore }, emit);\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 emit,\n connection: conn.connection,\n source: s.source,\n setContext: (value) => setContext(s, value),\n };\n};\n"],"mappings":";;;;AAYA,MAAaU,sBAMXK,SACAE,YAC6B;CAC7B,MAAME,OAAOlB,iBAAsB;CACnC,MAAMmB,IAAIf,oBAAoB,EAAEgB,cAAcN,QAAQM,aAAa,GAAGF,IAAI;CAC1E,MAAMG,OAAOL,QAAQF,SAASK,EAAEF,GAAG;CACnClB,gBAAgBI,aAAagB,GAAGE,IAAI,GAAG,CAACF,GAAGE,IAAI,CAAC;CAEhD,OAAO;EACLC,OAAOH,EAAEI;EACTC,UAAUL,EAAEK;EACZN;EACAN,YAAYS,KAAKT;EACjBa,QAAQN,EAAEM;EACVvB,aAAawB,UAAUxB,WAAWiB,GAAGO,KAAK;CAC5C;AACF"}
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"}
@@ -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 { StatewireResponse } from "../client/connection.js";
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
- onSnapshot: (args: {
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
- lastSeq: number;
26
+ syn: StatewireSyn;
13
27
  }) => void;
14
28
  onOps: (args: {
15
29
  ops: unknown[];
16
- res?: StatewireResponse[];
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: "receipt";
48
- response?: StatewireResponse;
49
- } | {
50
- type: "rejected";
51
- message: string;
39
+ type: "ok";
52
40
  } | {
53
- type: "seq-gap";
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 command POST. */
49
+ /** The live wire a remote session drives: connection status plus frame submission. */
61
50
  type RemoteLink = {
62
51
  connection: StatewireClient.Connection;
63
- send: (args: {
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, TEvent = any>(options: StatewireClient.SSEOptions) => ResourceElement<Statewire<TState | undefined, TCommands, TEvent>>;
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, StatewireWireCommand, contextOps, encodeWireCommand };
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;EACA,aAAa;IAAQ;IAAgB;;EACrC,QAAQ;IACN;IACA,MAAM;IACN;;EAEF,WAAW,MAAM,cAAc;;KAGrB;EACN;EAAgB;EAA4B;;EAC5C;EAAgB;EAAgB,SAAS;;;cAGlC,aAAc,OAAO;;;SAAA;;cAIrB,oBAAqB,SAAS;;;;;;;;;;KAU/B;EACN;EAAiB,WAAW;;EAC5B;EAAkB;;EAClB;;EACA;;EACA;;KAEM,kBAAkB,gBAAgB;;KAGlC;EACV,YAAY,gBAAgB;EAC5B,OAAO;IACL;IACA,SAAS;QACL,QAAQ;;KAMJ;EAAc;;cAsOb,eACX,cACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,eACtB,gBAAgB,UAAU,oBAAoB,WAAW"}
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"}
@@ -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, overloadedError, parseRetryAfter, useStatewireConn } from "../client/connection.js";
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
- /** The wire shape of a client context submission: one root replace op. */
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 requestHeaders = async (options, extra, ctx) => {
10
+ const resolveHeaders = async (options, ctx) => {
24
11
  const { headers } = options;
25
- const merged = new Headers(typeof headers === "function" ? await headers(ctx) : headers);
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
- const headers = await requestHeaders(options, {
62
- "Statewire-Client-Id": c.ctx.clientId,
63
- accept: "text/event-stream"
64
- }, { staleAuth: stale });
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
- headers,
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 overloaded = (res) => overloadedError(parseRetryAfter(res.headers.get("Retry-After")));
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 requestHeaders(options, {
92
+ headers: withExtra(await resolveHeaders(options, { staleAuth: stale }), {
95
93
  "content-type": "application/json",
96
94
  "Statewire-Client-Id": c.ctx.clientId,
97
- "Statewire-Command-Seq": String(seq),
98
- ...c.lease !== null && { "Statewire-Lease": c.lease }
99
- }, { staleAuth: stale }),
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 sendCommand = async (c, options, auth, args) => {
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 postCommand(c, options, args, timeout, false);
106
- if (res.status === 401 && typeof options.headers === "function") res = await postCommand(c, options, args, timeout, true);
107
- if (res.ok) {
108
- let body;
109
- try {
110
- body = await res.json();
111
- } catch {
112
- return {
113
- type: "rejected",
114
- message: "statewire: protocol error: malformed /commands receipt body"
115
- };
116
- }
117
- return body?.res !== void 0 ? {
118
- type: "receipt",
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 === 429 || res.status === 503) throw overloaded(res);
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: (args) => sendCommand(c_0, latest.current, auth, args)
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, contextOps, encodeWireCommand };
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":";;;;;;;;;AAuCA,MAAaqC,cAAcC,UAAmC,CAC5D;CAAEC,IAAI;CAAWC,MAAM,CAAA;CAAIF;AAAM,CAAC;AAGpC,MAAaG,qBAAqBC,aAAmC;CACnE,GAAIA,QAAQT,WAAWU,KAAAA,KAAa;EAClCV,QAAQS,QAAQT;EAChB,GAAG5B,aAAaqC,QAAQR,MAAM;CAChC;CACA,GAAIQ,QAAQP,YAAYQ,KAAAA,KAAa,EACnCR,SAASE,WAAWK,QAAQP,OAAO,EACrC;AACF;AAoBA,MAAMoB,WAAWC,YACfA,QAAQE,IAAIC,QAAQ,OAAO,EAAE;AAI/B,MAAMG,iBAAiB,OACrBN,SACAO,OACAC,QACG;CACH,MAAM,EAAEE,YAAYV;CACpB,MAAMW,SAAS,IAAIC,QACjB,OAAOF,YAAY,aAAa,MAAMA,QAAQF,GAAG,IAAIE,OACvD;CACA,KAAK,MAAM,CAACG,MAAM/B,UAAUgC,OAAOC,QAAQR,KAAK,GAAGI,OAAOK,IAAIH,MAAM/B,KAAK;CACzE,OAAO6B;AACT;AAEA,MAAMM,aAAa,OACjBjB,SACAkB,OACAC,MACAI,YACsB;CACtB,IAAI;EACF,OAAO,OAAOvB,QAAQyB,SAASC,WAAWD,MAAAA,CAAOP,OAAOC,IAAI;CAC9D,QAAQ;EACN,MAAMlE,SAASsE,OAAO;CACxB;AACF;AAEA,MAAMI,UAAU,OACdC,GACAC,QACAG,WACAE,SAC2C;CAC3C,MAAMC,UAAU,IAAIC,YAAY;CAChC,MAAMC,QAAQtF,gBAAgB;CAC9B,SAAS;EACP,MAAMuF,QAAQC,iBACNP,UAAUQ,MAAM,GACtBxF,OAAOyF,kBACT;EACA,IAAIC;EACJ,IAAI;GACFA,SAAS,MAAMb,OAAOe,KAAK;EAC7B,QAAQ;GACN,MAAM3F,SAAS+E,UAAUX,MAAM;EACjC,UAAU;GACRwB,aAAaP,KAAK;EACpB;EACA,IAAII,OAAOI,MAAM,OAAO;EACxB,KAAK,MAAMC,QAAQV,MAAMF,QAAQa,OAAON,OAAO5D,OAAO,EAAEmE,QAAQ,KAAK,CAAC,CAAC,GAAG;GACxE,IAAIf,KAAK,GAAG,OAAO;GACnB,MAAMgB,MAAMC,KAAKd,MAAMU,IAAI;GAC3B,MAAMD,OAAOpG,mBAAmBQ,eAAe0E,GAAGsB,GAAG,CAAC;GACtD,IAAIJ,MAAM,OAAOA;EACnB;CACF;AACF;AAEA,MAAMM,gBAAgB,OACpBxB,GACA5B,SACAqD,MACAb,UAC2C;CAC3C,MAAMnC,QAAQgD,KAAKhD;CACnBgD,KAAKhD,QAAQ;CACb,MAAMK,UAAU,MAAMJ,eACpBN,SACA;EACE,uBAAuB4B,EAAEpB,IAAI1C;EAC7BwF,QAAQ;CACV,GACA,EAAE7C,WAAWJ,MAAM,CACrB;CACA,MAAM2B,YAAY,IAAIC,gBAAgB;CACtC,MAAMsB,cAAchB,iBACZP,UAAUQ,MAAM,GACtBxF,OAAOyF,kBACT;CACA,IAAIrE;CACJ,IAAI;EACFA,MAAM,MAAM6C,WACVjB,SACA,GAAGD,QAAQC,OAAO,EAAC,UACnB;GACEU;GACA8C,OAAO;GACPnC,QAAQC,YAAYmC,IAAI,CAACjB,MAAMnB,QAAQW,UAAUX,MAAM,CAAC;EAC1D,GACAW,UAAUX,MACZ;CACF,UAAU;EACRwB,aAAaU,WAAW;CAC1B;CACA,IAAInF,IAAIsF,WAAW,OAAOtF,IAAIsF,WAAW,KAAK,MAAMC,WAAWvF,GAAG;CAClE,IAAIA,IAAIsF,WAAW,KAAK;EACtB,IAAIrD,OAAOuB,EAAEgC,UAAU,EAAEC,OAAO,eAAe;EAC/CR,KAAKhD,QAAQ;EACb,MAAM,IAAIyD,MAAM,cAAc;CAChC;CACA,IAAI,CAAC1F,IAAI2F,MAAM,CAAC3F,IAAI4F,MAAM,MAAM,IAAIF,MAAM,QAAQ1F,IAAIsF,QAAQ;CAC9D,MAAM7B,SAASzD,IAAI4F,KAAKC,UAAU;CAClC,IAAI;EACF,OAAO,MAAMtC,QAAQC,GAAGC,QAAQG,iBAAiBQ,MAAMnB,OAAO6C,OAAO;CACvE,UAAU;EACRrC,OAAOsC,OAAO,CAAC,CAACC,YAAY,CAAC,CAAC;CAChC;AACF;AAEA,MAAMT,cAAcvF,QAClBjB,gBAAgBC,gBAAgBgB,IAAIsC,QAAQ2D,IAAI,aAAa,CAAC,CAAC;AAEjE,MAAMC,cAAc,OAClB1C,GACA5B,SACA,EAAEH,KAAKX,WACPqC,SACAlB,UAEAY,WACEjB,SACA,GAAGD,QAAQC,OAAO,EAAC,YACnB;CACEvB,QAAQ;CACRiC,SAAS,MAAMJ,eACbN,SACA;EACE,gBAAgB;EAChB,uBAAuB4B,EAAEpB,IAAI1C;EAC7B,yBAAyByG,OAAO1E,GAAG;EACnC,GAAI+B,EAAE4C,UAAU,QAAQ,EAAE,mBAAmB5C,EAAE4C,MAAM;CACvD,GACA,EAAE/D,WAAWJ,MAAM,CACrB;CACA2D,MAAMb,KAAKsB,UAAUxF,kBAAkBC,OAAO,CAAC;CAC/CmC,QAAQC,YAAYmC,IAAI,CACtB7B,EAAE8C,UAAUrD,UAAUC,YAAYkB,MAAM,GACxCjB,OAAO,CACR;AACH,GACAA,OACF;AAEF,MAAMoD,cAAc,OAClB/C,GACA5B,SACAqD,MACArF,SACmC;CACnC,MAAMuD,UAAUD,YAAYC,QAAQvE,OAAOyF,kBAAkB;CAC7D,IAAIrE,MAAM,MAAMkG,YAAY1C,GAAG5B,SAAShC,MAAMuD,SAAS,KAAK;CAC5D,IAAInD,IAAIsF,WAAW,OAAO,OAAO1D,QAAQU,YAAY,YACnDtC,MAAM,MAAMkG,YAAY1C,GAAG5B,SAAShC,MAAMuD,SAAS,IAAI;CAEzD,IAAInD,IAAI2F,IAAI;EACV,IAAIC;EACJ,IAAI;GACFA,OAAQ,MAAM5F,IAAIwG,KAAK;EACzB,QAAQ;GACN,OAAO;IACLvF,MAAM;IACNE,SAAS;GACX;EACF;EACA,OAAOyE,MAAM5F,QAAQe,KAAAA,IACjB;GAAEE,MAAM;GAAWC,UAAUxC,eAAekH,KAAK5F,GAAG;EAAE,IACtD,EAAEiB,MAAM,UAAU;CACxB;CACA,IAAIjB,IAAIsF,WAAW,OAAOtF,IAAIsF,WAAW,KAAK,MAAMC,WAAWvF,GAAG;CAClE,IAAIA,IAAIsF,WAAW,KAAK;EAGtBL,KAAKhD,QAAQ;EACbuB,EAAEgC,UAAU,EAAEC,OAAO,eAAe;EACpCjC,EAAElC,WAAWmF,UAAU;EACvB,MAAM,IAAIf,MAAM,cAAc;CAChC;CAGA,MAAME,OAAQ,MAAM5F,IAAIwG,KAAK,CAAC,CAACR,YAAY,IAAI;CAI/C,IAAIJ,MAAMc,UAAU,YAClB,OAAO;EACLzF,MAAM;EACNE,SAASyE,KAAKe,aAAa,EAAE,EAAExF,WAAW,QAAQnB,IAAIsF;CACxD;CAEF,IAAItF,IAAIsF,WAAW,OAAOtF,IAAIsF,WAAW,OAAOtF,IAAIsF,WAAW,KAAK;EAClE,MAAMsB,UACJ5G,IAAIsF,WAAW,MACV,YACDtF,IAAIsF,WAAW,MACZ,mBACA;EACT,IAAIM,MAAMc,UAAUE,SAAS,OAAO,EAAE3F,MAAM2F,QAAQ;CACtD;CACA,MAAM,IAAIlB,MAAM,QAAQ1F,IAAIsF,QAAQ;AACtC;AAEA,MAAMwB,cACJlF,SACAQ,QACe;CACf,MAAM2E,SAAS3I,OAAOwD,OAAO;CAC7BmF,OAAOC,UAAUpF;CACjB,MAAM,CAACqD,QAAQ5G,gBAA2B,EAAE4D,OAAO,MAAM,EAAE;CAC3D,MAAMuB,MAAIvE,iBAAiB;EACzB6C,KAAKF,QAAQE;EACbmF,UAAUrF,QAAQqF;EAClB7E;EACA8E,cAAc1D,GAAGY,UAAUY,cAAcxB,GAAG5B,SAASqD,MAAMb,KAAK;CAClE,CAAC;CACD,MAAM,CAAC+C,QAAQ9I,gBACM;EACjB,IAAIiD,aAAa;GACf,OAAOkC,IAAElC;EACX;EACAE,OAAO5B,SAAS2G,YAAY/C,KAAGuD,OAAOC,SAAS/B,MAAMrF,IAAI;CAC3D,EACF;CACA,OAAOuH;AACT;AAEA,MAAMC,mBACJxF,YAC6BpC,mBAAmBoC,SAASkF,UAAU;AAErE,MAAaO,eAAe9I,SAAS6I,eAAe"}
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":"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"}
@@ -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, any>>;
12
- /** Captured `/commands` POST bodies, in POST order. */
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 an ack watermark on the stream. */
21
- ack: (seq: number) => Promise<void>;
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":";;;;;;;cAUa,YAAI;KAIL,cAAc;;EAExB,WAAW,gBAAgB,UAAU;;EAErC;IAAY;IAAiB;IAAoB;;;EAEjD,WAAW,OAAO,WAAW;;EAE7B,MAAM,gBAAgB;;EAEtB,eAAe,UAAU,sBAAsB;;EAE/C,SAAS,gBAAgB,qBAAqB;WACrC;WACA;;cAUE,sBAAuB,QAClC,cAAc,WACb,cAAc"}
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 command POSTs (receipt only — acks and results are delivered from
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 headers = new Headers(init?.headers);
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 lastSeq = clients.get(id) ?? 0;
33
- clients.set(id, lastSeq);
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
- lastSeq,
43
- lease: "mock-lease"
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("/commands")) {
49
- const seq = Number(headers.get("Statewire-Command-Seq"));
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
- ack: (seq) => sendEnvelope({ ack: seq }),
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: {