statewire 0.0.3 → 0.0.4

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.
@@ -1,6 +1,6 @@
1
+ import { StatewireClient } from "../statewire.js";
1
2
  import { StatewireHost } from "../host.js";
2
3
  import { StatewireRemoteProps } from "../transports/sse.js";
3
- import { StatewireClient } from "../statewire.js";
4
4
  //#region src/client/connection.d.ts
5
5
  type Episode = {
6
6
  cause: "dropped" | "evicted" | "error";
@@ -1,8 +1,8 @@
1
- import { CommandsInvocation, CommandsListener } from "../host/host.js";
2
- import { RemoteLink, StatewireRemoteProps, StatewireWireCommand } from "../transports/sse.js";
3
1
  import { StatewireSessionRecord, StatewireSessionStore, StatewireSessionStoreOptions } from "./sessionStore.js";
4
2
  import { Statewire, StatewireSendError } from "../types.js";
5
3
  import { StatewirePath } from "../wire/ops.js";
4
+ import { CommandsInvocation, CommandsListener } from "../host/host.js";
5
+ import { RemoteLink, StatewireRemoteProps, StatewireWireCommand } from "../transports/sse.js";
6
6
  //#region src/client/session.d.ts
7
7
  /**
8
8
  * queued/trying/posted: overlay live, result pending. awaiting-result: acked
@@ -1,5 +1,5 @@
1
- import { StatewireHost } from "../host.js";
2
1
  import { Statewire } from "../types.js";
2
+ import { StatewireHost } from "../host.js";
3
3
  //#region src/host/host.d.ts
4
4
  /** Host-side accessor for a `useStatewireState` state object's source (`snapshot()` reads the committed state). */
5
5
  declare const getStateHost: (state: object) => StatewireHost.StateHost | undefined;
package/dist/host.d.ts CHANGED
@@ -1,7 +1,7 @@
1
+ import { Statewire } from "./types.js";
1
2
  import { ServerOp, append } from "./host/draft.js";
2
3
  import { StatewireReject } from "./host/engine.js";
3
4
  import { CommandExecution, getStateHost, useStatewireCommands, useStatewireEmit, useStatewireState } from "./host/host.js";
4
- import { Statewire } from "./types.js";
5
5
  //#region src/host.d.ts
6
6
  declare const executionResult: unique symbol;
7
7
  type UnwrapExecution<V> = V extends StatewireHost.Execution<infer T> ? T : V;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { StatewireSSE } from "./transports/sse.js";
2
1
  import { StatewireClient, StatewireResource, useStatewire } from "./statewire.js";
3
2
  import { Statewire, StatewireSendError } from "./types.js";
3
+ import { StatewireSSE } from "./transports/sse.js";
4
4
  import { StatewireWS } from "./transports/ws.js";
5
5
  export { type Statewire, StatewireClient, StatewireResource, StatewireSSE, StatewireSendError, StatewireWS, useStatewire };
@@ -1,4 +1,3 @@
1
- import { ConnectionState } from "./transports/sse.js";
2
1
  import { StatewireSessionStoreOptions } from "./client/sessionStore.js";
3
2
  import { Statewire } from "./types.js";
4
3
  import { ResourceElement } from "@assistant-ui/tap";
@@ -40,6 +39,22 @@ declare namespace StatewireClient {
40
39
  /** Replace this client's ambient context on the server; throws on transports without a client context channel. */
41
40
  setContext: (value: Record<string, unknown>) => Promise<unknown>;
42
41
  };
42
+ type ConnectionState = {
43
+ status: "connecting";
44
+ } | {
45
+ status: "live";
46
+ } | {
47
+ status: "reconnecting";
48
+ cause: "dropped" | "evicted" | "error";
49
+ attempt: number;
50
+ nextRetryAt: number;
51
+ lastError?: string;
52
+ message?: string;
53
+ } | {
54
+ status: "gone";
55
+ message?: string;
56
+ payload?: unknown;
57
+ };
43
58
  type Connection = ConnectionState & {
44
59
  reconnect: () => void;
45
60
  };
@@ -1 +1 @@
1
- {"version":3,"file":"statewire.d.ts","names":[],"sources":["../src/statewire.ts"],"mappings":";;;;;cAaa,eACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW,YACnD,gBAAgB,QAAQ,QAAQ;cAqCtB,oBACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW,YACnD,gBAAgB,gBAAgB,QAAQ,QAAQ;cAGtC,gBACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD;mBAEiB;UACT;EAEI,YAAA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW;MAK5D,SAAS;MAIT,cAAc,gBAAgB;MAI9B,YAAY,UAAU,cAAc;EAIxC,WAAW,OAAO,0BAA0B;EAI5C,UAAU;EAIV;;kBAOe;OACH,QACV,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD;IAEA,WAAW,gBAAgB,UAAU,QAAQ,WAAW;IACxD,aAAa,WAAW,YAAY,SAAS;IAC7C,WAAW,OAAO,QAAQ;MAAO,OAAO,YAAY;;IACpD,iBACE,OAAO,YAAY,SACnB;MAAO,SAAS,UAAU;;IAE5B,mBACE,QAAQ,UAAU,cAAc,YAChC;MAAO,OAAO,YAAY;;;OAIlB,QACV,QACA,kBAAkB,UAAU,WAAW,UAAU;;IAGjD,OAAO;;IAEP,gBAAgB;IAChB,YAAY;IACZ,UAAU,UAAU,cAAc;;IAElC,aAAa,OAAO,4BAA4B;;OAGtC,aAAa;IAAoB;;OACjC,mBAAmB;;;;;OAMnB,WACV,QACA,kBAAkB,UAAU,WAAW,UAAU,aAEjD,OAAO,QACP,SAAS,UAAU,QAAQ,YAC3B,KAAK;OAGK;IACV;IACA;IACA;;;;;;;;;;;OAYU;IACN;IAAkB;IAAiB;IAAiB;;IACpD;IAAyB;;IACzB;IAAwB;IAAiB;;IACzC;IAAiB;;IAEjB;IACA;;IAQA;IACA;;IAQA;IACA;;OAGM,aAAa;IACvB;IACA,UAAU,qBAAqB,cAAc,QAAQ;IACrD,eAAe,WAAW;IAC1B,YAAY,YAAY;;OAGd,YAAY;IACtB;IACA;IACA,QACI,gCACO,yBAAyB,QAAQ;IAC5C,iBAAiB,aAAa,kCAAkC;IAChE,YAAY,YAAY"}
1
+ {"version":3,"file":"statewire.d.ts","names":[],"sources":["../src/statewire.ts"],"mappings":";;;;cAYa,eACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW,YACnD,gBAAgB,QAAQ,QAAQ;cAqCtB,oBACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW,YACnD,gBAAgB,gBAAgB,QAAQ,QAAQ;cAGtC,gBACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD;mBAEiB;UACT;EAEI,YAAA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW;MAK5D,SAAS;MAIT,cAAc,gBAAgB;MAI9B,YAAY,UAAU,cAAc;EAIxC,WAAW,OAAO,0BAA0B;EAI5C,UAAU;EAIV;;kBAOe;OACH,QACV,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD;IAEA,WAAW,gBAAgB,UAAU,QAAQ,WAAW;IACxD,aAAa,WAAW,YAAY,SAAS;IAC7C,WAAW,OAAO,QAAQ;MAAO,OAAO,YAAY;;IACpD,iBACE,OAAO,YAAY,SACnB;MAAO,SAAS,UAAU;;IAE5B,mBACE,QAAQ,UAAU,cAAc,YAChC;MAAO,OAAO,YAAY;;;OAIlB,QACV,QACA,kBAAkB,UAAU,WAAW,UAAU;;IAGjD,OAAO;;IAEP,gBAAgB;IAChB,YAAY;IACZ,UAAU,UAAU,cAAc;;IAElC,aAAa,OAAO,4BAA4B;;OAGtC;IACN;;IACA;;IAEA;IACA;IACA;IACA;IACA;IACA;;IAEA;IAAgB;IAAkB;;OAC5B,aAAa;IAAoB;;OACjC,mBAAmB;;;;;OAMnB,WACV,QACA,kBAAkB,UAAU,WAAW,UAAU,aAEjD,OAAO,QACP,SAAS,UAAU,QAAQ,YAC3B,KAAK;OAGK;IACV;IACA;IACA;;;;;;;;;;;OAYU;IACN;IAAkB;IAAiB;IAAiB;;IACpD;IAAyB;;IACzB;IAAwB;IAAiB;;IACzC;IAAiB;;IAEjB;IACA;;IAQA;IACA;;IAQA;IACA;;OAGM,aAAa;IACvB;IACA,UAAU,qBAAqB,cAAc,QAAQ;IACrD,eAAe,WAAW;IAC1B,YAAY,YAAY;;OAGd,YAAY;IACtB;IACA;IACA,QACI,gCACO,yBAAyB,QAAQ;IAC5C,iBAAiB,aAAa,kCAAkC;IAChE,YAAY,YAAY"}
@@ -1 +1 @@
1
- {"version":3,"file":"statewire.js","names":["useMemo","useRef","useState","useSyncExternalStore","createTapRoot","flushTapSync","resource","useResource","ResourceElement","LIVE_CONNECTION","useStatewireCore","Statewire","ConnectionState","StatewireSessionStoreOptions","useStatewire","Commands","options","StatewireClient","Options","TState","TCommands","TEvent","Helpers","latest","current","value","transport","core","subscribe","listener","state","getState","connection","commands","CommandsProxy","setContext","contextValue","Record","Error","StatewireResource","root","disposed","constructor","ClientRoot","getValue","Connection","Promise","dispose","unmount","optimistic","Optimistic","NonNullable","onEvent","event","ctx","onStateChange","changes","Changes","onCommandChange","update","CommandUpdate","reconnect","ConnectionStatus","draft","command","Command","OptimisticContext","ts","seq","clientId","SendFailure","fate","cause","message","payload","SSEOptions","url","headers","HeadersInit","fetch","globalThis","onChange","WSOptions","protocols","query","webSocket","WebSocket"],"sources":["../src/statewire.ts"],"sourcesContent":["import { useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport {\n createTapRoot,\n flushTapSync,\n resource,\n useResource,\n} from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { LIVE_CONNECTION, useStatewireCore } from \"./client/core\";\nimport type { Statewire } from \"./types\";\nimport type { ConnectionState } from \"./transports/sse\";\nimport type { StatewireSessionStoreOptions } from \"./client/sessionStore\";\n\nexport const useStatewire = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.Options<TState, TCommands, TEvent>,\n): StatewireClient.Helpers<TState, TCommands> => {\n const latest = useRef(options);\n latest.current = options;\n\n const value = useResource(options.transport) as Statewire<unknown>;\n const core = useStatewireCore(() => latest.current, value);\n const [subscribe] = useState(\n () => (listener: () => void) =>\n core.subscribe(() => flushTapSync(listener)),\n );\n\n const state = useSyncExternalStore(\n subscribe,\n core.getState,\n core.getState,\n ) as TState;\n const connection = value.connection ?? LIVE_CONNECTION;\n\n return useMemo(\n () => ({\n state,\n getState: core.getState as () => TState,\n connection,\n commands: core.commands as Statewire.CommandsProxy<TCommands>,\n setContext: (contextValue: Record<string, unknown>) => {\n if (!value.setContext) {\n throw new Error(\n \"statewire: this transport has no client context channel\",\n );\n }\n return value.setContext(contextValue);\n },\n }),\n [state, connection, core, value],\n );\n};\n\nexport const StatewireResource = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.Options<TState, TCommands, TEvent>,\n): ResourceElement<StatewireClient.Helpers<TState, TCommands>> =>\n resource(useStatewire<TState, TCommands, TEvent>)(options);\n\nexport class StatewireClient<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n> {\n private readonly root;\n private disposed = false;\n\n constructor(options: StatewireClient.Options<TState, TCommands, TEvent>) {\n const ClientRoot = () => useStatewire<TState, TCommands, TEvent>(options);\n this.root = createTapRoot(ClientRoot);\n }\n\n get state(): TState {\n return this.root.getValue().getState();\n }\n\n get connection(): StatewireClient.Connection {\n return this.root.getValue().connection;\n }\n\n get commands(): Statewire.CommandsProxy<TCommands> {\n return this.root.getValue().commands;\n }\n\n setContext(value: Record<string, unknown>): Promise<unknown> {\n return this.root.getValue().setContext(value);\n }\n\n subscribe(listener: () => void): () => void {\n return this.root.subscribe(listener);\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n this.root.unmount();\n }\n}\n\nexport namespace StatewireClient {\n export type Options<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n > = {\n transport: ResourceElement<Statewire<TState, TCommands, TEvent>>;\n optimistic?: Optimistic<NonNullable<TState>, TCommands>;\n onEvent?: (event: TEvent, ctx: { state: NonNullable<TState> }) => void;\n onStateChange?: (\n state: NonNullable<TState>,\n ctx: { changes: Statewire.Changes },\n ) => void;\n onCommandChange?: (\n update: Statewire.CommandUpdate<TCommands>,\n ctx: { state: NonNullable<TState> },\n ) => void;\n };\n\n export type Helpers<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = {\n /** The statewire's state type — remote statewires include `undefined` until the first attach snapshot applies. */\n state: TState;\n /** Identity-stable across renders; reads the current canonical state without subscribing the caller. */\n getState: () => TState;\n connection: Connection;\n commands: Statewire.CommandsProxy<TCommands>;\n /** Replace this client's ambient context on the server; throws on transports without a client context channel. */\n setContext: (value: Record<string, unknown>) => Promise<unknown>;\n };\n\n export type Connection = ConnectionState & { reconnect: () => void };\n export type ConnectionStatus = Connection[\"status\"];\n\n /**\n * Mutate the draft in place; the client records the next state plus the exact\n * changed paths. Re-run against the new base on every rebase.\n */\n export type Optimistic<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = (\n draft: TState,\n command: Statewire.Command<TCommands>,\n ctx: OptimisticContext,\n ) => void;\n\n export type OptimisticContext = {\n ts: number;\n seq: number;\n clientId?: string;\n };\n\n /**\n * fate: \"aborted\" = never sent, \"rejected\" = the handler refused,\n * \"unknown-command\" = no such command, \"invalid-params\" = the args did not\n * match the command's schema, \"lost\" = sent but never covered by an ack,\n * \"crashed\" = acked but the processing died, \"result-unavailable\" = acked\n * but the result is gone.\n * cause is which channel settled it: \"server\" = the server's answer;\n * the rest are channel deaths the client settled locally.\n */\n export type SendFailure =\n | { fate: \"rejected\"; cause: \"server\"; message: string; payload?: unknown }\n | { fate: \"unknown-command\"; cause: \"server\" }\n | { fate: \"invalid-params\"; cause: \"server\"; message: string }\n | { fate: \"crashed\"; cause: \"server\" }\n | {\n fate: \"result-unavailable\";\n cause:\n | \"server\"\n | \"unmount\"\n | \"stream-gone\"\n | \"session-lost\"\n | \"superseded\";\n }\n | {\n fate: \"aborted\";\n cause:\n | \"cascade\"\n | \"unmount\"\n | \"stream-gone\"\n | \"session-lost\"\n | \"superseded\";\n }\n | {\n fate: \"lost\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\" | \"superseded\";\n };\n\n export type SSEOptions = StatewireSessionStoreOptions & {\n url: string;\n headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n fetch?: typeof globalThis.fetch;\n onChange?: (connection: Connection) => void;\n };\n\n export type WSOptions = StatewireSessionStoreOptions & {\n url: string;\n protocols?: string | string[];\n query?:\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>);\n webSocket?: new (url: string, protocols?: string | string[]) => WebSocket;\n onChange?: (connection: Connection) => void;\n };\n}\n"],"mappings":";;;;AAaA,MAAac,gBAKXE,YAC+C;CAC/C,MAAMO,SAAStB,OAAOe,OAAO;CAC7BO,OAAOC,UAAUR;CAEjB,MAAMS,QAAQlB,YAAYS,QAAQU,SAAS;CAC3C,MAAMC,OAAOjB,uBAAuBa,OAAOC,SAASC,KAAK;CACzD,MAAM,CAACG,aAAa1B,gBACX2B,aACLF,KAAKC,gBAAgBvB,aAAawB,QAAQ,CAAC,CAC/C;CAEA,MAAMC,QAAQ3B,qBACZyB,WACAD,KAAKI,UACLJ,KAAKI,QACP;CACA,MAAMC,aAAaP,MAAMO,cAAcvB;CAEvC,OAAOT,eACE;EACL8B;EACAC,UAAUJ,KAAKI;EACfC;EACAC,UAAUN,KAAKM;EACfE,aAAaC,iBAA0C;GACrD,IAAI,CAACX,MAAMU,YACT,MAAM,IAAIG,MACR,yDACF;GAEF,OAAOb,MAAMU,WAAWC,YAAY;EACtC;CACF,IACA;EAACN;EAAOE;EAAYL;EAAMF;CAAK,CACjC;AACF;AAEA,MAAac,qBAKXvB,YAEAV,SAASQ,YAAuC,CAAC,CAACE,OAAO;AAE3D,IAAaC,kBAAb,MAIE;CACA;CACA,WAAmB;CAEnByB,YAAY1B,SAA6D;EACvE,MAAM2B,mBAAmB7B,aAAwCE,OAAO;EACxE,KAAKwB,OAAOpC,cAAcuC,UAAU;CACtC;CAEA,IAAIb,QAAgB;EAClB,OAAO,KAAKU,KAAKI,SAAS,CAAC,CAACb,SAAS;CACvC;CAEA,IAAIC,aAAyC;EAC3C,OAAO,KAAKQ,KAAKI,SAAS,CAAC,CAACZ;CAC9B;CAEA,IAAIC,WAA+C;EACjD,OAAO,KAAKO,KAAKI,SAAS,CAAC,CAACX;CAC9B;CAEAE,WAAWV,OAAkD;EAC3D,OAAO,KAAKe,KAAKI,SAAS,CAAC,CAACT,WAAWV,KAAK;CAC9C;CAEAG,UAAUC,UAAkC;EAC1C,OAAO,KAAKW,KAAKZ,UAAUC,QAAQ;CACrC;CAEAkB,UAAgB;EACd,IAAI,KAAKN,UAAU;EACnB,KAAKA,WAAW;EAChB,KAAKD,KAAKQ,QAAQ;CACpB;AACF"}
1
+ {"version":3,"file":"statewire.js","names":["useMemo","useRef","useState","useSyncExternalStore","createTapRoot","flushTapSync","resource","useResource","ResourceElement","LIVE_CONNECTION","useStatewireCore","Statewire","StatewireSessionStoreOptions","useStatewire","Commands","options","StatewireClient","Options","TState","TCommands","TEvent","Helpers","latest","current","value","transport","core","subscribe","listener","state","getState","connection","commands","CommandsProxy","setContext","contextValue","Record","Error","StatewireResource","root","disposed","constructor","ClientRoot","getValue","Connection","Promise","dispose","unmount","optimistic","Optimistic","NonNullable","onEvent","event","ctx","onStateChange","changes","Changes","onCommandChange","update","CommandUpdate","ConnectionState","status","cause","attempt","nextRetryAt","lastError","message","payload","reconnect","ConnectionStatus","draft","command","Command","OptimisticContext","ts","seq","clientId","SendFailure","fate","SSEOptions","url","headers","HeadersInit","fetch","globalThis","onChange","WSOptions","protocols","query","webSocket","WebSocket"],"sources":["../src/statewire.ts"],"sourcesContent":["import { useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport {\n createTapRoot,\n flushTapSync,\n resource,\n useResource,\n} from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { LIVE_CONNECTION, useStatewireCore } from \"./client/core\";\nimport type { Statewire } from \"./types\";\nimport type { StatewireSessionStoreOptions } from \"./client/sessionStore\";\n\nexport const useStatewire = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.Options<TState, TCommands, TEvent>,\n): StatewireClient.Helpers<TState, TCommands> => {\n const latest = useRef(options);\n latest.current = options;\n\n const value = useResource(options.transport) as Statewire<unknown>;\n const core = useStatewireCore(() => latest.current, value);\n const [subscribe] = useState(\n () => (listener: () => void) =>\n core.subscribe(() => flushTapSync(listener)),\n );\n\n const state = useSyncExternalStore(\n subscribe,\n core.getState,\n core.getState,\n ) as TState;\n const connection = value.connection ?? LIVE_CONNECTION;\n\n return useMemo(\n () => ({\n state,\n getState: core.getState as () => TState,\n connection,\n commands: core.commands as Statewire.CommandsProxy<TCommands>,\n setContext: (contextValue: Record<string, unknown>) => {\n if (!value.setContext) {\n throw new Error(\n \"statewire: this transport has no client context channel\",\n );\n }\n return value.setContext(contextValue);\n },\n }),\n [state, connection, core, value],\n );\n};\n\nexport const StatewireResource = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.Options<TState, TCommands, TEvent>,\n): ResourceElement<StatewireClient.Helpers<TState, TCommands>> =>\n resource(useStatewire<TState, TCommands, TEvent>)(options);\n\nexport class StatewireClient<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n> {\n private readonly root;\n private disposed = false;\n\n constructor(options: StatewireClient.Options<TState, TCommands, TEvent>) {\n const ClientRoot = () => useStatewire<TState, TCommands, TEvent>(options);\n this.root = createTapRoot(ClientRoot);\n }\n\n get state(): TState {\n return this.root.getValue().getState();\n }\n\n get connection(): StatewireClient.Connection {\n return this.root.getValue().connection;\n }\n\n get commands(): Statewire.CommandsProxy<TCommands> {\n return this.root.getValue().commands;\n }\n\n setContext(value: Record<string, unknown>): Promise<unknown> {\n return this.root.getValue().setContext(value);\n }\n\n subscribe(listener: () => void): () => void {\n return this.root.subscribe(listener);\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n this.root.unmount();\n }\n}\n\nexport namespace StatewireClient {\n export type Options<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n > = {\n transport: ResourceElement<Statewire<TState, TCommands, TEvent>>;\n optimistic?: Optimistic<NonNullable<TState>, TCommands>;\n onEvent?: (event: TEvent, ctx: { state: NonNullable<TState> }) => void;\n onStateChange?: (\n state: NonNullable<TState>,\n ctx: { changes: Statewire.Changes },\n ) => void;\n onCommandChange?: (\n update: Statewire.CommandUpdate<TCommands>,\n ctx: { state: NonNullable<TState> },\n ) => void;\n };\n\n export type Helpers<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = {\n /** The statewire's state type — remote statewires include `undefined` until the first attach snapshot applies. */\n state: TState;\n /** Identity-stable across renders; reads the current canonical state without subscribing the caller. */\n getState: () => TState;\n connection: Connection;\n commands: Statewire.CommandsProxy<TCommands>;\n /** Replace this client's ambient context on the server; throws on transports without a client context channel. */\n setContext: (value: Record<string, unknown>) => Promise<unknown>;\n };\n\n export type ConnectionState =\n | { status: \"connecting\" }\n | { status: \"live\" }\n | {\n status: \"reconnecting\";\n cause: \"dropped\" | \"evicted\" | \"error\";\n attempt: number;\n nextRetryAt: number;\n lastError?: string;\n message?: string;\n }\n | { status: \"gone\"; message?: string; payload?: unknown };\n export type Connection = ConnectionState & { reconnect: () => void };\n export type ConnectionStatus = Connection[\"status\"];\n\n /**\n * Mutate the draft in place; the client records the next state plus the exact\n * changed paths. Re-run against the new base on every rebase.\n */\n export type Optimistic<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = (\n draft: TState,\n command: Statewire.Command<TCommands>,\n ctx: OptimisticContext,\n ) => void;\n\n export type OptimisticContext = {\n ts: number;\n seq: number;\n clientId?: string;\n };\n\n /**\n * fate: \"aborted\" = never sent, \"rejected\" = the handler refused,\n * \"unknown-command\" = no such command, \"invalid-params\" = the args did not\n * match the command's schema, \"lost\" = sent but never covered by an ack,\n * \"crashed\" = acked but the processing died, \"result-unavailable\" = acked\n * but the result is gone.\n * cause is which channel settled it: \"server\" = the server's answer;\n * the rest are channel deaths the client settled locally.\n */\n export type SendFailure =\n | { fate: \"rejected\"; cause: \"server\"; message: string; payload?: unknown }\n | { fate: \"unknown-command\"; cause: \"server\" }\n | { fate: \"invalid-params\"; cause: \"server\"; message: string }\n | { fate: \"crashed\"; cause: \"server\" }\n | {\n fate: \"result-unavailable\";\n cause:\n | \"server\"\n | \"unmount\"\n | \"stream-gone\"\n | \"session-lost\"\n | \"superseded\";\n }\n | {\n fate: \"aborted\";\n cause:\n | \"cascade\"\n | \"unmount\"\n | \"stream-gone\"\n | \"session-lost\"\n | \"superseded\";\n }\n | {\n fate: \"lost\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\" | \"superseded\";\n };\n\n export type SSEOptions = StatewireSessionStoreOptions & {\n url: string;\n headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n fetch?: typeof globalThis.fetch;\n onChange?: (connection: Connection) => void;\n };\n\n export type WSOptions = StatewireSessionStoreOptions & {\n url: string;\n protocols?: string | string[];\n query?:\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>);\n webSocket?: new (url: string, protocols?: string | string[]) => WebSocket;\n onChange?: (connection: Connection) => void;\n };\n}\n"],"mappings":";;;;AAYA,MAAaa,gBAKXE,YAC+C;CAC/C,MAAMO,SAASrB,OAAOc,OAAO;CAC7BO,OAAOC,UAAUR;CAEjB,MAAMS,QAAQjB,YAAYQ,QAAQU,SAAS;CAC3C,MAAMC,OAAOhB,uBAAuBY,OAAOC,SAASC,KAAK;CACzD,MAAM,CAACG,aAAazB,gBACX0B,aACLF,KAAKC,gBAAgBtB,aAAauB,QAAQ,CAAC,CAC/C;CAEA,MAAMC,QAAQ1B,qBACZwB,WACAD,KAAKI,UACLJ,KAAKI,QACP;CACA,MAAMC,aAAaP,MAAMO,cAActB;CAEvC,OAAOT,eACE;EACL6B;EACAC,UAAUJ,KAAKI;EACfC;EACAC,UAAUN,KAAKM;EACfE,aAAaC,iBAA0C;GACrD,IAAI,CAACX,MAAMU,YACT,MAAM,IAAIG,MACR,yDACF;GAEF,OAAOb,MAAMU,WAAWC,YAAY;EACtC;CACF,IACA;EAACN;EAAOE;EAAYL;EAAMF;CAAK,CACjC;AACF;AAEA,MAAac,qBAKXvB,YAEAT,SAASO,YAAuC,CAAC,CAACE,OAAO;AAE3D,IAAaC,kBAAb,MAIE;CACA;CACA,WAAmB;CAEnByB,YAAY1B,SAA6D;EACvE,MAAM2B,mBAAmB7B,aAAwCE,OAAO;EACxE,KAAKwB,OAAOnC,cAAcsC,UAAU;CACtC;CAEA,IAAIb,QAAgB;EAClB,OAAO,KAAKU,KAAKI,SAAS,CAAC,CAACb,SAAS;CACvC;CAEA,IAAIC,aAAyC;EAC3C,OAAO,KAAKQ,KAAKI,SAAS,CAAC,CAACZ;CAC9B;CAEA,IAAIC,WAA+C;EACjD,OAAO,KAAKO,KAAKI,SAAS,CAAC,CAACX;CAC9B;CAEAE,WAAWV,OAAkD;EAC3D,OAAO,KAAKe,KAAKI,SAAS,CAAC,CAACT,WAAWV,KAAK;CAC9C;CAEAG,UAAUC,UAAkC;EAC1C,OAAO,KAAKW,KAAKZ,UAAUC,QAAQ;CACrC;CAEAkB,UAAgB;EACd,IAAI,KAAKN,UAAU;EACnB,KAAKA,WAAW;EAChB,KAAKD,KAAKQ,QAAQ;CACpB;AACF"}
@@ -1,5 +1,5 @@
1
- import { StatewireHost } from "../host.js";
2
1
  import { Statewire } from "../types.js";
2
+ import { StatewireHost } from "../host.js";
3
3
  import "../index.js";
4
4
  //#region src/transports/fixtures.d.ts
5
5
  /** Let several rounds of queued microtasks and op batches drain. */
@@ -1,7 +1,7 @@
1
- import { RemoteLink, StatewireRemoteProps } from "./sse.js";
2
1
  import { StatewireSessionStoreOptions } from "../client/sessionStore.js";
3
2
  import { StatewireClient } from "../statewire.js";
4
3
  import { Statewire } from "../types.js";
4
+ import { RemoteLink, StatewireRemoteProps } from "./sse.js";
5
5
  //#region src/transports/remote.d.ts
6
6
  declare const useStatewireRemote: <TOptions extends {
7
7
  url: string;
@@ -1,8 +1,8 @@
1
+ import { StatewireClient } from "../statewire.js";
2
+ import { Statewire } from "../types.js";
1
3
  import { StatewireMetaEntry } from "../wire/meta.js";
2
4
  import { StatewireHost } from "../host.js";
3
5
  import { StatewireResponse } from "../client/connection.js";
4
- import { StatewireClient } from "../statewire.js";
5
- import { Statewire } from "../types.js";
6
6
  import { ResourceElement } from "@assistant-ui/tap";
7
7
  //#region src/transports/sse.d.ts
8
8
  type StatewireRemoteProps = {
@@ -56,22 +56,7 @@ type StatewireSubmitResult = {
56
56
  } | {
57
57
  type: "stale-lease";
58
58
  };
59
- type ConnectionState = {
60
- status: "connecting";
61
- } | {
62
- status: "live";
63
- } | {
64
- status: "reconnecting";
65
- cause: "dropped" | "evicted" | "error";
66
- attempt: number;
67
- nextRetryAt: number;
68
- lastError?: string;
69
- message?: string;
70
- } | {
71
- status: "gone";
72
- message?: string;
73
- payload?: unknown;
74
- };
59
+ type ConnectionState = StatewireClient.ConnectionState;
75
60
  /** The live wire a remote session drives: connection status plus command POST. */
76
61
  type RemoteLink = {
77
62
  connection: StatewireClient.Connection;
@@ -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;;;;;;;;;;KAQ/B;EACN;EAAiB,WAAW;;EAC5B;EAAkB;;EAClB;;EACA;;EACA;;KAEM;EACN;;EACA;;EAEA;EACA;EACA;EACA;EACA;EACA;;EAEA;EAAgB;EAAkB;;;KAG5B;EACV,YAAY,gBAAgB;EAC5B,OAAO;IACL;IACA,SAAS;QACL,QAAQ;;cAgMH,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;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;;cAgMH,eACX,cACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,eACtB,gBAAgB,UAAU,oBAAoB,WAAW"}
@@ -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","status","cause","attempt","nextRetryAt","lastError","payload","RemoteLink","connection","Connection","send","seq","Promise","baseUrl","options","SSEOptions","url","replace","requestHeaders","extra","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","ctx","accept","headerTimer","cache","any","overloaded","ok","body","Error","getReader","aborted","cancel","catch","get","sendCommand","String","lease","stringify","lifetime","json","verdict","const","error","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 && { context: contextOps(command.context) }),\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 =\n | { status: \"connecting\" }\n | { status: \"live\" }\n | {\n status: \"reconnecting\";\n cause: \"dropped\" | \"evicted\" | \"error\";\n attempt: number;\n nextRetryAt: number;\n lastError?: string;\n message?: string;\n }\n | { status: \"gone\"; message?: string; payload?: unknown };\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\nconst requestHeaders = async (\n options: StatewireClient.SSEOptions,\n extra: Record<string, string>,\n) => {\n const { headers } = options;\n const merged = new Headers(\n typeof headers === \"function\" ? await headers() : 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 abort: AbortController,\n): Promise<\"stop\" | \"reattach\" | \"ended\"> => {\n const headers = await requestHeaders(options, {\n \"Statewire-Client-Id\": c.ctx.clientId,\n accept: \"text/event-stream\",\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.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 sendCommand = async (\n c: Conn,\n options: StatewireClient.SSEOptions,\n { seq, command }: { seq: number; command: StatewireWireCommand },\n): Promise<StatewireSubmitResult> => {\n const timeout = AbortSignal.timeout(timing.heartbeatTimeoutMs);\n const res = await timedFetch(\n options,\n `${baseUrl(options)}/commands`,\n {\n method: \"POST\",\n headers: await requestHeaders(options, {\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 body: JSON.stringify(encodeWireCommand(command)),\n signal: AbortSignal.any([\n c.lifetime?.signal ?? AbortSignal.abort(),\n timeout,\n ]),\n },\n timeout,\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 === 409 || res.status === 412 || res.status === 423) {\n // Only a body naming the same verdict as the status is statewire's own;\n // a bare status is middleware noise — a transport fault, retried.\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 const body = (await res.json().catch(() => null)) as {\n error?: string;\n } | null;\n if (body?.error === verdict) return { type: verdict };\n throw new Error(`HTTP ${res.status}`);\n }\n if (res.status === 429 || res.status === 503) throw overloaded(res);\n if (res.status >= 400 && res.status < 500) {\n return { type: \"rejected\", message: `HTTP ${res.status}` };\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 c = useStatewireConn({\n url: options.url,\n onChange: options.onChange,\n ctx,\n attemptOnce: (c, abort) => attemptStream(c, options, abort),\n });\n const [link] = useState(\n (): RemoteLink => ({\n get connection() {\n return c.connection;\n },\n send: (args) => sendCommand(c, latest.current, 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","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","requestHeaders","extra","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","ctx","accept","headerTimer","cache","any","status","overloaded","ok","body","Error","getReader","aborted","cancel","catch","get","sendCommand","String","lease","stringify","lifetime","json","verdict","const","error","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\nconst requestHeaders = async (\n options: StatewireClient.SSEOptions,\n extra: Record<string, string>,\n) => {\n const { headers } = options;\n const merged = new Headers(\n typeof headers === \"function\" ? await headers() : 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 abort: AbortController,\n): Promise<\"stop\" | \"reattach\" | \"ended\"> => {\n const headers = await requestHeaders(options, {\n \"Statewire-Client-Id\": c.ctx.clientId,\n accept: \"text/event-stream\",\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.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 sendCommand = async (\n c: Conn,\n options: StatewireClient.SSEOptions,\n { seq, command }: { seq: number; command: StatewireWireCommand },\n): Promise<StatewireSubmitResult> => {\n const timeout = AbortSignal.timeout(timing.heartbeatTimeoutMs);\n const res = await timedFetch(\n options,\n `${baseUrl(options)}/commands`,\n {\n method: \"POST\",\n headers: await requestHeaders(options, {\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 body: JSON.stringify(encodeWireCommand(command)),\n signal: AbortSignal.any([\n c.lifetime?.signal ?? AbortSignal.abort(),\n timeout,\n ]),\n },\n timeout,\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 === 409 || res.status === 412 || res.status === 423) {\n // Only a body naming the same verdict as the status is statewire's own;\n // a bare status is middleware noise — a transport fault, retried.\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 const body = (await res.json().catch(() => null)) as {\n error?: string;\n } | null;\n if (body?.error === verdict) return { type: verdict };\n throw new Error(`HTTP ${res.status}`);\n }\n if (res.status === 429 || res.status === 503) throw overloaded(res);\n if (res.status >= 400 && res.status < 500) {\n return { type: \"rejected\", message: `HTTP ${res.status}` };\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 c = useStatewireConn({\n url: options.url,\n onChange: options.onChange,\n ctx,\n attemptOnce: (c, abort) => attemptStream(c, options, abort),\n });\n const [link] = useState(\n (): RemoteLink => ({\n get connection() {\n return c.connection;\n },\n send: (args) => sendCommand(c, latest.current, 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,5 +1,5 @@
1
- import { StatewireEnvelope } from "../client/connection.js";
2
1
  import { Statewire } from "../types.js";
2
+ import { StatewireEnvelope } from "../client/connection.js";
3
3
  import { SocketRecorder, commandFrame, createCounterElement, createSocketRecorder, settle } from "./fixtures.js";
4
4
  import { ResourceElement } from "@assistant-ui/tap";
5
5
  //#region src/transports/testing.d.ts
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","names":["ResourceElement","StatewireEnvelope","recordMutations","StatewireSSE","Statewire","openSSEStream","sseFrame","FakeStreamHandle","tick","Promise","r","setTimeout","applyOptimistic","state","TState","mutate","draft","MockTransport","transport","commands","method","params","context","setState","ack","seq","sendEnvelope","envelope","finish","reason","message","clientId","attached","mockCount","createMockTransport","initialState","finished","clients","Map","streams","Set","stream","send","fakeFetch","input","RequestInfo","URL","init","RequestInit","Response","TypeError","url","String","headers","Headers","id","get","endsWith","lastSeq","set","handle","ops","op","path","value","syn","lease","signal","undefined","Number","Math","max","push","JSON","parse","body","json","status","fetch","next","fin","close","size"],"sources":["../../src/transports/testing.ts"],"sourcesContent":["import type { ResourceElement } from \"@assistant-ui/tap\";\nimport type { StatewireEnvelope } from \"../client/connection\";\nimport { recordMutations } from \"../util/recordMutations\";\nimport { StatewireSSE } from \"./sse\";\nimport type { Statewire } from \"../types\";\nimport { openSSEStream, sseFrame } from \"./fake-sse\";\nimport type { FakeStreamHandle } from \"./fake-sse\";\n\nexport * from \"./fixtures\";\n\n/** Let queued microtasks and op batches drain through the statewire client. */\nexport const tick = () => new Promise<void>((r) => setTimeout(r, 0));\n\n/** Run a mutator against a recording draft, as the client does for `optimistic`. */\nexport const applyOptimistic = <TState>(\n state: TState,\n mutate: (draft: TState) => void,\n): TState => recordMutations(state, mutate).state;\n\nexport type MockTransport<TState> = {\n /** Pass as the `transport:` option of useStatewire / Harness. */\n transport: ResourceElement<Statewire<TState | undefined, any, any>>;\n /** Captured `/commands` POST bodies, in POST order. */\n commands: { method?: string; params?: unknown[]; context?: unknown }[];\n /** Replace the replicated state and broadcast it as a root snapshot op. */\n setState: (state: TState) => Promise<void>;\n /** Deliver an ack watermark on the stream. */\n ack: (seq: number) => Promise<void>;\n /** Deliver an arbitrary envelope on the stream. */\n sendEnvelope: (envelope: StatewireEnvelope) => Promise<void>;\n /** End the stream with a fin; further attaches fail like a dead server. */\n finish: (reason: string, message?: string) => Promise<void>;\n readonly clientId: string;\n readonly attached: boolean;\n};\n\n/**\n * A mock statewire backend: serves `initialState` as the attach snapshot,\n * records command POSTs (receipt only — acks and results are delivered from\n * test code), and exposes the wire as a transport ResourceElement.\n */\nlet mockCount = 0;\n\nexport const createMockTransport = <TState>(\n initialState: TState,\n): MockTransport<TState> => {\n let state: unknown = initialState;\n let clientId = \"\";\n let finished = false;\n const clients = new Map<string, number>();\n const streams = new Set<FakeStreamHandle>();\n const commands: MockTransport<unknown>[\"commands\"] = [];\n\n const sendEnvelope = (envelope: StatewireEnvelope) => {\n for (const stream of streams) stream.send(sseFrame(envelope));\n return tick();\n };\n\n const fakeFetch = (async (\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> => {\n if (finished) throw new TypeError(\"fetch failed (finished)\");\n const url = String(input);\n const headers = new Headers(init?.headers);\n const id = headers.get(\"Statewire-Client-Id\") ?? \"\";\n if (url.endsWith(\"/stream\")) {\n clientId = id;\n const lastSeq = clients.get(id) ?? 0;\n clients.set(id, lastSeq);\n return openSSEStream(\n streams,\n id,\n (handle) => {\n handle.send(\n sseFrame({\n ops: [{ op: \"replace\", path: [], value: state }],\n syn: { lastSeq, lease: \"mock-lease\" },\n }),\n );\n },\n init?.signal ?? undefined,\n );\n }\n if (url.endsWith(\"/commands\")) {\n const seq = Number(headers.get(\"Statewire-Command-Seq\"));\n clients.set(id, Math.max(clients.get(id) ?? 0, seq));\n commands.push(\n JSON.parse(String(init?.body)) as MockTransport<unknown>[\"commands\"][number],\n );\n return Response.json({});\n }\n return new Response(null, { status: 404 });\n }) as typeof fetch;\n\n return {\n transport: StatewireSSE({\n url: `http://mock-${++mockCount}/api`,\n fetch: fakeFetch,\n }),\n commands,\n setState: (next) => {\n state = next;\n return sendEnvelope({ ops: [{ op: \"replace\", path: [], value: next }] });\n },\n ack: (seq) => sendEnvelope({ ack: seq }),\n sendEnvelope,\n finish: async (reason, message) => {\n await sendEnvelope({\n fin: { reason, ...(message !== undefined && { message }) },\n });\n finished = true;\n for (const stream of [...streams]) stream.close();\n await tick();\n },\n get clientId() {\n return clientId;\n },\n get attached() {\n return streams.size > 0;\n },\n };\n};\n"],"mappings":";;;;;;AAWA,MAAaQ,aAAa,IAAIC,SAAeC,MAAMC,WAAWD,GAAG,CAAC,CAAC;;AAGnE,MAAaE,mBACXC,OACAE,WACWb,gBAAgBW,OAAOE,MAAM,CAAC,CAACF;;;;;;AAwB5C,IAAIoB,YAAY;AAEhB,MAAaC,uBACXC,iBAC0B;CAC1B,IAAItB,QAAiBsB;CACrB,IAAIJ,WAAW;CACf,IAAIK,WAAW;CACf,MAAMC,0BAAU,IAAIC,IAAoB;CACxC,MAAMC,0BAAU,IAAIC,IAAsB;CAC1C,MAAMrB,WAA+C,CAAA;CAErD,MAAMO,gBAAgBC,aAAgC;EACpD,KAAK,MAAMc,UAAUF,SAASE,OAAOC,KAAKpC,SAASqB,QAAQ,CAAC;EAC5D,OAAOnB,KAAK;CACd;CAEA,MAAMmC,aAAa,OACjBC,OACAG,SACsB;EACtB,IAAIX,UAAU,MAAM,IAAIc,UAAU,yBAAyB;EAC3D,MAAMC,MAAMC,OAAOR,KAAK;EACxB,MAAMS,UAAU,IAAIC,QAAQP,MAAMM,OAAO;EACzC,MAAME,KAAKF,QAAQG,IAAI,qBAAqB,KAAK;EACjD,IAAIL,IAAIM,SAAS,SAAS,GAAG;GAC3B1B,WAAWwB;GACX,MAAMG,UAAUrB,QAAQmB,IAAID,EAAE,KAAK;GACnClB,QAAQsB,IAAIJ,IAAIG,OAAO;GACvB,OAAOrD,cACLkC,SACAgB,KACCK,WAAW;IACVA,OAAOlB,KACLpC,SAAS;KACPuD,KAAK,CAAC;MAAEC,IAAI;MAAWC,MAAM,CAAA;MAAIC,OAAOnD;KAAM,CAAC;KAC/CoD,KAAK;MAAEP;MAASQ,OAAO;KAAa;IACtC,CAAC,CACH;GACF,GACAnB,MAAMoB,UAAUC,KAAAA,CAClB;EACF;EACA,IAAIjB,IAAIM,SAAS,WAAW,GAAG;GAC7B,MAAMhC,MAAM4C,OAAOhB,QAAQG,IAAI,uBAAuB,CAAC;GACvDnB,QAAQsB,IAAIJ,IAAIe,KAAKC,IAAIlC,QAAQmB,IAAID,EAAE,KAAK,GAAG9B,GAAG,CAAC;GACnDN,SAASqD,KACPC,KAAKC,MAAMtB,OAAOL,MAAM4B,IAAI,CAAC,CAC/B;GACA,OAAO1B,SAAS2B,KAAK,CAAC,CAAC;EACzB;EACA,OAAO,IAAI3B,SAAS,MAAM,EAAE4B,QAAQ,IAAI,CAAC;CAC3C;CAEA,OAAO;EACL3D,WAAWf,aAAa;GACtBgD,KAAK,eAAe,EAAElB,UAAS;GAC/B6C,OAAOnC;EACT,CAAC;EACDxB;EACAI,WAAWwD,SAAS;GAClBlE,QAAQkE;GACR,OAAOrD,aAAa,EAAEmC,KAAK,CAAC;IAAEC,IAAI;IAAWC,MAAM,CAAA;IAAIC,OAAOe;GAAK,CAAC,EAAE,CAAC;EACzE;EACAvD,MAAMC,QAAQC,aAAa,EAAEF,KAAKC,IAAI,CAAC;EACvCC;EACAE,QAAQ,OAAOC,QAAQC,YAAY;GACjC,MAAMJ,aAAa,EACjBsD,KAAK;IAAEnD;IAAQ,GAAIC,YAAYsC,KAAAA,KAAa,EAAEtC,QAAQ;GAAG,EAC3D,CAAC;GACDM,WAAW;GACX,KAAK,MAAMK,UAAU,CAAC,GAAGF,OAAO,GAAGE,OAAOwC,MAAM;GAChD,MAAMzE,KAAK;EACb;EACA,IAAIuB,WAAW;GACb,OAAOA;EACT;EACA,IAAIC,WAAW;GACb,OAAOO,QAAQ2C,OAAO;EACxB;CACF;AACF"}
1
+ {"version":3,"file":"testing.js","names":["ResourceElement","StatewireEnvelope","recordMutations","StatewireSSE","Statewire","openSSEStream","sseFrame","FakeStreamHandle","tick","Promise","r","setTimeout","applyOptimistic","state","TState","mutate","draft","MockTransport","transport","commands","method","params","context","setState","ack","seq","sendEnvelope","envelope","finish","reason","message","clientId","attached","mockCount","createMockTransport","initialState","finished","clients","Map","streams","Set","stream","send","fakeFetch","input","RequestInfo","URL","init","RequestInit","Response","TypeError","url","String","headers","Headers","id","get","endsWith","lastSeq","set","handle","ops","op","path","value","syn","lease","signal","undefined","Number","Math","max","push","JSON","parse","body","json","status","fetch","next","fin","close","size"],"sources":["../../src/transports/testing.ts"],"sourcesContent":["import type { ResourceElement } from \"@assistant-ui/tap\";\nimport type { StatewireEnvelope } from \"../client/connection\";\nimport { recordMutations } from \"../util/recordMutations\";\nimport { StatewireSSE } from \"./sse\";\nimport type { Statewire } from \"../types\";\nimport { openSSEStream, sseFrame } from \"./fake-sse\";\nimport type { FakeStreamHandle } from \"./fake-sse\";\n\nexport * from \"./fixtures\";\n\n/** Let queued microtasks and op batches drain through the statewire client. */\nexport const tick = () => new Promise<void>((r) => setTimeout(r, 0));\n\n/** Run a mutator against a recording draft, as the client does for `optimistic`. */\nexport const applyOptimistic = <TState>(\n state: TState,\n mutate: (draft: TState) => void,\n): TState => recordMutations(state, mutate).state;\n\nexport type MockTransport<TState> = {\n /** Pass as the `transport:` option of useStatewire / Harness. */\n transport: ResourceElement<Statewire<TState | undefined, any, any>>;\n /** Captured `/commands` POST bodies, in POST order. */\n commands: { method?: string; params?: unknown[]; context?: unknown }[];\n /** Replace the replicated state and broadcast it as a root snapshot op. */\n setState: (state: TState) => Promise<void>;\n /** Deliver an ack watermark on the stream. */\n ack: (seq: number) => Promise<void>;\n /** Deliver an arbitrary envelope on the stream. */\n sendEnvelope: (envelope: StatewireEnvelope) => Promise<void>;\n /** End the stream with a fin; further attaches fail like a dead server. */\n finish: (reason: string, message?: string) => Promise<void>;\n readonly clientId: string;\n readonly attached: boolean;\n};\n\n/**\n * A mock statewire backend: serves `initialState` as the attach snapshot,\n * records command POSTs (receipt only — acks and results are delivered from\n * test code), and exposes the wire as a transport ResourceElement.\n */\nlet mockCount = 0;\n\nexport const createMockTransport = <TState>(\n initialState: TState,\n): MockTransport<TState> => {\n let state: unknown = initialState;\n let clientId = \"\";\n let finished = false;\n const clients = new Map<string, number>();\n const streams = new Set<FakeStreamHandle>();\n const commands: MockTransport<unknown>[\"commands\"] = [];\n\n const sendEnvelope = (envelope: StatewireEnvelope) => {\n for (const stream of streams) stream.send(sseFrame(envelope));\n return tick();\n };\n\n const fakeFetch = (async (\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> => {\n if (finished) throw new TypeError(\"fetch failed (finished)\");\n const url = String(input);\n const headers = new Headers(init?.headers);\n const id = headers.get(\"Statewire-Client-Id\") ?? \"\";\n if (url.endsWith(\"/stream\")) {\n clientId = id;\n const lastSeq = clients.get(id) ?? 0;\n clients.set(id, lastSeq);\n return openSSEStream(\n streams,\n id,\n (handle) => {\n handle.send(\n sseFrame({\n ops: [{ op: \"replace\", path: [], value: state }],\n syn: { lastSeq, lease: \"mock-lease\" },\n }),\n );\n },\n init?.signal ?? undefined,\n );\n }\n if (url.endsWith(\"/commands\")) {\n const seq = Number(headers.get(\"Statewire-Command-Seq\"));\n clients.set(id, Math.max(clients.get(id) ?? 0, seq));\n commands.push(\n JSON.parse(\n String(init?.body),\n ) as MockTransport<unknown>[\"commands\"][number],\n );\n return Response.json({});\n }\n return new Response(null, { status: 404 });\n }) as typeof fetch;\n\n return {\n transport: StatewireSSE({\n url: `http://mock-${++mockCount}/api`,\n fetch: fakeFetch,\n }),\n commands,\n setState: (next) => {\n state = next;\n return sendEnvelope({ ops: [{ op: \"replace\", path: [], value: next }] });\n },\n ack: (seq) => sendEnvelope({ ack: seq }),\n sendEnvelope,\n finish: async (reason, message) => {\n await sendEnvelope({\n fin: { reason, ...(message !== undefined && { message }) },\n });\n finished = true;\n for (const stream of [...streams]) stream.close();\n await tick();\n },\n get clientId() {\n return clientId;\n },\n get attached() {\n return streams.size > 0;\n },\n };\n};\n"],"mappings":";;;;;;AAWA,MAAaQ,aAAa,IAAIC,SAAeC,MAAMC,WAAWD,GAAG,CAAC,CAAC;;AAGnE,MAAaE,mBACXC,OACAE,WACWb,gBAAgBW,OAAOE,MAAM,CAAC,CAACF;;;;;;AAwB5C,IAAIoB,YAAY;AAEhB,MAAaC,uBACXC,iBAC0B;CAC1B,IAAItB,QAAiBsB;CACrB,IAAIJ,WAAW;CACf,IAAIK,WAAW;CACf,MAAMC,0BAAU,IAAIC,IAAoB;CACxC,MAAMC,0BAAU,IAAIC,IAAsB;CAC1C,MAAMrB,WAA+C,CAAA;CAErD,MAAMO,gBAAgBC,aAAgC;EACpD,KAAK,MAAMc,UAAUF,SAASE,OAAOC,KAAKpC,SAASqB,QAAQ,CAAC;EAC5D,OAAOnB,KAAK;CACd;CAEA,MAAMmC,aAAa,OACjBC,OACAG,SACsB;EACtB,IAAIX,UAAU,MAAM,IAAIc,UAAU,yBAAyB;EAC3D,MAAMC,MAAMC,OAAOR,KAAK;EACxB,MAAMS,UAAU,IAAIC,QAAQP,MAAMM,OAAO;EACzC,MAAME,KAAKF,QAAQG,IAAI,qBAAqB,KAAK;EACjD,IAAIL,IAAIM,SAAS,SAAS,GAAG;GAC3B1B,WAAWwB;GACX,MAAMG,UAAUrB,QAAQmB,IAAID,EAAE,KAAK;GACnClB,QAAQsB,IAAIJ,IAAIG,OAAO;GACvB,OAAOrD,cACLkC,SACAgB,KACCK,WAAW;IACVA,OAAOlB,KACLpC,SAAS;KACPuD,KAAK,CAAC;MAAEC,IAAI;MAAWC,MAAM,CAAA;MAAIC,OAAOnD;KAAM,CAAC;KAC/CoD,KAAK;MAAEP;MAASQ,OAAO;KAAa;IACtC,CAAC,CACH;GACF,GACAnB,MAAMoB,UAAUC,KAAAA,CAClB;EACF;EACA,IAAIjB,IAAIM,SAAS,WAAW,GAAG;GAC7B,MAAMhC,MAAM4C,OAAOhB,QAAQG,IAAI,uBAAuB,CAAC;GACvDnB,QAAQsB,IAAIJ,IAAIe,KAAKC,IAAIlC,QAAQmB,IAAID,EAAE,KAAK,GAAG9B,GAAG,CAAC;GACnDN,SAASqD,KACPC,KAAKC,MACHtB,OAAOL,MAAM4B,IAAI,CACnB,CACF;GACA,OAAO1B,SAAS2B,KAAK,CAAC,CAAC;EACzB;EACA,OAAO,IAAI3B,SAAS,MAAM,EAAE4B,QAAQ,IAAI,CAAC;CAC3C;CAEA,OAAO;EACL3D,WAAWf,aAAa;GACtBgD,KAAK,eAAe,EAAElB,UAAS;GAC/B6C,OAAOnC;EACT,CAAC;EACDxB;EACAI,WAAWwD,SAAS;GAClBlE,QAAQkE;GACR,OAAOrD,aAAa,EAAEmC,KAAK,CAAC;IAAEC,IAAI;IAAWC,MAAM,CAAA;IAAIC,OAAOe;GAAK,CAAC,EAAE,CAAC;EACzE;EACAvD,MAAMC,QAAQC,aAAa,EAAEF,KAAKC,IAAI,CAAC;EACvCC;EACAE,QAAQ,OAAOC,QAAQC,YAAY;GACjC,MAAMJ,aAAa,EACjBsD,KAAK;IAAEnD;IAAQ,GAAIC,YAAYsC,KAAAA,KAAa,EAAEtC,QAAQ;GAAG,EAC3D,CAAC;GACDM,WAAW;GACX,KAAK,MAAMK,UAAU,CAAC,GAAGF,OAAO,GAAGE,OAAOwC,MAAM;GAChD,MAAMzE,KAAK;EACb;EACA,IAAIuB,WAAW;GACb,OAAOA;EACT;EACA,IAAIC,WAAW;GACb,OAAOO,QAAQ2C,OAAO;EACxB;CACF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "statewire",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "State over the wire: snapshot-first delta streaming and command submission for any state and any command",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/statewire.ts CHANGED
@@ -8,7 +8,6 @@ import {
8
8
  import type { ResourceElement } from "@assistant-ui/tap";
9
9
  import { LIVE_CONNECTION, useStatewireCore } from "./client/core";
10
10
  import type { Statewire } from "./types";
11
- import type { ConnectionState } from "./transports/sse";
12
11
  import type { StatewireSessionStoreOptions } from "./client/sessionStore";
13
12
 
14
13
  export const useStatewire = <
@@ -136,6 +135,18 @@ export namespace StatewireClient {
136
135
  setContext: (value: Record<string, unknown>) => Promise<unknown>;
137
136
  };
138
137
 
138
+ export type ConnectionState =
139
+ | { status: "connecting" }
140
+ | { status: "live" }
141
+ | {
142
+ status: "reconnecting";
143
+ cause: "dropped" | "evicted" | "error";
144
+ attempt: number;
145
+ nextRetryAt: number;
146
+ lastError?: string;
147
+ message?: string;
148
+ }
149
+ | { status: "gone"; message?: string; payload?: unknown };
139
150
  export type Connection = ConnectionState & { reconnect: () => void };
140
151
  export type ConnectionStatus = Connection["status"];
141
152
 
@@ -58,18 +58,7 @@ export type StatewireSubmitResult =
58
58
  | { type: "unknown-client" }
59
59
  | { type: "stale-lease" };
60
60
 
61
- export type ConnectionState =
62
- | { status: "connecting" }
63
- | { status: "live" }
64
- | {
65
- status: "reconnecting";
66
- cause: "dropped" | "evicted" | "error";
67
- attempt: number;
68
- nextRetryAt: number;
69
- lastError?: string;
70
- message?: string;
71
- }
72
- | { status: "gone"; message?: string; payload?: unknown };
61
+ export type ConnectionState = StatewireClient.ConnectionState;
73
62
 
74
63
  /** The live wire a remote session drives: connection status plus command POST. */
75
64
  export type RemoteLink = {