statewire 0.0.4 → 0.4.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.
@@ -1 +1 @@
1
- {"version":3,"file":"host.js","names":["useEffect","useState","applyOps","sanitizeValue","StatewirePath","StatewireReject","createStateDraft","createMethodProxy","ServerOp","StateDraft","StatewireSendError","Statewire","StatewireHost","isDev","process","env","stateHosts","WeakMap","StateHost","getStateHost","state","get","StateCell","current","draft","changed","onOp","op","onChange","ctl","StateCtl","host","bump","useStatewireState","init","TState","setVersion","cell","commit","c","flush","v","apply","ops","result","events","length","Error","invalidate","push","changedPaths","snapshot","setOnOp","listener","setOnChange","takeChanged","value","record","changedPath","set","proxy","const","ExecutionSettled","type","error","ExecutionBind","onAck","onSettle","settled","ExecutionState","acked","bind","executionStates","CommandExecution","Object","assign","executor","resolvers","ack","T","PromiseLike","Execution","execution","freeze","settle","withResolvers","ExecutionResolvers","resolve","reject","promise","Promise","res","rej","a","CommandsInvocation","method","params","ts","seq","clientId","CommandsOutcome","payload","CommandsListener","onStart","inv","onPosted","onCovered","outcome","CommandsSubmission","caller","Caller","CommandsHost","setListener","submit","call","commandsHosts","getCommandsHost","commands","bindCommandsHost","QueuedCall","origin","ActiveInvocation","CommandsCell","handlers","Record","args","queue","running","unmounted","stallTimer","ReturnType","setTimeout","STALL_WARN_MS","LOCAL_CALLER","context","isConnected","toSendError","fate","cause","message","String","undefined","ackInvocation","settleInvocation","pump","watchStall","shift","handler","console","warn","useStatewireCommands","Commands","THandlers","ThisType","CommandsProxy","HandledCommands","built","Date","now","clearTimeout","queueMicrotask","queued","EmitHost","setSink","sink","event","emitHosts","getEmitHost","emit","useStatewireEmit","_temp"],"sources":["../../src/host/host.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { applyOps, sanitizeValue } from \"../wire/ops\";\nimport type { StatewirePath } from \"../wire/ops\";\nimport { StatewireReject } from \"./engine\";\nimport { createStateDraft } from \"./draft\";\nimport { createMethodProxy } from \"../util/methodProxy\";\nimport type { ServerOp, StateDraft } from \"./draft\";\nimport { StatewireSendError } from \"../types\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireHost } from \"../host\";\n\nconst isDev = () =>\n typeof process === \"undefined\" || process.env[\"NODE_ENV\"] !== \"production\";\n\n// ─── state ──────────────────────────────────────────────\n\nconst stateHosts = new WeakMap<object, StatewireHost.StateHost>();\n\n/** Host-side accessor for a `useStatewireState` state object's source (`snapshot()` reads the committed state). */\nexport const getStateHost = (\n state: object,\n): StatewireHost.StateHost | undefined => stateHosts.get(state);\n\ntype StateCell = {\n current: unknown;\n draft: StateDraft<object> | null;\n changed: StatewirePath[];\n onOp: ((op: ServerOp) => void) | null;\n onChange: (() => void) | null;\n ctl: StatewireHost.StateCtl;\n host: StatewireHost.StateHost;\n bump: () => void;\n};\n\nexport const useStatewireState = <TState>(\n init: () => TState,\n): readonly [TState, StatewireHost.StateCtl<TState>] => {\n const [, setVersion] = useState(0);\n const [cell] = useState((): StateCell => {\n const commit = () => {\n if (c.draft) c.current = c.draft.flush();\n return c.current;\n };\n const c: StateCell = {\n current: sanitizeValue(init()),\n draft: null,\n changed: [[]],\n onOp: null,\n onChange: null,\n bump: () => setVersion((v) => v + 1),\n ctl: {\n apply: (ops) => {\n const result = applyOps(commit(), ops);\n if (result.events.length > 0) {\n throw new Error(\n \"statewire: event ops do not apply to state — route them to emit\",\n );\n }\n c.current = result.state;\n c.draft?.invalidate();\n c.changed.push(...result.changedPaths);\n if (c.onOp) for (const op of ops) c.onOp(op as ServerOp);\n c.bump();\n c.onChange?.();\n },\n snapshot: commit,\n },\n host: {\n snapshot: commit,\n setOnOp: (listener) => {\n c.onOp = listener;\n },\n setOnChange: (listener) => {\n c.onChange = listener;\n },\n takeChanged: () => {\n const changed = c.changed;\n c.changed = [];\n return changed;\n },\n },\n };\n return c;\n });\n\n let value = cell.ctl.snapshot();\n if (typeof value === \"object\" && value !== null) {\n cell.draft ??= createStateDraft<object>({\n snapshot: () => cell.current,\n record: (op, changedPath) => {\n cell.changed.push(changedPath);\n cell.onOp?.(op);\n cell.bump();\n cell.onChange?.();\n },\n });\n stateHosts.set(cell.draft.proxy, cell.host);\n value = cell.draft.proxy;\n }\n return [value as TState, cell.ctl as StatewireHost.StateCtl<TState>] as const;\n};\n\n// ─── commands ───────────────────────────────────────────\n\ntype ExecutionSettled =\n | { type: \"resolved\"; value: unknown }\n | { type: \"rejected\"; error: unknown };\n\ntype ExecutionBind = {\n onAck: () => void;\n onSettle: (settled: ExecutionSettled) => void;\n};\n\ntype ExecutionState = {\n acked: boolean;\n settled: ExecutionSettled | null;\n bind: ExecutionBind | null;\n};\n\nconst executionStates = new WeakMap<object, ExecutionState>();\n\n/**\n * `CommandExecution(executor)` runs the (possibly async) `executor({ ack })`\n * immediately: its return value resolves the execution, a throw/rejection\n * rejects it; `ack()` releases the client's ack watermark before the executor\n * finishes. `CommandExecution.withResolvers()` returns `[execution,\n * { ack, resolve, reject }]` instead — settle them from your own spawned work.\n */\nexport const CommandExecution = Object.assign(\n <T = void>(\n executor: (resolvers: {\n ack: () => void;\n }) => T | void | PromiseLike<T | void>,\n ): StatewireHost.Execution<T> => {\n const state: ExecutionState = { acked: false, settled: null, bind: null };\n const execution = Object.freeze(\n {},\n ) as unknown as StatewireHost.Execution<T>;\n executionStates.set(execution, state);\n const settle = (settled: ExecutionSettled) => {\n if (state.settled) return;\n state.settled = settled;\n state.acked = true;\n state.bind?.onSettle(settled);\n };\n const ack = () => {\n if (state.acked) return;\n state.acked = true;\n state.bind?.onAck();\n };\n void (async () => {\n try {\n settle({ type: \"resolved\", value: await executor({ ack }) });\n } catch (error) {\n settle({ type: \"rejected\", error });\n }\n })();\n return execution;\n },\n {\n withResolvers: <T = void>(): [\n StatewireHost.Execution<T>,\n StatewireHost.ExecutionResolvers<T>,\n ] => {\n let resolve!: (value: T) => void;\n let reject!: (error: unknown) => void;\n const promise = new Promise<T>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n let ack!: () => void;\n const execution = CommandExecution<T>(({ ack: a }) => {\n ack = a;\n return promise;\n });\n return [execution, { ack: () => ack(), resolve, reject }];\n },\n },\n);\n\nexport type CommandsInvocation = {\n readonly method: string;\n readonly params: readonly unknown[];\n readonly ts: number;\n readonly seq: number;\n readonly clientId?: string;\n};\n\nexport type CommandsOutcome =\n | { type: \"accepted\"; payload?: unknown }\n | { type: \"rejected\"; error: unknown };\n\nexport type CommandsListener = {\n onStart: (inv: CommandsInvocation) => void;\n onPosted?: (inv: CommandsInvocation) => void;\n onAck: (inv: CommandsInvocation) => void;\n /** Canonical state now covers the invocation's effect — drop any overlay. */\n onCovered?: (inv: CommandsInvocation) => void;\n onSettle: (inv: CommandsInvocation, outcome: CommandsOutcome) => void;\n};\n\nexport type CommandsSubmission = {\n method: string;\n params: readonly unknown[];\n clientId?: string;\n caller?: StatewireHost.Caller;\n onAck?: () => void;\n onSettle?: (outcome: CommandsOutcome) => void;\n};\n\nexport type CommandsHost = {\n setListener: (listener: CommandsListener | null) => void;\n submit?: (call: CommandsSubmission) => Promise<unknown>;\n};\n\nconst commandsHosts = new WeakMap<object, CommandsHost>();\n\nexport const getCommandsHost = (commands: object): CommandsHost | undefined =>\n commandsHosts.get(commands);\n\nexport const bindCommandsHost = (commands: object, host: CommandsHost) => {\n commandsHosts.set(commands, host);\n};\n\ntype QueuedCall = {\n method: string;\n params: readonly unknown[];\n ts: number;\n resolve: (value: unknown) => void;\n reject: (error: unknown) => void;\n origin?: CommandsSubmission;\n};\n\ntype ActiveInvocation = CommandsInvocation & {\n acked: boolean;\n origin?: CommandsSubmission;\n};\n\ntype CommandsCell = {\n handlers: Record<string, (...args: never[]) => unknown> | null;\n queue: QueuedCall[];\n running: boolean;\n listener: CommandsListener | null;\n seq: number;\n unmounted: boolean;\n stallTimer: ReturnType<typeof setTimeout> | null;\n};\n\nconst STALL_WARN_MS = 5000;\n\n// In-process dispatch has no wire client: never any ambient context, trivially attached.\nconst LOCAL_CALLER: StatewireHost.Caller = Object.freeze({\n context: null,\n isConnected: true,\n});\n\nconst toSendError = (error: unknown): StatewireSendError =>\n error instanceof StatewireSendError\n ? error\n : new StatewireSendError({\n fate: \"rejected\",\n cause: \"server\",\n message: error instanceof Error ? error.message : String(error),\n ...(error instanceof StatewireReject &&\n error.payload !== undefined && { payload: error.payload }),\n });\n\nconst ackInvocation = (cell: CommandsCell, inv: ActiveInvocation) => {\n if (inv.acked) return;\n inv.acked = true;\n cell.listener?.onAck(inv);\n inv.origin?.onAck?.();\n};\n\nconst settleInvocation = (\n cell: CommandsCell,\n inv: ActiveInvocation,\n call: QueuedCall,\n settled: ExecutionSettled,\n) => {\n ackInvocation(cell, inv);\n if (settled.type === \"resolved\") {\n const outcome: CommandsOutcome =\n settled.value === undefined\n ? { type: \"accepted\" }\n : { type: \"accepted\", payload: sanitizeValue(settled.value) };\n cell.listener?.onSettle(inv, outcome);\n inv.origin?.onSettle?.(outcome);\n call.resolve(outcome.payload);\n } else {\n const error = toSendError(settled.error);\n const outcome: CommandsOutcome = { type: \"rejected\", error };\n cell.listener?.onSettle(inv, outcome);\n inv.origin?.onSettle?.(outcome);\n call.reject(error);\n }\n};\n\nconst pump = (cell: CommandsCell) => {\n if (cell.running || !cell.handlers) {\n watchStall(cell);\n return;\n }\n const call = cell.queue.shift();\n if (!call) return;\n const handler = cell.handlers[call.method];\n if (handler === undefined) {\n const error = new StatewireSendError({\n fate: \"unknown-command\",\n cause: \"server\",\n });\n call.origin?.onSettle?.({ type: \"rejected\", error });\n call.reject(error);\n pump(cell);\n return;\n }\n const inv: ActiveInvocation = {\n method: call.method,\n params: call.params,\n ts: call.ts,\n seq: ++cell.seq,\n acked: false,\n ...(call.origin !== undefined && { origin: call.origin }),\n ...(call.origin?.clientId !== undefined && {\n clientId: call.origin.clientId,\n }),\n };\n cell.running = true;\n cell.listener?.onStart(inv);\n cell.listener?.onPosted?.(inv);\n cell.listener?.onCovered?.(inv);\n void (async () => {\n try {\n const value = await (\n handler as (...args: readonly unknown[]) => unknown\n ).call(call.origin?.caller ?? LOCAL_CALLER, ...call.params);\n const execution =\n typeof value === \"object\" && value !== null\n ? executionStates.get(value)\n : undefined;\n if (execution) {\n if (execution.acked) ackInvocation(cell, inv);\n if (execution.settled) {\n settleInvocation(cell, inv, call, execution.settled);\n } else {\n execution.bind = {\n onAck: () => ackInvocation(cell, inv),\n onSettle: (settled) => settleInvocation(cell, inv, call, settled),\n };\n }\n } else {\n settleInvocation(cell, inv, call, { type: \"resolved\", value });\n }\n } catch (error) {\n settleInvocation(cell, inv, call, { type: \"rejected\", error });\n } finally {\n cell.running = false;\n pump(cell);\n }\n })();\n};\n\nconst watchStall = (cell: CommandsCell) => {\n if (!isDev() || cell.stallTimer !== null || cell.queue.length === 0) return;\n if (cell.handlers) return;\n cell.stallTimer = setTimeout(() => {\n cell.stallTimer = null;\n if (cell.handlers === null && cell.queue.length > 0 && !cell.unmounted) {\n console.warn(\n `statewire: ${cell.queue.length} buffered command(s) waiting over ${STALL_WARN_MS}ms — did the host forget to arm useStatewireCommands?`,\n );\n }\n }, STALL_WARN_MS);\n};\n\nexport const useStatewireCommands = <THandlers extends Statewire.Commands>(\n commands:\n | (THandlers & ThisType<StatewireHost.Caller>)\n | undefined\n | null\n | false,\n): Statewire.CommandsProxy<StatewireHost.HandledCommands<THandlers>> => {\n const [built] = useState(() => {\n const cell: CommandsCell = {\n handlers: null,\n queue: [],\n running: false,\n listener: null,\n seq: 0,\n unmounted: false,\n stallTimer: null,\n };\n const proxy = createMethodProxy<\n Statewire.CommandsProxy<StatewireHost.HandledCommands<THandlers>>\n >(\n (method, params) =>\n new Promise((resolve, reject) => {\n if (cell.unmounted) {\n reject(new Error(\"statewire: commands called after unmount\"));\n return;\n }\n cell.queue.push({\n method,\n params,\n ts: Date.now(),\n resolve,\n reject,\n });\n pump(cell);\n }),\n );\n commandsHosts.set(proxy, {\n setListener: (listener) => {\n cell.listener = listener;\n },\n submit: (origin) =>\n new Promise((resolve, reject) => {\n if (cell.unmounted) {\n reject(new Error(\"statewire: commands called after unmount\"));\n return;\n }\n cell.queue.push({\n method: origin.method,\n params: origin.params,\n ts: Date.now(),\n resolve,\n reject,\n origin,\n });\n pump(cell);\n }),\n });\n return { cell, proxy };\n });\n\n useEffect(() => {\n const { cell } = built;\n cell.unmounted = false;\n return () => {\n cell.unmounted = true;\n cell.handlers = null;\n if (cell.stallTimer !== null) {\n clearTimeout(cell.stallTimer);\n cell.stallTimer = null;\n }\n queueMicrotask(() => {\n if (!cell.unmounted) return;\n const queued = cell.queue;\n cell.queue = [];\n for (const call of queued) {\n call.reject(new Error(\"statewire: unmounted\"));\n }\n });\n };\n }, [built]);\n\n useEffect(() => {\n built.cell.handlers = commands || null;\n pump(built.cell);\n });\n\n return built.proxy;\n};\n\n// ─── emit ───────────────────────────────────────────────\n\nexport type EmitHost = {\n setSink: (sink: ((event: unknown) => void) | null) => void;\n};\n\nconst emitHosts = new WeakMap<object, EmitHost>();\n\nexport const getEmitHost = (emit: object | undefined): EmitHost | undefined =>\n emit && emitHosts.get(emit);\n\nexport const useStatewireEmit = <TEvent>(): ((event: TEvent) => void) => {\n const [built] = useState(() => {\n const cell = { sink: null as ((event: unknown) => void) | null };\n const emit = (event: TEvent) => {\n if (!cell.sink) {\n throw new Error(\"statewire: emit before a host bound the event sink\");\n }\n cell.sink(sanitizeValue(event));\n };\n emitHosts.set(emit, {\n setSink: (sink) => {\n cell.sink = sink;\n },\n });\n return emit;\n });\n return built;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"host.js","names":["useEffect","useState","applyOps","sanitizeValue","StatewirePath","StatewireReject","createStateDraft","createMethodProxy","ServerOp","StateDraft","StatewireSendError","Statewire","StatewireHost","isDev","process","env","stateHosts","WeakMap","StateHost","getStateHost","state","get","StateCell","current","draft","changed","onOp","op","onChange","ctl","StateCtl","host","bump","useStatewireState","init","TState","setVersion","cell","commit","c","flush","v","apply","ops","result","events","length","Error","invalidate","push","changedPaths","snapshot","setOnOp","listener","setOnChange","takeChanged","value","record","changedPath","set","proxy","const","Settled","type","error","CommandsInvocation","method","params","ts","seq","clientId","CommandsOutcome","payload","CommandsListener","onStart","inv","onPosted","onAck","onCovered","onSettle","outcome","CommandsSubmission","caller","Caller","CommandsHost","setListener","submit","call","Promise","commandsHosts","getCommandsHost","commands","bindCommandsHost","QueuedCall","resolve","reject","origin","ActiveInvocation","acked","CommandsCell","handlers","Record","args","queue","running","unmounted","stallTimer","ReturnType","setTimeout","STALL_WARN_MS","LOCAL_CALLER","Object","freeze","context","isConnected","toSendError","fate","cause","message","String","undefined","ackInvocation","settleInvocation","settled","isThenable","PromiseLike","then","dispatch","handler","ctx","Ctx","ack","this","resolved","pump","watchStall","shift","console","warn","useStatewireCommands","Commands","THandlers","ThisType","CommandsProxy","HandledCommands","built","Date","now","clearTimeout","queueMicrotask","queued","EmitHost","setSink","sink","event","emitHosts","getEmitHost","emit","useStatewireEmit","_temp"],"sources":["../../src/host/host.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { applyOps, sanitizeValue } from \"../wire/ops\";\nimport type { StatewirePath } from \"../wire/ops\";\nimport { StatewireReject } from \"./engine\";\nimport { createStateDraft } from \"./draft\";\nimport { createMethodProxy } from \"../util/methodProxy\";\nimport type { ServerOp, StateDraft } from \"./draft\";\nimport { StatewireSendError } from \"../types\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireHost } from \"../host\";\n\nconst isDev = () =>\n typeof process === \"undefined\" || process.env[\"NODE_ENV\"] !== \"production\";\n\n// ─── state ──────────────────────────────────────────────\n\nconst stateHosts = new WeakMap<object, StatewireHost.StateHost>();\n\n/** Host-side accessor for a `useStatewireState` state object's source (`snapshot()` reads the committed state). */\nexport const getStateHost = (\n state: object,\n): StatewireHost.StateHost | undefined => stateHosts.get(state);\n\ntype StateCell = {\n current: unknown;\n draft: StateDraft<object> | null;\n changed: StatewirePath[];\n onOp: ((op: ServerOp) => void) | null;\n onChange: (() => void) | null;\n ctl: StatewireHost.StateCtl;\n host: StatewireHost.StateHost;\n bump: () => void;\n};\n\nexport const useStatewireState = <TState>(\n init: () => TState,\n): readonly [TState, StatewireHost.StateCtl<TState>] => {\n const [, setVersion] = useState(0);\n const [cell] = useState((): StateCell => {\n const commit = () => {\n if (c.draft) c.current = c.draft.flush();\n return c.current;\n };\n const c: StateCell = {\n current: sanitizeValue(init()),\n draft: null,\n changed: [[]],\n onOp: null,\n onChange: null,\n bump: () => setVersion((v) => v + 1),\n ctl: {\n apply: (ops) => {\n const result = applyOps(commit(), ops);\n if (result.events.length > 0) {\n throw new Error(\n \"statewire: event ops do not apply to state — route them to emit\",\n );\n }\n c.current = result.state;\n c.draft?.invalidate();\n c.changed.push(...result.changedPaths);\n if (c.onOp) for (const op of ops) c.onOp(op as ServerOp);\n c.bump();\n c.onChange?.();\n },\n snapshot: commit,\n },\n host: {\n snapshot: commit,\n setOnOp: (listener) => {\n c.onOp = listener;\n },\n setOnChange: (listener) => {\n c.onChange = listener;\n },\n takeChanged: () => {\n const changed = c.changed;\n c.changed = [];\n return changed;\n },\n },\n };\n return c;\n });\n\n let value = cell.ctl.snapshot();\n if (typeof value === \"object\" && value !== null) {\n cell.draft ??= createStateDraft<object>({\n snapshot: () => cell.current,\n record: (op, changedPath) => {\n cell.changed.push(changedPath);\n cell.onOp?.(op);\n cell.bump();\n cell.onChange?.();\n },\n });\n stateHosts.set(cell.draft.proxy, cell.host);\n value = cell.draft.proxy;\n }\n return [value as TState, cell.ctl as StatewireHost.StateCtl<TState>] as const;\n};\n\n// ─── commands ───────────────────────────────────────────\n\ntype Settled =\n | { type: \"resolved\"; value: unknown }\n | { type: \"rejected\"; error: unknown };\n\nexport type CommandsInvocation = {\n readonly method: string;\n readonly params: readonly unknown[];\n readonly ts: number;\n readonly seq: number;\n readonly clientId?: string;\n};\n\nexport type CommandsOutcome =\n | { type: \"accepted\"; payload?: unknown }\n | { type: \"rejected\"; error: unknown };\n\nexport type CommandsListener = {\n onStart: (inv: CommandsInvocation) => void;\n onPosted?: (inv: CommandsInvocation) => void;\n onAck: (inv: CommandsInvocation) => void;\n /** Canonical state now covers the invocation's effect — drop any overlay. */\n onCovered?: (inv: CommandsInvocation) => void;\n onSettle: (inv: CommandsInvocation, outcome: CommandsOutcome) => void;\n};\n\nexport type CommandsSubmission = {\n method: string;\n params: readonly unknown[];\n clientId?: string;\n caller?: StatewireHost.Caller;\n onAck?: () => void;\n onSettle?: (outcome: CommandsOutcome) => void;\n};\n\nexport type CommandsHost = {\n setListener: (listener: CommandsListener | null) => void;\n submit?: (call: CommandsSubmission) => Promise<unknown>;\n};\n\nconst commandsHosts = new WeakMap<object, CommandsHost>();\n\nexport const getCommandsHost = (commands: object): CommandsHost | undefined =>\n commandsHosts.get(commands);\n\nexport const bindCommandsHost = (commands: object, host: CommandsHost) => {\n commandsHosts.set(commands, host);\n};\n\ntype QueuedCall = {\n method: string;\n params: readonly unknown[];\n ts: number;\n resolve: (value: unknown) => void;\n reject: (error: unknown) => void;\n origin?: CommandsSubmission;\n};\n\ntype ActiveInvocation = CommandsInvocation & {\n acked: boolean;\n origin?: CommandsSubmission;\n};\n\ntype CommandsCell = {\n handlers: Record<string, (...args: never[]) => unknown> | null;\n queue: QueuedCall[];\n running: boolean;\n listener: CommandsListener | null;\n seq: number;\n unmounted: boolean;\n stallTimer: ReturnType<typeof setTimeout> | null;\n};\n\nconst STALL_WARN_MS = 5000;\n\n// In-process dispatch has no wire client: never any ambient context, trivially attached.\nconst LOCAL_CALLER: StatewireHost.Caller = Object.freeze({\n get clientId(): string {\n throw new Error(\"statewire: in-process dispatch has no client id\");\n },\n context: null,\n isConnected: true,\n});\n\nconst toSendError = (error: unknown): StatewireSendError =>\n error instanceof StatewireSendError\n ? error\n : new StatewireSendError({\n fate: \"rejected\",\n cause: \"server\",\n message: error instanceof Error ? error.message : String(error),\n ...(error instanceof StatewireReject &&\n error.payload !== undefined && { payload: error.payload }),\n });\n\nconst ackInvocation = (cell: CommandsCell, inv: ActiveInvocation) => {\n if (inv.acked) return;\n inv.acked = true;\n cell.listener?.onAck(inv);\n inv.origin?.onAck?.();\n};\n\nconst settleInvocation = (\n cell: CommandsCell,\n inv: ActiveInvocation,\n call: QueuedCall,\n settled: Settled,\n) => {\n ackInvocation(cell, inv);\n if (settled.type === \"resolved\") {\n const outcome: CommandsOutcome =\n settled.value === undefined\n ? { type: \"accepted\" }\n : { type: \"accepted\", payload: sanitizeValue(settled.value) };\n cell.listener?.onSettle(inv, outcome);\n inv.origin?.onSettle?.(outcome);\n call.resolve(outcome.payload);\n } else {\n const error = toSendError(settled.error);\n const outcome: CommandsOutcome = { type: \"rejected\", error };\n cell.listener?.onSettle(inv, outcome);\n inv.origin?.onSettle?.(outcome);\n call.reject(error);\n }\n};\n\nconst isThenable = (value: unknown): value is PromiseLike<unknown> =>\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as { then?: unknown }).then === \"function\";\n\nconst dispatch = (cell: CommandsCell, call: QueuedCall) => {\n const handler = cell.handlers![call.method];\n if (handler === undefined) {\n const error = new StatewireSendError({\n fate: \"unknown-command\",\n cause: \"server\",\n });\n call.origin?.onSettle?.({ type: \"rejected\", error });\n call.reject(error);\n return;\n }\n const inv: ActiveInvocation = {\n method: call.method,\n params: call.params,\n ts: call.ts,\n seq: ++cell.seq,\n acked: false,\n ...(call.origin !== undefined && { origin: call.origin }),\n ...(call.origin?.clientId !== undefined && {\n clientId: call.origin.clientId,\n }),\n };\n cell.listener?.onStart(inv);\n cell.listener?.onPosted?.(inv);\n cell.listener?.onCovered?.(inv);\n const ctx: StatewireHost.Ctx = {\n caller: call.origin?.caller ?? LOCAL_CALLER,\n ack: () => ackInvocation(cell, inv),\n };\n try {\n const value = (\n handler as (\n this: StatewireHost.Ctx,\n ...args: readonly unknown[]\n ) => unknown\n ).call(ctx, ...call.params);\n if (isThenable(value)) {\n value.then(\n (resolved) =>\n settleInvocation(cell, inv, call, {\n type: \"resolved\",\n value: resolved,\n }),\n (error: unknown) =>\n settleInvocation(cell, inv, call, { type: \"rejected\", error }),\n );\n } else {\n settleInvocation(cell, inv, call, { type: \"resolved\", value });\n }\n } catch (error) {\n settleInvocation(cell, inv, call, { type: \"rejected\", error });\n }\n};\n\n// Prefix-only advancement: each handler runs synchronously to its first\n// await, then the pump moves on; settlement rides the returned promise.\nconst pump = (cell: CommandsCell) => {\n if (cell.running || !cell.handlers) {\n watchStall(cell);\n return;\n }\n cell.running = true;\n try {\n while (cell.handlers) {\n const call = cell.queue.shift();\n if (!call) break;\n dispatch(cell, call);\n }\n } finally {\n cell.running = false;\n }\n};\n\nconst watchStall = (cell: CommandsCell) => {\n if (!isDev() || cell.stallTimer !== null || cell.queue.length === 0) return;\n if (cell.handlers) return;\n cell.stallTimer = setTimeout(() => {\n cell.stallTimer = null;\n if (cell.handlers === null && cell.queue.length > 0 && !cell.unmounted) {\n console.warn(\n `statewire: ${cell.queue.length} buffered command(s) waiting over ${STALL_WARN_MS}ms — did the host forget to arm useStatewireCommands?`,\n );\n }\n }, STALL_WARN_MS);\n};\n\nexport const useStatewireCommands = <THandlers extends Statewire.Commands>(\n commands:\n | (THandlers & ThisType<StatewireHost.Ctx>)\n | undefined\n | null\n | false,\n): Statewire.CommandsProxy<StatewireHost.HandledCommands<THandlers>> => {\n const [built] = useState(() => {\n const cell: CommandsCell = {\n handlers: null,\n queue: [],\n running: false,\n listener: null,\n seq: 0,\n unmounted: false,\n stallTimer: null,\n };\n const proxy = createMethodProxy<\n Statewire.CommandsProxy<StatewireHost.HandledCommands<THandlers>>\n >(\n (method, params) =>\n new Promise((resolve, reject) => {\n if (cell.unmounted) {\n reject(new Error(\"statewire: commands called after unmount\"));\n return;\n }\n cell.queue.push({\n method,\n params,\n ts: Date.now(),\n resolve,\n reject,\n });\n pump(cell);\n }),\n );\n commandsHosts.set(proxy, {\n setListener: (listener) => {\n cell.listener = listener;\n },\n submit: (origin) =>\n new Promise((resolve, reject) => {\n if (cell.unmounted) {\n reject(new Error(\"statewire: commands called after unmount\"));\n return;\n }\n cell.queue.push({\n method: origin.method,\n params: origin.params,\n ts: Date.now(),\n resolve,\n reject,\n origin,\n });\n pump(cell);\n }),\n });\n return { cell, proxy };\n });\n\n useEffect(() => {\n const { cell } = built;\n cell.unmounted = false;\n return () => {\n cell.unmounted = true;\n cell.handlers = null;\n if (cell.stallTimer !== null) {\n clearTimeout(cell.stallTimer);\n cell.stallTimer = null;\n }\n queueMicrotask(() => {\n if (!cell.unmounted) return;\n const queued = cell.queue;\n cell.queue = [];\n for (const call of queued) {\n call.reject(new Error(\"statewire: unmounted\"));\n }\n });\n };\n }, [built]);\n\n useEffect(() => {\n built.cell.handlers = commands || null;\n pump(built.cell);\n });\n\n return built.proxy;\n};\n\n// ─── emit ───────────────────────────────────────────────\n\nexport type EmitHost = {\n setSink: (sink: ((event: unknown) => void) | null) => void;\n};\n\nconst emitHosts = new WeakMap<object, EmitHost>();\n\nexport const getEmitHost = (emit: object | undefined): EmitHost | undefined =>\n emit && emitHosts.get(emit);\n\nexport const useStatewireEmit = <TEvent>(): ((event: TEvent) => void) => {\n const [built] = useState(() => {\n const cell = { sink: null as ((event: unknown) => void) | null };\n const emit = (event: TEvent) => {\n if (!cell.sink) {\n throw new Error(\"statewire: emit before a host bound the event sink\");\n }\n cell.sink(sanitizeValue(event));\n };\n emitHosts.set(emit, {\n setSink: (sink) => {\n cell.sink = sink;\n },\n });\n return emit;\n });\n return built;\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"socket.d.ts","names":[],"sources":["../../src/host/socket.ts"],"mappings":";;;;;;;;;;cAulBa,sBACX,WAAW,gBAAgB,YAC3B,UAAU,sBAAsB,sBAC/B,sBAAsB"}
1
+ {"version":3,"file":"socket.d.ts","names":[],"sources":["../../src/host/socket.ts"],"mappings":";;;;;;;;;;cAwlBa,sBACX,WAAW,gBAAgB,YAC3B,UAAU,sBAAsB,sBAC/B,sBAAsB"}
@@ -262,6 +262,7 @@ const createSocketHost = (options) => {
262
262
  };
263
263
  const settleWire = (rec, adm, outcome) => settleResponse(rec, adm, responseOf(adm, outcome));
264
264
  const callerOf = (rec) => ({
265
+ clientId: rec.id,
265
266
  get context() {
266
267
  return rec.context;
267
268
  },
@@ -1 +1 @@
1
- {"version":3,"file":"socket.js","names":["useEffect","useState","createTapRoot","flushTapSync","useResource","ResourceElement","getCommandsHost","getEmitHost","getStateHost","CommandsHost","CommandsOutcome","StatewireSendError","Statewire","StatewireHost","StatewireHostInternal","encodeValue","reviveValue","createMacrotaskPoster","randomId","StatewireMetaEntry","StatewirePath","StatewireEnvelope","StatewireResponse","CLIENT_ID_RE","RESULT_TTL_MS","RESULT_LIMIT","WireOp","op","path","value","meta","Admission","seq","method","acked","pendingSent","settled","release","ClientRec","id","lastSeq","context","Record","results","Map","ts","res","inflight","Conn","clientId","socket","Socket","open","lastSentAt","Frame","params","parseFrame","raw","frame","JSON","parse","Array","isArray","undefined","Number","isInteger","contextValue","length","Error","encodePayload","payload","responseOf","adm","outcome","type","failure","error","fate","code","message","String","createSocketHost","options","SocketHostOptions","heartbeatMs","heartbeatIntervalMs","clients","conns","Set","pendingOps","dirtyAcks","pendingRes","inbox","Promise","resolve","finished","Fin","disposed","flushScheduled","heartbeatTimer","ReturnType","setInterval","stateHost","StateHost","commandsHost","emitHost","current","post","flush","scheduleFlush","recordOp","push","attach","next","nextState","state","nextCommands","commands","nextEmit","emit","submit","setOnOp","setSink","event","closeConn","conn","reason","delete","close","size","clearInterval","sendTo","data","Date","now","send","ensureHeartbeat","unref","fin","takeChanged","rec","values","queueRes","ops","acks","envelope","clientRes","get","has","ack","Object","keys","stringify","response","list","set","advance","add","retain","newest","Math","max","entry","min","duplicateAnswer","cached","settleResponse","settleWire","callerOf","Caller","isConnected","admit","released","caller","onAck","onSettle","catch","then","connect","SocketConnection","dead","refuse","test","known","created","other","snapshot","syn","lease","finish","resume","suspend","useSocketHost","transport","$","_c","t0","h","t1","t2","t3","StatewireSocketHost","SocketHost","SocketHostRoot","root","getValue","dispose","unmount"],"sources":["../../src/host/socket.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { createTapRoot, flushTapSync, useResource } from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { getCommandsHost, getEmitHost, getStateHost } from \"./host\";\nimport type { CommandsHost, CommandsOutcome } from \"./host\";\nimport { StatewireSendError } from \"../types\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireHost } from \"../host\";\nimport type { StatewireHostInternal } from \"../host-internal\";\nimport { encodeValue, reviveValue } from \"../wire/meta\";\nimport { createMacrotaskPoster } from \"../util/macrotask\";\nimport { randomId } from \"../util/randomId\";\nimport type { StatewireMetaEntry } from \"../wire/meta\";\nimport type { StatewirePath } from \"../wire/ops\";\nimport type {\n StatewireEnvelope,\n StatewireResponse,\n} from \"../client/connection\";\n\nconst CLIENT_ID_RE = /^[A-Za-z0-9._~-]{1,256}$/;\nconst RESULT_TTL_MS = 30_000;\nconst RESULT_LIMIT = 256;\n\ntype WireOp = {\n op: string;\n path?: StatewirePath;\n value?: unknown;\n meta?: StatewireMetaEntry[];\n};\n\ntype Admission = {\n seq: number;\n method: string;\n acked: boolean;\n pendingSent: boolean;\n settled: boolean;\n release: () => void;\n};\n\ntype ClientRec = {\n id: string;\n lastSeq: number;\n context: Record<string, unknown> | null;\n results: Map<number, { ts: number; res: StatewireResponse }>;\n inflight: Map<number, Admission>;\n};\n\ntype Conn = {\n clientId: string;\n socket: StatewireHostInternal.Socket;\n open: boolean;\n lastSentAt: number;\n};\n\ntype Frame = {\n method?: string;\n seq: number;\n params: unknown[];\n context?: unknown;\n};\n\nconst parseFrame = (raw: string): Frame | null => {\n let frame: unknown;\n try {\n frame = JSON.parse(raw);\n } catch {\n return null;\n }\n if (typeof frame !== \"object\" || frame === null || Array.isArray(frame)) {\n return null;\n }\n const { method, seq, params, meta, context } = frame as Record<\n string,\n unknown\n >;\n if (method === undefined && context === undefined) return null;\n if (typeof seq !== \"number\" || !Number.isInteger(seq) || seq < 1) return null;\n if (method === undefined) {\n return { seq, params: [], ...(context !== undefined && { context }) };\n }\n if (typeof method !== \"string\") return null;\n if (!Array.isArray(params)) return null;\n if (meta !== undefined) {\n try {\n reviveValue(params, meta);\n } catch {\n return null;\n }\n }\n return { method, seq, params, ...(context !== undefined && { context }) };\n};\n\n/** v1 context submissions are exactly one root replace with an object value. */\nconst contextValue = (context: unknown): Record<string, unknown> => {\n if (!Array.isArray(context) || context.length !== 1) {\n throw new Error(\"context must be a single-op array\");\n }\n const op = context[0] as Record<string, unknown> | null;\n if (\n typeof op !== \"object\" ||\n op === null ||\n op[\"op\"] !== \"replace\" ||\n !Array.isArray(op[\"path\"]) ||\n op[\"path\"].length !== 0\n ) {\n throw new Error(\"context op must be a root replace\");\n }\n const value = op[\"value\"];\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new Error(\"context value must be an object\");\n }\n return value as Record<string, unknown>;\n};\n\nconst encodePayload = (\n payload: unknown,\n): { payload?: unknown; meta?: StatewireMetaEntry[] } => {\n if (payload === undefined) return {};\n const { value, meta } = encodeValue(payload);\n return { payload: value, ...(meta !== undefined && { meta }) };\n};\n\nconst responseOf = (\n adm: Admission,\n outcome: CommandsOutcome,\n): StatewireResponse => {\n if (outcome.type === \"accepted\") {\n return {\n seq: adm.seq,\n type: \"accepted\",\n ...encodePayload(outcome.payload),\n };\n }\n const failure =\n outcome.error instanceof StatewireSendError ? outcome.error.failure : null;\n switch (failure?.fate) {\n case \"unknown-command\":\n return {\n seq: adm.seq,\n type: \"rejected\",\n code: \"unknown-command\",\n message: `unknown method: '${adm.method}'`,\n };\n case \"invalid-params\":\n return {\n seq: adm.seq,\n type: \"rejected\",\n code: \"invalid-params\",\n message: failure.message,\n };\n case \"crashed\":\n return { seq: adm.seq, type: \"crashed\" };\n case \"rejected\":\n return {\n seq: adm.seq,\n type: \"rejected\",\n message: failure.message,\n ...encodePayload(failure.payload),\n };\n default:\n return {\n seq: adm.seq,\n type: \"rejected\",\n message:\n outcome.error instanceof Error\n ? outcome.error.message\n : String(outcome.error),\n };\n }\n};\n\nconst createSocketHost = (\n options?: StatewireHostInternal.SocketHostOptions,\n) => {\n const heartbeatMs = options?.heartbeatIntervalMs ?? 15_000;\n\n const clients = new Map<string, ClientRec>();\n const conns = new Set<Conn>();\n let pendingOps: WireOp[] = [];\n let dirtyAcks = new Set<string>();\n let pendingRes = new Map<string, StatewireResponse[]>();\n let inbox: Promise<void> = Promise.resolve();\n let finished: StatewireHost.Fin | null = null;\n let disposed = false;\n let flushScheduled = false;\n let heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n\n let stateHost: StatewireHost.StateHost | null = null;\n let commandsHost: CommandsHost | null = null;\n let emitHost: ReturnType<typeof getEmitHost> | null = null;\n let current: Statewire<unknown> | null = null;\n\n const post = createMacrotaskPoster(() => {\n flushScheduled = false;\n flush();\n });\n const scheduleFlush = () => {\n if (flushScheduled || disposed) return;\n flushScheduled = true;\n post();\n };\n\n const recordOp = (op: {\n op: string;\n path?: StatewirePath;\n value?: unknown;\n }) => {\n if (op.op === \"replace\" || op.op === \"add\" || op.op === \"event\") {\n const { value, meta } = encodeValue(op.value);\n pendingOps.push({ ...op, value, ...(meta !== undefined && { meta }) });\n } else {\n pendingOps.push(op);\n }\n scheduleFlush();\n };\n\n const attach = (next: Statewire<unknown>) => {\n if (next === current) return;\n current = next;\n const nextState =\n typeof next.state === \"object\" && next.state !== null\n ? getStateHost(next.state)\n : undefined;\n const nextCommands = getCommandsHost(next.commands);\n const nextEmit = getEmitHost(next.emit);\n if (!nextState || !nextCommands?.submit || !nextEmit) {\n throw new Error(\n \"StatewireSocketHost requires a statewire/host transport (useStatewireState with object state, useStatewireCommands, useStatewireEmit)\",\n );\n }\n if (nextState !== stateHost) {\n stateHost?.setOnOp(null);\n stateHost = nextState;\n stateHost.setOnOp(recordOp);\n }\n commandsHost = nextCommands;\n if (nextEmit !== emitHost) {\n emitHost?.setSink(null);\n emitHost = nextEmit;\n emitHost.setSink((event) => recordOp({ op: \"event\", value: event }));\n }\n };\n\n const closeConn = (conn: Conn, code?: number, reason?: string) => {\n if (!conn.open) return;\n conn.open = false;\n conns.delete(conn);\n try {\n conn.socket.close(code, reason);\n } catch {\n /* peer already gone */\n }\n if (conns.size === 0 && heartbeatTimer !== null) {\n clearInterval(heartbeatTimer);\n heartbeatTimer = null;\n }\n };\n\n const sendTo = (conn: Conn, data: string) => {\n if (!conn.open) return;\n conn.lastSentAt = Date.now();\n try {\n conn.socket.send(data);\n } catch {\n closeConn(conn);\n }\n };\n\n const ensureHeartbeat = () => {\n if (heartbeatTimer !== null || heartbeatMs <= 0) return;\n heartbeatTimer = setInterval(() => {\n const now = Date.now();\n for (const conn of [...conns]) {\n if (now - conn.lastSentAt >= heartbeatMs) sendTo(conn, \"{}\");\n }\n }, heartbeatMs);\n (heartbeatTimer as unknown as { unref?: () => void }).unref?.();\n };\n\n const flush = (fin?: StatewireHost.Fin) => {\n flushTapSync(() => {});\n stateHost?.takeChanged();\n for (const rec of clients.values()) {\n for (const adm of rec.inflight.values()) {\n if (adm.acked && !adm.settled && !adm.pendingSent) {\n adm.pendingSent = true;\n queueRes(rec.id, { seq: adm.seq, type: \"pending\" });\n }\n }\n }\n if (\n pendingOps.length === 0 &&\n dirtyAcks.size === 0 &&\n pendingRes.size === 0 &&\n fin === undefined\n ) {\n return;\n }\n const ops = pendingOps;\n const acks = dirtyAcks;\n const res = pendingRes;\n pendingOps = [];\n dirtyAcks = new Set();\n pendingRes = new Map();\n for (const conn of [...conns]) {\n const envelope: StatewireEnvelope = {};\n if (ops.length > 0) envelope.ops = ops;\n const clientRes = res.get(conn.clientId);\n if (clientRes !== undefined) envelope.res = clientRes;\n if (acks.has(conn.clientId)) {\n const rec = clients.get(conn.clientId);\n if (rec !== undefined) envelope.ack = rec.lastSeq;\n }\n if (fin !== undefined) envelope.fin = fin;\n if (Object.keys(envelope).length === 0) continue;\n sendTo(conn, JSON.stringify(envelope));\n if (fin !== undefined) closeConn(conn);\n }\n };\n\n const queueRes = (clientId: string, response: StatewireResponse) => {\n const list = pendingRes.get(clientId);\n if (list) list.push(response);\n else pendingRes.set(clientId, [response]);\n };\n\n const advance = (rec: ClientRec, seq: number) => {\n rec.lastSeq = seq;\n dirtyAcks.add(rec.id);\n };\n\n const retain = (rec: ClientRec, response: StatewireResponse) => {\n const now = Date.now();\n rec.results.set(response.seq, { ts: now, res: response });\n const newest = Math.max(...rec.results.keys());\n for (const [seq, entry] of rec.results) {\n if (seq !== newest && now - entry.ts > RESULT_TTL_MS) {\n rec.results.delete(seq);\n }\n }\n while (rec.results.size > RESULT_LIMIT) {\n rec.results.delete(Math.min(...rec.results.keys()));\n }\n };\n\n const duplicateAnswer = (rec: ClientRec, seq: number): StatewireResponse => {\n const cached = rec.results.get(seq);\n if (cached) {\n const newest = Math.max(...rec.results.keys());\n if (seq === newest || Date.now() - cached.ts <= RESULT_TTL_MS) {\n return cached.res;\n }\n rec.results.delete(seq);\n }\n if (rec.inflight.has(seq)) return { seq, type: \"pending\" };\n return { seq, type: \"result-unavailable\" };\n };\n\n const settleResponse = (\n rec: ClientRec,\n adm: Admission,\n response: StatewireResponse,\n ) => {\n if (adm.settled) return;\n adm.settled = true;\n rec.inflight.delete(adm.seq);\n retain(rec, response);\n if (!adm.acked) {\n adm.acked = true;\n advance(rec, adm.seq);\n }\n if (\n adm.pendingSent ||\n response.type !== \"accepted\" ||\n response.payload !== undefined\n ) {\n queueRes(rec.id, response);\n }\n scheduleFlush();\n adm.release();\n };\n\n const settleWire = (\n rec: ClientRec,\n adm: Admission,\n outcome: CommandsOutcome,\n ) => settleResponse(rec, adm, responseOf(adm, outcome));\n\n const callerOf = (rec: ClientRec): StatewireHost.Caller => ({\n get context() {\n return rec.context;\n },\n get isConnected() {\n if (clients.get(rec.id) !== rec) return false;\n for (const conn of conns) {\n if (conn.clientId === rec.id && conn.open) return true;\n }\n return false;\n },\n });\n\n const admit = (conn: Conn, frame: Frame): Promise<void> | void => {\n if (!conn.open || disposed || finished !== null) return;\n const rec = clients.get(conn.clientId)!;\n if (frame.seq <= rec.lastSeq || rec.inflight.has(frame.seq)) {\n queueRes(rec.id, duplicateAnswer(rec, frame.seq));\n scheduleFlush();\n return;\n }\n if (frame.seq > rec.lastSeq + 1) {\n closeConn(\n conn,\n 1008,\n `seq gap: expected ${rec.lastSeq + 1}, got ${frame.seq}`,\n );\n return;\n }\n const adm: Admission = {\n seq: frame.seq,\n method: frame.method ?? \"\",\n acked: false,\n pendingSent: false,\n settled: false,\n release: () => {},\n };\n if (frame.context !== undefined) {\n let value: Record<string, unknown>;\n try {\n value = contextValue(frame.context);\n } catch (error) {\n rec.inflight.set(frame.seq, adm);\n settleResponse(rec, adm, {\n seq: frame.seq,\n type: \"rejected\",\n code: \"invalid-context\",\n message: `invalid context: ${(error as Error).message}`,\n });\n return;\n }\n rec.context = value;\n if (frame.method === undefined) {\n rec.inflight.set(frame.seq, adm);\n settleResponse(rec, adm, { seq: frame.seq, type: \"accepted\" });\n return;\n }\n }\n const released = new Promise<void>((resolve) => {\n adm.release = resolve;\n });\n rec.inflight.set(frame.seq, adm);\n commandsHost!.submit!({\n method: frame.method!,\n params: frame.params,\n clientId: rec.id,\n caller: callerOf(rec),\n onAck: () => {\n if (adm.settled || adm.acked) return;\n adm.acked = true;\n advance(rec, adm.seq);\n scheduleFlush();\n adm.release();\n },\n onSettle: (outcome) => settleWire(rec, adm, outcome),\n }).catch((error: unknown) => {\n if (!adm.settled) settleWire(rec, adm, { type: \"rejected\", error });\n });\n return released;\n };\n\n const message = (conn: Conn, data: string) => {\n if (!conn.open || disposed || finished !== null) return;\n const frame = parseFrame(data);\n if (frame === null) {\n closeConn(conn, 1008, \"invalid frame\");\n return;\n }\n inbox = inbox.then(() => admit(conn, frame));\n };\n\n const connect = (\n clientId: string,\n socket: StatewireHostInternal.Socket,\n ): StatewireHostInternal.SocketConnection => {\n const dead: StatewireHostInternal.SocketConnection = {\n clientId,\n message: () => {},\n close: () => {},\n };\n const refuse = (code: number, reason: string) => {\n try {\n socket.close(code, reason);\n } catch {\n /* peer already gone */\n }\n return dead;\n };\n if (disposed) return refuse(1001, \"disposed\");\n if (!CLIENT_ID_RE.test(clientId)) return refuse(1008, \"invalid client id\");\n flush();\n if (finished !== null) {\n try {\n socket.send(JSON.stringify({ fin: finished }));\n } catch {\n /* peer already gone */\n }\n return refuse(1000, \"finished\");\n }\n const known = clients.has(clientId);\n const rec = known\n ? clients.get(clientId)!\n : (() => {\n const created: ClientRec = {\n id: clientId,\n lastSeq: 0,\n context: null,\n results: new Map(),\n inflight: new Map(),\n };\n clients.set(clientId, created);\n return created;\n })();\n for (const other of [...conns]) {\n if (other.clientId !== clientId) continue;\n sendTo(other, JSON.stringify({ fin: { reason: \"superseded\" } }));\n closeConn(other);\n }\n const conn: Conn = { clientId, socket, open: true, lastSentAt: 0 };\n conns.add(conn);\n ensureHeartbeat();\n const { value, meta } = encodeValue(stateHost!.snapshot());\n const snapshot: StatewireEnvelope = {\n ops: [\n {\n op: \"replace\",\n path: [],\n value,\n ...(meta !== undefined && { meta }),\n },\n ],\n syn: { lastSeq: known ? rec.lastSeq : -1, lease: randomId() },\n };\n sendTo(conn, JSON.stringify(snapshot));\n return {\n clientId,\n message: (data) => message(conn, data),\n close: () => closeConn(conn),\n };\n };\n\n const finish = (fin: StatewireHost.Fin) => {\n if (finished !== null || disposed) return;\n finished = fin;\n flush(fin);\n };\n\n return {\n connect,\n finish,\n attach,\n resume: () => {\n disposed = false;\n stateHost?.setOnOp(recordOp);\n emitHost?.setSink((event) => recordOp({ op: \"event\", value: event }));\n },\n suspend: () => {\n disposed = true;\n if (heartbeatTimer !== null) {\n clearInterval(heartbeatTimer);\n heartbeatTimer = null;\n }\n stateHost?.setOnOp(null);\n emitHost?.setSink(null);\n },\n };\n};\n\nconst useSocketHost = (\n transport: ResourceElement<Statewire>,\n options?: StatewireHostInternal.SocketHostOptions,\n): Pick<StatewireHostInternal.SocketHost, \"connect\" | \"finish\"> => {\n const [h] = useState(() => createSocketHost(options));\n const value = useResource(transport) as Statewire<unknown>;\n h.attach(value);\n useEffect(() => {\n h.attach(value);\n });\n useEffect(() => {\n h.resume();\n return () => h.suspend();\n }, [h]);\n return h;\n};\n\n/**\n * Transport-agnostic WS binding over a statewire/host transport:\n * feed it sockets (anything with `send`/`close`) and raw text frames, and it\n * speaks the Statewire envelope protocol — snapshot-first attach with `syn`,\n * delta/ack/res envelopes, per-client-id supersede, and `fin` on finish.\n */\nexport const StatewireSocketHost = (\n transport: ResourceElement<Statewire>,\n options?: StatewireHostInternal.SocketHostOptions,\n): StatewireHostInternal.SocketHost => {\n const SocketHostRoot = () => useSocketHost(transport, options);\n const root = createTapRoot(SocketHostRoot);\n let disposed = false;\n return {\n connect: (clientId, socket) =>\n flushTapSync(() => root.getValue().connect(clientId, socket)),\n finish: (fin) => flushTapSync(() => root.getValue().finish(fin)),\n dispose: () => {\n if (disposed) return;\n disposed = true;\n flushTapSync(() => root.getValue().finish({ reason: \"evicted\" }));\n root.unmount();\n },\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"socket.js","names":["useEffect","useState","createTapRoot","flushTapSync","useResource","ResourceElement","getCommandsHost","getEmitHost","getStateHost","CommandsHost","CommandsOutcome","StatewireSendError","Statewire","StatewireHost","StatewireHostInternal","encodeValue","reviveValue","createMacrotaskPoster","randomId","StatewireMetaEntry","StatewirePath","StatewireEnvelope","StatewireResponse","CLIENT_ID_RE","RESULT_TTL_MS","RESULT_LIMIT","WireOp","op","path","value","meta","Admission","seq","method","acked","pendingSent","settled","release","ClientRec","id","lastSeq","context","Record","results","Map","ts","res","inflight","Conn","clientId","socket","Socket","open","lastSentAt","Frame","params","parseFrame","raw","frame","JSON","parse","Array","isArray","undefined","Number","isInteger","contextValue","length","Error","encodePayload","payload","responseOf","adm","outcome","type","failure","error","fate","code","message","String","createSocketHost","options","SocketHostOptions","heartbeatMs","heartbeatIntervalMs","clients","conns","Set","pendingOps","dirtyAcks","pendingRes","inbox","Promise","resolve","finished","Fin","disposed","flushScheduled","heartbeatTimer","ReturnType","setInterval","stateHost","StateHost","commandsHost","emitHost","current","post","flush","scheduleFlush","recordOp","push","attach","next","nextState","state","nextCommands","commands","nextEmit","emit","submit","setOnOp","setSink","event","closeConn","conn","reason","delete","close","size","clearInterval","sendTo","data","Date","now","send","ensureHeartbeat","unref","fin","takeChanged","rec","values","queueRes","ops","acks","envelope","clientRes","get","has","ack","Object","keys","stringify","response","list","set","advance","add","retain","newest","Math","max","entry","min","duplicateAnswer","cached","settleResponse","settleWire","callerOf","Caller","isConnected","admit","released","caller","onAck","onSettle","catch","then","connect","SocketConnection","dead","refuse","test","known","created","other","snapshot","syn","lease","finish","resume","suspend","useSocketHost","transport","$","_c","t0","h","t1","t2","t3","StatewireSocketHost","SocketHost","SocketHostRoot","root","getValue","dispose","unmount"],"sources":["../../src/host/socket.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { createTapRoot, flushTapSync, useResource } from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { getCommandsHost, getEmitHost, getStateHost } from \"./host\";\nimport type { CommandsHost, CommandsOutcome } from \"./host\";\nimport { StatewireSendError } from \"../types\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireHost } from \"../host\";\nimport type { StatewireHostInternal } from \"../host-internal\";\nimport { encodeValue, reviveValue } from \"../wire/meta\";\nimport { createMacrotaskPoster } from \"../util/macrotask\";\nimport { randomId } from \"../util/randomId\";\nimport type { StatewireMetaEntry } from \"../wire/meta\";\nimport type { StatewirePath } from \"../wire/ops\";\nimport type {\n StatewireEnvelope,\n StatewireResponse,\n} from \"../client/connection\";\n\nconst CLIENT_ID_RE = /^[A-Za-z0-9._~-]{1,256}$/;\nconst RESULT_TTL_MS = 30_000;\nconst RESULT_LIMIT = 256;\n\ntype WireOp = {\n op: string;\n path?: StatewirePath;\n value?: unknown;\n meta?: StatewireMetaEntry[];\n};\n\ntype Admission = {\n seq: number;\n method: string;\n acked: boolean;\n pendingSent: boolean;\n settled: boolean;\n release: () => void;\n};\n\ntype ClientRec = {\n id: string;\n lastSeq: number;\n context: Record<string, unknown> | null;\n results: Map<number, { ts: number; res: StatewireResponse }>;\n inflight: Map<number, Admission>;\n};\n\ntype Conn = {\n clientId: string;\n socket: StatewireHostInternal.Socket;\n open: boolean;\n lastSentAt: number;\n};\n\ntype Frame = {\n method?: string;\n seq: number;\n params: unknown[];\n context?: unknown;\n};\n\nconst parseFrame = (raw: string): Frame | null => {\n let frame: unknown;\n try {\n frame = JSON.parse(raw);\n } catch {\n return null;\n }\n if (typeof frame !== \"object\" || frame === null || Array.isArray(frame)) {\n return null;\n }\n const { method, seq, params, meta, context } = frame as Record<\n string,\n unknown\n >;\n if (method === undefined && context === undefined) return null;\n if (typeof seq !== \"number\" || !Number.isInteger(seq) || seq < 1) return null;\n if (method === undefined) {\n return { seq, params: [], ...(context !== undefined && { context }) };\n }\n if (typeof method !== \"string\") return null;\n if (!Array.isArray(params)) return null;\n if (meta !== undefined) {\n try {\n reviveValue(params, meta);\n } catch {\n return null;\n }\n }\n return { method, seq, params, ...(context !== undefined && { context }) };\n};\n\n/** v1 context submissions are exactly one root replace with an object value. */\nconst contextValue = (context: unknown): Record<string, unknown> => {\n if (!Array.isArray(context) || context.length !== 1) {\n throw new Error(\"context must be a single-op array\");\n }\n const op = context[0] as Record<string, unknown> | null;\n if (\n typeof op !== \"object\" ||\n op === null ||\n op[\"op\"] !== \"replace\" ||\n !Array.isArray(op[\"path\"]) ||\n op[\"path\"].length !== 0\n ) {\n throw new Error(\"context op must be a root replace\");\n }\n const value = op[\"value\"];\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new Error(\"context value must be an object\");\n }\n return value as Record<string, unknown>;\n};\n\nconst encodePayload = (\n payload: unknown,\n): { payload?: unknown; meta?: StatewireMetaEntry[] } => {\n if (payload === undefined) return {};\n const { value, meta } = encodeValue(payload);\n return { payload: value, ...(meta !== undefined && { meta }) };\n};\n\nconst responseOf = (\n adm: Admission,\n outcome: CommandsOutcome,\n): StatewireResponse => {\n if (outcome.type === \"accepted\") {\n return {\n seq: adm.seq,\n type: \"accepted\",\n ...encodePayload(outcome.payload),\n };\n }\n const failure =\n outcome.error instanceof StatewireSendError ? outcome.error.failure : null;\n switch (failure?.fate) {\n case \"unknown-command\":\n return {\n seq: adm.seq,\n type: \"rejected\",\n code: \"unknown-command\",\n message: `unknown method: '${adm.method}'`,\n };\n case \"invalid-params\":\n return {\n seq: adm.seq,\n type: \"rejected\",\n code: \"invalid-params\",\n message: failure.message,\n };\n case \"crashed\":\n return { seq: adm.seq, type: \"crashed\" };\n case \"rejected\":\n return {\n seq: adm.seq,\n type: \"rejected\",\n message: failure.message,\n ...encodePayload(failure.payload),\n };\n default:\n return {\n seq: adm.seq,\n type: \"rejected\",\n message:\n outcome.error instanceof Error\n ? outcome.error.message\n : String(outcome.error),\n };\n }\n};\n\nconst createSocketHost = (\n options?: StatewireHostInternal.SocketHostOptions,\n) => {\n const heartbeatMs = options?.heartbeatIntervalMs ?? 15_000;\n\n const clients = new Map<string, ClientRec>();\n const conns = new Set<Conn>();\n let pendingOps: WireOp[] = [];\n let dirtyAcks = new Set<string>();\n let pendingRes = new Map<string, StatewireResponse[]>();\n let inbox: Promise<void> = Promise.resolve();\n let finished: StatewireHost.Fin | null = null;\n let disposed = false;\n let flushScheduled = false;\n let heartbeatTimer: ReturnType<typeof setInterval> | null = null;\n\n let stateHost: StatewireHost.StateHost | null = null;\n let commandsHost: CommandsHost | null = null;\n let emitHost: ReturnType<typeof getEmitHost> | null = null;\n let current: Statewire<unknown> | null = null;\n\n const post = createMacrotaskPoster(() => {\n flushScheduled = false;\n flush();\n });\n const scheduleFlush = () => {\n if (flushScheduled || disposed) return;\n flushScheduled = true;\n post();\n };\n\n const recordOp = (op: {\n op: string;\n path?: StatewirePath;\n value?: unknown;\n }) => {\n if (op.op === \"replace\" || op.op === \"add\" || op.op === \"event\") {\n const { value, meta } = encodeValue(op.value);\n pendingOps.push({ ...op, value, ...(meta !== undefined && { meta }) });\n } else {\n pendingOps.push(op);\n }\n scheduleFlush();\n };\n\n const attach = (next: Statewire<unknown>) => {\n if (next === current) return;\n current = next;\n const nextState =\n typeof next.state === \"object\" && next.state !== null\n ? getStateHost(next.state)\n : undefined;\n const nextCommands = getCommandsHost(next.commands);\n const nextEmit = getEmitHost(next.emit);\n if (!nextState || !nextCommands?.submit || !nextEmit) {\n throw new Error(\n \"StatewireSocketHost requires a statewire/host transport (useStatewireState with object state, useStatewireCommands, useStatewireEmit)\",\n );\n }\n if (nextState !== stateHost) {\n stateHost?.setOnOp(null);\n stateHost = nextState;\n stateHost.setOnOp(recordOp);\n }\n commandsHost = nextCommands;\n if (nextEmit !== emitHost) {\n emitHost?.setSink(null);\n emitHost = nextEmit;\n emitHost.setSink((event) => recordOp({ op: \"event\", value: event }));\n }\n };\n\n const closeConn = (conn: Conn, code?: number, reason?: string) => {\n if (!conn.open) return;\n conn.open = false;\n conns.delete(conn);\n try {\n conn.socket.close(code, reason);\n } catch {\n /* peer already gone */\n }\n if (conns.size === 0 && heartbeatTimer !== null) {\n clearInterval(heartbeatTimer);\n heartbeatTimer = null;\n }\n };\n\n const sendTo = (conn: Conn, data: string) => {\n if (!conn.open) return;\n conn.lastSentAt = Date.now();\n try {\n conn.socket.send(data);\n } catch {\n closeConn(conn);\n }\n };\n\n const ensureHeartbeat = () => {\n if (heartbeatTimer !== null || heartbeatMs <= 0) return;\n heartbeatTimer = setInterval(() => {\n const now = Date.now();\n for (const conn of [...conns]) {\n if (now - conn.lastSentAt >= heartbeatMs) sendTo(conn, \"{}\");\n }\n }, heartbeatMs);\n (heartbeatTimer as unknown as { unref?: () => void }).unref?.();\n };\n\n const flush = (fin?: StatewireHost.Fin) => {\n flushTapSync(() => {});\n stateHost?.takeChanged();\n for (const rec of clients.values()) {\n for (const adm of rec.inflight.values()) {\n if (adm.acked && !adm.settled && !adm.pendingSent) {\n adm.pendingSent = true;\n queueRes(rec.id, { seq: adm.seq, type: \"pending\" });\n }\n }\n }\n if (\n pendingOps.length === 0 &&\n dirtyAcks.size === 0 &&\n pendingRes.size === 0 &&\n fin === undefined\n ) {\n return;\n }\n const ops = pendingOps;\n const acks = dirtyAcks;\n const res = pendingRes;\n pendingOps = [];\n dirtyAcks = new Set();\n pendingRes = new Map();\n for (const conn of [...conns]) {\n const envelope: StatewireEnvelope = {};\n if (ops.length > 0) envelope.ops = ops;\n const clientRes = res.get(conn.clientId);\n if (clientRes !== undefined) envelope.res = clientRes;\n if (acks.has(conn.clientId)) {\n const rec = clients.get(conn.clientId);\n if (rec !== undefined) envelope.ack = rec.lastSeq;\n }\n if (fin !== undefined) envelope.fin = fin;\n if (Object.keys(envelope).length === 0) continue;\n sendTo(conn, JSON.stringify(envelope));\n if (fin !== undefined) closeConn(conn);\n }\n };\n\n const queueRes = (clientId: string, response: StatewireResponse) => {\n const list = pendingRes.get(clientId);\n if (list) list.push(response);\n else pendingRes.set(clientId, [response]);\n };\n\n const advance = (rec: ClientRec, seq: number) => {\n rec.lastSeq = seq;\n dirtyAcks.add(rec.id);\n };\n\n const retain = (rec: ClientRec, response: StatewireResponse) => {\n const now = Date.now();\n rec.results.set(response.seq, { ts: now, res: response });\n const newest = Math.max(...rec.results.keys());\n for (const [seq, entry] of rec.results) {\n if (seq !== newest && now - entry.ts > RESULT_TTL_MS) {\n rec.results.delete(seq);\n }\n }\n while (rec.results.size > RESULT_LIMIT) {\n rec.results.delete(Math.min(...rec.results.keys()));\n }\n };\n\n const duplicateAnswer = (rec: ClientRec, seq: number): StatewireResponse => {\n const cached = rec.results.get(seq);\n if (cached) {\n const newest = Math.max(...rec.results.keys());\n if (seq === newest || Date.now() - cached.ts <= RESULT_TTL_MS) {\n return cached.res;\n }\n rec.results.delete(seq);\n }\n if (rec.inflight.has(seq)) return { seq, type: \"pending\" };\n return { seq, type: \"result-unavailable\" };\n };\n\n const settleResponse = (\n rec: ClientRec,\n adm: Admission,\n response: StatewireResponse,\n ) => {\n if (adm.settled) return;\n adm.settled = true;\n rec.inflight.delete(adm.seq);\n retain(rec, response);\n if (!adm.acked) {\n adm.acked = true;\n advance(rec, adm.seq);\n }\n if (\n adm.pendingSent ||\n response.type !== \"accepted\" ||\n response.payload !== undefined\n ) {\n queueRes(rec.id, response);\n }\n scheduleFlush();\n adm.release();\n };\n\n const settleWire = (\n rec: ClientRec,\n adm: Admission,\n outcome: CommandsOutcome,\n ) => settleResponse(rec, adm, responseOf(adm, outcome));\n\n const callerOf = (rec: ClientRec): StatewireHost.Caller => ({\n clientId: rec.id,\n get context() {\n return rec.context;\n },\n get isConnected() {\n if (clients.get(rec.id) !== rec) return false;\n for (const conn of conns) {\n if (conn.clientId === rec.id && conn.open) return true;\n }\n return false;\n },\n });\n\n const admit = (conn: Conn, frame: Frame): Promise<void> | void => {\n if (!conn.open || disposed || finished !== null) return;\n const rec = clients.get(conn.clientId)!;\n if (frame.seq <= rec.lastSeq || rec.inflight.has(frame.seq)) {\n queueRes(rec.id, duplicateAnswer(rec, frame.seq));\n scheduleFlush();\n return;\n }\n if (frame.seq > rec.lastSeq + 1) {\n closeConn(\n conn,\n 1008,\n `seq gap: expected ${rec.lastSeq + 1}, got ${frame.seq}`,\n );\n return;\n }\n const adm: Admission = {\n seq: frame.seq,\n method: frame.method ?? \"\",\n acked: false,\n pendingSent: false,\n settled: false,\n release: () => {},\n };\n if (frame.context !== undefined) {\n let value: Record<string, unknown>;\n try {\n value = contextValue(frame.context);\n } catch (error) {\n rec.inflight.set(frame.seq, adm);\n settleResponse(rec, adm, {\n seq: frame.seq,\n type: \"rejected\",\n code: \"invalid-context\",\n message: `invalid context: ${(error as Error).message}`,\n });\n return;\n }\n rec.context = value;\n if (frame.method === undefined) {\n rec.inflight.set(frame.seq, adm);\n settleResponse(rec, adm, { seq: frame.seq, type: \"accepted\" });\n return;\n }\n }\n const released = new Promise<void>((resolve) => {\n adm.release = resolve;\n });\n rec.inflight.set(frame.seq, adm);\n commandsHost!.submit!({\n method: frame.method!,\n params: frame.params,\n clientId: rec.id,\n caller: callerOf(rec),\n onAck: () => {\n if (adm.settled || adm.acked) return;\n adm.acked = true;\n advance(rec, adm.seq);\n scheduleFlush();\n adm.release();\n },\n onSettle: (outcome) => settleWire(rec, adm, outcome),\n }).catch((error: unknown) => {\n if (!adm.settled) settleWire(rec, adm, { type: \"rejected\", error });\n });\n return released;\n };\n\n const message = (conn: Conn, data: string) => {\n if (!conn.open || disposed || finished !== null) return;\n const frame = parseFrame(data);\n if (frame === null) {\n closeConn(conn, 1008, \"invalid frame\");\n return;\n }\n inbox = inbox.then(() => admit(conn, frame));\n };\n\n const connect = (\n clientId: string,\n socket: StatewireHostInternal.Socket,\n ): StatewireHostInternal.SocketConnection => {\n const dead: StatewireHostInternal.SocketConnection = {\n clientId,\n message: () => {},\n close: () => {},\n };\n const refuse = (code: number, reason: string) => {\n try {\n socket.close(code, reason);\n } catch {\n /* peer already gone */\n }\n return dead;\n };\n if (disposed) return refuse(1001, \"disposed\");\n if (!CLIENT_ID_RE.test(clientId)) return refuse(1008, \"invalid client id\");\n flush();\n if (finished !== null) {\n try {\n socket.send(JSON.stringify({ fin: finished }));\n } catch {\n /* peer already gone */\n }\n return refuse(1000, \"finished\");\n }\n const known = clients.has(clientId);\n const rec = known\n ? clients.get(clientId)!\n : (() => {\n const created: ClientRec = {\n id: clientId,\n lastSeq: 0,\n context: null,\n results: new Map(),\n inflight: new Map(),\n };\n clients.set(clientId, created);\n return created;\n })();\n for (const other of [...conns]) {\n if (other.clientId !== clientId) continue;\n sendTo(other, JSON.stringify({ fin: { reason: \"superseded\" } }));\n closeConn(other);\n }\n const conn: Conn = { clientId, socket, open: true, lastSentAt: 0 };\n conns.add(conn);\n ensureHeartbeat();\n const { value, meta } = encodeValue(stateHost!.snapshot());\n const snapshot: StatewireEnvelope = {\n ops: [\n {\n op: \"replace\",\n path: [],\n value,\n ...(meta !== undefined && { meta }),\n },\n ],\n syn: { lastSeq: known ? rec.lastSeq : -1, lease: randomId() },\n };\n sendTo(conn, JSON.stringify(snapshot));\n return {\n clientId,\n message: (data) => message(conn, data),\n close: () => closeConn(conn),\n };\n };\n\n const finish = (fin: StatewireHost.Fin) => {\n if (finished !== null || disposed) return;\n finished = fin;\n flush(fin);\n };\n\n return {\n connect,\n finish,\n attach,\n resume: () => {\n disposed = false;\n stateHost?.setOnOp(recordOp);\n emitHost?.setSink((event) => recordOp({ op: \"event\", value: event }));\n },\n suspend: () => {\n disposed = true;\n if (heartbeatTimer !== null) {\n clearInterval(heartbeatTimer);\n heartbeatTimer = null;\n }\n stateHost?.setOnOp(null);\n emitHost?.setSink(null);\n },\n };\n};\n\nconst useSocketHost = (\n transport: ResourceElement<Statewire>,\n options?: StatewireHostInternal.SocketHostOptions,\n): Pick<StatewireHostInternal.SocketHost, \"connect\" | \"finish\"> => {\n const [h] = useState(() => createSocketHost(options));\n const value = useResource(transport) as Statewire<unknown>;\n h.attach(value);\n useEffect(() => {\n h.attach(value);\n });\n useEffect(() => {\n h.resume();\n return () => h.suspend();\n }, [h]);\n return h;\n};\n\n/**\n * Transport-agnostic WS binding over a statewire/host transport:\n * feed it sockets (anything with `send`/`close`) and raw text frames, and it\n * speaks the Statewire envelope protocol — snapshot-first attach with `syn`,\n * delta/ack/res envelopes, per-client-id supersede, and `fin` on finish.\n */\nexport const StatewireSocketHost = (\n transport: ResourceElement<Statewire>,\n options?: StatewireHostInternal.SocketHostOptions,\n): StatewireHostInternal.SocketHost => {\n const SocketHostRoot = () => useSocketHost(transport, options);\n const root = createTapRoot(SocketHostRoot);\n let disposed = false;\n return {\n connect: (clientId, socket) =>\n flushTapSync(() => root.getValue().connect(clientId, socket)),\n finish: (fin) => flushTapSync(() => root.getValue().finish(fin)),\n dispose: () => {\n if (disposed) return;\n disposed = true;\n flushTapSync(() => root.getValue().finish({ reason: \"evicted\" }));\n root.unmount();\n },\n };\n};\n"],"mappings":"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"}
package/dist/host.d.ts CHANGED
@@ -1,10 +1,8 @@
1
1
  import { Statewire } from "./types.js";
2
2
  import { ServerOp, append } from "./host/draft.js";
3
3
  import { StatewireReject } from "./host/engine.js";
4
- import { CommandExecution, getStateHost, useStatewireCommands, useStatewireEmit, useStatewireState } from "./host/host.js";
4
+ import { getStateHost, useStatewireCommands, useStatewireEmit, useStatewireState } from "./host/host.js";
5
5
  //#region src/host.d.ts
6
- declare const executionResult: unique symbol;
7
- type UnwrapExecution<V> = V extends StatewireHost.Execution<infer T> ? T : V;
8
6
  declare namespace StatewireHost {
9
7
  type StateCtl<TState = unknown> = {
10
8
  /** Apply server-vocabulary ops (root replace = whole-state swap). */
@@ -18,30 +16,28 @@ declare namespace StatewireHost {
18
16
  };
19
17
  type StateSource = Statewire.StateSource;
20
18
  /**
21
- * Handle a command handler returns to keep the command open past its own
22
- * return: `ack()` releases the client's optimistic overlay / ack watermark,
23
- * `resolve`/`reject` settle the command (and imply ack). `ack` is idempotent
24
- * and a no-op after settle.
19
+ * Per-invocation handle bound as `this` of a command handler: `caller` is
20
+ * the submitting client, `ack()` releases the client's optimistic overlay /
21
+ * ack watermark before the handler settles (idempotent, a no-op after
22
+ * settle). The command settles when the handler's returned promise does.
25
23
  */
26
- type Execution<T = void> = {
27
- readonly [executionResult]: (value: T) => void;
28
- };
29
- type ExecutionResolvers<T = void> = {
30
- ack: () => void;
31
- resolve: (value: T) => void;
32
- reject: (error: unknown) => void;
24
+ type Ctx = {
25
+ readonly caller: Caller;
26
+ readonly ack: () => void;
33
27
  };
34
28
  /**
35
- * The submitting client, bound as `this` of a command handler. Holdable
36
- * across a long run: `context` reads the client's current ambient context
37
- * live (null until the client sends one), `isConnected` reports whether the
38
- * client has an active stream attachment.
29
+ * The submitting client (`this.caller` in a command handler). Holdable
30
+ * across a long run: `clientId` is the client's `Statewire-Client-Id`
31
+ * (in-process dispatch has none — reading it throws), `context` reads the
32
+ * client's current ambient context live (null until the client sends one),
33
+ * `isConnected` reports whether the client has an active stream attachment.
39
34
  */
40
35
  type Caller = {
36
+ readonly clientId: string;
41
37
  readonly context: Record<string, unknown> | null;
42
38
  readonly isConnected: boolean;
43
39
  };
44
- type HandledCommands<THandlers extends Statewire.Commands> = { [K in keyof THandlers]: (...args: Parameters<THandlers[K]>) => UnwrapExecution<Awaited<ReturnType<THandlers[K]>>>; };
40
+ type HandledCommands<THandlers extends Statewire.Commands> = { [K in keyof THandlers]: (...args: Parameters<THandlers[K]>) => Awaited<ReturnType<THandlers[K]>>; };
45
41
  type Fin = {
46
42
  reason: "evicted" | "error" | "gone" | "superseded" | (string & {});
47
43
  message?: string;
@@ -50,5 +46,5 @@ declare namespace StatewireHost {
50
46
  };
51
47
  }
52
48
  //#endregion
53
- export { CommandExecution, StatewireHost, StatewireReject, append, getStateHost, useStatewireCommands, useStatewireEmit, useStatewireState };
49
+ export { StatewireHost, StatewireReject, append, getStateHost, useStatewireCommands, useStatewireEmit, useStatewireState };
54
50
  //# sourceMappingURL=host.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"host.d.ts","names":[],"sources":["../src/host.ts"],"mappings":";;;;;cAac;KAET,gBAAgB,KAAK,UAAU,cAAc,gBAAgB,KAAK,IAAI;kBAE1D;OACH,SAAS;;IAEnB,QAAQ,cAAc,UAAU;;IAEhC,gBAAgB;;OAGN,YAAY,UAAU;;IAEhC,UAAU,YAAY,IAAI;;OAGhB,cAAc,UAAU;;;;;;;OAQxB,UAAU;cACV,mBAAmB,OAAO;;OAG1B,mBAAmB;IAC7B;IACA,UAAU,OAAO;IACjB,SAAS;;;;;;;;OASC;aACD,SAAS;aACT;;OAGC,gBAAgB,kBAAkB,UAAU,eACrD,WAAW,gBACP,MAAM,WAAW,UAAU,QAC3B,gBAAgB,QAAQ,WAAW,UAAU;OAGxC;IACV;IACA;IACA;IACA"}
1
+ {"version":3,"file":"host.d.ts","names":[],"sources":["../src/host.ts"],"mappings":";;;;;kBAYiB;OACH,SAAS;;IAEnB,QAAQ,cAAc,UAAU;;IAEhC,gBAAgB;;OAGN,YAAY,UAAU;;IAEhC,UAAU,YAAY,IAAI;;OAGhB,cAAc,UAAU;;;;;;;OAQxB;aACD,QAAQ;aACR;;;;;;;;;OAUC;aACD;aACA,SAAS;aACT;;OAGC,gBAAgB,kBAAkB,UAAU,eACrD,WAAW,gBACP,MAAM,WAAW,UAAU,QAC3B,QAAQ,WAAW,UAAU;OAGxB;IACV;IACA;IACA;IACA"}
package/dist/host.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { StatewireReject } from "./host/engine.js";
2
2
  import { append } from "./host/draft.js";
3
- import { CommandExecution, getStateHost, useStatewireCommands, useStatewireEmit, useStatewireState } from "./host/host.js";
4
- export { CommandExecution, StatewireReject, append, getStateHost, useStatewireCommands, useStatewireEmit, useStatewireState };
3
+ import { getStateHost, useStatewireCommands, useStatewireEmit, useStatewireState } from "./host/host.js";
4
+ export { StatewireReject, append, getStateHost, useStatewireCommands, useStatewireEmit, useStatewireState };
@@ -98,16 +98,28 @@ declare namespace StatewireClient {
98
98
  cause: "server" | "unmount" | "stream-gone" | "session-lost" | "superseded";
99
99
  } | {
100
100
  fate: "aborted";
101
- cause: "cascade" | "unmount" | "stream-gone" | "session-lost" | "superseded";
101
+ cause: "unmount" | "stream-gone" | "session-lost" | "superseded";
102
+ predecessorLost?: true;
102
103
  } | {
103
104
  fate: "lost";
104
105
  cause: "unmount" | "stream-gone" | "session-lost" | "superseded";
105
106
  };
107
+ /**
108
+ * A pending command offered for replay while the session is being evicted.
109
+ * Return "replay" to re-enqueue it under the rotated client id instead of
110
+ * settling it as a session-lost failure.
111
+ */
112
+ type SessionLostCommand = {
113
+ method: string;
114
+ params: unknown[];
115
+ seq: number;
116
+ };
106
117
  type SSEOptions = StatewireSessionStoreOptions & {
107
118
  url: string;
108
119
  headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
109
120
  fetch?: typeof globalThis.fetch;
110
121
  onChange?: (connection: Connection) => void;
122
+ onCommandSessionLost?: (command: SessionLostCommand) => "replay" | void;
111
123
  };
112
124
  type WSOptions = StatewireSessionStoreOptions & {
113
125
  url: string;
@@ -115,6 +127,7 @@ declare namespace StatewireClient {
115
127
  query?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
116
128
  webSocket?: new (url: string, protocols?: string | string[]) => WebSocket;
117
129
  onChange?: (connection: Connection) => void;
130
+ onCommandSessionLost?: (command: SessionLostCommand) => "replay" | void;
118
131
  };
119
132
  }
120
133
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"statewire.d.ts","names":[],"sources":["../src/statewire.ts"],"mappings":";;;;cAYa,eACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW,YACnD,gBAAgB,QAAQ,QAAQ;cAqCtB,oBACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW,YACnD,gBAAgB,gBAAgB,QAAQ,QAAQ;cAGtC,gBACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD;mBAEiB;UACT;EAEI,YAAA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW;MAK5D,SAAS;MAIT,cAAc,gBAAgB;MAI9B,YAAY,UAAU,cAAc;EAIxC,WAAW,OAAO,0BAA0B;EAI5C,UAAU;EAIV;;kBAOe;OACH,QACV,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD;IAEA,WAAW,gBAAgB,UAAU,QAAQ,WAAW;IACxD,aAAa,WAAW,YAAY,SAAS;IAC7C,WAAW,OAAO,QAAQ;MAAO,OAAO,YAAY;;IACpD,iBACE,OAAO,YAAY,SACnB;MAAO,SAAS,UAAU;;IAE5B,mBACE,QAAQ,UAAU,cAAc,YAChC;MAAO,OAAO,YAAY;;;OAIlB,QACV,QACA,kBAAkB,UAAU,WAAW,UAAU;;IAGjD,OAAO;;IAEP,gBAAgB;IAChB,YAAY;IACZ,UAAU,UAAU,cAAc;;IAElC,aAAa,OAAO,4BAA4B;;OAGtC;IACN;;IACA;;IAEA;IACA;IACA;IACA;IACA;IACA;;IAEA;IAAgB;IAAkB;;OAC5B,aAAa;IAAoB;;OACjC,mBAAmB;;;;;OAMnB,WACV,QACA,kBAAkB,UAAU,WAAW,UAAU,aAEjD,OAAO,QACP,SAAS,UAAU,QAAQ,YAC3B,KAAK;OAGK;IACV;IACA;IACA;;;;;;;;;;;OAYU;IACN;IAAkB;IAAiB;IAAiB;;IACpD;IAAyB;;IACzB;IAAwB;IAAiB;;IACzC;IAAiB;;IAEjB;IACA;;IAQA;IACA;;IAQA;IACA;;OAGM,aAAa;IACvB;IACA,UAAU,qBAAqB,cAAc,QAAQ;IACrD,eAAe,WAAW;IAC1B,YAAY,YAAY;;OAGd,YAAY;IACtB;IACA;IACA,QACI,gCACO,yBAAyB,QAAQ;IAC5C,iBAAiB,aAAa,kCAAkC;IAChE,YAAY,YAAY"}
1
+ {"version":3,"file":"statewire.d.ts","names":[],"sources":["../src/statewire.ts"],"mappings":";;;;cAYa,eACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW,YACnD,gBAAgB,QAAQ,QAAQ;cAqCtB,oBACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW,YACnD,gBAAgB,gBAAgB,QAAQ,QAAQ;cAGtC,gBACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD;mBAEiB;UACT;EAEI,YAAA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW;MAK5D,SAAS;MAIT,cAAc,gBAAgB;MAI9B,YAAY,UAAU,cAAc;EAIxC,WAAW,OAAO,0BAA0B;EAI5C,UAAU;EAIV;;kBAOe;OACH,QACV,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD;IAEA,WAAW,gBAAgB,UAAU,QAAQ,WAAW;IACxD,aAAa,WAAW,YAAY,SAAS;IAC7C,WAAW,OAAO,QAAQ;MAAO,OAAO,YAAY;;IACpD,iBACE,OAAO,YAAY,SACnB;MAAO,SAAS,UAAU;;IAE5B,mBACE,QAAQ,UAAU,cAAc,YAChC;MAAO,OAAO,YAAY;;;OAIlB,QACV,QACA,kBAAkB,UAAU,WAAW,UAAU;;IAGjD,OAAO;;IAEP,gBAAgB;IAChB,YAAY;IACZ,UAAU,UAAU,cAAc;;IAElC,aAAa,OAAO,4BAA4B;;OAGtC;IACN;;IACA;;IAEA;IACA;IACA;IACA;IACA;IACA;;IAEA;IAAgB;IAAkB;;OAC5B,aAAa;IAAoB;;OACjC,mBAAmB;;;;;OAMnB,WACV,QACA,kBAAkB,UAAU,WAAW,UAAU,aAEjD,OAAO,QACP,SAAS,UAAU,QAAQ,YAC3B,KAAK;OAGK;IACV;IACA;IACA;;;;;;;;;;;OAYU;IACN;IAAkB;IAAiB;IAAiB;;IACpD;IAAyB;;IACzB;IAAwB;IAAiB;;IACzC;IAAiB;;IAEjB;IACA;;IAQA;IACA;IACA;;IAGA;IACA;;;;;;;OAQM;IACV;IACA;IACA;;OAGU,aAAa;IACvB;IACA,UAAU,qBAAqB,cAAc,QAAQ;IACrD,eAAe,WAAW;IAC1B,YAAY,YAAY;IACxB,wBAAwB,SAAS;;OAGvB,YAAY;IACtB;IACA;IACA,QACI,gCACO,yBAAyB,QAAQ;IAC5C,iBAAiB,aAAa,kCAAkC;IAChE,YAAY,YAAY;IACxB,wBAAwB,SAAS"}
@@ -1 +1 @@
1
- {"version":3,"file":"statewire.js","names":["useMemo","useRef","useState","useSyncExternalStore","createTapRoot","flushTapSync","resource","useResource","ResourceElement","LIVE_CONNECTION","useStatewireCore","Statewire","StatewireSessionStoreOptions","useStatewire","Commands","options","StatewireClient","Options","TState","TCommands","TEvent","Helpers","latest","current","value","transport","core","subscribe","listener","state","getState","connection","commands","CommandsProxy","setContext","contextValue","Record","Error","StatewireResource","root","disposed","constructor","ClientRoot","getValue","Connection","Promise","dispose","unmount","optimistic","Optimistic","NonNullable","onEvent","event","ctx","onStateChange","changes","Changes","onCommandChange","update","CommandUpdate","ConnectionState","status","cause","attempt","nextRetryAt","lastError","message","payload","reconnect","ConnectionStatus","draft","command","Command","OptimisticContext","ts","seq","clientId","SendFailure","fate","SSEOptions","url","headers","HeadersInit","fetch","globalThis","onChange","WSOptions","protocols","query","webSocket","WebSocket"],"sources":["../src/statewire.ts"],"sourcesContent":["import { useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport {\n createTapRoot,\n flushTapSync,\n resource,\n useResource,\n} from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { LIVE_CONNECTION, useStatewireCore } from \"./client/core\";\nimport type { Statewire } from \"./types\";\nimport type { StatewireSessionStoreOptions } from \"./client/sessionStore\";\n\nexport const useStatewire = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.Options<TState, TCommands, TEvent>,\n): StatewireClient.Helpers<TState, TCommands> => {\n const latest = useRef(options);\n latest.current = options;\n\n const value = useResource(options.transport) as Statewire<unknown>;\n const core = useStatewireCore(() => latest.current, value);\n const [subscribe] = useState(\n () => (listener: () => void) =>\n core.subscribe(() => flushTapSync(listener)),\n );\n\n const state = useSyncExternalStore(\n subscribe,\n core.getState,\n core.getState,\n ) as TState;\n const connection = value.connection ?? LIVE_CONNECTION;\n\n return useMemo(\n () => ({\n state,\n getState: core.getState as () => TState,\n connection,\n commands: core.commands as Statewire.CommandsProxy<TCommands>,\n setContext: (contextValue: Record<string, unknown>) => {\n if (!value.setContext) {\n throw new Error(\n \"statewire: this transport has no client context channel\",\n );\n }\n return value.setContext(contextValue);\n },\n }),\n [state, connection, core, value],\n );\n};\n\nexport const StatewireResource = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.Options<TState, TCommands, TEvent>,\n): ResourceElement<StatewireClient.Helpers<TState, TCommands>> =>\n resource(useStatewire<TState, TCommands, TEvent>)(options);\n\nexport class StatewireClient<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n> {\n private readonly root;\n private disposed = false;\n\n constructor(options: StatewireClient.Options<TState, TCommands, TEvent>) {\n const ClientRoot = () => useStatewire<TState, TCommands, TEvent>(options);\n this.root = createTapRoot(ClientRoot);\n }\n\n get state(): TState {\n return this.root.getValue().getState();\n }\n\n get connection(): StatewireClient.Connection {\n return this.root.getValue().connection;\n }\n\n get commands(): Statewire.CommandsProxy<TCommands> {\n return this.root.getValue().commands;\n }\n\n setContext(value: Record<string, unknown>): Promise<unknown> {\n return this.root.getValue().setContext(value);\n }\n\n subscribe(listener: () => void): () => void {\n return this.root.subscribe(listener);\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n this.root.unmount();\n }\n}\n\nexport namespace StatewireClient {\n export type Options<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n > = {\n transport: ResourceElement<Statewire<TState, TCommands, TEvent>>;\n optimistic?: Optimistic<NonNullable<TState>, TCommands>;\n onEvent?: (event: TEvent, ctx: { state: NonNullable<TState> }) => void;\n onStateChange?: (\n state: NonNullable<TState>,\n ctx: { changes: Statewire.Changes },\n ) => void;\n onCommandChange?: (\n update: Statewire.CommandUpdate<TCommands>,\n ctx: { state: NonNullable<TState> },\n ) => void;\n };\n\n export type Helpers<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = {\n /** The statewire's state type — remote statewires include `undefined` until the first attach snapshot applies. */\n state: TState;\n /** Identity-stable across renders; reads the current canonical state without subscribing the caller. */\n getState: () => TState;\n connection: Connection;\n commands: Statewire.CommandsProxy<TCommands>;\n /** Replace this client's ambient context on the server; throws on transports without a client context channel. */\n setContext: (value: Record<string, unknown>) => Promise<unknown>;\n };\n\n export type ConnectionState =\n | { status: \"connecting\" }\n | { status: \"live\" }\n | {\n status: \"reconnecting\";\n cause: \"dropped\" | \"evicted\" | \"error\";\n attempt: number;\n nextRetryAt: number;\n lastError?: string;\n message?: string;\n }\n | { status: \"gone\"; message?: string; payload?: unknown };\n export type Connection = ConnectionState & { reconnect: () => void };\n export type ConnectionStatus = Connection[\"status\"];\n\n /**\n * Mutate the draft in place; the client records the next state plus the exact\n * changed paths. Re-run against the new base on every rebase.\n */\n export type Optimistic<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = (\n draft: TState,\n command: Statewire.Command<TCommands>,\n ctx: OptimisticContext,\n ) => void;\n\n export type OptimisticContext = {\n ts: number;\n seq: number;\n clientId?: string;\n };\n\n /**\n * fate: \"aborted\" = never sent, \"rejected\" = the handler refused,\n * \"unknown-command\" = no such command, \"invalid-params\" = the args did not\n * match the command's schema, \"lost\" = sent but never covered by an ack,\n * \"crashed\" = acked but the processing died, \"result-unavailable\" = acked\n * but the result is gone.\n * cause is which channel settled it: \"server\" = the server's answer;\n * the rest are channel deaths the client settled locally.\n */\n export type SendFailure =\n | { fate: \"rejected\"; cause: \"server\"; message: string; payload?: unknown }\n | { fate: \"unknown-command\"; cause: \"server\" }\n | { fate: \"invalid-params\"; cause: \"server\"; message: string }\n | { fate: \"crashed\"; cause: \"server\" }\n | {\n fate: \"result-unavailable\";\n cause:\n | \"server\"\n | \"unmount\"\n | \"stream-gone\"\n | \"session-lost\"\n | \"superseded\";\n }\n | {\n fate: \"aborted\";\n cause:\n | \"cascade\"\n | \"unmount\"\n | \"stream-gone\"\n | \"session-lost\"\n | \"superseded\";\n }\n | {\n fate: \"lost\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\" | \"superseded\";\n };\n\n export type SSEOptions = StatewireSessionStoreOptions & {\n url: string;\n headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n fetch?: typeof globalThis.fetch;\n onChange?: (connection: Connection) => void;\n };\n\n export type WSOptions = StatewireSessionStoreOptions & {\n url: string;\n protocols?: string | string[];\n query?:\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>);\n webSocket?: new (url: string, protocols?: string | string[]) => WebSocket;\n onChange?: (connection: Connection) => void;\n };\n}\n"],"mappings":";;;;AAYA,MAAaa,gBAKXE,YAC+C;CAC/C,MAAMO,SAASrB,OAAOc,OAAO;CAC7BO,OAAOC,UAAUR;CAEjB,MAAMS,QAAQjB,YAAYQ,QAAQU,SAAS;CAC3C,MAAMC,OAAOhB,uBAAuBY,OAAOC,SAASC,KAAK;CACzD,MAAM,CAACG,aAAazB,gBACX0B,aACLF,KAAKC,gBAAgBtB,aAAauB,QAAQ,CAAC,CAC/C;CAEA,MAAMC,QAAQ1B,qBACZwB,WACAD,KAAKI,UACLJ,KAAKI,QACP;CACA,MAAMC,aAAaP,MAAMO,cAActB;CAEvC,OAAOT,eACE;EACL6B;EACAC,UAAUJ,KAAKI;EACfC;EACAC,UAAUN,KAAKM;EACfE,aAAaC,iBAA0C;GACrD,IAAI,CAACX,MAAMU,YACT,MAAM,IAAIG,MACR,yDACF;GAEF,OAAOb,MAAMU,WAAWC,YAAY;EACtC;CACF,IACA;EAACN;EAAOE;EAAYL;EAAMF;CAAK,CACjC;AACF;AAEA,MAAac,qBAKXvB,YAEAT,SAASO,YAAuC,CAAC,CAACE,OAAO;AAE3D,IAAaC,kBAAb,MAIE;CACA;CACA,WAAmB;CAEnByB,YAAY1B,SAA6D;EACvE,MAAM2B,mBAAmB7B,aAAwCE,OAAO;EACxE,KAAKwB,OAAOnC,cAAcsC,UAAU;CACtC;CAEA,IAAIb,QAAgB;EAClB,OAAO,KAAKU,KAAKI,SAAS,CAAC,CAACb,SAAS;CACvC;CAEA,IAAIC,aAAyC;EAC3C,OAAO,KAAKQ,KAAKI,SAAS,CAAC,CAACZ;CAC9B;CAEA,IAAIC,WAA+C;EACjD,OAAO,KAAKO,KAAKI,SAAS,CAAC,CAACX;CAC9B;CAEAE,WAAWV,OAAkD;EAC3D,OAAO,KAAKe,KAAKI,SAAS,CAAC,CAACT,WAAWV,KAAK;CAC9C;CAEAG,UAAUC,UAAkC;EAC1C,OAAO,KAAKW,KAAKZ,UAAUC,QAAQ;CACrC;CAEAkB,UAAgB;EACd,IAAI,KAAKN,UAAU;EACnB,KAAKA,WAAW;EAChB,KAAKD,KAAKQ,QAAQ;CACpB;AACF"}
1
+ {"version":3,"file":"statewire.js","names":["useMemo","useRef","useState","useSyncExternalStore","createTapRoot","flushTapSync","resource","useResource","ResourceElement","LIVE_CONNECTION","useStatewireCore","Statewire","StatewireSessionStoreOptions","useStatewire","Commands","options","StatewireClient","Options","TState","TCommands","TEvent","Helpers","latest","current","value","transport","core","subscribe","listener","state","getState","connection","commands","CommandsProxy","setContext","contextValue","Record","Error","StatewireResource","root","disposed","constructor","ClientRoot","getValue","Connection","Promise","dispose","unmount","optimistic","Optimistic","NonNullable","onEvent","event","ctx","onStateChange","changes","Changes","onCommandChange","update","CommandUpdate","ConnectionState","status","cause","attempt","nextRetryAt","lastError","message","payload","reconnect","ConnectionStatus","draft","command","Command","OptimisticContext","ts","seq","clientId","SendFailure","fate","predecessorLost","SessionLostCommand","method","params","SSEOptions","url","headers","HeadersInit","fetch","globalThis","onChange","onCommandSessionLost","WSOptions","protocols","query","webSocket","WebSocket"],"sources":["../src/statewire.ts"],"sourcesContent":["import { useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport {\n createTapRoot,\n flushTapSync,\n resource,\n useResource,\n} from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { LIVE_CONNECTION, useStatewireCore } from \"./client/core\";\nimport type { Statewire } from \"./types\";\nimport type { StatewireSessionStoreOptions } from \"./client/sessionStore\";\n\nexport const useStatewire = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.Options<TState, TCommands, TEvent>,\n): StatewireClient.Helpers<TState, TCommands> => {\n const latest = useRef(options);\n latest.current = options;\n\n const value = useResource(options.transport) as Statewire<unknown>;\n const core = useStatewireCore(() => latest.current, value);\n const [subscribe] = useState(\n () => (listener: () => void) =>\n core.subscribe(() => flushTapSync(listener)),\n );\n\n const state = useSyncExternalStore(\n subscribe,\n core.getState,\n core.getState,\n ) as TState;\n const connection = value.connection ?? LIVE_CONNECTION;\n\n return useMemo(\n () => ({\n state,\n getState: core.getState as () => TState,\n connection,\n commands: core.commands as Statewire.CommandsProxy<TCommands>,\n setContext: (contextValue: Record<string, unknown>) => {\n if (!value.setContext) {\n throw new Error(\n \"statewire: this transport has no client context channel\",\n );\n }\n return value.setContext(contextValue);\n },\n }),\n [state, connection, core, value],\n );\n};\n\nexport const StatewireResource = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.Options<TState, TCommands, TEvent>,\n): ResourceElement<StatewireClient.Helpers<TState, TCommands>> =>\n resource(useStatewire<TState, TCommands, TEvent>)(options);\n\nexport class StatewireClient<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n> {\n private readonly root;\n private disposed = false;\n\n constructor(options: StatewireClient.Options<TState, TCommands, TEvent>) {\n const ClientRoot = () => useStatewire<TState, TCommands, TEvent>(options);\n this.root = createTapRoot(ClientRoot);\n }\n\n get state(): TState {\n return this.root.getValue().getState();\n }\n\n get connection(): StatewireClient.Connection {\n return this.root.getValue().connection;\n }\n\n get commands(): Statewire.CommandsProxy<TCommands> {\n return this.root.getValue().commands;\n }\n\n setContext(value: Record<string, unknown>): Promise<unknown> {\n return this.root.getValue().setContext(value);\n }\n\n subscribe(listener: () => void): () => void {\n return this.root.subscribe(listener);\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n this.root.unmount();\n }\n}\n\nexport namespace StatewireClient {\n export type Options<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n > = {\n transport: ResourceElement<Statewire<TState, TCommands, TEvent>>;\n optimistic?: Optimistic<NonNullable<TState>, TCommands>;\n onEvent?: (event: TEvent, ctx: { state: NonNullable<TState> }) => void;\n onStateChange?: (\n state: NonNullable<TState>,\n ctx: { changes: Statewire.Changes },\n ) => void;\n onCommandChange?: (\n update: Statewire.CommandUpdate<TCommands>,\n ctx: { state: NonNullable<TState> },\n ) => void;\n };\n\n export type Helpers<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = {\n /** The statewire's state type — remote statewires include `undefined` until the first attach snapshot applies. */\n state: TState;\n /** Identity-stable across renders; reads the current canonical state without subscribing the caller. */\n getState: () => TState;\n connection: Connection;\n commands: Statewire.CommandsProxy<TCommands>;\n /** Replace this client's ambient context on the server; throws on transports without a client context channel. */\n setContext: (value: Record<string, unknown>) => Promise<unknown>;\n };\n\n export type ConnectionState =\n | { status: \"connecting\" }\n | { status: \"live\" }\n | {\n status: \"reconnecting\";\n cause: \"dropped\" | \"evicted\" | \"error\";\n attempt: number;\n nextRetryAt: number;\n lastError?: string;\n message?: string;\n }\n | { status: \"gone\"; message?: string; payload?: unknown };\n export type Connection = ConnectionState & { reconnect: () => void };\n export type ConnectionStatus = Connection[\"status\"];\n\n /**\n * Mutate the draft in place; the client records the next state plus the exact\n * changed paths. Re-run against the new base on every rebase.\n */\n export type Optimistic<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = (\n draft: TState,\n command: Statewire.Command<TCommands>,\n ctx: OptimisticContext,\n ) => void;\n\n export type OptimisticContext = {\n ts: number;\n seq: number;\n clientId?: string;\n };\n\n /**\n * fate: \"aborted\" = never sent, \"rejected\" = the handler refused,\n * \"unknown-command\" = no such command, \"invalid-params\" = the args did not\n * match the command's schema, \"lost\" = sent but never covered by an ack,\n * \"crashed\" = acked but the processing died, \"result-unavailable\" = acked\n * but the result is gone.\n * cause is which channel settled it: \"server\" = the server's answer;\n * the rest are channel deaths the client settled locally.\n */\n export type SendFailure =\n | { fate: \"rejected\"; cause: \"server\"; message: string; payload?: unknown }\n | { fate: \"unknown-command\"; cause: \"server\" }\n | { fate: \"invalid-params\"; cause: \"server\"; message: string }\n | { fate: \"crashed\"; cause: \"server\" }\n | {\n fate: \"result-unavailable\";\n cause:\n | \"server\"\n | \"unmount\"\n | \"stream-gone\"\n | \"session-lost\"\n | \"superseded\";\n }\n | {\n fate: \"aborted\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\" | \"superseded\";\n predecessorLost?: true;\n }\n | {\n fate: \"lost\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\" | \"superseded\";\n };\n\n /**\n * A pending command offered for replay while the session is being evicted.\n * Return \"replay\" to re-enqueue it under the rotated client id instead of\n * settling it as a session-lost failure.\n */\n export type SessionLostCommand = {\n method: string;\n params: unknown[];\n seq: number;\n };\n\n export type SSEOptions = StatewireSessionStoreOptions & {\n url: string;\n headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n fetch?: typeof globalThis.fetch;\n onChange?: (connection: Connection) => void;\n onCommandSessionLost?: (command: SessionLostCommand) => \"replay\" | void;\n };\n\n export type WSOptions = StatewireSessionStoreOptions & {\n url: string;\n protocols?: string | string[];\n query?:\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>);\n webSocket?: new (url: string, protocols?: string | string[]) => WebSocket;\n onChange?: (connection: Connection) => void;\n onCommandSessionLost?: (command: SessionLostCommand) => \"replay\" | void;\n };\n}\n"],"mappings":";;;;AAYA,MAAaa,gBAKXE,YAC+C;CAC/C,MAAMO,SAASrB,OAAOc,OAAO;CAC7BO,OAAOC,UAAUR;CAEjB,MAAMS,QAAQjB,YAAYQ,QAAQU,SAAS;CAC3C,MAAMC,OAAOhB,uBAAuBY,OAAOC,SAASC,KAAK;CACzD,MAAM,CAACG,aAAazB,gBACX0B,aACLF,KAAKC,gBAAgBtB,aAAauB,QAAQ,CAAC,CAC/C;CAEA,MAAMC,QAAQ1B,qBACZwB,WACAD,KAAKI,UACLJ,KAAKI,QACP;CACA,MAAMC,aAAaP,MAAMO,cAActB;CAEvC,OAAOT,eACE;EACL6B;EACAC,UAAUJ,KAAKI;EACfC;EACAC,UAAUN,KAAKM;EACfE,aAAaC,iBAA0C;GACrD,IAAI,CAACX,MAAMU,YACT,MAAM,IAAIG,MACR,yDACF;GAEF,OAAOb,MAAMU,WAAWC,YAAY;EACtC;CACF,IACA;EAACN;EAAOE;EAAYL;EAAMF;CAAK,CACjC;AACF;AAEA,MAAac,qBAKXvB,YAEAT,SAASO,YAAuC,CAAC,CAACE,OAAO;AAE3D,IAAaC,kBAAb,MAIE;CACA;CACA,WAAmB;CAEnByB,YAAY1B,SAA6D;EACvE,MAAM2B,mBAAmB7B,aAAwCE,OAAO;EACxE,KAAKwB,OAAOnC,cAAcsC,UAAU;CACtC;CAEA,IAAIb,QAAgB;EAClB,OAAO,KAAKU,KAAKI,SAAS,CAAC,CAACb,SAAS;CACvC;CAEA,IAAIC,aAAyC;EAC3C,OAAO,KAAKQ,KAAKI,SAAS,CAAC,CAACZ;CAC9B;CAEA,IAAIC,WAA+C;EACjD,OAAO,KAAKO,KAAKI,SAAS,CAAC,CAACX;CAC9B;CAEAE,WAAWV,OAAkD;EAC3D,OAAO,KAAKe,KAAKI,SAAS,CAAC,CAACT,WAAWV,KAAK;CAC9C;CAEAG,UAAUC,UAAkC;EAC1C,OAAO,KAAKW,KAAKZ,UAAUC,QAAQ;CACrC;CAEAkB,UAAgB;EACd,IAAI,KAAKN,UAAU;EACnB,KAAKA,WAAW;EAChB,KAAKD,KAAKQ,QAAQ;CACpB;AACF"}
@@ -1,12 +1,12 @@
1
- import { StatewireSessionStoreOptions } from "../client/sessionStore.js";
2
1
  import { StatewireClient } from "../statewire.js";
3
2
  import { Statewire } from "../types.js";
4
3
  import { RemoteLink, StatewireRemoteProps } from "./sse.js";
4
+ import { StatewireSessionOptions } from "../client/session.js";
5
5
  //#region src/transports/remote.d.ts
6
6
  declare const useStatewireRemote: <TOptions extends {
7
7
  url: string;
8
8
  onChange?: (connection: StatewireClient.Connection) => void;
9
- } & StatewireSessionStoreOptions>(options: TOptions, useLink: (options: TOptions, ctx: StatewireRemoteProps) => RemoteLink) => Statewire<any, any, any>;
9
+ } & StatewireSessionOptions>(options: TOptions, useLink: (options: TOptions, ctx: StatewireRemoteProps) => RemoteLink) => Statewire<any, any, any>;
10
10
  //#endregion
11
11
  export { useStatewireRemote };
12
12
  //# sourceMappingURL=remote.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"remote.d.ts","names":[],"sources":["../../src/transports/remote.ts"],"mappings":";;;;;cAYa,qBACX;EACE;EACA,YAAY,YAAY,gBAAgB;IACtC,8BAEJ,SAAS,UACT,UAAU,SAAS,UAAU,KAAK,yBAAyB,eAC1D"}
1
+ {"version":3,"file":"remote.d.ts","names":[],"sources":["../../src/transports/remote.ts"],"mappings":";;;;;cAYa,qBACX;EACE;EACA,YAAY,YAAY,gBAAgB;IACtC,yBAEJ,SAAS,UACT,UAAU,SAAS,UAAU,KAAK,yBAAyB,eAC1D"}
@@ -4,7 +4,10 @@ import { useEffect } from "@assistant-ui/tap/react-shim";
4
4
  //#region src/transports/remote.ts
5
5
  const useStatewireRemote = (options, useLink) => {
6
6
  const emit = useStatewireEmit();
7
- const s = useStatewireSession({ sessionStore: options.sessionStore }, emit);
7
+ const s = useStatewireSession({
8
+ sessionStore: options.sessionStore,
9
+ onCommandSessionLost: options.onCommandSessionLost
10
+ }, emit);
8
11
  const conn = useLink(options, s.ctx);
9
12
  useEffect(() => setTransport(s, conn), [s, conn]);
10
13
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"remote.js","names":["useEffect","useStatewireEmit","Statewire","setContext","setTransport","useStatewireSession","StatewireSessionStoreOptions","StatewireClient","RemoteLink","StatewireRemoteProps","useStatewireRemote","url","onChange","connection","Connection","options","TOptions","useLink","ctx","emit","s","sessionStore","conn","state","canonical","commands","source","value"],"sources":["../../src/transports/remote.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { useStatewireEmit } from \"../host/host\";\nimport type { Statewire } from \"../types\";\nimport {\n setContext,\n setTransport,\n useStatewireSession,\n} from \"../client/session\";\nimport type { StatewireSessionStoreOptions } from \"../client/sessionStore\";\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 } & StatewireSessionStoreOptions,\n>(\n options: TOptions,\n useLink: (options: TOptions, ctx: StatewireRemoteProps) => RemoteLink,\n): Statewire<any, any, any> => {\n const emit = useStatewireEmit<any>();\n const s = useStatewireSession({ sessionStore: options.sessionStore }, emit);\n const conn = useLink(options, s.ctx);\n useEffect(() => setTransport(s, conn), [s, conn]);\n\n return {\n state: s.canonical,\n commands: s.commands,\n emit,\n connection: conn.connection,\n source: s.source,\n setContext: (value) => setContext(s, value),\n };\n};\n"],"mappings":";;;;AAYA,MAAaU,sBAMXK,SACAE,YAC6B;CAC7B,MAAME,OAAOlB,iBAAsB;CACnC,MAAMmB,IAAIf,oBAAoB,EAAEgB,cAAcN,QAAQM,aAAa,GAAGF,IAAI;CAC1E,MAAMG,OAAOL,QAAQF,SAASK,EAAEF,GAAG;CACnClB,gBAAgBI,aAAagB,GAAGE,IAAI,GAAG,CAACF,GAAGE,IAAI,CAAC;CAEhD,OAAO;EACLC,OAAOH,EAAEI;EACTC,UAAUL,EAAEK;EACZN;EACAN,YAAYS,KAAKT;EACjBa,QAAQN,EAAEM;EACVvB,aAAawB,UAAUxB,WAAWiB,GAAGO,KAAK;CAC5C;AACF"}
1
+ {"version":3,"file":"remote.js","names":["useEffect","useStatewireEmit","Statewire","setContext","setTransport","useStatewireSession","StatewireSessionOptions","StatewireClient","RemoteLink","StatewireRemoteProps","useStatewireRemote","url","onChange","connection","Connection","options","TOptions","useLink","ctx","emit","s","sessionStore","onCommandSessionLost","conn","state","canonical","commands","source","value"],"sources":["../../src/transports/remote.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { useStatewireEmit } from \"../host/host\";\nimport type { Statewire } from \"../types\";\nimport {\n setContext,\n setTransport,\n useStatewireSession,\n} from \"../client/session\";\nimport type { StatewireSessionOptions } from \"../client/session\";\nimport type { StatewireClient } from \"../statewire\";\nimport type { RemoteLink, StatewireRemoteProps } from \"./sse\";\n\nexport const useStatewireRemote = <\n TOptions extends {\n url: string;\n onChange?: (connection: StatewireClient.Connection) => void;\n } & StatewireSessionOptions,\n>(\n options: TOptions,\n useLink: (options: TOptions, ctx: StatewireRemoteProps) => RemoteLink,\n): Statewire<any, any, any> => {\n const emit = useStatewireEmit<any>();\n const s = useStatewireSession(\n {\n sessionStore: options.sessionStore,\n onCommandSessionLost: options.onCommandSessionLost,\n },\n emit,\n );\n const conn = useLink(options, s.ctx);\n useEffect(() => setTransport(s, conn), [s, conn]);\n\n return {\n state: s.canonical,\n commands: s.commands,\n emit,\n connection: conn.connection,\n source: s.source,\n setContext: (value) => setContext(s, value),\n };\n};\n"],"mappings":";;;;AAYA,MAAaU,sBAMXK,SACAE,YAC6B;CAC7B,MAAME,OAAOlB,iBAAsB;CACnC,MAAMmB,IAAIf,oBACR;EACEgB,cAAcN,QAAQM;EACtBC,sBAAsBP,QAAQO;CAChC,GACAH,IACF;CACA,MAAMI,OAAON,QAAQF,SAASK,EAAEF,GAAG;CACnClB,gBAAgBI,aAAagB,GAAGG,IAAI,GAAG,CAACH,GAAGG,IAAI,CAAC;CAEhD,OAAO;EACLC,OAAOJ,EAAEK;EACTC,UAAUN,EAAEM;EACZP;EACAN,YAAYU,KAAKV;EACjBc,QAAQP,EAAEO;EACVxB,aAAayB,UAAUzB,WAAWiB,GAAGQ,KAAK;CAC5C;AACF"}
@@ -7,12 +7,14 @@ const walkStatePath = (root, path, mode) => {
7
7
  for (let depth = 0; depth < path.length; depth++) {
8
8
  const k = path[depth];
9
9
  const mustResolve = mode === "resolve" || mode === "parent" && depth < path.length - 1;
10
- if (mode !== "read") if (typeof k === "number") {
11
- if (!Array.isArray(node) || !Number.isInteger(k) || k < 0) return;
12
- if (mustResolve && k >= node.length) return void 0;
13
- } else {
14
- if (isUnsafeKey(k, path[depth - 1]) || typeof node !== "object" || node === null || Array.isArray(node)) return;
15
- if (mustResolve && !Object.hasOwn(node, k)) return void 0;
10
+ if (mode !== "read") {
11
+ if (typeof k === "number") {
12
+ if (!Array.isArray(node) || !Number.isInteger(k) || k < 0) return;
13
+ if (mustResolve && k >= node.length) return void 0;
14
+ } else {
15
+ if (isUnsafeKey(k, path[depth - 1]) || typeof node !== "object" || node === null || Array.isArray(node)) return;
16
+ if (mustResolve && !Object.hasOwn(node, k)) return void 0;
17
+ }
16
18
  }
17
19
  parent = node;
18
20
  key = k;
@@ -1 +1 @@
1
- {"version":3,"file":"statePath.js","names":["StatePath","isUnsafeKey","key","parent","WalkedPath","leaf","walkStatePath","root","path","mode","undefined","node","depth","length","k","mustResolve","Array","isArray","Number","isInteger","Object","hasOwn","Record","readAtPath"],"sources":["../../src/util/statePath.ts"],"sourcesContent":["export type StatePath = readonly (string | number)[];\n\nexport const isUnsafeKey = (\n key: string | number,\n parent: string | number | undefined,\n) => key === \"__proto__\" || (key === \"prototype\" && parent === \"constructor\");\n\nexport type WalkedPath = {\n /** Container of the final key; undefined when the path is empty. */\n parent: unknown;\n key: string | number;\n leaf: unknown;\n};\n\n// \"read\": lenient, never fails — missing steps yield an undefined leaf.\n// \"parent\": every step before the last must resolve; the leaf may not exist.\n// \"resolve\": every step including the last must resolve.\n// Strict modes require array+integer for number keys, plain object for string\n// keys, and reject unsafe (prototype-polluting) segments.\nexport const walkStatePath = (\n root: unknown,\n path: StatePath,\n mode: \"read\" | \"parent\" | \"resolve\",\n): WalkedPath | undefined => {\n let parent: unknown = undefined;\n let key: string | number = \"\";\n let node: unknown = root;\n for (let depth = 0; depth < path.length; depth++) {\n const k = path[depth]!;\n const mustResolve =\n mode === \"resolve\" || (mode === \"parent\" && depth < path.length - 1);\n if (mode !== \"read\") {\n if (typeof k === \"number\") {\n if (!Array.isArray(node) || !Number.isInteger(k) || k < 0) {\n return undefined;\n }\n if (mustResolve && k >= node.length) return undefined;\n } else {\n if (\n isUnsafeKey(k, path[depth - 1]) ||\n typeof node !== \"object\" ||\n node === null ||\n Array.isArray(node)\n ) {\n return undefined;\n }\n if (mustResolve && !Object.hasOwn(node, k)) return undefined;\n }\n }\n parent = node;\n key = k;\n node =\n typeof node === \"object\" && node !== null && Object.hasOwn(node, k)\n ? (node as Record<string | number, unknown>)[k]\n : undefined;\n }\n return { parent, key, leaf: node };\n};\n\nexport const readAtPath = (root: unknown, path: StatePath): unknown =>\n walkStatePath(root, path, \"read\")!.leaf;\n"],"mappings":";AAEA,MAAaC,eACXC,KACAC,WACGD,QAAQ,eAAgBA,QAAQ,eAAeC,WAAW;AAc/D,MAAaG,iBACXC,MACAC,MACAC,SAC2B;CAC3B,IAAIN,SAAkBO,KAAAA;CACtB,IAAIR,MAAuB;CAC3B,IAAIS,OAAgBJ;CACpB,KAAK,IAAIK,QAAQ,GAAGA,QAAQJ,KAAKK,QAAQD,SAAS;EAChD,MAAME,IAAIN,KAAKI;EACf,MAAMG,cACJN,SAAS,aAAcA,SAAS,YAAYG,QAAQJ,KAAKK,SAAS;EACpE,IAAIJ,SAAS,QACX,IAAI,OAAOK,MAAM,UAAU;GACzB,IAAI,CAACE,MAAMC,QAAQN,IAAI,KAAK,CAACO,OAAOC,UAAUL,CAAC,KAAKA,IAAI,GACtD;GAEF,IAAIC,eAAeD,KAAKH,KAAKE,QAAQ,OAAOH,KAAAA;EAC9C,OAAO;GACL,IACET,YAAYa,GAAGN,KAAKI,QAAQ,EAAE,KAC9B,OAAOD,SAAS,YAChBA,SAAS,QACTK,MAAMC,QAAQN,IAAI,GAElB;GAEF,IAAII,eAAe,CAACK,OAAOC,OAAOV,MAAMG,CAAC,GAAG,OAAOJ,KAAAA;EACrD;EAEFP,SAASQ;EACTT,MAAMY;EACNH,OACE,OAAOA,SAAS,YAAYA,SAAS,QAAQS,OAAOC,OAAOV,MAAMG,CAAC,IAC7DH,KAA0CG,KAC3CJ,KAAAA;CACR;CACA,OAAO;EAAEP;EAAQD;EAAKG,MAAMM;CAAK;AACnC;AAEA,MAAaY,cAAchB,MAAeC,SACxCF,cAAcC,MAAMC,MAAM,MAAM,CAAC,CAAEH"}
1
+ {"version":3,"file":"statePath.js","names":["StatePath","isUnsafeKey","key","parent","WalkedPath","leaf","walkStatePath","root","path","mode","undefined","node","depth","length","k","mustResolve","Array","isArray","Number","isInteger","Object","hasOwn","Record","readAtPath"],"sources":["../../src/util/statePath.ts"],"sourcesContent":["export type StatePath = readonly (string | number)[];\n\nexport const isUnsafeKey = (\n key: string | number,\n parent: string | number | undefined,\n) => key === \"__proto__\" || (key === \"prototype\" && parent === \"constructor\");\n\nexport type WalkedPath = {\n /** Container of the final key; undefined when the path is empty. */\n parent: unknown;\n key: string | number;\n leaf: unknown;\n};\n\n// \"read\": lenient, never fails — missing steps yield an undefined leaf.\n// \"parent\": every step before the last must resolve; the leaf may not exist.\n// \"resolve\": every step including the last must resolve.\n// Strict modes require array+integer for number keys, plain object for string\n// keys, and reject unsafe (prototype-polluting) segments.\nexport const walkStatePath = (\n root: unknown,\n path: StatePath,\n mode: \"read\" | \"parent\" | \"resolve\",\n): WalkedPath | undefined => {\n let parent: unknown = undefined;\n let key: string | number = \"\";\n let node: unknown = root;\n for (let depth = 0; depth < path.length; depth++) {\n const k = path[depth]!;\n const mustResolve =\n mode === \"resolve\" || (mode === \"parent\" && depth < path.length - 1);\n if (mode !== \"read\") {\n if (typeof k === \"number\") {\n if (!Array.isArray(node) || !Number.isInteger(k) || k < 0) {\n return undefined;\n }\n if (mustResolve && k >= node.length) return undefined;\n } else {\n if (\n isUnsafeKey(k, path[depth - 1]) ||\n typeof node !== \"object\" ||\n node === null ||\n Array.isArray(node)\n ) {\n return undefined;\n }\n if (mustResolve && !Object.hasOwn(node, k)) return undefined;\n }\n }\n parent = node;\n key = k;\n node =\n typeof node === \"object\" && node !== null && Object.hasOwn(node, k)\n ? (node as Record<string | number, unknown>)[k]\n : undefined;\n }\n return { parent, key, leaf: node };\n};\n\nexport const readAtPath = (root: unknown, path: StatePath): unknown =>\n walkStatePath(root, path, \"read\")!.leaf;\n"],"mappings":";AAEA,MAAaC,eACXC,KACAC,WACGD,QAAQ,eAAgBA,QAAQ,eAAeC,WAAW;AAc/D,MAAaG,iBACXC,MACAC,MACAC,SAC2B;CAC3B,IAAIN,SAAkBO,KAAAA;CACtB,IAAIR,MAAuB;CAC3B,IAAIS,OAAgBJ;CACpB,KAAK,IAAIK,QAAQ,GAAGA,QAAQJ,KAAKK,QAAQD,SAAS;EAChD,MAAME,IAAIN,KAAKI;EACf,MAAMG,cACJN,SAAS,aAAcA,SAAS,YAAYG,QAAQJ,KAAKK,SAAS;EACpE,IAAIJ,SAAS,QAAQ;GACnB,IAAI,OAAOK,MAAM,UAAU;IACzB,IAAI,CAACE,MAAMC,QAAQN,IAAI,KAAK,CAACO,OAAOC,UAAUL,CAAC,KAAKA,IAAI,GACtD;IAEF,IAAIC,eAAeD,KAAKH,KAAKE,QAAQ,OAAOH,KAAAA;GAC9C,OAAO;IACL,IACET,YAAYa,GAAGN,KAAKI,QAAQ,EAAE,KAC9B,OAAOD,SAAS,YAChBA,SAAS,QACTK,MAAMC,QAAQN,IAAI,GAElB;IAEF,IAAII,eAAe,CAACK,OAAOC,OAAOV,MAAMG,CAAC,GAAG,OAAOJ,KAAAA;GACrD;EACF;EACAP,SAASQ;EACTT,MAAMY;EACNH,OACE,OAAOA,SAAS,YAAYA,SAAS,QAAQS,OAAOC,OAAOV,MAAMG,CAAC,IAC7DH,KAA0CG,KAC3CJ,KAAAA;CACR;CACA,OAAO;EAAEP;EAAQD;EAAKG,MAAMM;CAAK;AACnC;AAEA,MAAaY,cAAchB,MAAeC,SACxCF,cAAcC,MAAMC,MAAM,MAAM,CAAC,CAAEH"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "statewire",
3
- "version": "0.0.4",
3
+ "version": "0.4.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": {