statewire 0.17.1 → 0.18.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/dist/client/connection.d.ts +2 -3
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +61 -25
- package/dist/client/connection.js.map +1 -1
- package/dist/client/session.d.ts +6 -2
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +189 -40
- package/dist/client/session.js.map +1 -1
- package/dist/client/storageError.d.ts +6 -0
- package/dist/client/storageError.d.ts.map +1 -0
- package/dist/client/storageError.js +12 -0
- package/dist/client/storageError.js.map +1 -0
- package/dist/statewire.d.ts +25 -11
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js.map +1 -1
- package/dist/storage.d.ts +33 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +371 -0
- package/dist/storage.js.map +1 -0
- package/dist/transports/sse.d.ts +2 -0
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +3 -0
- package/dist/transports/sse.js.map +1 -1
- package/package.json +5 -1
- package/src/client/connection.ts +57 -28
- package/src/client/session.ts +262 -72
- package/src/client/storageError.ts +9 -0
- package/src/statewire.ts +31 -21
- package/src/storage.ts +601 -0
- package/src/transports/sse.ts +5 -0
package/dist/transports/sse.js
CHANGED
|
@@ -42,6 +42,9 @@ const consume = async (c, reader, heartbeat, dead) => {
|
|
|
42
42
|
}
|
|
43
43
|
};
|
|
44
44
|
const attemptStream = async (c, options, abort) => {
|
|
45
|
+
const persistence = c.ctx.beforeAttach?.();
|
|
46
|
+
if (persistence) await persistence;
|
|
47
|
+
if (abort.signal.aborted) return "ended";
|
|
45
48
|
c.lease = null;
|
|
46
49
|
const headers = await resolveHeaders(options.headers, c.authStale);
|
|
47
50
|
for (const [name, value] of Object.entries(negotiationHeaders(options))) if (!headers.has(name)) headers.set(name, value);
|
|
@@ -1 +1 @@
|
|
|
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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"}
|
|
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","beforeAttach","Promise","clientId","takeAttachLanes","onHello","args","syn","onOps","cmds","onCmds","onRefusal","onIdle","level","onDetach","onFinish","Fin","StatewireSubmitResult","type","ConnectionState","RemoteLink","storageFailed","error","Error","connection","Connection","parked","send","frame","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","persistence","aborted","lease","authStale","has","negotiation","lanes","body","undefined","stringify","headerTimer","res","accept","cache","any","ok","clone","json","catch","fin","httpFin","get","getReader","cancel","postFrame","lifetime","retryAfterMs","header","trim","test","retry","Number","isSafeInteger","reason","status","message","code","refusalSyn","sendFrame","headerRetry","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 beforeAttach?: () => void | Promise<void>;\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 storageFailed: (error: Error) => void;\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 const persistence = c.ctx.beforeAttach?.();\n if (persistence) await persistence;\n if (abort.signal.aborted) return \"ended\";\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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "statewire",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "State over the wire: snapshot-first delta streaming and command submission for any state and any command",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
"./testing": {
|
|
29
29
|
"types": "./dist/transports/testing.d.ts",
|
|
30
30
|
"default": "./dist/transports/testing.js"
|
|
31
|
+
},
|
|
32
|
+
"./storage": {
|
|
33
|
+
"types": "./dist/storage.d.ts",
|
|
34
|
+
"default": "./dist/storage.js"
|
|
31
35
|
}
|
|
32
36
|
},
|
|
33
37
|
"main": "./dist/index.js",
|
package/src/client/connection.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { useEffect, useRef, useState } from "react";
|
|
|
8
8
|
import { flushTapSync } from "@assistant-ui/tap";
|
|
9
9
|
import { reviveOps, reviveValue } from "../wire/meta";
|
|
10
10
|
import { backoff, timing } from "./timing";
|
|
11
|
+
import { isStorageError } from "./storageError";
|
|
11
12
|
import type {
|
|
12
13
|
ConnectionState,
|
|
13
14
|
RemoteLink,
|
|
@@ -127,26 +128,28 @@ export type Conn = {
|
|
|
127
128
|
reconnect: () => void;
|
|
128
129
|
};
|
|
129
130
|
|
|
130
|
-
export const degradedOf = (state: ConnectionState): boolean =>
|
|
131
|
-
(state.status === "connecting" && state.attempt > 0) ||
|
|
132
|
-
(state.status === "disconnected" &&
|
|
133
|
-
state.reason !== "idle" &&
|
|
134
|
-
state.reason !== "deferred");
|
|
135
|
-
|
|
136
131
|
const sameState = (a: ConnectionState, b: ConnectionState): boolean => {
|
|
137
132
|
if (a.status !== b.status) return false;
|
|
138
|
-
if (a.status === "
|
|
133
|
+
if (a.status === "retrying" && b.status === "retrying") {
|
|
139
134
|
return (
|
|
140
135
|
a.attempt === b.attempt &&
|
|
141
136
|
a.cause === b.cause &&
|
|
142
137
|
a.code === b.code &&
|
|
143
138
|
a.nextRetryAt === b.nextRetryAt &&
|
|
144
|
-
a.lastError === b.lastError
|
|
139
|
+
a.lastError === b.lastError &&
|
|
140
|
+
a.message === b.message
|
|
145
141
|
);
|
|
146
142
|
}
|
|
147
|
-
if (a.status === "
|
|
143
|
+
if (a.status === "standby" && b.status === "standby") {
|
|
148
144
|
return a.reason === b.reason;
|
|
149
145
|
}
|
|
146
|
+
if (a.status === "stopped" && b.status === "stopped") {
|
|
147
|
+
return (
|
|
148
|
+
a.reason === b.reason &&
|
|
149
|
+
a.message === b.message &&
|
|
150
|
+
a.payload === b.payload
|
|
151
|
+
);
|
|
152
|
+
}
|
|
150
153
|
return true;
|
|
151
154
|
};
|
|
152
155
|
|
|
@@ -156,12 +159,25 @@ const setStatus = (c: Conn, next: ConnectionState) => {
|
|
|
156
159
|
...next,
|
|
157
160
|
...(c.error === undefined ? {} : { error: c.error }),
|
|
158
161
|
reconnect: c.reconnect,
|
|
159
|
-
degraded: degradedOf(next),
|
|
160
162
|
};
|
|
161
163
|
c.notify(c.connection);
|
|
162
164
|
c.onChange(c.connection);
|
|
163
165
|
};
|
|
164
166
|
|
|
167
|
+
const storageFailed = (c: Conn, error: Error) => {
|
|
168
|
+
c.error = error;
|
|
169
|
+
c.onError(error);
|
|
170
|
+
flushTapSync(() =>
|
|
171
|
+
setStatus(c, {
|
|
172
|
+
status: "stopped",
|
|
173
|
+
reason: "storage-error",
|
|
174
|
+
degraded: true,
|
|
175
|
+
message: error.message,
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
c.abort?.abort();
|
|
179
|
+
};
|
|
180
|
+
|
|
165
181
|
const protocolError = (message: string): never => {
|
|
166
182
|
throw new Error(`statewire: protocol error: ${message}`);
|
|
167
183
|
};
|
|
@@ -318,7 +334,7 @@ const handleHello = (c: Conn, env: StatewireEnvelope) => {
|
|
|
318
334
|
c.liveAt = Date.now();
|
|
319
335
|
c.authDenials = 0;
|
|
320
336
|
c.episode = null;
|
|
321
|
-
setStatus(c, { status: "connected" });
|
|
337
|
+
setStatus(c, { status: "connected", degraded: false });
|
|
322
338
|
};
|
|
323
339
|
|
|
324
340
|
let warnedUnknownSynFact = false;
|
|
@@ -384,7 +400,7 @@ export const settleFin = (c: Conn, fin: StatewireHost.Fin): AttemptOutcome => {
|
|
|
384
400
|
c.attempt = 0;
|
|
385
401
|
c.episode = null;
|
|
386
402
|
c.error = undefined;
|
|
387
|
-
setStatus(c, { status: "
|
|
403
|
+
setStatus(c, { status: "standby", reason: "idle", degraded: false });
|
|
388
404
|
return "idle";
|
|
389
405
|
}
|
|
390
406
|
const denial = finish.reason === "unauthorized";
|
|
@@ -421,7 +437,8 @@ export const settleFin = (c: Conn, fin: StatewireHost.Fin): AttemptOutcome => {
|
|
|
421
437
|
}
|
|
422
438
|
c.ctx.onFinish(finish);
|
|
423
439
|
setStatus(c, {
|
|
424
|
-
status: "
|
|
440
|
+
status: "stopped",
|
|
441
|
+
degraded: true,
|
|
425
442
|
reason: denial
|
|
426
443
|
? "unauthorized"
|
|
427
444
|
: finish.reason === "forbidden"
|
|
@@ -450,13 +467,18 @@ export const handleEnvelope = (
|
|
|
450
467
|
if (c.connection.status !== "connected") {
|
|
451
468
|
c.attempt = 0;
|
|
452
469
|
c.episode = null;
|
|
453
|
-
setStatus(c, { status: "connected" });
|
|
470
|
+
setStatus(c, { status: "connected", degraded: false });
|
|
454
471
|
}
|
|
455
472
|
return null;
|
|
456
473
|
};
|
|
457
474
|
|
|
458
475
|
const dialing = (c: Conn) =>
|
|
459
|
-
setStatus(
|
|
476
|
+
setStatus(
|
|
477
|
+
c,
|
|
478
|
+
c.attempt === 0
|
|
479
|
+
? { status: "connecting", degraded: false }
|
|
480
|
+
: { status: "retrying", attempt: c.attempt, degraded: true },
|
|
481
|
+
);
|
|
460
482
|
|
|
461
483
|
const scheduleRetry = (
|
|
462
484
|
c: Conn,
|
|
@@ -474,7 +496,8 @@ const scheduleRetry = (
|
|
|
474
496
|
c.retryAt - Date.now(),
|
|
475
497
|
);
|
|
476
498
|
setStatus(c, {
|
|
477
|
-
status: "
|
|
499
|
+
status: "retrying",
|
|
500
|
+
degraded: true,
|
|
478
501
|
attempt: c.attempt,
|
|
479
502
|
...c.episode,
|
|
480
503
|
nextRetryAt: Date.now() + delay,
|
|
@@ -485,7 +508,7 @@ const scheduleRetry = (
|
|
|
485
508
|
|
|
486
509
|
const reconnectConn = (c: Conn) => {
|
|
487
510
|
if (!c.lifetime) return;
|
|
488
|
-
if (c.connection.status === "
|
|
511
|
+
if (c.connection.status === "standby" || c.connection.status === "stopped") {
|
|
489
512
|
// A reconnect out of a park starts a fresh episode: first dial, attempt 0.
|
|
490
513
|
c.attempt = 0;
|
|
491
514
|
c.authDenials = 0;
|
|
@@ -517,10 +540,16 @@ const run = async (c: Conn, lifetime: AbortController, gate: Gate) => {
|
|
|
517
540
|
let outcome: AttemptOutcome | undefined;
|
|
518
541
|
let failure: string | undefined;
|
|
519
542
|
try {
|
|
543
|
+
const pending = c.ctx.beforeAttach?.();
|
|
544
|
+
if (pending) await pending;
|
|
545
|
+
if (!alive()) return;
|
|
520
546
|
outcome = await c.attemptOnce(abort);
|
|
521
547
|
} catch (error) {
|
|
522
548
|
if (!abort.signal.aborted) {
|
|
523
|
-
if (
|
|
549
|
+
if (isStorageError(error)) {
|
|
550
|
+
storageFailed(c, error);
|
|
551
|
+
outcome = "stop";
|
|
552
|
+
} else if (
|
|
524
553
|
error instanceof SyntaxError ||
|
|
525
554
|
(error instanceof Error &&
|
|
526
555
|
/^(statewire: protocol error:|Statewire cannot apply)/.test(
|
|
@@ -552,11 +581,7 @@ const run = async (c: Conn, lifetime: AbortController, gate: Gate) => {
|
|
|
552
581
|
c.ctx.onDetach();
|
|
553
582
|
});
|
|
554
583
|
if (!alive()) return;
|
|
555
|
-
if (
|
|
556
|
-
c.connection.status === "disconnected" &&
|
|
557
|
-
!["deferred", "idle"].includes(c.connection.reason)
|
|
558
|
-
)
|
|
559
|
-
outcome = "stop";
|
|
584
|
+
if (c.connection.status === "stopped") outcome = "stop";
|
|
560
585
|
if (outcome === "stop" || outcome === "idle") {
|
|
561
586
|
await waitGate(gate);
|
|
562
587
|
continue;
|
|
@@ -591,6 +616,8 @@ export const attachFrame = async (
|
|
|
591
616
|
c: Conn,
|
|
592
617
|
headers: StatewireClient.HeadersOption | undefined,
|
|
593
618
|
) => {
|
|
619
|
+
const pending = c.ctx.beforeAttach?.();
|
|
620
|
+
if (pending) await pending;
|
|
594
621
|
const resolved = await resolveHeaders(headers, c.authStale);
|
|
595
622
|
for (const [name, value] of Object.entries(
|
|
596
623
|
negotiationHeaders(c.negotiation ?? {}),
|
|
@@ -599,6 +626,8 @@ export const attachFrame = async (
|
|
|
599
626
|
}
|
|
600
627
|
c.negotiation = parseNegotiation(resolved);
|
|
601
628
|
c.authStale = false;
|
|
629
|
+
const persistence = c.ctx.beforeAttach?.();
|
|
630
|
+
if (persistence) await persistence;
|
|
602
631
|
const lanes = flushTapSync(() => c.ctx.takeAttachLanes());
|
|
603
632
|
return {
|
|
604
633
|
headers: resolved,
|
|
@@ -652,9 +681,8 @@ export const useLink = (
|
|
|
652
681
|
reconnect: () => reconnectConn(conn),
|
|
653
682
|
connection: {
|
|
654
683
|
...((props.isNew ?? false)
|
|
655
|
-
? { status: "
|
|
656
|
-
: { status: "connecting",
|
|
657
|
-
degraded: false,
|
|
684
|
+
? { status: "standby", reason: "deferred", degraded: false }
|
|
685
|
+
: { status: "connecting", degraded: false }),
|
|
658
686
|
reconnect: () => conn.reconnect(),
|
|
659
687
|
},
|
|
660
688
|
};
|
|
@@ -669,6 +697,7 @@ export const useLink = (
|
|
|
669
697
|
return c.parked;
|
|
670
698
|
},
|
|
671
699
|
send: (frame) => latest.current.send(c, frame),
|
|
700
|
+
storageFailed: (error) => storageFailed(c, error),
|
|
672
701
|
}),
|
|
673
702
|
);
|
|
674
703
|
|
|
@@ -688,8 +717,8 @@ export const useLink = (
|
|
|
688
717
|
setStatus(
|
|
689
718
|
c,
|
|
690
719
|
c.parked
|
|
691
|
-
? { status: "
|
|
692
|
-
: { status: "connecting",
|
|
720
|
+
? { status: "standby", reason: "deferred", degraded: false }
|
|
721
|
+
: { status: "connecting", degraded: false },
|
|
693
722
|
);
|
|
694
723
|
const lifetime = new AbortController();
|
|
695
724
|
c.lifetime = lifetime;
|