statewire 0.15.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -6
- package/dist/client/connection.d.ts +5 -1
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +19 -4
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +2 -0
- package/dist/client/core.js.map +1 -1
- package/dist/client/session.d.ts +15 -4
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +233 -52
- package/dist/client/session.js.map +1 -1
- package/dist/client/sessionStore.d.ts +5 -1
- package/dist/client/sessionStore.d.ts.map +1 -1
- package/dist/client/sessionStore.js +3 -1
- package/dist/client/sessionStore.js.map +1 -1
- package/dist/host/host.d.ts +1 -0
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js.map +1 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +27 -9
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +10 -3
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +103 -39
- package/dist/host/socket.js.map +1 -1
- package/dist/host-internal.d.ts +9 -1
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host-internal.js +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -3
- package/dist/statewire.d.ts +33 -2
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +34 -3
- package/dist/statewire.js.map +1 -1
- package/dist/transports/fixtures.js +1 -0
- package/dist/transports/fixtures.js.map +1 -1
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +15 -8
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +2 -2
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +11 -11
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +1 -1
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +14 -6
- package/dist/transports/testing.js.map +1 -1
- package/dist/transports/ws.d.ts +2 -2
- package/dist/transports/ws.d.ts.map +1 -1
- package/dist/transports/ws.js +5 -5
- package/dist/transports/ws.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/util/randomId.js +1 -1
- package/dist/util/randomId.js.map +1 -1
- package/dist/wire/documents.d.ts +25 -2
- package/dist/wire/documents.d.ts.map +1 -1
- package/dist/wire/documents.js +27 -7
- package/dist/wire/documents.js.map +1 -1
- package/dist/wire/protocol.d.ts +55 -0
- package/dist/wire/protocol.d.ts.map +1 -0
- package/dist/wire/protocol.js +221 -0
- package/dist/wire/protocol.js.map +1 -0
- package/dist/wire/version.d.ts +1 -3
- package/dist/wire/version.d.ts.map +1 -1
- package/dist/wire/version.js +1 -3
- package/dist/wire/version.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +43 -13
- package/src/client/core.ts +6 -0
- package/src/client/session.ts +339 -42
- package/src/client/sessionStore.ts +8 -0
- package/src/host/host.ts +1 -0
- package/src/host/http.ts +36 -7
- package/src/host/socket.ts +141 -50
- package/src/host-internal.ts +12 -1
- package/src/index.ts +5 -2
- package/src/statewire.ts +109 -6
- package/src/transports/fixtures.ts +1 -1
- package/src/transports/remote.ts +16 -7
- package/src/transports/sse.ts +28 -18
- package/src/transports/testing.ts +12 -4
- package/src/transports/ws.ts +8 -8
- package/src/types.ts +11 -0
- package/src/util/randomId.ts +3 -3
- package/src/wire/documents.ts +126 -8
- package/src/wire/protocol.ts +327 -0
- package/src/wire/version.ts +0 -2
package/dist/statewire.js
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import { CONNECTED_CONNECTION, useStatewireCore } from "./client/core.js";
|
|
2
|
-
import { SessionStorageContext } from "./client/sessionStore.js";
|
|
2
|
+
import { DocumentStorageContext, ProtocolContext, SessionStorageContext } from "./client/sessionStore.js";
|
|
3
3
|
import { useMemo, useRef, useState, useSyncExternalStore } from "@assistant-ui/tap/react-shim";
|
|
4
4
|
import { createTapRoot, flushTapSync, resource, useContextProvider, useResource, useResources, withKey } from "@assistant-ui/tap";
|
|
5
5
|
//#region src/statewire.ts
|
|
6
|
-
const useSessionTransport = ({ transport, session }) => useContextProvider(SessionStorageContext, session, () => useResource(transport));
|
|
6
|
+
const useSessionTransport = ({ transport, session, documents, protocol }) => useContextProvider(SessionStorageContext, session, () => useContextProvider(DocumentStorageContext, documents, () => useContextProvider(ProtocolContext, protocol, () => useResource(transport))));
|
|
7
7
|
const SessionTransport = resource(useSessionTransport);
|
|
8
8
|
const useStatewire = (options) => {
|
|
9
9
|
const latest = useRef(options);
|
|
10
10
|
latest.current = options;
|
|
11
11
|
const session = useResources(options.storage?.session !== void 0 ? [withKey("session", options.storage.session)] : [])[0];
|
|
12
|
+
const documents = useResources(options.storage?.documents === void 0 ? [] : [withKey("documents", options.storage.documents)])[0];
|
|
13
|
+
const protocol = {
|
|
14
|
+
...options.protocol !== void 0 && { protocol: options.protocol },
|
|
15
|
+
...options.wireVersion !== void 0 && { wireVersion: options.wireVersion },
|
|
16
|
+
...options.applicationProtocols !== void 0 && { applicationProtocols: options.applicationProtocols },
|
|
17
|
+
...options.keyPrefixes !== void 0 && { keyPrefixes: options.keyPrefixes }
|
|
18
|
+
};
|
|
12
19
|
const value = useResource(SessionTransport({
|
|
13
20
|
transport: options.transport,
|
|
14
|
-
session
|
|
21
|
+
session,
|
|
22
|
+
documents,
|
|
23
|
+
protocol
|
|
15
24
|
}));
|
|
16
25
|
const core = useStatewireCore(() => latest.current, value);
|
|
17
26
|
const [subscribe] = useState(() => (listener) => core.subscribe(() => flushTapSync(listener)));
|
|
@@ -26,6 +35,16 @@ const useStatewire = (options) => {
|
|
|
26
35
|
};
|
|
27
36
|
return {
|
|
28
37
|
state,
|
|
38
|
+
documents: value.documents,
|
|
39
|
+
selection: value.selection,
|
|
40
|
+
invoke: (protocol_0, method, params) => {
|
|
41
|
+
if (!value.invoke) throw new Error("statewire: transport does not support protocol commands");
|
|
42
|
+
return value.invoke(protocol_0, method, params);
|
|
43
|
+
},
|
|
44
|
+
updateContext: (ops) => {
|
|
45
|
+
if (!value.updateContext) throw new Error("statewire: transport does not support context documents");
|
|
46
|
+
value.updateContext(ops);
|
|
47
|
+
},
|
|
29
48
|
getState: core.getState,
|
|
30
49
|
connection,
|
|
31
50
|
commands: core.commands,
|
|
@@ -64,6 +83,18 @@ var StatewireClient = class {
|
|
|
64
83
|
get commands() {
|
|
65
84
|
return this.root.getValue().commands;
|
|
66
85
|
}
|
|
86
|
+
get documents() {
|
|
87
|
+
return this.root.getValue().documents;
|
|
88
|
+
}
|
|
89
|
+
get selection() {
|
|
90
|
+
return this.root.getValue().selection;
|
|
91
|
+
}
|
|
92
|
+
invoke(protocol, method, params) {
|
|
93
|
+
return this.root.getValue().invoke(protocol, method, params);
|
|
94
|
+
}
|
|
95
|
+
updateContext(ops) {
|
|
96
|
+
this.root.getValue().updateContext(ops);
|
|
97
|
+
}
|
|
67
98
|
get isBusy() {
|
|
68
99
|
return this.root.getValue().isBusy;
|
|
69
100
|
}
|
package/dist/statewire.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statewire.js","names":["StatewireHost","useMemo","useRef","useState","useSyncExternalStore","createTapRoot","flushTapSync","resource","useContextProvider","useResource","useResources","withKey","ResourceElement","CONNECTED_CONNECTION","useStatewireCore","Statewire","TERMINAL","SessionStorageContext","useSessionTransport","transport","session","StatewireClient","SessionStorage","SessionTransport","useStatewire","Commands","options","Options","TState","TCommands","Helpers","latest","current","storage","undefined","value","core","subscribe","listener","state","getState","isBusy","getBusy","failed","getFailed","connection","failures","NonNullable","Error","commands","CommandsProxy","setContext","contextValue","Record","retryFailed","entry","FailedCommand","retry","discardFailed","discard","StatewireResource","root","disposed","constructor","ClientRoot","getValue","Connection","Promise","dispose","unmount","initialState","optimistic","Optimistic","materialize","Materialize","onStateChange","ctx","changes","Changes","onCommandChange","update","CommandUpdate","onConnectionChange","onError","error","ConnectionError","failureCovered","ConnectionState","status","reason","message","payload","attempt","cause","code","nextRetryAt","lastError","reconnect","degraded","ConnectionStatus","draft","command","Command","OptimisticContext","ts","seq","clientId","SendFailure","fate","predecessorLost","OnSessionLost","pending","ReadonlyArray","method","params","meta","failure","SessionCommand","SessionRecord","nextSeq","ctxNextSeq","verdicts","SessionVerdict","type","load","save","record","lock","fn","T","fin","Fin","HeadersOption","HeadersInit","staleAuth","SSEOptions","sessionStore","url","isNew","headers","fetch","globalThis","onSessionLost","onChange","onDone","WSOptions","protocols","query","webSocket","WebSocket"],"sources":["../src/statewire.ts"],"sourcesContent":["import type { StatewireHost } from \"./host\";\nimport { useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport {\n createTapRoot,\n flushTapSync,\n resource,\n useContextProvider,\n useResource,\n useResources,\n withKey,\n} from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { CONNECTED_CONNECTION, useStatewireCore } from \"./client/core\";\nimport type { Statewire } from \"./types\";\nimport type { TERMINAL } from \"./client/connection\";\nimport { SessionStorageContext } from \"./client/sessionStore\";\n\nconst useSessionTransport = ({\n transport,\n session,\n}: {\n transport: ResourceElement<Statewire<any, any>>;\n session: StatewireClient.SessionStorage | undefined;\n}): Statewire<unknown> =>\n useContextProvider(SessionStorageContext, session, () =>\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useResource(transport),\n );\n\n// The transport owns the session; it reads the storage off the context this\n// wrapper's own fiber provides, so plain-React callers get a provider too.\nconst SessionTransport = resource(useSessionTransport);\n\nexport const useStatewire = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n>(\n options: StatewireClient.Options<TState, TCommands>,\n): StatewireClient.Helpers<TState, TCommands> => {\n const latest = useRef(options);\n latest.current = options;\n\n const session = useResources(\n options.storage?.session !== undefined\n ? [withKey(\"session\", options.storage.session)]\n : [],\n )[0];\n const value = useResource(\n SessionTransport({ transport: options.transport, session }),\n );\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 isBusy = useSyncExternalStore(subscribe, core.getBusy, core.getBusy);\n const failed = useSyncExternalStore(\n subscribe,\n core.getFailed,\n core.getFailed,\n );\n const connection = value.connection ?? CONNECTED_CONNECTION;\n\n return useMemo(() => {\n const failures = (): NonNullable<typeof value.failures> => {\n if (!value.failures) {\n throw new Error(\"statewire: this transport has no failed-send lane\");\n }\n return value.failures;\n };\n return {\n state,\n getState: core.getState as () => TState,\n connection,\n commands: core.commands as Statewire.CommandsProxy<TCommands>,\n isBusy,\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 value.setContext(contextValue);\n },\n failed,\n retryFailed: (entry: StatewireClient.FailedCommand) =>\n failures().retry(entry),\n discardFailed: (entry: StatewireClient.FailedCommand) =>\n failures().discard(entry),\n };\n }, [state, isBusy, failed, connection, core, value]);\n};\n\nexport const StatewireResource = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n>(\n options: StatewireClient.Options<TState, TCommands>,\n): ResourceElement<StatewireClient.Helpers<TState, TCommands>> =>\n resource(useStatewire<TState, TCommands>)(options);\n\nexport class StatewireClient<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n> {\n private readonly root;\n private disposed = false;\n\n constructor(options: StatewireClient.Options<TState, TCommands>) {\n const ClientRoot = () => useStatewire<TState, TCommands>(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 get isBusy(): boolean {\n return this.root.getValue().isBusy;\n }\n\n setContext(value: Record<string, unknown>): void {\n this.root.getValue().setContext(value);\n }\n\n get failed(): readonly StatewireClient.FailedCommand[] {\n return this.root.getValue().failed;\n }\n\n retryFailed(entry: StatewireClient.FailedCommand): Promise<unknown> {\n return this.root.getValue().retryFailed(entry);\n }\n\n discardFailed(entry: StatewireClient.FailedCommand): void {\n this.root.getValue().discardFailed(entry);\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 > = {\n transport: ResourceElement<Statewire<TState, TCommands>>;\n /** Durable session: the mounted storage holds this client's lane identity and its un-durable commands, which resend on the next attach. */\n storage?:\n | { session?: ResourceElement<SessionStorage> | undefined }\n | undefined;\n /** Served as the canonical state until the attach snapshot; own commands fold over it. */\n initialState?: TState;\n optimistic?: Optimistic<NonNullable<TState>, TCommands>;\n materialize?: Materialize<NonNullable<TState>>;\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 onConnectionChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** Drops a retained failed entry once a canonical snapshot shows its effect landed; checked against each new snapshot. */\n failureCovered?: (\n state: NonNullable<TState>,\n entry: FailedCommand,\n ) => boolean;\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 /** Negation of the server's broadcast idle level (a transport without idle is always busy). */\n isBusy: boolean;\n /** Replace this client's ambient context on the server; throws on transports without a client context channel. */\n setContext: (value: Record<string, unknown>) => void;\n /** Non-accepted sends retained in the session record; reload with it. */\n failed: readonly FailedCommand[];\n /** Re-enqueues the entry's command on fresh clocks and removes the entry. */\n retryFailed: (entry: FailedCommand) => Promise<unknown>;\n /** Removes the entry. */\n discardFailed: (entry: FailedCommand) => void;\n };\n\n /** `cause`/`nextRetryAt`/`lastError` are present only while waiting out the backoff timer; a dial in flight carries `attempt` alone. */\n export type ConnectionState =\n | {\n status: \"disconnected\";\n reason:\n | \"deferred\"\n | \"idle\"\n | \"gone\"\n | \"forbidden\"\n | \"protocol-error\"\n | \"unauthorized\";\n message?: string;\n payload?: unknown;\n }\n | {\n status: \"connecting\";\n /** 0 = the episode's first dial, >0 = retry. */\n attempt: number;\n cause?: \"dropped\" | \"reconnect\" | \"error\" | \"unauthorized\";\n code?: string;\n nextRetryAt?: number;\n lastError?: string;\n message?: string;\n }\n | { status: \"connected\" };\n export type Connection = ConnectionState & {\n error?: ConnectionError;\n reconnect: () => void;\n /** True while retrying (`connecting` with `attempt > 0`) or parked `gone`. */\n degraded: boolean;\n };\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 * Mutate the draft to make server-accepted, still-uncommitted intents in\n * the state concrete (e.g. dispatch records into messages). Runs after the\n * optimistic folds on every published state.\n */\n export type Materialize<TState> = (draft: TState) => void;\n\n /**\n * fate: \"aborted\" = never sent, \"rejected\" = the server refused it\n * (`code` \"unknown-command\" | \"invalid-params\" for the static rejects,\n * code-less for handler rejects), \"lost\" = sent but no verdict will ever\n * arrive — the record is gone (a proven seq gap) or the channel died: it\n * may or may not have run, \"failed\" = the handler ran and threw,\n * \"crashed\" = the handler died involuntarily, \"expired\" = settled —\n * result, rejected, failed, or crashed — but the verdict outlived the\n * server's retention.\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 | {\n fate: \"rejected\";\n cause: \"server\";\n code?: string;\n message?: string;\n payload?: unknown;\n }\n | { fate: \"failed\"; cause: \"server\"; message?: string }\n | { fate: \"crashed\"; cause: \"server\" }\n | { fate: \"expired\"; cause: \"server\" }\n | {\n fate: \"aborted\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\";\n predecessorLost?: true;\n }\n | {\n fate: \"lost\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\";\n };\n\n /** Held commands a proven record gap settled `{fate: \"lost\", cause: \"session-lost\"}`; return \"requeue\" to re-enqueue them on the reset clocks. */\n export type OnSessionLost = (\n pending: ReadonlyArray<{\n method: string;\n params: readonly unknown[];\n ts: number;\n seq: number;\n clientId?: string;\n }>,\n ) => \"requeue\" | \"drop\";\n\n /** A non-accepted send retained for the user: the wire statement, its failure, and its original clocks. */\n export type FailedCommand = {\n readonly command: string;\n readonly params: readonly unknown[];\n readonly meta?: unknown;\n readonly failure: SendFailure;\n readonly ts: number;\n readonly seq: number;\n };\n\n /** A command as persisted while un-durable: its lane seq plus the wire statement. */\n export type SessionCommand = {\n seq: number;\n method: string;\n params: unknown[];\n meta?: unknown;\n };\n\n /** The persisted session: the lane identity, the lane clocks, and the commands the server has not marked durable. */\n export type SessionRecord = {\n clientId: string;\n nextSeq: number;\n ctxNextSeq: number;\n commands: readonly SessionCommand[];\n /** Terminal verdicts parked for the sharer that minted the seq (shared storage only). */\n verdicts?: readonly SessionVerdict[];\n /** Non-accepted sends retained for the user; survive record rebases and resets. */\n failed?: readonly FailedCommand[];\n };\n\n /** A terminal verdict one sharer's connection carried, parked for the sharer awaiting the seq. */\n export type SessionVerdict = {\n seq: number;\n type: (typeof TERMINAL)[number];\n code?: string;\n message?: string;\n payload?: unknown;\n meta?: unknown;\n };\n\n /** Session storage seam; `save` must persist synchronously in the caller's flow. */\n export type SessionStorage = {\n load: () => SessionRecord | null;\n save: (record: SessionRecord) => void;\n /** Serializes record mutations across sharers; presence marks the storage shared. */\n lock?: <T>(fn: () => T | Promise<T>) => Promise<T>;\n /** Notifies of record saves by other sharers; returns unsubscribe. Required alongside `lock`. */\n subscribe?: (listener: () => void) => () => void;\n };\n\n /** Static headers, or a resolver invoked per request/attach; `staleAuth` is true right after an auth rejection. */\n export type ConnectionError = Error & { fin: StatewireHost.Fin };\n\n export type HeadersOption =\n | HeadersInit\n | ((ctx: { staleAuth: boolean }) => HeadersInit | Promise<HeadersInit>);\n\n export type SSEOptions = {\n sessionStore?: SessionStorage | undefined;\n url: string;\n /** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */\n isNew?: boolean;\n headers?: HeadersOption;\n fetch?: typeof globalThis.fetch;\n onSessionLost?: OnSessionLost;\n onChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** The broadcast idle flag's rising edge: the instance went quiet. */\n onDone?: () => void;\n };\n\n export type WSOptions = {\n sessionStore?: SessionStorage | undefined;\n url: string;\n /** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */\n isNew?: boolean;\n headers?: HeadersOption;\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 onSessionLost?: OnSessionLost;\n onChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** The broadcast idle flag's rising edge: the instance went quiet. */\n onDone?: () => void;\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"statewire.js","names":["StatewireDocuments","StatewireProtocol","StatewireHost","useMemo","useRef","useState","useSyncExternalStore","createTapRoot","flushTapSync","resource","useContextProvider","useResource","useResources","withKey","ResourceElement","CONNECTED_CONNECTION","useStatewireCore","Statewire","TERMINAL","DocumentStorageContext","ProtocolContext","SessionStorageContext","useSessionTransport","transport","session","documents","protocol","StatewireClient","SessionStorage","Storage","Options","SessionTransport","useStatewire","Commands","options","TState","TCommands","Helpers","latest","current","storage","undefined","wireVersion","applicationProtocols","keyPrefixes","value","core","subscribe","listener","state","getState","isBusy","getBusy","failed","getFailed","connection","failures","NonNullable","Error","selection","invoke","method","params","updateContext","ops","commands","CommandsProxy","setContext","contextValue","Record","retryFailed","entry","FailedCommand","retry","discardFailed","discard","StatewireResource","root","disposed","constructor","ClientRoot","getValue","Connection","Source","Selection","Promise","dispose","unmount","Readonly","Range","Offer","initialState","optimistic","Optimistic","materialize","Materialize","onStateChange","ctx","changes","Changes","onCommandChange","update","CommandUpdate","onConnectionChange","onError","error","ConnectionError","failureCovered","ConnectionState","status","reason","message","payload","attempt","cause","code","nextRetryAt","lastError","reconnect","degraded","ConnectionStatus","draft","command","Command","OptimisticContext","ts","seq","clientId","SendFailure","fate","predecessorLost","OnSessionLost","pending","ReadonlyArray","produced","SessionCommand","meta","failure","wire","protocols","SessionRecord","nextSeq","ctxNextSeq","context","verdicts","SessionVerdict","type","load","save","record","migrate","lock","fn","T","fin","Fin","HeadersOption","HeadersInit","staleAuth","HttpOptions","sessionStore","url","isNew","headers","fetch","globalThis","onSessionLost","onChange","onDone","WebsocketOptions","query","webSocket","WebSocket"],"sources":["../src/statewire.ts"],"sourcesContent":["import type { StatewireDocuments } from \"./wire/documents\";\nimport type { StatewireProtocol } from \"./wire/protocol\";\nimport type { StatewireHost } from \"./host\";\nimport { useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport {\n createTapRoot,\n flushTapSync,\n resource,\n useContextProvider,\n useResource,\n useResources,\n withKey,\n} from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { CONNECTED_CONNECTION, useStatewireCore } from \"./client/core\";\nimport type { Statewire } from \"./types\";\nimport type { TERMINAL } from \"./client/connection\";\nimport {\n DocumentStorageContext,\n ProtocolContext,\n SessionStorageContext,\n} from \"./client/sessionStore\";\n\nconst useSessionTransport = ({\n transport,\n session,\n documents,\n protocol,\n}: {\n transport: ResourceElement<Statewire<any, any>>;\n session: StatewireClient.SessionStorage | undefined;\n documents: StatewireDocuments.Storage | undefined;\n protocol: StatewireProtocol.Options;\n}): Statewire<unknown> =>\n useContextProvider(SessionStorageContext, session, () =>\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useContextProvider(DocumentStorageContext, documents, () =>\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useContextProvider(ProtocolContext, protocol, () =>\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useResource(transport),\n ),\n ),\n );\n\n// The transport owns the session; it reads the storage off the context this\n// wrapper's own fiber provides, so plain-React callers get a provider too.\nconst SessionTransport = resource(useSessionTransport);\n\nexport const useStatewire = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n>(\n options: StatewireClient.Options<TState, TCommands>,\n): StatewireClient.Helpers<TState, TCommands> => {\n const latest = useRef(options);\n latest.current = options;\n\n const session = useResources(\n options.storage?.session !== undefined\n ? [withKey(\"session\", options.storage.session)]\n : [],\n )[0];\n const documents = useResources(\n options.storage?.documents === undefined\n ? []\n : [withKey(\"documents\", options.storage.documents)],\n )[0];\n const protocol: StatewireProtocol.Options = {\n ...(options.protocol !== undefined && { protocol: options.protocol }),\n ...(options.wireVersion !== undefined && {\n wireVersion: options.wireVersion,\n }),\n ...(options.applicationProtocols !== undefined && {\n applicationProtocols: options.applicationProtocols,\n }),\n ...(options.keyPrefixes !== undefined && {\n keyPrefixes: options.keyPrefixes,\n }),\n };\n const value = useResource(\n SessionTransport({\n transport: options.transport,\n session,\n documents,\n protocol,\n }),\n );\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 isBusy = useSyncExternalStore(subscribe, core.getBusy, core.getBusy);\n const failed = useSyncExternalStore(\n subscribe,\n core.getFailed,\n core.getFailed,\n );\n const connection = value.connection ?? CONNECTED_CONNECTION;\n\n return useMemo(() => {\n const failures = (): NonNullable<typeof value.failures> => {\n if (!value.failures) {\n throw new Error(\"statewire: this transport has no failed-send lane\");\n }\n return value.failures;\n };\n return {\n state,\n documents: value.documents,\n selection: value.selection,\n invoke: (\n protocol: string,\n method: string,\n params: readonly unknown[],\n ) => {\n if (!value.invoke)\n throw new Error(\n \"statewire: transport does not support protocol commands\",\n );\n return value.invoke(protocol, method, params);\n },\n updateContext: (ops: readonly unknown[]) => {\n if (!value.updateContext)\n throw new Error(\n \"statewire: transport does not support context documents\",\n );\n value.updateContext(ops);\n },\n getState: core.getState as () => TState,\n connection,\n commands: core.commands as Statewire.CommandsProxy<TCommands>,\n isBusy,\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 value.setContext(contextValue);\n },\n failed,\n retryFailed: (entry: StatewireClient.FailedCommand) =>\n failures().retry(entry),\n discardFailed: (entry: StatewireClient.FailedCommand) =>\n failures().discard(entry),\n };\n }, [state, isBusy, failed, connection, core, value]);\n};\n\nexport const StatewireResource = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n>(\n options: StatewireClient.Options<TState, TCommands>,\n): ResourceElement<StatewireClient.Helpers<TState, TCommands>> =>\n resource(useStatewire<TState, TCommands>)(options);\n\nexport class StatewireClient<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n> {\n private readonly root;\n private disposed = false;\n\n constructor(options: StatewireClient.Options<TState, TCommands>) {\n const ClientRoot = () => useStatewire<TState, TCommands>(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 get documents(): StatewireDocuments.Source | undefined {\n return this.root.getValue().documents;\n }\n get selection(): StatewireProtocol.Selection | undefined {\n return this.root.getValue().selection;\n }\n invoke(\n protocol: string,\n method: string,\n params: readonly unknown[],\n ): Promise<unknown> {\n return this.root.getValue().invoke(protocol, method, params);\n }\n updateContext(ops: readonly unknown[]): void {\n this.root.getValue().updateContext(ops);\n }\n\n get isBusy(): boolean {\n return this.root.getValue().isBusy;\n }\n\n setContext(value: Record<string, unknown>): void {\n this.root.getValue().setContext(value);\n }\n\n get failed(): readonly StatewireClient.FailedCommand[] {\n return this.root.getValue().failed;\n }\n\n retryFailed(entry: StatewireClient.FailedCommand): Promise<unknown> {\n return this.root.getValue().retryFailed(entry);\n }\n\n discardFailed(entry: StatewireClient.FailedCommand): void {\n this.root.getValue().discardFailed(entry);\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 > = {\n protocol?: string;\n keyPrefixes?: Readonly<Record<string, readonly string[]>>;\n wireVersion?: StatewireProtocol.Range;\n applicationProtocols?: readonly StatewireProtocol.Offer[] | \"*\";\n transport: ResourceElement<Statewire<TState, TCommands>>;\n /** Durable session: the mounted storage holds this client's lane identity and its un-durable commands, which resend on the next attach. */\n storage?:\n | {\n session?: ResourceElement<SessionStorage> | undefined;\n documents?: ResourceElement<StatewireDocuments.Storage> | undefined;\n }\n | undefined;\n /** Served as the canonical state until the attach snapshot; own commands fold over it. */\n initialState?: TState;\n optimistic?: Optimistic<NonNullable<TState>, TCommands>;\n materialize?: Materialize<NonNullable<TState>>;\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 onConnectionChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** Drops a retained failed entry once a canonical snapshot shows its effect landed; checked against each new snapshot. */\n failureCovered?: (\n state: NonNullable<TState>,\n entry: FailedCommand,\n ) => boolean;\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 documents: StatewireDocuments.Source | undefined;\n selection: StatewireProtocol.Selection | undefined;\n invoke: (\n protocol: string,\n method: string,\n params: readonly unknown[],\n ) => Promise<unknown>;\n updateContext: (ops: readonly unknown[]) => void;\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 /** Negation of the server's broadcast idle level (a transport without idle is always busy). */\n isBusy: boolean;\n /** Replace this client's ambient context on the server; throws on transports without a client context channel. */\n setContext: (value: Record<string, unknown>) => void;\n /** Non-accepted sends retained in the session record; reload with it. */\n failed: readonly FailedCommand[];\n /** Re-enqueues the entry's command on fresh clocks and removes the entry. */\n retryFailed: (entry: FailedCommand) => Promise<unknown>;\n /** Removes the entry. */\n discardFailed: (entry: FailedCommand) => void;\n };\n\n /** `cause`/`nextRetryAt`/`lastError` are present only while waiting out the backoff timer; a dial in flight carries `attempt` alone. */\n export type ConnectionState =\n | {\n status: \"disconnected\";\n reason:\n | \"deferred\"\n | \"idle\"\n | \"gone\"\n | \"forbidden\"\n | \"protocol-error\"\n | \"unauthorized\";\n message?: string;\n payload?: unknown;\n }\n | {\n status: \"connecting\";\n /** 0 = the episode's first dial, >0 = retry. */\n attempt: number;\n cause?: \"dropped\" | \"reconnect\" | \"error\" | \"unauthorized\";\n code?: string;\n nextRetryAt?: number;\n lastError?: string;\n message?: string;\n }\n | { status: \"connected\" };\n export type Connection = ConnectionState & {\n error?: ConnectionError;\n reconnect: () => void;\n /** True while retrying (`connecting` with `attempt > 0`) or parked `gone`. */\n degraded: boolean;\n };\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 * Mutate the draft to make server-accepted, still-uncommitted intents in\n * the state concrete (e.g. dispatch records into messages). Runs after the\n * optimistic folds on every published state.\n */\n export type Materialize<TState> = (draft: TState) => void;\n\n /**\n * fate: \"aborted\" = never sent, \"rejected\" = the server refused it\n * (`code` \"unknown-command\" | \"invalid-params\" for the static rejects,\n * code-less for handler rejects), \"lost\" = sent but no verdict will ever\n * arrive — the record is gone (a proven seq gap) or the channel died: it\n * may or may not have run, \"failed\" = the handler ran and threw,\n * \"crashed\" = the handler died involuntarily, \"expired\" = settled —\n * result, rejected, failed, or crashed — but the verdict outlived the\n * server's retention.\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 | {\n fate: \"rejected\";\n cause: \"server\";\n code?: string;\n message?: string;\n payload?: unknown;\n }\n | { fate: \"failed\"; cause: \"server\"; message?: string }\n | { fate: \"crashed\"; cause: \"server\" }\n | { fate: \"expired\"; cause: \"server\" }\n | {\n fate: \"aborted\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\";\n predecessorLost?: true;\n }\n | {\n fate: \"lost\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\";\n };\n\n /** Held commands a proven record gap settled `{fate: \"lost\", cause: \"session-lost\"}`; return \"requeue\" to re-enqueue them on the reset clocks. */\n export type OnSessionLost = (\n pending: ReadonlyArray<{\n method: string;\n params: readonly unknown[];\n ts: number;\n seq: number;\n clientId?: string;\n }>,\n ) => \"requeue\" | \"drop\";\n\n /** A non-accepted send retained for the user: the wire statement, its failure, and its original clocks. */\n export type FailedCommand = {\n readonly protocol: string;\n readonly produced?: SessionCommand[\"produced\"];\n readonly command: string;\n readonly params: readonly unknown[];\n readonly meta?: unknown;\n readonly failure: SendFailure;\n readonly ts: number;\n readonly seq: number;\n };\n\n /** A command as persisted while un-durable: its lane seq plus the wire statement. */\n export type SessionCommand = {\n seq: number;\n protocol: string;\n produced?: {\n wire: StatewireProtocol.Range;\n protocols: readonly StatewireProtocol.Offer[] | \"*\";\n };\n method: string;\n params: unknown[];\n meta?: unknown;\n };\n\n /** The persisted session: the lane identity, the lane clocks, and the commands the server has not marked durable. */\n export type SessionRecord = {\n clientId: string;\n nextSeq: number;\n ctxNextSeq: number;\n context?: { ops: unknown[]; pending: { seq: number; ops: unknown[] }[] };\n commands: readonly SessionCommand[];\n /** Terminal verdicts parked for the sharer that minted the seq (shared storage only). */\n verdicts?: readonly SessionVerdict[];\n /** Non-accepted sends retained for the user; survive record rebases and resets. */\n failed?: readonly FailedCommand[];\n };\n\n /** A terminal verdict one sharer's connection carried, parked for the sharer awaiting the seq. */\n export type SessionVerdict = {\n seq: number;\n type: (typeof TERMINAL)[number];\n code?: string;\n message?: string;\n payload?: unknown;\n meta?: unknown;\n };\n\n /** Session storage seam; `save` must persist synchronously in the caller's flow. */\n export type SessionStorage = {\n load: () => SessionRecord | null;\n save: (record: SessionRecord) => void;\n /** Upgrade incompatible stored commands; null discards the old record and starts a new client identity. */\n migrate?: (\n record: SessionRecord,\n current: NonNullable<SessionCommand[\"produced\"]>,\n ) => SessionRecord | null;\n /** Serializes record mutations across sharers; presence marks the storage shared. */\n lock?: <T>(fn: () => T | Promise<T>) => Promise<T>;\n /** Notifies of record saves by other sharers; returns unsubscribe. Required alongside `lock`. */\n subscribe?: (listener: () => void) => () => void;\n };\n\n /** Static headers, or a resolver invoked per request/attach; `staleAuth` is true right after an auth rejection. */\n export type ConnectionError = Error & { fin: StatewireHost.Fin };\n\n export type HeadersOption =\n | HeadersInit\n | ((ctx: { staleAuth: boolean }) => HeadersInit | Promise<HeadersInit>);\n\n export type HttpOptions = StatewireProtocol.Options & {\n sessionStore?: SessionStorage | undefined;\n url: string;\n /** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */\n isNew?: boolean;\n headers?: HeadersOption;\n fetch?: typeof globalThis.fetch;\n onSessionLost?: OnSessionLost;\n onChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** The broadcast idle flag's rising edge: the instance went quiet. */\n onDone?: () => void;\n };\n\n export type WebsocketOptions = StatewireProtocol.Options & {\n sessionStore?: SessionStorage | undefined;\n url: string;\n /** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */\n isNew?: boolean;\n headers?: HeadersOption;\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 onSessionLost?: OnSessionLost;\n onChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** The broadcast idle flag's rising edge: the instance went quiet. */\n onDone?: () => void;\n };\n}\n"],"mappings":"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"}
|
|
@@ -19,6 +19,7 @@ const createCounterElement = () => resource(() => {
|
|
|
19
19
|
})();
|
|
20
20
|
/** Serialize a one-statement cmd-lane frame as a client would send it. */
|
|
21
21
|
const commandFrame = (method, seq, params = []) => JSON.stringify({ cmd: [{
|
|
22
|
+
protocol: "default",
|
|
22
23
|
method,
|
|
23
24
|
params,
|
|
24
25
|
seq
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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","protocol","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: [{ protocol: \"default\", 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;CAAEC,UAAU;CAAWN;CAAQE;CAAQD;AAAI,CAAC,EAAE,CAAC;;AAW3E,MAAae,6BAA6C;CACxD,MAAMR,OAAiB,CAAA;CACvB,MAAMC,SAA+C,CAAA;CACrD,OAAO;EACLD;EACAC;EACAG,YAAYT,KAAKc,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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote.d.ts","names":[],"sources":["../../src/transports/remote.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"remote.d.ts","names":[],"sources":["../../src/transports/remote.ts"],"mappings":";;;;;cAkBa,qBACX;EACE;EACA,YAAY,YAAY,gBAAgB;EACxC;IACE,yBAEJ,SAAS,UACT,UAAU,SAAS,UAAU,KAAK,yBAAyB,eAC1D"}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { ProtocolContext } from "../client/sessionStore.js";
|
|
2
|
+
import { discardFailed, documentSource, failedCommands, invokeProtocol, retryFailed, setContext, setTransport, updateContext, useStatewireSession } from "../client/session.js";
|
|
3
|
+
import { useContext, useEffect } from "@assistant-ui/tap/react-shim";
|
|
3
4
|
//#region src/transports/remote.ts
|
|
4
5
|
const useStatewireRemote = (options, useLink) => {
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const conn = useLink(
|
|
6
|
+
const configured = {
|
|
7
|
+
...useContext(ProtocolContext),
|
|
8
|
+
...options
|
|
9
|
+
};
|
|
10
|
+
const s = useStatewireSession(configured);
|
|
11
|
+
const conn = useLink(configured, s.ctx);
|
|
11
12
|
useEffect(() => setTransport(s, conn), [s, conn]);
|
|
12
13
|
return {
|
|
13
14
|
state: s.canonical,
|
|
15
|
+
documents: documentSource(s),
|
|
16
|
+
get selection() {
|
|
17
|
+
return s.selection;
|
|
18
|
+
},
|
|
19
|
+
invoke: (protocol, method, params) => invokeProtocol(s, protocol, method, params),
|
|
20
|
+
updateContext: (ops) => updateContext(s, ops),
|
|
14
21
|
commands: s.commands,
|
|
15
22
|
connection: conn.connection,
|
|
16
23
|
source: s.source,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote.js","names":["useEffect","Statewire","discardFailed","failedCommands","retryFailed","setContext","setTransport","useStatewireSession","StatewireSessionOptions","StatewireClient","RemoteLink","StatewireRemoteProps","useStatewireRemote","url","onChange","connection","Connection","onDone","options","TOptions","useLink","ctx","
|
|
1
|
+
{"version":3,"file":"remote.js","names":["useContext","useEffect","ProtocolContext","Statewire","documentSource","invokeProtocol","updateContext","discardFailed","failedCommands","retryFailed","setContext","setTransport","useStatewireSession","StatewireSessionOptions","StatewireClient","RemoteLink","StatewireRemoteProps","useStatewireRemote","url","onChange","connection","Connection","onDone","options","TOptions","useLink","ctx","inherited","configured","s","conn","state","canonical","documents","selection","invoke","protocol","method","params","ops","commands","source","durableSeq","isBusy","idle","value","failures","list","retry","entry","discard"],"sources":["../../src/transports/remote.ts"],"sourcesContent":["import { useContext, useEffect } from \"react\";\nimport { ProtocolContext } from \"../client/sessionStore\";\nimport type { Statewire } from \"../types\";\nimport {\n documentSource,\n invokeProtocol,\n updateContext,\n discardFailed,\n failedCommands,\n retryFailed,\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 inherited = useContext(ProtocolContext);\n const configured = { ...inherited, ...options };\n const s = useStatewireSession(configured);\n const conn = useLink(configured, s.ctx);\n useEffect(() => setTransport(s, conn), [s, conn]);\n\n return {\n state: s.canonical,\n documents: documentSource(s),\n get selection() {\n return s.selection;\n },\n invoke: (protocol, method, params) =>\n invokeProtocol(s, protocol, method, params),\n updateContext: (ops) => updateContext(s, ops),\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 failures: {\n list: () => failedCommands(s),\n retry: (entry) => retryFailed(s, entry),\n discard: (entry) => discardFailed(s, entry),\n },\n };\n};\n"],"mappings":";;;;AAkBA,MAAaiB,sBAOXM,SACAE,YACwB;CAExB,MAAMG,aAAa;EAAE,GADH5B,WAAWE,eACLyB;EAAW,GAAGJ;CAAQ;CAC9C,MAAMM,IAAIjB,oBAAoBgB,UAAU;CACxC,MAAME,OAAOL,QAAQG,YAAYC,EAAEH,GAAG;CACtCzB,gBAAgBU,aAAakB,GAAGC,IAAI,GAAG,CAACD,GAAGC,IAAI,CAAC;CAEhD,OAAO;EACLC,OAAOF,EAAEG;EACTC,WAAW7B,eAAeyB,CAAC;EAC3B,IAAIK,YAAY;GACd,OAAOL,EAAEK;EACX;EACAC,SAASC,UAAUC,QAAQC,WACzBjC,eAAewB,GAAGO,UAAUC,QAAQC,MAAM;EAC5ChC,gBAAgBiC,QAAQjC,cAAcuB,GAAGU,GAAG;EAC5CC,UAAUX,EAAEW;EACZpB,YAAYU,KAAKV;EACjBqB,QAAQZ,EAAEY;EACV,IAAIC,aAAa;GACf,OAAOb,EAAEa;EACX;EACA,IAAIC,SAAS;GACX,OAAO,CAACd,EAAEe;EACZ;EACAlC,aAAamC,UAAUnC,WAAWmB,GAAGgB,KAAK;EAC1CC,UAAU;GACRC,YAAYvC,eAAeqB,CAAC;GAC5BmB,QAAQC,UAAUxC,YAAYoB,GAAGoB,KAAK;GACtCC,UAAUD,YAAU1C,cAAcsB,GAAGoB,OAAK;EAC5C;CACF;AACF"}
|
package/dist/transports/sse.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ type RemoteLink = {
|
|
|
53
53
|
parked: boolean;
|
|
54
54
|
send: (frame: StatewireFrame) => Promise<StatewireSubmitResult>;
|
|
55
55
|
};
|
|
56
|
-
declare const
|
|
56
|
+
declare const StatewireHttp: <TState = any, TCommands extends Statewire.Commands = Statewire.Commands>(options: StatewireClient.HttpOptions) => ResourceElement<Statewire<TState | undefined, TCommands>>;
|
|
57
57
|
//#endregion
|
|
58
|
-
export { ConnectionState, RemoteLink, StatewireFrame,
|
|
58
|
+
export { ConnectionState, RemoteLink, StatewireFrame, StatewireHttp, StatewireRemoteProps, StatewireSubmitResult };
|
|
59
59
|
//# sourceMappingURL=sse.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.d.ts","names":[],"sources":["../../src/transports/sse.ts"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"sse.d.ts","names":[],"sources":["../../src/transports/sse.ts"],"mappings":";;;;;;;KA0BY;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;;EAE5B;EACA,OAAO,OAAO,mBAAmB,QAAQ;;cAiP9B,gBACX,cACA,kBAAkB,UAAU,WAAW,UAAU,UAEjD,SAAS,gBAAgB,gBACtB,gBAAgB,UAAU,oBAAoB"}
|
package/dist/transports/sse.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { negotiationHeaders, parseNegotiation } from "../wire/protocol.js";
|
|
2
2
|
import { createSseParser } from "./sse-parser.js";
|
|
3
3
|
import { timing } from "../client/timing.js";
|
|
4
|
-
import { classify, handleEnvelope, resolveHeaders, settleFin,
|
|
4
|
+
import { classify, handleEnvelope, resolveHeaders, settleFin, useLink, validateRefusalSyn } from "../client/connection.js";
|
|
5
5
|
import { useStatewireRemote } from "./remote.js";
|
|
6
6
|
import { flushTapSync, resource } from "@assistant-ui/tap";
|
|
7
7
|
//#region src/transports/sse.ts
|
|
@@ -44,6 +44,8 @@ const consume = async (c, reader, heartbeat, dead) => {
|
|
|
44
44
|
const attemptStream = async (c, options, abort) => {
|
|
45
45
|
c.lease = null;
|
|
46
46
|
const headers = await resolveHeaders(options.headers, c.authStale);
|
|
47
|
+
for (const [name, value] of Object.entries(negotiationHeaders(options))) if (!headers.has(name)) headers.set(name, value);
|
|
48
|
+
c.negotiation = parseNegotiation(headers);
|
|
47
49
|
c.authStale = false;
|
|
48
50
|
const lanes = flushTapSync(() => c.ctx.takeAttachLanes());
|
|
49
51
|
const body = lanes === null ? void 0 : JSON.stringify(lanes);
|
|
@@ -65,13 +67,11 @@ const attemptStream = async (c, options, abort) => {
|
|
|
65
67
|
} finally {
|
|
66
68
|
clearTimeout(headerTimer);
|
|
67
69
|
}
|
|
68
|
-
if (!res.ok)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (version !== void 0 && version !== "1") {
|
|
72
|
-
res.body.cancel().catch(() => {});
|
|
73
|
-
return unsupportedVersion(c, version);
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
const body = await res.clone().json().catch(() => null);
|
|
72
|
+
return flushTapSync(() => body?.fin === void 0 ? settleFin(c, httpFin(res)) : handleEnvelope(c, body)) ?? "stop";
|
|
74
73
|
}
|
|
74
|
+
if (!res.body) throw new Error("statewire: protocol error: stream response has no body");
|
|
75
75
|
c.lease = res.headers.get(LEASE_HEADER);
|
|
76
76
|
const reader = res.body.getReader();
|
|
77
77
|
try {
|
|
@@ -149,9 +149,9 @@ const useSseLink = (options, ctx) => useLink({
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
});
|
|
152
|
-
const
|
|
153
|
-
const
|
|
152
|
+
const useStatewireHttp = (options) => useStatewireRemote(options, useSseLink);
|
|
153
|
+
const StatewireHttp = resource(useStatewireHttp);
|
|
154
154
|
//#endregion
|
|
155
|
-
export {
|
|
155
|
+
export { StatewireHttp };
|
|
156
156
|
|
|
157
157
|
//# sourceMappingURL=sse.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.js","names":["flushTapSync","resource","ResourceElement","createSseParser","timing","settleFin","classify","handleEnvelope","resolveHeaders","unsupportedVersion","useLink","validateRefusalSyn","AttemptOutcome","Conn","StatewireCmd","StatewireEnvelope","StatewireSyn","Statewire","StatewireClient","StatewireHost","useStatewireRemote","PROTOCOL_VERSION","VERSION_HEADER","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","parked","send","frame","Promise","LEASE_HEADER","baseUrl","options","SSEOptions","url","replace","withExtra","headers","Headers","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","lease","authStale","lanes","body","undefined","stringify","headerTimer","res","accept","cache","any","ok","httpFin","Error","version","get","trim","cancel","catch","getReader","aborted","postFrame","lifetime","retryAfterMs","header","test","retry","Number","isSafeInteger","reason","status","message","code","refusalSyn","json","sendFrame","headerRetry","error","useSseLink","attemptOnce","SyntaxError","startsWith","useStatewireSSE","StatewireSSE","Commands","TState","TCommands"],"sources":["../../src/transports/sse.ts"],"sourcesContent":["import { 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 settleFin,\n classify,\n handleEnvelope,\n resolveHeaders,\n unsupportedVersion,\n useLink,\n validateRefusalSyn,\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\";\nimport { PROTOCOL_VERSION, VERSION_HEADER } from \"../wire/version\";\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 /** A deferred first attach still waiting; `reconnect()` releases it. */\n parked: boolean;\n send: (frame: StatewireFrame) => Promise<StatewireSubmitResult>;\n};\n\nconst LEASE_HEADER = \"Statewire-Lease\";\n\nconst baseUrl = (options: StatewireClient.SSEOptions) =>\n options.url.replace(/\\/$/, \"\");\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 abort: AbortController,\n): Promise<AttemptOutcome> => {\n c.lease = null;\n const headers = await resolveHeaders(options.headers, c.authStale);\n c.authStale = false;\n const lanes = flushTapSync(() => c.ctx.takeAttachLanes());\n const body = lanes === null ? undefined : JSON.stringify(lanes);\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: body === undefined ? \"GET\" : \"POST\",\n headers: withExtra(headers, {\n \"content-type\": \"application/json\",\n \"Statewire-Client-Id\": c.ctx.clientId,\n accept: \"text/event-stream\",\n }),\n ...(body === undefined ? {} : { body }),\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.ok) return flushTapSync(() => settleFin(c, httpFin(res)));\n if (!res.body)\n throw new Error(\"statewire: protocol error: stream response has no body\");\n const version = res.headers.get(VERSION_HEADER)?.trim();\n if (version !== undefined && version !== PROTOCOL_VERSION) {\n res.body.cancel().catch(() => {});\n return unsupportedVersion(c, version);\n }\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): Promise<Response> =>\n timedFetch(\n options,\n `${baseUrl(options)}/frames`,\n {\n method: \"POST\",\n headers: withExtra(new Headers(), {\n \"content-type\": \"application/json\",\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 if (header === undefined || !/^\\d+$/.test(header)) return null;\n const retry = Number(header) * 1000;\n return Number.isSafeInteger(retry) ? retry : null;\n};\n\nconst httpFin = (res: Response): StatewireHost.Fin => ({\n reason:\n res.status === 401\n ? \"unauthorized\"\n : res.status === 403\n ? \"forbidden\"\n : res.status === 429 || res.status === 503\n ? \"reconnect\"\n : res.status >= 400 && res.status < 500\n ? \"protocol-error\"\n : \"error\",\n status: res.status,\n message: `HTTP ${res.status}`,\n ...(res.status === 429 || res.status === 503 ? { code: \"overloaded\" } : {}),\n ...(retryAfterMs(res) !== null ? { retry: retryAfterMs(res)! } : {}),\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 validateRefusalSyn(syn);\n};\n\nconst sendFrame = async (\n c: Conn,\n options: StatewireClient.SSEOptions,\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 const res = await postFrame(c, options, lease, frame, timeout);\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 flushTapSync(() => settleFin(c, httpFin(res)));\n c.abort?.abort();\n throw new Error(`HTTP ${res.status}`);\n};\n\nconst useSseLink = (\n options: StatewireClient.SSEOptions,\n ctx: StatewireRemoteProps,\n): RemoteLink =>\n useLink({\n ...options,\n ctx,\n attemptOnce: (c, abort) => attemptStream(c, options, abort),\n send: async (c, frame) => {\n try {\n return await sendFrame(c, options, frame);\n } catch (error) {\n if (\n error instanceof SyntaxError ||\n (error instanceof Error &&\n error.message.startsWith(\"statewire: protocol error:\"))\n ) {\n flushTapSync(() =>\n settleFin(c, { reason: \"protocol-error\", message: error.message }),\n );\n c.abort?.abort();\n }\n throw error;\n }\n },\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
|
+
{"version":3,"file":"sse.js","names":["flushTapSync","resource","ResourceElement","createSseParser","timing","settleFin","classify","handleEnvelope","resolveHeaders","useLink","validateRefusalSyn","AttemptOutcome","Conn","StatewireCmd","StatewireEnvelope","StatewireSyn","Statewire","StatewireClient","StatewireHost","useStatewireRemote","negotiationHeaders","parseNegotiation","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","parked","send","frame","Promise","LEASE_HEADER","baseUrl","options","HttpOptions","url","replace","withExtra","headers","Headers","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","lease","authStale","has","negotiation","lanes","body","undefined","stringify","headerTimer","res","accept","cache","any","ok","clone","json","catch","fin","httpFin","Error","get","getReader","aborted","cancel","postFrame","lifetime","retryAfterMs","header","trim","test","retry","Number","isSafeInteger","reason","status","message","code","refusalSyn","sendFrame","headerRetry","error","useSseLink","attemptOnce","SyntaxError","startsWith","useStatewireHttp","StatewireHttp","Commands","TState","TCommands"],"sources":["../../src/transports/sse.ts"],"sourcesContent":["import { 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 settleFin,\n classify,\n handleEnvelope,\n resolveHeaders,\n useLink,\n validateRefusalSyn,\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\";\nimport { negotiationHeaders, parseNegotiation } from \"../wire/protocol\";\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 /** A deferred first attach still waiting; `reconnect()` releases it. */\n parked: boolean;\n send: (frame: StatewireFrame) => Promise<StatewireSubmitResult>;\n};\n\nconst LEASE_HEADER = \"Statewire-Lease\";\n\nconst baseUrl = (options: StatewireClient.HttpOptions) =>\n options.url.replace(/\\/$/, \"\");\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.HttpOptions,\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.HttpOptions,\n abort: AbortController,\n): Promise<AttemptOutcome> => {\n c.lease = null;\n const headers = await resolveHeaders(options.headers, c.authStale);\n for (const [name, value] of Object.entries(negotiationHeaders(options))) {\n if (!headers.has(name)) headers.set(name, value);\n }\n c.negotiation = parseNegotiation(headers);\n c.authStale = false;\n const lanes = flushTapSync(() => c.ctx.takeAttachLanes());\n const body = lanes === null ? undefined : JSON.stringify(lanes);\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: body === undefined ? \"GET\" : \"POST\",\n headers: withExtra(headers, {\n \"content-type\": \"application/json\",\n \"Statewire-Client-Id\": c.ctx.clientId,\n accept: \"text/event-stream\",\n }),\n ...(body === undefined ? {} : { body }),\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.ok) {\n const body = await res\n .clone()\n .json()\n .catch(() => null);\n return (\n flushTapSync(() =>\n body?.fin === undefined\n ? settleFin(c, httpFin(res))\n : handleEnvelope(c, body),\n ) ?? \"stop\"\n );\n }\n if (!res.body)\n throw new Error(\"statewire: protocol error: stream response has no body\");\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.HttpOptions,\n lease: string,\n frame: StatewireFrame,\n timeout: AbortSignal,\n): Promise<Response> =>\n timedFetch(\n options,\n `${baseUrl(options)}/frames`,\n {\n method: \"POST\",\n headers: withExtra(new Headers(), {\n \"content-type\": \"application/json\",\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 if (header === undefined || !/^\\d+$/.test(header)) return null;\n const retry = Number(header) * 1000;\n return Number.isSafeInteger(retry) ? retry : null;\n};\n\nconst httpFin = (res: Response): StatewireHost.Fin => ({\n reason:\n res.status === 401\n ? \"unauthorized\"\n : res.status === 403\n ? \"forbidden\"\n : res.status === 429 || res.status === 503\n ? \"reconnect\"\n : res.status >= 400 && res.status < 500\n ? \"protocol-error\"\n : \"error\",\n status: res.status,\n message: `HTTP ${res.status}`,\n ...(res.status === 429 || res.status === 503 ? { code: \"overloaded\" } : {}),\n ...(retryAfterMs(res) !== null ? { retry: retryAfterMs(res)! } : {}),\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 validateRefusalSyn(syn);\n};\n\nconst sendFrame = async (\n c: Conn,\n options: StatewireClient.HttpOptions,\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 const res = await postFrame(c, options, lease, frame, timeout);\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 flushTapSync(() => settleFin(c, httpFin(res)));\n c.abort?.abort();\n throw new Error(`HTTP ${res.status}`);\n};\n\nconst useSseLink = (\n options: StatewireClient.HttpOptions,\n ctx: StatewireRemoteProps,\n): RemoteLink =>\n useLink({\n ...options,\n ctx,\n attemptOnce: (c, abort) => attemptStream(c, options, abort),\n send: async (c, frame) => {\n try {\n return await sendFrame(c, options, frame);\n } catch (error) {\n if (\n error instanceof SyntaxError ||\n (error instanceof Error &&\n error.message.startsWith(\"statewire: protocol error:\"))\n ) {\n flushTapSync(() =>\n settleFin(c, { reason: \"protocol-error\", message: error.message }),\n );\n c.abort?.abort();\n }\n throw error;\n }\n },\n });\n\nconst useStatewireHttp = (\n options: StatewireClient.HttpOptions,\n): Statewire<any, any> => useStatewireRemote(options, useSseLink);\n\nexport const StatewireHttp = resource(useStatewireHttp) as <\n TState = any,\n TCommands extends Statewire.Commands = Statewire.Commands,\n>(\n options: StatewireClient.HttpOptions,\n) => ResourceElement<Statewire<TState | undefined, TCommands>>;\n"],"mappings":"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"}
|
|
@@ -28,7 +28,7 @@ type MockTransport<TState> = {
|
|
|
28
28
|
readonly clientId: string;
|
|
29
29
|
readonly attached: boolean;
|
|
30
30
|
};
|
|
31
|
-
declare const createMockTransport: <TState>(initialState: TState) => MockTransport<TState>;
|
|
31
|
+
declare const createMockTransport: <TState>(initialState: TState, protocol?: string) => MockTransport<TState>;
|
|
32
32
|
//#endregion
|
|
33
33
|
export { MockTransport, SocketRecorder, applyOptimistic, commandFrame, createCounterElement, createMockTransport, createSocketRecorder, settle, tick };
|
|
34
34
|
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
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,
|
|
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,QACd,sBACC,cAAc"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { applyOptimistic } from "../util/recordMutations.js";
|
|
2
|
-
import {
|
|
2
|
+
import { StatewireHttp } from "./sse.js";
|
|
3
3
|
import { openSSEStream, sseFrame } from "./fake-sse.js";
|
|
4
4
|
import { commandFrame, createCounterElement, createSocketRecorder, settle } from "./fixtures.js";
|
|
5
5
|
//#region src/transports/testing.ts
|
|
@@ -11,7 +11,7 @@ const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
|
11
11
|
* from test code), and exposes the wire as a transport ResourceElement.
|
|
12
12
|
*/
|
|
13
13
|
let mockCount = 0;
|
|
14
|
-
const createMockTransport = (initialState) => {
|
|
14
|
+
const createMockTransport = (initialState, protocol = "default") => {
|
|
15
15
|
let state = initialState;
|
|
16
16
|
let clientId = "";
|
|
17
17
|
let finished = false;
|
|
@@ -25,11 +25,13 @@ const createMockTransport = (initialState) => {
|
|
|
25
25
|
const admit = (id, frame) => {
|
|
26
26
|
const rec = clients.get(id);
|
|
27
27
|
for (const statement of frame.ctx ?? []) {
|
|
28
|
+
if (statement.seq <= rec.ctxSeq) continue;
|
|
28
29
|
rec.ctxSeq = Math.max(rec.ctxSeq, statement.seq);
|
|
29
30
|
const root = statement.ops[0];
|
|
30
31
|
commands.push({ context: root?.value });
|
|
31
32
|
}
|
|
32
33
|
for (const statement of frame.cmd ?? []) {
|
|
34
|
+
if (statement.seq <= rec.seq) continue;
|
|
33
35
|
rec.seq = Math.max(rec.seq, statement.seq);
|
|
34
36
|
commands.push({
|
|
35
37
|
method: statement.method,
|
|
@@ -53,13 +55,19 @@ const createMockTransport = (initialState) => {
|
|
|
53
55
|
return openSSEStream(streams, id, (handle) => {
|
|
54
56
|
handle.send(sseFrame({
|
|
55
57
|
ops: [{
|
|
56
|
-
op: "
|
|
57
|
-
|
|
58
|
+
op: "mount",
|
|
59
|
+
protocol,
|
|
60
|
+
main: true,
|
|
58
61
|
value: state
|
|
59
62
|
}],
|
|
60
63
|
syn: {
|
|
61
64
|
seq: rec.seq,
|
|
62
|
-
ctxSeq: rec.ctxSeq
|
|
65
|
+
ctxSeq: rec.ctxSeq,
|
|
66
|
+
version: "2026-09-07",
|
|
67
|
+
protocols: [{
|
|
68
|
+
name: protocol,
|
|
69
|
+
version: "2026-09-07"
|
|
70
|
+
}]
|
|
63
71
|
}
|
|
64
72
|
}));
|
|
65
73
|
}, init?.signal ?? void 0, { "Statewire-Lease": "mock-lease" });
|
|
@@ -71,7 +79,7 @@ const createMockTransport = (initialState) => {
|
|
|
71
79
|
return new Response(null, { status: 404 });
|
|
72
80
|
});
|
|
73
81
|
return {
|
|
74
|
-
transport:
|
|
82
|
+
transport: StatewireHttp({
|
|
75
83
|
url: `http://mock-${++mockCount}/api`,
|
|
76
84
|
fetch: fakeFetch
|
|
77
85
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","names":["ResourceElement","StatewireEnvelope","
|
|
1
|
+
{"version":3,"file":"testing.js","names":["ResourceElement","StatewireEnvelope","StatewireHttp","StatewireFrame","Statewire","openSSEStream","sseFrame","FakeStreamHandle","tick","Promise","r","setTimeout","applyOptimistic","MockTransport","transport","TState","commands","method","params","context","setState","state","applied","seq","settle","payload","sendEnvelope","envelope","finish","reason","message","clientId","attached","mockCount","createMockTransport","initialState","protocol","finished","clients","Map","ctxSeq","streams","Set","stream","send","admit","id","frame","rec","get","statement","ctx","Math","max","root","ops","value","push","cmd","fakeFetch","input","RequestInfo","URL","init","RequestInit","Response","TypeError","url","String","headers","Headers","endsWith","has","set","JSON","parse","body","handle","op","main","syn","version","protocols","name","signal","undefined","json","status","fetch","next","path","type","fin","close","size"],"sources":["../../src/transports/testing.ts"],"sourcesContent":["import type { ResourceElement } from \"@assistant-ui/tap\";\nimport type { StatewireEnvelope } from \"../client/connection\";\nimport { StatewireHttp } from \"./sse\";\nimport type { StatewireFrame } 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\nexport { applyOptimistic } from \"../util/recordMutations\";\n\nexport type MockTransport<TState> = {\n /** Pass as the `transport:` option of useStatewire / Harness. */\n transport: ResourceElement<Statewire<TState | undefined, any>>;\n /** Captured statements in arrival order: cmd as `{method, params}`, ctx as `{context}`. */\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 a bare ack cmd packet on the stream. */\n applied: (seq: number) => Promise<void>;\n /** Deliver a terminal result cmd packet on the stream. */\n settle: (seq: number, payload?: unknown) => 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 submitted frames (receipt only — acks and results are delivered\n * from test code), and exposes the wire as a transport ResourceElement.\n */\nlet mockCount = 0;\n\nexport const createMockTransport = <TState>(\n initialState: TState,\n protocol = \"default\",\n): MockTransport<TState> => {\n let state: unknown = initialState;\n let clientId = \"\";\n let finished = false;\n const clients = new Map<string, { seq: number; ctxSeq: 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 admit = (id: string, frame: StatewireFrame) => {\n const rec = clients.get(id)!;\n for (const statement of frame.ctx ?? []) {\n if (statement.seq <= rec.ctxSeq) continue;\n rec.ctxSeq = Math.max(rec.ctxSeq, statement.seq);\n const root = statement.ops[0] as { value?: unknown } | undefined;\n commands.push({ context: root?.value });\n }\n for (const statement of frame.cmd ?? []) {\n if (statement.seq <= rec.seq) continue;\n rec.seq = Math.max(rec.seq, statement.seq);\n commands.push({ method: statement.method, params: statement.params });\n }\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 if (!clients.has(id)) clients.set(id, { seq: 0, ctxSeq: 0 });\n const rec = clients.get(id)!;\n const frame = JSON.parse(String(init?.body ?? \"{}\")) as StatewireFrame;\n admit(id, frame);\n return openSSEStream(\n streams,\n id,\n (handle) => {\n handle.send(\n sseFrame({\n ops: [{ op: \"mount\", protocol, main: true, value: state }],\n syn: {\n seq: rec.seq,\n ctxSeq: rec.ctxSeq,\n version: \"2026-09-07\",\n protocols: [{ name: protocol, version: \"2026-09-07\" }],\n },\n }),\n );\n },\n init?.signal ?? undefined,\n { \"Statewire-Lease\": \"mock-lease\" },\n );\n }\n if (url.endsWith(\"/frames\")) {\n admit(clientId, JSON.parse(String(init?.body ?? \"{}\")) as StatewireFrame);\n return Response.json({});\n }\n return new Response(null, { status: 404 });\n }) as typeof fetch;\n\n return {\n transport: StatewireHttp({\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 applied: (seq) => sendEnvelope({ cmd: [{ seq, type: \"applied\" }] }),\n settle: (seq, payload) =>\n sendEnvelope({\n cmd: [\n {\n seq,\n type: \"result\",\n ...(payload !== undefined && { payload }),\n },\n ],\n }),\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;;;;;;AA4BnE,IAAIuB,YAAY;AAEhB,MAAaC,uBACXC,cACAC,WAAW,cACe;CAC1B,IAAIf,QAAiBc;CACrB,IAAIJ,WAAW;CACf,IAAIM,WAAW;CACf,MAAMC,0BAAU,IAAIC,IAA6C;CACjE,MAAME,0BAAU,IAAIC,IAAsB;CAC1C,MAAM1B,WAA+C,CAAA;CAErD,MAAMU,gBAAgBC,aAAgC;EACpD,KAAK,MAAMgB,UAAUF,SAASE,OAAOC,KAAKtC,SAASqB,QAAQ,CAAC;EAC5D,OAAOnB,KAAK;CACd;CAEA,MAAMqC,SAASC,IAAYC,UAA0B;EACnD,MAAMC,MAAMV,QAAQW,IAAIH,EAAE;EAC1B,KAAK,MAAMI,aAAaH,MAAMI,OAAO,CAAA,GAAI;GACvC,IAAID,UAAU3B,OAAOyB,IAAIR,QAAQ;GACjCQ,IAAIR,SAASY,KAAKC,IAAIL,IAAIR,QAAQU,UAAU3B,GAAG;GAC/C,MAAM+B,OAAOJ,UAAUK,IAAI;GAC3BvC,SAASyC,KAAK,EAAEtC,SAASmC,MAAME,MAAM,CAAC;EACxC;EACA,KAAK,MAAMN,aAAaH,MAAMW,OAAO,CAAA,GAAI;GACvC,IAAIR,UAAU3B,OAAOyB,IAAIzB,KAAK;GAC9ByB,IAAIzB,MAAM6B,KAAKC,IAAIL,IAAIzB,KAAK2B,UAAU3B,GAAG;GACzCP,SAASyC,KAAK;IAAExC,QAAQiC,UAAUjC;IAAQC,QAAQgC,UAAUhC;GAAO,CAAC;EACtE;CACF;CAEA,MAAMyC,aAAa,OACjBC,OACAG,SACsB;EACtB,IAAI1B,UAAU,MAAM,IAAI6B,UAAU,yBAAyB;EAC3D,MAAMC,MAAMC,OAAOR,KAAK;EAExB,MAAMd,KAAKuB,IADSC,QAAQP,MAAMM,OACvBA,CAAO,CAACpB,IAAI,qBAAqB,KAAK;EACjD,IAAIkB,IAAII,SAAS,SAAS,GAAG;GAC3BxC,WAAWe;GACX,IAAI,CAACR,QAAQkC,IAAI1B,EAAE,GAAGR,QAAQmC,IAAI3B,IAAI;IAAEvB,KAAK;IAAGiB,QAAQ;GAAE,CAAC;GAC3D,MAAMQ,MAAMV,QAAQW,IAAIH,EAAE;GAC1B,MAAMC,QAAQ2B,KAAKC,MAAMP,OAAOL,MAAMa,QAAQ,IAAI,CAAC;GACnD/B,MAAMC,IAAIC,KAAK;GACf,OAAO1C,cACLoC,SACAK,KACC+B,WAAW;IACVA,OAAOjC,KACLtC,SAAS;KACPiD,KAAK,CAAC;MAAEuB,IAAI;MAAS1C;MAAU2C,MAAM;MAAMvB,OAAOnC;KAAM,CAAC;KACzD2D,KAAK;MACHzD,KAAKyB,IAAIzB;MACTiB,QAAQQ,IAAIR;MACZyC,SAAS;MACTC,WAAW,CAAC;OAAEC,MAAM/C;OAAU6C,SAAS;MAAa,CAAC;KACvD;IACF,CAAC,CACH;GACF,GACAlB,MAAMqB,UAAUC,KAAAA,GAChB,EAAE,mBAAmB,aAAa,CACpC;EACF;EACA,IAAIlB,IAAII,SAAS,SAAS,GAAG;GAC3B1B,MAAMd,UAAU2C,KAAKC,MAAMP,OAAOL,MAAMa,QAAQ,IAAI,CAAC,CAAmB;GACxE,OAAOX,SAASqB,KAAK,CAAC,CAAC;EACzB;EACA,OAAO,IAAIrB,SAAS,MAAM,EAAEsB,QAAQ,IAAI,CAAC;CAC3C;CAEA,OAAO;EACLzE,WAAWZ,cAAc;GACvBiE,KAAK,eAAe,EAAElC,UAAS;GAC/BuD,OAAO7B;EACT,CAAC;EACD3C;EACAI,WAAWqE,SAAS;GAClBpE,QAAQoE;GACR,OAAO/D,aAAa,EAAE6B,KAAK,CAAC;IAAEuB,IAAI;IAAWY,MAAM,CAAA;IAAIlC,OAAOiC;GAAK,CAAC,EAAE,CAAC;EACzE;EACAnE,UAAUC,QAAQG,aAAa,EAAEgC,KAAK,CAAC;GAAEnC;GAAKoE,MAAM;EAAU,CAAC,EAAE,CAAC;EAClEnE,SAASD,KAAKE,YACZC,aAAa,EACXgC,KAAK,CACH;GACEnC;GACAoE,MAAM;GACN,GAAIlE,YAAY4D,KAAAA,KAAa,EAAE5D,QAAQ;EACzC,CAAC,EAEL,CAAC;EACHC;EACAE,QAAQ,OAAOC,QAAQC,YAAY;GACjC,MAAMJ,aAAa,EACjBkE,KAAK;IAAE/D;IAAQ,GAAIC,YAAYuD,KAAAA,KAAa,EAAEvD,QAAQ;GAAG,EAC3D,CAAC;GACDO,WAAW;GACX,KAAK,MAAMM,UAAU,CAAC,GAAGF,OAAO,GAAGE,OAAOkD,MAAM;GAChD,MAAMrF,KAAK;EACb;EACA,IAAIuB,WAAW;GACb,OAAOA;EACT;EACA,IAAIC,WAAW;GACb,OAAOS,QAAQqD,OAAO;EACxB;CACF;AACF"}
|
package/dist/transports/ws.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { Statewire } from "../types.js";
|
|
|
3
3
|
import { WS_SUBPROTOCOL } from "../wire/version.js";
|
|
4
4
|
import { ResourceElement } from "@assistant-ui/tap";
|
|
5
5
|
//#region src/transports/ws.d.ts
|
|
6
|
-
declare const
|
|
6
|
+
declare const StatewireWebsocket: <TState = any, TCommands extends Statewire.Commands = Statewire.Commands>(options: StatewireClient.WebsocketOptions) => ResourceElement<Statewire<TState | undefined, TCommands>>;
|
|
7
7
|
//#endregion
|
|
8
|
-
export {
|
|
8
|
+
export { StatewireWebsocket, WS_SUBPROTOCOL };
|
|
9
9
|
//# sourceMappingURL=ws.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws.d.ts","names":[],"sources":["../../src/transports/ws.ts"],"mappings":";;;;;cAwLa,
|
|
1
|
+
{"version":3,"file":"ws.d.ts","names":[],"sources":["../../src/transports/ws.ts"],"mappings":";;;;;cAwLa,qBACX,cACA,kBAAkB,UAAU,WAAW,UAAU,UAEjD,SAAS,gBAAgB,qBACtB,gBAAgB,UAAU,oBAAoB"}
|
package/dist/transports/ws.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { WS_SUBPROTOCOL } from "../wire/version.js";
|
|
2
1
|
import { CLIENT_QUERY_PARAM } from "../host/adapter.js";
|
|
2
|
+
import { WS_SUBPROTOCOL } from "../wire/version.js";
|
|
3
3
|
import { timing } from "../client/timing.js";
|
|
4
4
|
import { attachFrame, handleEnvelope, unsupportedVersion, useLink } from "../client/connection.js";
|
|
5
5
|
import { useStatewireRemote } from "./remote.js";
|
|
@@ -35,7 +35,7 @@ const runSocket = (c, ws, frame, abort) => new Promise((resolve, reject) => {
|
|
|
35
35
|
ws.onerror = () => {};
|
|
36
36
|
ws.onopen = () => {
|
|
37
37
|
const negotiated = ws.protocol.trim();
|
|
38
|
-
if (negotiated !== "
|
|
38
|
+
if (negotiated !== "statewire.v1") {
|
|
39
39
|
outcome = unsupportedVersion(c, negotiated);
|
|
40
40
|
ws.close();
|
|
41
41
|
return;
|
|
@@ -134,12 +134,12 @@ const useWsLink = (options, ctx) => {
|
|
|
134
134
|
} else t2 = $[9];
|
|
135
135
|
return useLink(t2);
|
|
136
136
|
};
|
|
137
|
-
const
|
|
138
|
-
const
|
|
137
|
+
const useStatewireWebsocket = (options) => useStatewireRemote(options, useWsLink);
|
|
138
|
+
const StatewireWebsocket = resource(useStatewireWebsocket);
|
|
139
139
|
function _temp() {
|
|
140
140
|
return { socket: null };
|
|
141
141
|
}
|
|
142
142
|
//#endregion
|
|
143
|
-
export {
|
|
143
|
+
export { StatewireWebsocket, WS_SUBPROTOCOL };
|
|
144
144
|
|
|
145
145
|
//# sourceMappingURL=ws.js.map
|