statewire 0.20.0 → 0.21.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":["captureContext","useEffect","useState","applyOps","sanitizeValue","StatewirePath","StatewireReject","createStateDraft","createMethodProxy","StateDraft","StatewireSendError","Statewire","StatewireHost","isDev","process","env","stateHosts","WeakMap","StateHost","documentIdentity","protocol","key","JSON","stringify","getStateHost","state","get","StateCell","current","draft","changed","documents","Map","Document","onOp","op","Op","onChange","onDocuments","ctl","StateCtl","host","bump","useStatewireState","init","TState","setVersion","cell","commit","c","flush","v","apply","ops","result","invalidate","push","changedPaths","snapshot","setOnOp","listener","setOnChange","takeChanged","mount","value","options","main","undefined","id","doc","Error","set","unmount","delete","setOnDocuments","record","changedPath","proxy","const","Settled","type","error","CommandsInvocation","method","params","ts","seq","clientId","CommandsOutcome","payload","CommandsListener","onStart","inv","onPosted","onApplied","onCovered","onSettle","outcome","CommandsSubmission","caller","Caller","context","Context","attach","Attach","CommandsHost","setListener","submit","call","Promise","methods","abort","commandsHosts","getCommandsHost","commands","bindCommandsHost","QueuedCall","resolve","reject","origin","ActiveInvocation","applied","controller","AbortController","settled","CommandsCell","handlers","Record","args","queue","active","running","unmounted","stallTimer","ReturnType","setTimeout","STALL_WARN_MS","LOCAL_CALLER","Object","freeze","auth","isConnected","toSendError","fate","cause","message","code","String","applyInvocation","settleInvocation","abortInvocations","clear","isThenable","PromiseLike","then","handlerOf","prototype","propertyIsEnumerable","handler","dispatch","ctx","Ctx","signal","this","resolved","pump","watchStall","shift","length","console","warn","useStatewireCommands","Commands","THandlers","ThisType","CommandsOptions","CommandsProxy","HandledCommands","built","Date","now","keys","filter","clearTimeout","queueMicrotask","queued","startsWith"],"sources":["../../src/host/host.ts"],"sourcesContent":["import { captureContext } from \"../wire/context\";\nimport { 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 { 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\nexport const documentIdentity = (protocol: string, key?: readonly string[]) =>\n JSON.stringify([protocol, key ?? null]);\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 documents: Map<string, StatewireHost.Document>;\n onOp: ((op: Statewire.Op) => void) | null;\n onChange: (() => void) | null;\n onDocuments: (() => 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 documents: new Map(),\n onOp: null,\n onChange: null,\n onDocuments: null,\n bump: () => setVersion((v) => v + 1),\n ctl: {\n apply: (ops) => {\n const result = applyOps(commit(), ops);\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);\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 mount: (protocol, value, options) => {\n const key = options?.key;\n const main = options?.main ?? key === undefined;\n if (main) {\n for (const [id, doc] of c.documents) {\n if (\n doc.protocol === protocol &&\n doc.main &&\n id !== documentIdentity(protocol, key)\n )\n throw new Error(\n `statewire: ${protocol} already has a main document`,\n );\n }\n }\n c.documents.set(documentIdentity(protocol, key), {\n protocol,\n key: key === undefined ? null : [...key],\n main,\n value: sanitizeValue(value),\n });\n c.onDocuments?.();\n },\n unmount: (protocol, key) => {\n if (!c.documents.delete(documentIdentity(protocol, key)))\n throw new Error(\"statewire: document is not mounted\");\n c.onDocuments?.();\n },\n documents: () => c.documents,\n setOnDocuments: (listener) => {\n c.onDocuments = listener;\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 protocol?: string;\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 onApplied: (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 context?: Statewire.Context;\n attach?: StatewireHost.Attach;\n onApplied?: () => void;\n onSettle?: (outcome: CommandsOutcome) => void;\n};\n\nexport type CommandsHost = {\n setListener: (listener: CommandsListener | null) => void;\n submit?: (call: CommandsSubmission) => Promise<unknown>;\n /** Registered command names, read live. */\n methods?: () => string[];\n abort?: () => void;\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 context?: Statewire.Context;\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 applied: boolean;\n controller: AbortController;\n settled: boolean;\n origin?: CommandsSubmission;\n};\n\ntype CommandsCell = {\n context: (() => Statewire.Context) | undefined;\n handlers: Record<string, (...args: never[]) => unknown> | null;\n queue: QueuedCall[];\n active: Map<ActiveInvocation, 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: 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 auth: undefined,\n isConnected: true,\n});\n\nconst toSendError = (error: unknown): StatewireSendError =>\n error instanceof StatewireSendError\n ? error\n : error instanceof StatewireReject\n ? new StatewireSendError({\n fate: \"rejected\",\n cause: \"server\",\n message: error.message,\n ...(error.code !== undefined && { code: error.code }),\n ...(error.payload !== undefined && { payload: error.payload }),\n })\n : new StatewireSendError({\n fate: \"failed\",\n cause: \"server\",\n message: error instanceof Error ? error.message : String(error),\n });\n\nconst applyInvocation = (cell: CommandsCell, inv: ActiveInvocation) => {\n if (inv.applied) return;\n inv.applied = true;\n cell.listener?.onApplied(inv);\n cell.listener?.onCovered?.(inv);\n inv.origin?.onApplied?.();\n};\n\nconst settleInvocation = (\n cell: CommandsCell,\n inv: ActiveInvocation,\n call: QueuedCall,\n settled: Settled,\n) => {\n if (inv.settled) return;\n inv.settled = true;\n cell.active.delete(inv);\n if (settled.type === \"resolved\") {\n applyInvocation(cell, inv);\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 cell.listener?.onCovered?.(inv);\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 abortInvocations = (cell: CommandsCell) => {\n for (const [inv, call] of cell.active) {\n inv.settled = true;\n inv.controller.abort();\n const error = new StatewireSendError({ fate: \"crashed\", cause: \"server\" });\n const outcome: CommandsOutcome = { type: \"rejected\", error };\n cell.listener?.onSettle(inv, outcome);\n inv.origin?.onSettle?.(outcome);\n call.reject(error);\n }\n cell.active.clear();\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\n// A command is an own, enumerable, function-valued property — exactly what\n// `Object.keys` shows, so dispatch, `statewire/commands`, and the reserved-\n// namespace guard share one view; prototype-chain names are not commands.\nconst handlerOf = (\n handlers: Record<string, (...args: never[]) => unknown>,\n method: string,\n): ((...args: never[]) => unknown) | undefined => {\n if (!Object.prototype.propertyIsEnumerable.call(handlers, method)) {\n return undefined;\n }\n const handler = handlers[method];\n return typeof handler === \"function\" ? handler : undefined;\n};\n\nconst dispatch = (cell: CommandsCell, call: QueuedCall) => {\n const handler = handlerOf(cell.handlers!, call.method);\n if (handler === undefined) {\n const error = new StatewireSendError({\n fate: \"rejected\",\n cause: \"server\",\n code: \"unknown-command\",\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 applied: false,\n controller: new AbortController(),\n settled: false,\n ...(call.origin !== undefined && { origin: call.origin }),\n ...(call.origin?.clientId !== undefined && {\n clientId: call.origin.clientId,\n }),\n };\n cell.active.set(inv, call);\n cell.listener?.onStart(inv);\n cell.listener?.onPosted?.(inv);\n const attach = call.origin?.attach;\n const ctx: StatewireHost.Ctx = {\n caller: call.origin?.caller ?? LOCAL_CALLER,\n context: call.context ?? call.origin?.context ?? Object.freeze({}),\n get attach() {\n if (attach === undefined)\n throw new Error(\"statewire: in-process dispatch has no attach\");\n return attach;\n },\n signal: inv.controller.signal,\n applied: () => {\n if (!inv.settled) applyInvocation(cell, inv);\n },\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 options?: StatewireHost.CommandsOptions,\n): Statewire.CommandsProxy<StatewireHost.HandledCommands<THandlers>> => {\n const [built] = useState(() => {\n const cell: CommandsCell = {\n context: undefined,\n handlers: null,\n queue: [],\n active: new Map(),\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 context: captureContext(cell.context?.() ?? {}),\n method,\n params,\n ts: Date.now(),\n resolve,\n reject,\n });\n pump(cell);\n }),\n );\n commandsHosts.set(proxy, {\n abort: () => abortInvocations(cell),\n setListener: (listener) => {\n cell.listener = listener;\n },\n methods: () => {\n const handlers = cell.handlers;\n if (!handlers) return [];\n return Object.keys(handlers).filter(\n (method) => handlerOf(handlers, method) !== undefined,\n );\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 built.cell.context = options?.context;\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 abortInvocations(cell);\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 if (commands) {\n for (const method of Object.keys(commands)) {\n if (method.startsWith(\"statewire/\")) {\n throw new Error(\n `statewire: cannot register \"${method}\": the \"statewire/\" method namespace is reserved`,\n );\n }\n }\n }\n built.cell.handlers = commands || null;\n pump(built.cell);\n });\n\n return built.proxy;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"host.js","names":["captureContext","useEffect","useState","applyOps","sanitizeValue","StatewirePath","StatewireReject","createStateDraft","createMethodProxy","StateDraft","StatewireSendError","Statewire","StatewireHost","isDev","process","env","stateHosts","WeakMap","StateHost","documentIdentity","protocol","key","JSON","stringify","getStateHost","state","get","StateCell","current","draft","changed","documents","Map","Document","onOp","op","Op","onChange","onDocuments","ctl","StateCtl","host","bump","useStatewireState","init","TState","setVersion","cell","commit","c","flush","v","apply","ops","result","invalidate","push","changedPaths","snapshot","setOnOp","listener","setOnChange","takeChanged","mount","value","options","main","undefined","id","doc","Error","set","unmount","delete","setOnDocuments","record","changedPath","proxy","const","Settled","type","error","CommandsInvocation","method","params","ts","seq","clientId","CommandsOutcome","payload","CommandsListener","onStart","inv","onPosted","onApplied","onCovered","onSettle","outcome","CommandsSubmission","caller","Caller","context","Context","attach","Attach","CommandsHost","setListener","submit","call","Promise","methods","abort","commandsHosts","getCommandsHost","commands","bindCommandsHost","QueuedCall","resolve","reject","origin","ActiveInvocation","applied","controller","AbortController","settled","CommandsCell","handlers","Record","args","queue","active","running","unmounted","stallTimer","ReturnType","setTimeout","STALL_WARN_MS","LOCAL_CALLER","Object","freeze","auth","isConnected","toSendError","console","fate","cause","message","code","applyInvocation","settleInvocation","abortInvocations","clear","isThenable","PromiseLike","then","handlerOf","prototype","propertyIsEnumerable","handler","dispatch","ctx","Ctx","signal","this","resolved","pump","watchStall","shift","length","warn","useStatewireCommands","Commands","THandlers","ThisType","CommandsOptions","CommandsProxy","HandledCommands","built","Date","now","keys","filter","clearTimeout","queueMicrotask","queued","startsWith"],"sources":["../../src/host/host.ts"],"sourcesContent":["import { captureContext } from \"../wire/context\";\nimport { 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 { 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\nexport const documentIdentity = (protocol: string, key?: readonly string[]) =>\n JSON.stringify([protocol, key ?? null]);\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 documents: Map<string, StatewireHost.Document>;\n onOp: ((op: Statewire.Op) => void) | null;\n onChange: (() => void) | null;\n onDocuments: (() => 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 documents: new Map(),\n onOp: null,\n onChange: null,\n onDocuments: null,\n bump: () => setVersion((v) => v + 1),\n ctl: {\n apply: (ops) => {\n const result = applyOps(commit(), ops);\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);\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 mount: (protocol, value, options) => {\n const key = options?.key;\n const main = options?.main ?? key === undefined;\n if (main) {\n for (const [id, doc] of c.documents) {\n if (\n doc.protocol === protocol &&\n doc.main &&\n id !== documentIdentity(protocol, key)\n )\n throw new Error(\n `statewire: ${protocol} already has a main document`,\n );\n }\n }\n c.documents.set(documentIdentity(protocol, key), {\n protocol,\n key: key === undefined ? null : [...key],\n main,\n value: sanitizeValue(value),\n });\n c.onDocuments?.();\n },\n unmount: (protocol, key) => {\n if (!c.documents.delete(documentIdentity(protocol, key)))\n throw new Error(\"statewire: document is not mounted\");\n c.onDocuments?.();\n },\n documents: () => c.documents,\n setOnDocuments: (listener) => {\n c.onDocuments = listener;\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 protocol?: string;\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 onApplied: (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 context?: Statewire.Context;\n attach?: StatewireHost.Attach;\n onApplied?: () => void;\n onSettle?: (outcome: CommandsOutcome) => void;\n};\n\nexport type CommandsHost = {\n setListener: (listener: CommandsListener | null) => void;\n submit?: (call: CommandsSubmission) => Promise<unknown>;\n /** Registered command names, read live. */\n methods?: () => string[];\n abort?: () => void;\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 context?: Statewire.Context;\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 applied: boolean;\n controller: AbortController;\n settled: boolean;\n origin?: CommandsSubmission;\n};\n\ntype CommandsCell = {\n context: (() => Statewire.Context) | undefined;\n handlers: Record<string, (...args: never[]) => unknown> | null;\n queue: QueuedCall[];\n active: Map<ActiveInvocation, 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: 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 auth: undefined,\n isConnected: true,\n});\n\nconst toSendError = (error: unknown): StatewireSendError => {\n if (\n !(error instanceof StatewireSendError) &&\n !(error instanceof StatewireReject)\n )\n console.error(\"statewire command failed\", error);\n return error instanceof StatewireSendError\n ? error\n : error instanceof StatewireReject\n ? new StatewireSendError({\n fate: \"rejected\",\n cause: \"server\",\n message: error.message,\n ...(error.code !== undefined && { code: error.code }),\n ...(error.payload !== undefined && { payload: error.payload }),\n })\n : new StatewireSendError({\n fate: \"failed\",\n cause: \"server\",\n message: \"the command failed\",\n });\n};\n\nconst applyInvocation = (cell: CommandsCell, inv: ActiveInvocation) => {\n if (inv.applied) return;\n inv.applied = true;\n cell.listener?.onApplied(inv);\n cell.listener?.onCovered?.(inv);\n inv.origin?.onApplied?.();\n};\n\nconst settleInvocation = (\n cell: CommandsCell,\n inv: ActiveInvocation,\n call: QueuedCall,\n settled: Settled,\n) => {\n if (inv.settled) return;\n inv.settled = true;\n cell.active.delete(inv);\n if (settled.type === \"resolved\") {\n applyInvocation(cell, inv);\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 cell.listener?.onCovered?.(inv);\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 abortInvocations = (cell: CommandsCell) => {\n for (const [inv, call] of cell.active) {\n inv.settled = true;\n inv.controller.abort();\n const error = new StatewireSendError({ fate: \"crashed\", cause: \"server\" });\n const outcome: CommandsOutcome = { type: \"rejected\", error };\n cell.listener?.onSettle(inv, outcome);\n inv.origin?.onSettle?.(outcome);\n call.reject(error);\n }\n cell.active.clear();\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\n// A command is an own, enumerable, function-valued property — exactly what\n// `Object.keys` shows, so dispatch, `statewire/commands`, and the reserved-\n// namespace guard share one view; prototype-chain names are not commands.\nconst handlerOf = (\n handlers: Record<string, (...args: never[]) => unknown>,\n method: string,\n): ((...args: never[]) => unknown) | undefined => {\n if (!Object.prototype.propertyIsEnumerable.call(handlers, method)) {\n return undefined;\n }\n const handler = handlers[method];\n return typeof handler === \"function\" ? handler : undefined;\n};\n\nconst dispatch = (cell: CommandsCell, call: QueuedCall) => {\n const handler = handlerOf(cell.handlers!, call.method);\n if (handler === undefined) {\n const error = new StatewireSendError({\n fate: \"rejected\",\n cause: \"server\",\n code: \"unknown-command\",\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 applied: false,\n controller: new AbortController(),\n settled: false,\n ...(call.origin !== undefined && { origin: call.origin }),\n ...(call.origin?.clientId !== undefined && {\n clientId: call.origin.clientId,\n }),\n };\n cell.active.set(inv, call);\n cell.listener?.onStart(inv);\n cell.listener?.onPosted?.(inv);\n const attach = call.origin?.attach;\n const ctx: StatewireHost.Ctx = {\n caller: call.origin?.caller ?? LOCAL_CALLER,\n context: call.context ?? call.origin?.context ?? Object.freeze({}),\n get attach() {\n if (attach === undefined)\n throw new Error(\"statewire: in-process dispatch has no attach\");\n return attach;\n },\n signal: inv.controller.signal,\n applied: () => {\n if (!inv.settled) applyInvocation(cell, inv);\n },\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 options?: StatewireHost.CommandsOptions,\n): Statewire.CommandsProxy<StatewireHost.HandledCommands<THandlers>> => {\n const [built] = useState(() => {\n const cell: CommandsCell = {\n context: undefined,\n handlers: null,\n queue: [],\n active: new Map(),\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 context: captureContext(cell.context?.() ?? {}),\n method,\n params,\n ts: Date.now(),\n resolve,\n reject,\n });\n pump(cell);\n }),\n );\n commandsHosts.set(proxy, {\n abort: () => abortInvocations(cell),\n setListener: (listener) => {\n cell.listener = listener;\n },\n methods: () => {\n const handlers = cell.handlers;\n if (!handlers) return [];\n return Object.keys(handlers).filter(\n (method) => handlerOf(handlers, method) !== undefined,\n );\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 built.cell.context = options?.context;\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 abortInvocations(cell);\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 if (commands) {\n for (const method of Object.keys(commands)) {\n if (method.startsWith(\"statewire/\")) {\n throw new Error(\n `statewire: cannot register \"${method}\": the \"statewire/\" method namespace is reserved`,\n );\n }\n }\n }\n built.cell.handlers = commands || null;\n pump(built.cell);\n });\n\n return built.proxy;\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","names":[],"sources":["../../src/host/http.ts"],"mappings":";;;cAgUa,SACX,GAAG,iBACH,SAAS,YACR,QAAQ;cAkCE,SACX,GAAG,iBACH,SAAS,YACR,QAAQ"}
1
+ {"version":3,"file":"http.d.ts","names":[],"sources":["../../src/host/http.ts"],"mappings":";;;cA2Ua,SACX,GAAG,iBACH,SAAS,YACR,QAAQ;cAkCE,SACX,GAAG,iBACH,SAAS,YACR,QAAQ"}
package/dist/host/http.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { MAX_FRAME_BODY_BYTES } from "../wire/limits.js";
2
2
  import { negotiateProtocols } from "../wire/protocol.js";
3
- import { CLIENT_ID_RE, PRINCIPAL_MISMATCH_MESSAGE, SUBSCRIBER_LIMIT_MESSAGE, admitFrame, atSubscriberLimit, authEquals, closeConn, flush, parseFrame, parseFrameValue, register, sendTo } from "./socket.js";
3
+ import { CLIENT_ID_RE, PRINCIPAL_MISMATCH_MESSAGE, SUBSCRIBER_LIMIT_MESSAGE, admitFrame, atSubscriberLimit, authEquals, closeConn, expireLease, flush, parseFrame, parseFrameValue, register, sendTo } from "./socket.js";
4
4
  //#region src/host/http.ts
5
5
  const CLIENT_ID_HEADER = "Statewire-Client-Id";
6
6
  const LEASE_HEADER = "Statewire-Lease";
@@ -50,7 +50,10 @@ const clientIdResponse = (request) => {
50
50
  const headersOf = (request) => Object.fromEntries(request.headers);
51
51
  const authorize = async (h, headers) => {
52
52
  try {
53
- return { auth: await h.authorize?.({ headers }) };
53
+ return {
54
+ auth: await h.authorize?.({ headers }),
55
+ authorizedAt: Date.now()
56
+ };
54
57
  } catch (error) {
55
58
  if (error instanceof Response) return { refused: error };
56
59
  throw error;
@@ -66,7 +69,7 @@ const mintLease = () => {
66
69
  const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
67
70
  return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
68
71
  };
69
- const attachSse = (h, clientId, auth, selection, frame) => {
72
+ const attachSse = (h, clientId, auth, authorizedAt, selection, frame) => {
70
73
  let controller;
71
74
  let connection = null;
72
75
  let open = true;
@@ -109,7 +112,7 @@ const attachSse = (h, clientId, auth, selection, frame) => {
109
112
  return;
110
113
  }
111
114
  }
112
- }, false, auth, selection, frame);
115
+ }, false, auth, selection, frame, authorizedAt);
113
116
  connection = registration?.connection ?? null;
114
117
  if (!open) connection?.close();
115
118
  return {
@@ -140,7 +143,7 @@ const getStream = async (h, request) => {
140
143
  if (atSubscriberLimit(h)) return subscriberLimitResponse();
141
144
  const selection = negotiateProtocols(request.headers, h.wireVersion, [h.protocol]);
142
145
  if ("fin" in selection) return Response.json(selection, { status: selection.fin.status });
143
- const attachment = attachSse(h, client.clientId, authorized.auth, selection);
146
+ const attachment = attachSse(h, client.clientId, authorized.auth, authorized.authorizedAt, selection);
144
147
  const lease = attachment.registration === null ? null : mintLease();
145
148
  if (attachment.registration !== null) attachment.registration.conn.lease = lease;
146
149
  return streamResponse(attachment, lease);
@@ -172,7 +175,7 @@ const postStream = async (h, request) => {
172
175
  if ("fin" in selection) return Response.json(selection, { status: selection.fin.status });
173
176
  let attachment;
174
177
  try {
175
- attachment = attachSse(h, client.clientId, authorized.auth, selection, frame);
178
+ attachment = attachSse(h, client.clientId, authorized.auth, authorized.authorizedAt, selection, frame);
176
179
  } catch (error) {
177
180
  if (!(error instanceof Error) || error.name !== "StatewireViolation") throw error;
178
181
  return Response.json({ fin: {
@@ -211,9 +214,10 @@ const violationResponse = (h, clientId, reason) => {
211
214
  const frames = async (h, request) => {
212
215
  const lease = request.headers.get(LEASE_HEADER);
213
216
  const leased = lease === null ? void 0 : [...h.conns].find((conn) => conn.open && conn.lease === lease);
214
- if (h.disposed || h.finished !== null || leased === void 0) return Response.json({ error: "stale-lease" }, { status: 423 });
217
+ if (h.disposed || h.finished !== null || leased === void 0 || expireLease(h, leased)) return Response.json({ error: "stale-lease" }, { status: 423 });
215
218
  const clientId = leased.clientId;
216
219
  const raw = await readFrameBody(request);
220
+ if (!leased.open || h.disposed || h.finished !== null || expireLease(h, leased)) return Response.json({ error: "stale-lease" }, { status: 423 });
217
221
  if (raw === null) return Response.json({ error: "frame body too large" }, { status: 413 });
218
222
  let frame;
219
223
  try {
@@ -222,7 +226,7 @@ const frames = async (h, request) => {
222
226
  if (!(error instanceof Error) || error.name !== "StatewireViolation") throw error;
223
227
  return violationResponse(h, clientId, error.message);
224
228
  }
225
- if (!leased.open || h.disposed || h.finished !== null) return Response.json({ error: "stale-lease" }, { status: 423 });
229
+ if (!leased.open || h.disposed || h.finished !== null || expireLease(h, leased)) return Response.json({ error: "stale-lease" }, { status: 423 });
226
230
  let admission;
227
231
  try {
228
232
  admission = admitFrame(h, leased, frame);
@@ -230,6 +234,7 @@ const frames = async (h, request) => {
230
234
  if (!(error instanceof Error) || error.name !== "StatewireViolation") throw error;
231
235
  return violationResponse(h, clientId, error.message);
232
236
  }
237
+ if (admission.type === "expired") return Response.json({ error: "stale-lease" }, { status: 423 });
233
238
  if (admission.type === "refused") {
234
239
  const retry = admission.syn.syn.retry;
235
240
  return Response.json(admission.syn, {
@@ -1 +1 @@
1
- {"version":3,"file":"http.js","names":["StatewireHost","CLIENT_ID_RE","PRINCIPAL_MISMATCH_MESSAGE","SUBSCRIBER_LIMIT_MESSAGE","admitFrame","atSubscriberLimit","authEquals","closeConn","flush","parseFrame","parseFrameValue","register","sendTo","ParsedFrame","Registration","SocketHostState","MAX_FRAME_BODY_BYTES","negotiateProtocols","StatewireProtocol","CLIENT_ID_HEADER","LEASE_HEADER","encoder","TextEncoder","laggardFin","encode","readFrameBody","request","Request","Promise","declared","headers","get","Number","body","cancel","catch","reader","getReader","chunks","Uint8Array","size","done","value","read","length","push","offset","chunk","set","TextDecoder","decode","invalidClientIdReason","clientId","test","clientIdResponse","response","Response","json","detail","status","reason","headersOf","Record","Object","fromEntries","authorize","h","auth","refused","error","subscriberLimitResponse","principalMismatchResponse","rec","clients","undefined","principal","mintLease","bytes","crypto","getRandomValues","btoa","String","fromCharCode","replaceAll","replace","SseAttachment","ReadableStream","registration","activateBackpressure","attachSse","selection","Selection","frame","controller","ReadableStreamDefaultController","connection","open","backpressureActive","start","next","close","highWaterMark","streamQueueSize","socket","send","data","desiredSize","enqueue","streamResponse","attachment","lease","getStream","authorized","client","mismatch","wireVersion","protocol","fin","conn","postStream","raw","trim","JSON","parse","cmd","Error","name","message","stream","method","Allow","finClient","Fin","conns","stringify","violationResponse","frames","leased","find","disposed","finished","admission","ReturnType","type","retry","syn","Math","ceil"],"sources":["../../src/host/http.ts"],"sourcesContent":["import type { StatewireHost } from \"../host\";\nimport {\n CLIENT_ID_RE,\n PRINCIPAL_MISMATCH_MESSAGE,\n SUBSCRIBER_LIMIT_MESSAGE,\n admitFrame,\n atSubscriberLimit,\n authEquals,\n closeConn,\n flush,\n parseFrame,\n parseFrameValue,\n register,\n sendTo,\n} from \"./socket\";\nimport type { ParsedFrame, Registration, SocketHostState } from \"./socket\";\nimport { MAX_FRAME_BODY_BYTES } from \"../wire/limits\";\nimport { negotiateProtocols } from \"../wire/protocol\";\nimport type { StatewireProtocol } from \"../wire/protocol\";\n\nexport { MAX_FRAME_BODY_BYTES } from \"../wire/limits\";\n\nconst CLIENT_ID_HEADER = \"Statewire-Client-Id\";\nconst LEASE_HEADER = \"Statewire-Lease\";\nconst encoder = new TextEncoder();\nconst laggardFin = encoder.encode(\n 'data: {\"fin\":{\"reason\":\"reconnect\",\"code\":\"client-fell-behind\"}}\\n\\n',\n);\nconst readFrameBody = async (request: Request): Promise<string | null> => {\n const declared = request.headers.get(\"content-length\");\n if (declared !== null && Number(declared) > MAX_FRAME_BODY_BYTES) {\n await request.body?.cancel(\"frame body too large\").catch(() => {});\n return null;\n }\n if (request.body === null) return \"\";\n const reader = request.body.getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n size += value.length;\n if (size > MAX_FRAME_BODY_BYTES) {\n await reader.cancel(\"frame body too large\").catch(() => {});\n return null;\n }\n chunks.push(value);\n }\n const body = new Uint8Array(size);\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.length;\n }\n return new TextDecoder().decode(body);\n};\n\nconst invalidClientIdReason = (clientId: string): string | null => {\n if (clientId === \"\") return \"id must be non-empty\";\n if (clientId.length > 256) return \"id must be at most 256 characters\";\n if (!CLIENT_ID_RE.test(clientId)) {\n return \"id must contain only [A-Za-z0-9._~-]\";\n }\n return null;\n};\n\nconst clientIdResponse = (\n request: Request,\n): { clientId: string } | { response: Response } => {\n const clientId = request.headers.get(CLIENT_ID_HEADER);\n if (clientId === null) {\n return {\n response: Response.json(\n { detail: `missing ${CLIENT_ID_HEADER} header` },\n { status: 400 },\n ),\n };\n }\n const reason = invalidClientIdReason(clientId);\n if (reason !== null) {\n return {\n response: Response.json(\n { detail: `invalid ${CLIENT_ID_HEADER}: ${reason}` },\n { status: 400 },\n ),\n };\n }\n return { clientId };\n};\n\nconst headersOf = (request: Request): Record<string, string> =>\n Object.fromEntries(request.headers);\n\nconst authorize = async (\n h: SocketHostState,\n headers: Record<string, string>,\n): Promise<{ auth: unknown } | { refused: Response }> => {\n try {\n return { auth: await h.authorize?.({ headers }) };\n } catch (error) {\n if (error instanceof Response) return { refused: error };\n throw error;\n }\n};\n\nconst subscriberLimitResponse = (): Response =>\n Response.json({ detail: SUBSCRIBER_LIMIT_MESSAGE }, { status: 503 });\n\n// No awaits between this check and register: the binding cannot race.\nconst principalMismatchResponse = (\n h: SocketHostState,\n clientId: string,\n auth: unknown,\n): Response | null => {\n const rec = h.clients.get(clientId);\n if (rec !== undefined && !authEquals(rec.principal, auth)) {\n return Response.json(\n { detail: PRINCIPAL_MISMATCH_MESSAGE },\n { status: 403 },\n );\n }\n return null;\n};\n\nconst mintLease = (): string => {\n const bytes = crypto.getRandomValues(new Uint8Array(16));\n return btoa(String.fromCharCode(...bytes))\n .replaceAll(\"+\", \"-\")\n .replaceAll(\"/\", \"_\")\n .replace(/=+$/, \"\");\n};\n\ntype SseAttachment = {\n body: ReadableStream<Uint8Array>;\n registration: Registration | null;\n activateBackpressure: () => void;\n};\n\nconst attachSse = (\n h: SocketHostState,\n clientId: string,\n auth: unknown,\n selection: StatewireProtocol.Selection,\n frame?: ParsedFrame,\n): SseAttachment => {\n let controller: ReadableStreamDefaultController<Uint8Array>;\n let connection: Registration[\"connection\"] | null = null;\n let open = true;\n let backpressureActive = false;\n const body = new ReadableStream(\n {\n start: (next) => {\n controller = next;\n },\n cancel: () => {\n open = false;\n connection?.close();\n },\n },\n { highWaterMark: h.streamQueueSize },\n );\n const socket = {\n send: (data: string) => {\n if (!open) return;\n if (\n backpressureActive &&\n controller.desiredSize !== null &&\n controller.desiredSize <= 0\n ) {\n open = false;\n try {\n controller.enqueue(laggardFin);\n controller.close();\n } catch {\n /* stream already gone */\n }\n connection?.close();\n return;\n }\n const frame = data === \"{}\" ? \": ping\\n\\n\" : `data: ${data}\\n\\n`;\n try {\n controller.enqueue(encoder.encode(frame));\n } catch {\n open = false;\n connection?.close();\n }\n },\n close: () => {\n if (!open) return;\n open = false;\n try {\n controller.close();\n } catch {\n return;\n }\n },\n };\n const registration = register(\n h,\n clientId,\n socket,\n false,\n auth,\n selection,\n frame,\n );\n connection = registration?.connection ?? null;\n if (!open) connection?.close();\n return {\n body,\n registration,\n activateBackpressure: () => {\n backpressureActive = true;\n },\n };\n};\n\nconst streamResponse = (\n attachment: SseAttachment,\n lease: string | null,\n): Response => {\n attachment.activateBackpressure();\n return new Response(attachment.body, {\n status: 200,\n headers: {\n \"Content-Type\": \"text/event-stream\",\n ...(lease !== null && { [LEASE_HEADER]: lease }),\n },\n });\n};\n\nconst getStream = async (\n h: SocketHostState,\n request: Request,\n): Promise<Response> => {\n const authorized = await authorize(h, headersOf(request));\n if (\"refused\" in authorized) return authorized.refused;\n const client = clientIdResponse(request);\n if (\"response\" in client) return client.response;\n const mismatch = principalMismatchResponse(\n h,\n client.clientId,\n authorized.auth,\n );\n if (mismatch !== null) return mismatch;\n if (atSubscriberLimit(h)) return subscriberLimitResponse();\n const selection = negotiateProtocols(request.headers, h.wireVersion, [\n h.protocol,\n ]);\n if (\"fin\" in selection)\n return Response.json(selection, { status: selection.fin.status });\n const attachment = attachSse(h, client.clientId, authorized.auth, selection);\n const lease = attachment.registration === null ? null : mintLease();\n if (attachment.registration !== null)\n attachment.registration.conn.lease = lease;\n return streamResponse(attachment, lease);\n};\n\nconst postStream = async (\n h: SocketHostState,\n request: Request,\n): Promise<Response> => {\n const client = clientIdResponse(request);\n if (\"response\" in client) return client.response;\n const raw = await readFrameBody(request);\n if (raw === null) {\n return Response.json({ detail: \"frame body too large\" }, { status: 413 });\n }\n let body: unknown;\n try {\n body = raw.trim() ? JSON.parse(raw) : null;\n } catch {\n return Response.json({ detail: \"body must be JSON\" }, { status: 400 });\n }\n let frame: ParsedFrame;\n try {\n frame = body === null && !raw.trim() ? { cmd: [] } : parseFrameValue(body);\n } catch (error) {\n if (!(error instanceof Error) || error.name !== \"StatewireViolation\") {\n throw error;\n }\n return Response.json({ detail: error.message }, { status: 400 });\n }\n const authorized = await authorize(h, headersOf(request));\n if (\"refused\" in authorized) return authorized.refused;\n const mismatch = principalMismatchResponse(\n h,\n client.clientId,\n authorized.auth,\n );\n if (mismatch !== null) return mismatch;\n if (atSubscriberLimit(h)) return subscriberLimitResponse();\n const selection = negotiateProtocols(request.headers, h.wireVersion, [\n h.protocol,\n ]);\n if (\"fin\" in selection)\n return Response.json(selection, { status: selection.fin.status });\n let attachment: SseAttachment;\n try {\n attachment = attachSse(\n h,\n client.clientId,\n authorized.auth,\n selection,\n frame,\n );\n } catch (error) {\n if (!(error instanceof Error) || error.name !== \"StatewireViolation\")\n throw error;\n return Response.json(\n { fin: { reason: \"protocol-error\", message: error.message } },\n { status: 400 },\n );\n }\n if (attachment.registration === null) return streamResponse(attachment, null);\n const lease = mintLease();\n attachment.registration.conn.lease = lease;\n return streamResponse(attachment, lease);\n};\n\nexport const stream = async (\n h: SocketHostState,\n request: Request,\n): Promise<Response> => {\n if (request.method === \"GET\") return getStream(h, request);\n if (request.method === \"POST\") return postStream(h, request);\n return new Response(null, {\n status: 405,\n headers: { Allow: \"GET, POST\" },\n });\n};\n\nconst finClient = (\n h: SocketHostState,\n clientId: string,\n fin: StatewireHost.Fin,\n) => {\n flush(h);\n for (const conn of [...h.conns]) {\n if (conn.clientId !== clientId) continue;\n sendTo(h, conn, JSON.stringify({ fin }));\n closeConn(h, conn);\n }\n};\n\nconst violationResponse = (\n h: SocketHostState,\n clientId: string,\n reason: string,\n) => {\n finClient(h, clientId, {\n reason: \"protocol-error\",\n message: reason,\n });\n return Response.json({ error: reason }, { status: 400 });\n};\n\nexport const frames = async (\n h: SocketHostState,\n request: Request,\n): Promise<Response> => {\n const lease = request.headers.get(LEASE_HEADER);\n // A lease lives and dies with its attach: any live attach of the id accepts.\n const leased =\n lease === null\n ? undefined\n : [...h.conns].find((conn) => conn.open && conn.lease === lease);\n if (h.disposed || h.finished !== null || leased === undefined) {\n return Response.json({ error: \"stale-lease\" }, { status: 423 });\n }\n const clientId = leased.clientId;\n const raw = await readFrameBody(request);\n if (raw === null) {\n return Response.json({ error: \"frame body too large\" }, { status: 413 });\n }\n let frame: ParsedFrame;\n try {\n frame = parseFrame(raw);\n } catch (error) {\n if (!(error instanceof Error) || error.name !== \"StatewireViolation\") {\n throw error;\n }\n return violationResponse(h, clientId, error.message);\n }\n if (!leased.open || h.disposed || h.finished !== null) {\n return Response.json({ error: \"stale-lease\" }, { status: 423 });\n }\n let admission: ReturnType<typeof admitFrame>;\n try {\n admission = admitFrame(h, leased, frame);\n } catch (error) {\n if (!(error instanceof Error) || error.name !== \"StatewireViolation\")\n throw error;\n return violationResponse(h, clientId, error.message);\n }\n if (admission.type === \"refused\") {\n const retry = admission.syn.syn.retry;\n return Response.json(admission.syn, {\n status: retry === undefined ? 409 : 429,\n ...(retry !== undefined && {\n headers: { \"Retry-After\": String(Math.ceil(retry / 1000)) },\n }),\n });\n }\n return Response.json({});\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"http.js","names":["StatewireHost","CLIENT_ID_RE","PRINCIPAL_MISMATCH_MESSAGE","SUBSCRIBER_LIMIT_MESSAGE","admitFrame","atSubscriberLimit","authEquals","closeConn","flush","expireLease","parseFrame","parseFrameValue","register","sendTo","ParsedFrame","Registration","SocketHostState","MAX_FRAME_BODY_BYTES","negotiateProtocols","StatewireProtocol","CLIENT_ID_HEADER","LEASE_HEADER","encoder","TextEncoder","laggardFin","encode","readFrameBody","request","Request","Promise","declared","headers","get","Number","body","cancel","catch","reader","getReader","chunks","Uint8Array","size","done","value","read","length","push","offset","chunk","set","TextDecoder","decode","invalidClientIdReason","clientId","test","clientIdResponse","response","Response","json","detail","status","reason","headersOf","Record","Object","fromEntries","authorize","h","auth","authorizedAt","refused","Date","now","error","subscriberLimitResponse","principalMismatchResponse","rec","clients","undefined","principal","mintLease","bytes","crypto","getRandomValues","btoa","String","fromCharCode","replaceAll","replace","SseAttachment","ReadableStream","registration","activateBackpressure","attachSse","selection","Selection","frame","controller","ReadableStreamDefaultController","connection","open","backpressureActive","start","next","close","highWaterMark","streamQueueSize","socket","send","data","desiredSize","enqueue","streamResponse","attachment","lease","getStream","authorized","client","mismatch","wireVersion","protocol","fin","conn","postStream","raw","trim","JSON","parse","cmd","Error","name","message","stream","method","Allow","finClient","Fin","conns","stringify","violationResponse","frames","leased","find","disposed","finished","admission","ReturnType","type","retry","syn","Math","ceil"],"sources":["../../src/host/http.ts"],"sourcesContent":["import type { StatewireHost } from \"../host\";\nimport {\n CLIENT_ID_RE,\n PRINCIPAL_MISMATCH_MESSAGE,\n SUBSCRIBER_LIMIT_MESSAGE,\n admitFrame,\n atSubscriberLimit,\n authEquals,\n closeConn,\n flush,\n expireLease,\n parseFrame,\n parseFrameValue,\n register,\n sendTo,\n} from \"./socket\";\nimport type { ParsedFrame, Registration, SocketHostState } from \"./socket\";\nimport { MAX_FRAME_BODY_BYTES } from \"../wire/limits\";\nimport { negotiateProtocols } from \"../wire/protocol\";\nimport type { StatewireProtocol } from \"../wire/protocol\";\n\nexport { MAX_FRAME_BODY_BYTES } from \"../wire/limits\";\n\nconst CLIENT_ID_HEADER = \"Statewire-Client-Id\";\nconst LEASE_HEADER = \"Statewire-Lease\";\nconst encoder = new TextEncoder();\nconst laggardFin = encoder.encode(\n 'data: {\"fin\":{\"reason\":\"reconnect\",\"code\":\"client-fell-behind\"}}\\n\\n',\n);\nconst readFrameBody = async (request: Request): Promise<string | null> => {\n const declared = request.headers.get(\"content-length\");\n if (declared !== null && Number(declared) > MAX_FRAME_BODY_BYTES) {\n await request.body?.cancel(\"frame body too large\").catch(() => {});\n return null;\n }\n if (request.body === null) return \"\";\n const reader = request.body.getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n for (;;) {\n const { done, value } = await reader.read();\n if (done) break;\n size += value.length;\n if (size > MAX_FRAME_BODY_BYTES) {\n await reader.cancel(\"frame body too large\").catch(() => {});\n return null;\n }\n chunks.push(value);\n }\n const body = new Uint8Array(size);\n let offset = 0;\n for (const chunk of chunks) {\n body.set(chunk, offset);\n offset += chunk.length;\n }\n return new TextDecoder().decode(body);\n};\n\nconst invalidClientIdReason = (clientId: string): string | null => {\n if (clientId === \"\") return \"id must be non-empty\";\n if (clientId.length > 256) return \"id must be at most 256 characters\";\n if (!CLIENT_ID_RE.test(clientId)) {\n return \"id must contain only [A-Za-z0-9._~-]\";\n }\n return null;\n};\n\nconst clientIdResponse = (\n request: Request,\n): { clientId: string } | { response: Response } => {\n const clientId = request.headers.get(CLIENT_ID_HEADER);\n if (clientId === null) {\n return {\n response: Response.json(\n { detail: `missing ${CLIENT_ID_HEADER} header` },\n { status: 400 },\n ),\n };\n }\n const reason = invalidClientIdReason(clientId);\n if (reason !== null) {\n return {\n response: Response.json(\n { detail: `invalid ${CLIENT_ID_HEADER}: ${reason}` },\n { status: 400 },\n ),\n };\n }\n return { clientId };\n};\n\nconst headersOf = (request: Request): Record<string, string> =>\n Object.fromEntries(request.headers);\n\nconst authorize = async (\n h: SocketHostState,\n headers: Record<string, string>,\n): Promise<{ auth: unknown; authorizedAt: number } | { refused: Response }> => {\n try {\n const auth = await h.authorize?.({ headers });\n return { auth, authorizedAt: Date.now() };\n } catch (error) {\n if (error instanceof Response) return { refused: error };\n throw error;\n }\n};\n\nconst subscriberLimitResponse = (): Response =>\n Response.json({ detail: SUBSCRIBER_LIMIT_MESSAGE }, { status: 503 });\n\n// No awaits between this check and register: the binding cannot race.\nconst principalMismatchResponse = (\n h: SocketHostState,\n clientId: string,\n auth: unknown,\n): Response | null => {\n const rec = h.clients.get(clientId);\n if (rec !== undefined && !authEquals(rec.principal, auth)) {\n return Response.json(\n { detail: PRINCIPAL_MISMATCH_MESSAGE },\n { status: 403 },\n );\n }\n return null;\n};\n\nconst mintLease = (): string => {\n const bytes = crypto.getRandomValues(new Uint8Array(16));\n return btoa(String.fromCharCode(...bytes))\n .replaceAll(\"+\", \"-\")\n .replaceAll(\"/\", \"_\")\n .replace(/=+$/, \"\");\n};\n\ntype SseAttachment = {\n body: ReadableStream<Uint8Array>;\n registration: Registration | null;\n activateBackpressure: () => void;\n};\n\nconst attachSse = (\n h: SocketHostState,\n clientId: string,\n auth: unknown,\n authorizedAt: number,\n selection: StatewireProtocol.Selection,\n frame?: ParsedFrame,\n): SseAttachment => {\n let controller: ReadableStreamDefaultController<Uint8Array>;\n let connection: Registration[\"connection\"] | null = null;\n let open = true;\n let backpressureActive = false;\n const body = new ReadableStream(\n {\n start: (next) => {\n controller = next;\n },\n cancel: () => {\n open = false;\n connection?.close();\n },\n },\n { highWaterMark: h.streamQueueSize },\n );\n const socket = {\n send: (data: string) => {\n if (!open) return;\n if (\n backpressureActive &&\n controller.desiredSize !== null &&\n controller.desiredSize <= 0\n ) {\n open = false;\n try {\n controller.enqueue(laggardFin);\n controller.close();\n } catch {\n /* stream already gone */\n }\n connection?.close();\n return;\n }\n const frame = data === \"{}\" ? \": ping\\n\\n\" : `data: ${data}\\n\\n`;\n try {\n controller.enqueue(encoder.encode(frame));\n } catch {\n open = false;\n connection?.close();\n }\n },\n close: () => {\n if (!open) return;\n open = false;\n try {\n controller.close();\n } catch {\n return;\n }\n },\n };\n const registration = register(\n h,\n clientId,\n socket,\n false,\n auth,\n selection,\n frame,\n authorizedAt,\n );\n connection = registration?.connection ?? null;\n if (!open) connection?.close();\n return {\n body,\n registration,\n activateBackpressure: () => {\n backpressureActive = true;\n },\n };\n};\n\nconst streamResponse = (\n attachment: SseAttachment,\n lease: string | null,\n): Response => {\n attachment.activateBackpressure();\n return new Response(attachment.body, {\n status: 200,\n headers: {\n \"Content-Type\": \"text/event-stream\",\n ...(lease !== null && { [LEASE_HEADER]: lease }),\n },\n });\n};\n\nconst getStream = async (\n h: SocketHostState,\n request: Request,\n): Promise<Response> => {\n const authorized = await authorize(h, headersOf(request));\n if (\"refused\" in authorized) return authorized.refused;\n const client = clientIdResponse(request);\n if (\"response\" in client) return client.response;\n const mismatch = principalMismatchResponse(\n h,\n client.clientId,\n authorized.auth,\n );\n if (mismatch !== null) return mismatch;\n if (atSubscriberLimit(h)) return subscriberLimitResponse();\n const selection = negotiateProtocols(request.headers, h.wireVersion, [\n h.protocol,\n ]);\n if (\"fin\" in selection)\n return Response.json(selection, { status: selection.fin.status });\n const attachment = attachSse(\n h,\n client.clientId,\n authorized.auth,\n authorized.authorizedAt,\n selection,\n );\n const lease = attachment.registration === null ? null : mintLease();\n if (attachment.registration !== null)\n attachment.registration.conn.lease = lease;\n return streamResponse(attachment, lease);\n};\n\nconst postStream = async (\n h: SocketHostState,\n request: Request,\n): Promise<Response> => {\n const client = clientIdResponse(request);\n if (\"response\" in client) return client.response;\n const raw = await readFrameBody(request);\n if (raw === null) {\n return Response.json({ detail: \"frame body too large\" }, { status: 413 });\n }\n let body: unknown;\n try {\n body = raw.trim() ? JSON.parse(raw) : null;\n } catch {\n return Response.json({ detail: \"body must be JSON\" }, { status: 400 });\n }\n let frame: ParsedFrame;\n try {\n frame = body === null && !raw.trim() ? { cmd: [] } : parseFrameValue(body);\n } catch (error) {\n if (!(error instanceof Error) || error.name !== \"StatewireViolation\") {\n throw error;\n }\n return Response.json({ detail: error.message }, { status: 400 });\n }\n const authorized = await authorize(h, headersOf(request));\n if (\"refused\" in authorized) return authorized.refused;\n const mismatch = principalMismatchResponse(\n h,\n client.clientId,\n authorized.auth,\n );\n if (mismatch !== null) return mismatch;\n if (atSubscriberLimit(h)) return subscriberLimitResponse();\n const selection = negotiateProtocols(request.headers, h.wireVersion, [\n h.protocol,\n ]);\n if (\"fin\" in selection)\n return Response.json(selection, { status: selection.fin.status });\n let attachment: SseAttachment;\n try {\n attachment = attachSse(\n h,\n client.clientId,\n authorized.auth,\n authorized.authorizedAt,\n selection,\n frame,\n );\n } catch (error) {\n if (!(error instanceof Error) || error.name !== \"StatewireViolation\")\n throw error;\n return Response.json(\n { fin: { reason: \"protocol-error\", message: error.message } },\n { status: 400 },\n );\n }\n if (attachment.registration === null) return streamResponse(attachment, null);\n const lease = mintLease();\n attachment.registration.conn.lease = lease;\n return streamResponse(attachment, lease);\n};\n\nexport const stream = async (\n h: SocketHostState,\n request: Request,\n): Promise<Response> => {\n if (request.method === \"GET\") return getStream(h, request);\n if (request.method === \"POST\") return postStream(h, request);\n return new Response(null, {\n status: 405,\n headers: { Allow: \"GET, POST\" },\n });\n};\n\nconst finClient = (\n h: SocketHostState,\n clientId: string,\n fin: StatewireHost.Fin,\n) => {\n flush(h);\n for (const conn of [...h.conns]) {\n if (conn.clientId !== clientId) continue;\n sendTo(h, conn, JSON.stringify({ fin }));\n closeConn(h, conn);\n }\n};\n\nconst violationResponse = (\n h: SocketHostState,\n clientId: string,\n reason: string,\n) => {\n finClient(h, clientId, {\n reason: \"protocol-error\",\n message: reason,\n });\n return Response.json({ error: reason }, { status: 400 });\n};\n\nexport const frames = async (\n h: SocketHostState,\n request: Request,\n): Promise<Response> => {\n const lease = request.headers.get(LEASE_HEADER);\n // A lease lives and dies with its attach: any live attach of the id accepts.\n const leased =\n lease === null\n ? undefined\n : [...h.conns].find((conn) => conn.open && conn.lease === lease);\n if (\n h.disposed ||\n h.finished !== null ||\n leased === undefined ||\n expireLease(h, leased)\n ) {\n return Response.json({ error: \"stale-lease\" }, { status: 423 });\n }\n const clientId = leased.clientId;\n const raw = await readFrameBody(request);\n if (\n !leased.open ||\n h.disposed ||\n h.finished !== null ||\n expireLease(h, leased)\n ) {\n return Response.json({ error: \"stale-lease\" }, { status: 423 });\n }\n if (raw === null) {\n return Response.json({ error: \"frame body too large\" }, { status: 413 });\n }\n let frame: ParsedFrame;\n try {\n frame = parseFrame(raw);\n } catch (error) {\n if (!(error instanceof Error) || error.name !== \"StatewireViolation\") {\n throw error;\n }\n return violationResponse(h, clientId, error.message);\n }\n if (\n !leased.open ||\n h.disposed ||\n h.finished !== null ||\n expireLease(h, leased)\n ) {\n return Response.json({ error: \"stale-lease\" }, { status: 423 });\n }\n let admission: ReturnType<typeof admitFrame>;\n try {\n admission = admitFrame(h, leased, frame);\n } catch (error) {\n if (!(error instanceof Error) || error.name !== \"StatewireViolation\")\n throw error;\n return violationResponse(h, clientId, error.message);\n }\n if (admission.type === \"expired\") {\n return Response.json({ error: \"stale-lease\" }, { status: 423 });\n }\n if (admission.type === \"refused\") {\n const retry = admission.syn.syn.retry;\n return Response.json(admission.syn, {\n status: retry === undefined ? 409 : 429,\n ...(retry !== undefined && {\n headers: { \"Retry-After\": String(Math.ceil(retry / 1000)) },\n }),\n });\n }\n return Response.json({});\n};\n"],"mappings":"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"}
@@ -62,6 +62,8 @@ type Conn = {
62
62
  lease: string | null;
63
63
  /** The awaited `authorize` return captured at this attach's authorization. */
64
64
  auth: unknown;
65
+ authorizedAt: number | undefined;
66
+ leaseTimer: ReturnType<typeof setTimeout> | null;
65
67
  lanes: Map<string, Lane>;
66
68
  contexts: Map<string, Statewire.Context>;
67
69
  documents: Map<string, StatewireHost.Document>;
@@ -104,6 +106,7 @@ type SocketHostState = {
104
106
  key: readonly string[] | undefined;
105
107
  wireVersion: StatewireProtocol.Range;
106
108
  heartbeatMs: number;
109
+ maxLeaseLifetimeMs: number | null;
107
110
  wsInitTimeoutMs: number;
108
111
  maxSubscribers: number;
109
112
  maxInflightCommands: number;
@@ -136,10 +139,13 @@ type SocketHostState = {
136
139
  };
137
140
  declare const closeConn: (h: SocketHostState, conn: Conn, code?: number, reason?: string) => void;
138
141
  declare const sendTo: (h: SocketHostState, conn: Conn, data: string) => void;
142
+ declare const expireLease: (h: SocketHostState, conn: Conn) => boolean;
139
143
  declare const atSubscriberLimit: (h: SocketHostState) => boolean;
140
144
  declare const flush: (h: SocketHostState, fin?: StatewireHost.Fin) => void;
141
145
  type FrameAdmission = {
142
146
  type: "ok";
147
+ } | {
148
+ type: "expired";
143
149
  } | {
144
150
  type: "refused";
145
151
  syn: {
@@ -162,7 +168,7 @@ type Registration = {
162
168
  * before the snapshot (their documents and answers ride the hello), durable
163
169
  * ones after it. A violation in the attach frame closes the attach and throws.
164
170
  */
165
- declare const register: (h: SocketHostState, clientId: string, socket: StatewireHostInternal.Socket, sawFrame?: boolean, auth?: unknown, selection?: StatewireProtocol.Selection, frame?: ParsedFrame) => Registration | null;
171
+ declare const register: (h: SocketHostState, clientId: string, socket: StatewireHostInternal.Socket, sawFrame?: boolean, auth?: unknown, selection?: StatewireProtocol.Selection, frame?: ParsedFrame, authorizedAt?: number) => Registration | null;
166
172
  /**
167
173
  * Transport-agnostic WS binding over a statewire/host transport:
168
174
  * feed it sockets (anything with `send`/`close`) and raw text frames, and it
@@ -174,5 +180,5 @@ declare const register: (h: SocketHostState, clientId: string, socket: Statewire
174
180
  */
175
181
  declare const StatewireSocketHost: (transport: ResourceElement<Statewire>, options?: StatewireHostInternal.SocketHostOptions, persistence?: StatewireHostInternal.SocketHostPersistence) => StatewireHostInternal.SocketHost;
176
182
  //#endregion
177
- export { CLIENT_ID_RE, ClientRec, Conn, FrameAdmission, PRINCIPAL_MISMATCH_MESSAGE, ParsedFrame, Registration, SUBSCRIBER_LIMIT_MESSAGE, SocketHostState, StatewireSocketHost, admitFrame, atSubscriberLimit, authEquals, closeConn, flush, parseFrame, parseFrameValue, register, sendTo };
183
+ export { CLIENT_ID_RE, ClientRec, Conn, FrameAdmission, PRINCIPAL_MISMATCH_MESSAGE, ParsedFrame, Registration, SUBSCRIBER_LIMIT_MESSAGE, SocketHostState, StatewireSocketHost, admitFrame, atSubscriberLimit, authEquals, closeConn, expireLease, flush, parseFrame, parseFrameValue, register, sendTo };
178
184
  //# sourceMappingURL=socket.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"socket.d.ts","names":[],"sources":["../../src/host/socket.ts"],"mappings":";;;;;;;;;cAiCa,cAAY;cACZ;;;cAKA,aAAc,YAAY;cAoC1B;KAGR,MAAM,sBAAsB;KAE5B;EACH;EACA;EACA,OAAO;EACP;EACA,OAAO,kBAAkB;EACzB;EACA;EACA;;KAGG;EACH;EACA;EACA;EACA;EACA;EACA;;;KAIG;EACH;EACA;EACA,MAAM;EACN;EACA,SAAS;IAAc;IAAY,KAAK;;EACxC,UAAU,YAAY;;KAGZ;EACV;EACA;EACA,OAAO,YAAY;;EAEnB;;KAIU;EACV,WAAW,kBAAkB;EAC7B;EACA,QAAQ,sBAAsB;EAC9B;EACA;EACA;EACA;EACA;;EAEA;EACA,OAAO,YAAY;EACnB,UAAU,YAAY,UAAU;EAChC,WAAW,YAAY,cAAc;EACrC,UAAU;EACV,MAAM;IAAc;IAAa;;EACjC;EACA,gBAAgB;EAChB,aAAa;EACb,QAAQ,cAAc;;KAGnB;EACH,QAAQ,sBAAsB;EAC9B;EACA;EACA;EACA,OAAO,kBAAkB;EACzB,YAAY,sBAAsB;;KAc/B;EACH;EACA;EACA;EACA;EACA;;KAGU;EACV,KAAK;EACL;IAAQ;IAAkB,OAAO,UAAU;;;cAQhC,kBAAmB,mBAAiB;cAqFpC,aAAc,gBAAc;KAyF7B;EACV,UAAU,kBAAkB;EAC5B;EACA,aAAa,kBAAkB;EAC/B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,WAAW,sBAAsB;EACjC,SAAS,YAAY;EACrB,OAAO,IAAI;EACX,cAAc,IAAI;EAClB,YAAY;EACZ,gBAAgB,YAAY;EAC5B,aAAa,YAAY;EACzB;EACA;EACA,UAAU,cAAc;EACxB;EACA;EACA,gBAAgB,kBAAkB;EAClC,WAAW,cAAc;EACzB,cAAc;EACd,SAAS;EACT,aAAa,sBAAsB;EACnC;EACA,UACE,kBACA,QAAQ,sBAAsB,QAC9B,UAAU,sBAAsB,yBAC7B,sBAAsB;EAC3B,SAAS,KAAK,cAAc;EAC5B,SAAS,SAAS,YAAY,QAAQ;EACtC,SAAS,SAAS,YAAY,QAAQ;;cAsD3B,YACX,GAAG,iBACH,MAAM,MACN,eACA;cAiBW,SAAU,GAAG,iBAAiB,MAAM,MAAM;cAmB1C,oBAAqB,GAAG;cAsLxB,QAAS,GAAG,iBAAiB,MAAM,cAAc;KAoUlD;EACN;;EAEA;EACA;IACE;MAAO;QAAa;QAAc;;MAAiB;;;;cAyE9C,aACX,GAAG,iBACH,MAAM,MACN,OAAO,gBACN;KA8YS;EACV,MAAM;EACN,YAAY,sBAAsB;;;;;;;cAQvB,WACX,GAAG,iBACH,kBACA,QAAQ,sBAAsB,QAC9B,oBACA,gBACA,YAAW,kBAAkB,WAI7B,QAAO,gBACN;;;;;;;;;;cA0UU,sBACX,WAAW,gBAAgB,YAC3B,UAAU,sBAAsB,mBAChC,cAAc,sBAAsB,0BACnC,sBAAsB"}
1
+ {"version":3,"file":"socket.d.ts","names":[],"sources":["../../src/host/socket.ts"],"mappings":";;;;;;;;;cAiCa,cAAY;cACZ;;;cAKA,aAAc,YAAY;cAoC1B;KAGR,MAAM,sBAAsB;KAE5B;EACH;EACA;EACA,OAAO;EACP;EACA,OAAO,kBAAkB;EACzB;EACA;EACA;;KAGG;EACH;EACA;EACA;EACA;EACA;EACA;;;KAIG;EACH;EACA;EACA,MAAM;EACN;EACA,SAAS;IAAc;IAAY,KAAK;;EACxC,UAAU,YAAY;;KAGZ;EACV;EACA;EACA,OAAO,YAAY;;EAEnB;;KAIU;EACV,WAAW,kBAAkB;EAC7B;EACA,QAAQ,sBAAsB;EAC9B;EACA;EACA;EACA;EACA;;EAEA;EACA;EACA,YAAY,kBAAkB;EAC9B,OAAO,YAAY;EACnB,UAAU,YAAY,UAAU;EAChC,WAAW,YAAY,cAAc;EACrC,UAAU;EACV,MAAM;IAAc;IAAa;;EACjC;EACA,gBAAgB;EAChB,aAAa;EACb,QAAQ,cAAc;;KAGnB;EACH,QAAQ,sBAAsB;EAC9B;EACA;EACA;EACA,OAAO,kBAAkB;EACzB,YAAY,sBAAsB;;KAc/B;EACH;EACA;EACA;EACA;EACA;;KAGU;EACV,KAAK;EACL;IAAQ;IAAkB,OAAO,UAAU;;;cAQhC,kBAAmB,mBAAiB;cAqFpC,aAAc,gBAAc;KAuF7B;EACV,UAAU,kBAAkB;EAC5B;EACA,aAAa,kBAAkB;EAC/B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,WAAW,sBAAsB;EACjC,SAAS,YAAY;EACrB,OAAO,IAAI;EACX,cAAc,IAAI;EAClB,YAAY;EACZ,gBAAgB,YAAY;EAC5B,aAAa,YAAY;EACzB;EACA;EACA,UAAU,cAAc;EACxB;EACA;EACA,gBAAgB,kBAAkB;EAClC,WAAW,cAAc;EACzB,cAAc;EACd,SAAS;EACT,aAAa,sBAAsB;EACnC;EACA,UACE,kBACA,QAAQ,sBAAsB,QAC9B,UAAU,sBAAsB,yBAC7B,sBAAsB;EAC3B,SAAS,KAAK,cAAc;EAC5B,SAAS,SAAS,YAAY,QAAQ;EACtC,SAAS,SAAS,YAAY,QAAQ;;cAsD3B,YACX,GAAG,iBACH,MAAM,MACN,eACA;cAqBW,SAAU,GAAG,iBAAiB,MAAM,MAAM;cA0B1C,cAAe,GAAG,iBAAiB,MAAM;cAoCzC,oBAAqB,GAAG;cAsLxB,QAAS,GAAG,iBAAiB,MAAM,cAAc;KAoUlD;EACN;;EACA;;EAEA;EACA;IACE;MAAO;QAAa;QAAc;;MAAiB;;;;cAyE9C,aACX,GAAG,iBACH,MAAM,MACN,OAAO,gBACN;KAkaS;EACV,MAAM;EACN,YAAY,sBAAsB;;;;;;;cAQvB,WACX,GAAG,iBACH,kBACA,QAAQ,sBAAsB,QAC9B,oBACA,gBACA,YAAW,kBAAkB,WAI7B,QAAO,aACP,0BACC;;;;;;;;;;cA6WU,sBACX,WAAW,gBAAgB,YAC3B,UAAU,sBAAsB,mBAChC,cAAc,sBAAsB,0BACnC,sBAAsB"}
@@ -149,11 +149,13 @@ const cmdOf = (adm, outcome) => {
149
149
  ...encodePayload(failure.payload)
150
150
  };
151
151
  }
152
- default: return {
153
- ...at,
154
- type: "failed",
155
- message: outcome.error instanceof Error ? outcome.error.message : String(outcome.error)
156
- };
152
+ default:
153
+ console.error("statewire command failed", outcome.error);
154
+ return {
155
+ ...at,
156
+ type: "failed",
157
+ message: "the command failed"
158
+ };
157
159
  }
158
160
  };
159
161
  const parseInitialFrame = (raw) => {
@@ -211,6 +213,10 @@ const closeConn = (h, conn, code, reason) => {
211
213
  if (!conn.open) return;
212
214
  conn.open = false;
213
215
  h.conns.delete(conn);
216
+ if (conn.leaseTimer !== null) {
217
+ clearTimeout(conn.leaseTimer);
218
+ conn.leaseTimer = null;
219
+ }
214
220
  try {
215
221
  conn.socket.close(code, reason);
216
222
  } catch {}
@@ -229,6 +235,35 @@ const sendTo = (h, conn, data) => {
229
235
  closeConn(h, conn);
230
236
  }
231
237
  };
238
+ const expiredAt = (h, authorizedAt) => {
239
+ const now = Date.now();
240
+ return authorizedAt === void 0 || !Number.isFinite(authorizedAt) || authorizedAt < 0 || authorizedAt > now || h.maxLeaseLifetimeMs !== null && now - authorizedAt >= h.maxLeaseLifetimeMs;
241
+ };
242
+ const leaseExpiredFin = JSON.stringify({ fin: {
243
+ reason: "reconnect",
244
+ code: "lease-expired"
245
+ } });
246
+ const expireLease = (h, conn) => {
247
+ if (!conn.open || expiredAt(h, conn.authorizedAt)) {
248
+ if (conn.open) {
249
+ sendTo(h, conn, leaseExpiredFin);
250
+ closeConn(h, conn, 1e3, "lease-expired");
251
+ }
252
+ return true;
253
+ }
254
+ return false;
255
+ };
256
+ const armLeaseTimer = (h, conn) => {
257
+ if (conn.leaseTimer !== null) clearTimeout(conn.leaseTimer);
258
+ conn.leaseTimer = null;
259
+ if (h.disposed || h.maxLeaseLifetimeMs === null || expireLease(h, conn)) return;
260
+ const delay = h.maxLeaseLifetimeMs - (Date.now() - conn.authorizedAt);
261
+ conn.leaseTimer = setTimeout(() => {
262
+ conn.leaseTimer = null;
263
+ if (!expireLease(h, conn)) armLeaseTimer(h, conn);
264
+ }, Math.min(delay, 2147483647));
265
+ conn.leaseTimer.unref?.();
266
+ };
232
267
  const violate = (h, conn, reason) => {
233
268
  sendTo(h, conn, JSON.stringify({ fin: {
234
269
  reason: "protocol-error",
@@ -626,6 +661,7 @@ const installContexts = (h, conn, frame) => {
626
661
  for (const entry of frame.ctx ?? []) h.current?.contextChanged?.(entry.protocol, captureContext(entry.value), conn.attach);
627
662
  };
628
663
  const admitFrame = (h, conn, frame) => {
664
+ if (expireLease(h, conn)) return { type: "expired" };
629
665
  validateContexts(h, conn, frame);
630
666
  installContexts(h, conn, frame);
631
667
  const rec = h.clients.get(conn.clientId);
@@ -659,6 +695,7 @@ const admitFrame = (h, conn, frame) => {
659
695
  return refusal(h, conn, retry);
660
696
  }
661
697
  }
698
+ if (expireLease(h, conn)) return { type: "expired" };
662
699
  for (const s of fresh) admitCmd(h, lanes.get(s.protocol), conn, s);
663
700
  for (const [protocol, lane] of lanes) {
664
701
  const last = fresh.findLast((s) => s.protocol === protocol);
@@ -673,7 +710,7 @@ const admitFrame = (h, conn, frame) => {
673
710
  return { type: "ok" };
674
711
  };
675
712
  const message = (h, conn, data) => {
676
- if (!conn.open || h.disposed || h.finished !== null) return;
713
+ if (!conn.open || h.disposed || h.finished !== null || expireLease(h, conn)) return;
677
714
  if (frameIsOversized(data)) {
678
715
  sendTo(h, conn, JSON.stringify({ fin: {
679
716
  reason: "protocol-error",
@@ -755,7 +792,18 @@ const normalizedHeaders = (headers) => {
755
792
  };
756
793
  const connectAuthorized = (h, clientId, socket, options) => {
757
794
  const authorize = h.authorize;
758
- if (options.authorized === true) return register(h, clientId, socket, options.authorized === true, options.auth, options.selection)?.connection ?? null;
795
+ if (options.authorized === true) {
796
+ if (expiredAt(h, options.authorizedAt)) {
797
+ try {
798
+ socket.send(leaseExpiredFin);
799
+ } catch {}
800
+ try {
801
+ socket.close(1e3, "lease-expired");
802
+ } catch {}
803
+ return null;
804
+ }
805
+ return register(h, clientId, socket, options.authorized === true, options.auth, options.selection, void 0, options.authorizedAt)?.connection ?? null;
806
+ }
759
807
  const refuse = (code, reason) => {
760
808
  try {
761
809
  socket.close(code, reason);
@@ -846,19 +894,20 @@ const connectAuthorized = (h, clientId, socket, options) => {
846
894
  pending.authorizing = true;
847
895
  Promise.resolve().then(() => authorize?.({ headers })).then((auth) => {
848
896
  if (!pending.open || h.disposed || h.finished !== null) return;
897
+ const authorizedAt = Date.now();
849
898
  const selection = negotiateProtocols(new Headers(headers), h.wireVersion, [h.protocol]);
850
899
  if ("fin" in selection) {
851
900
  sendPending(pending, JSON.stringify(selection));
852
901
  closePending(h, pending, 1008, selection.fin.message);
853
902
  return;
854
903
  }
855
- options.onAuthorized?.(auth, selection);
904
+ options.onAuthorized?.(auth, selection, authorizedAt);
856
905
  pending.open = false;
857
906
  h.pendingConns.delete(pending);
858
907
  if (pending.timer !== null) clearTimeout(pending.timer);
859
908
  let registration;
860
909
  try {
861
- registration = register(h, clientId, socket, true, auth, selection, frame);
910
+ registration = register(h, clientId, socket, true, auth, selection, frame, authorizedAt);
862
911
  } catch (error) {
863
912
  if (violationReason(error) === null) throw error;
864
913
  return;
@@ -902,7 +951,7 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0, selectio
902
951
  name: h.protocol.name,
903
952
  version: h.protocol.version
904
953
  }]
905
- }, frame = { cmd: [] }) => {
954
+ }, frame = { cmd: [] }, authorizedAt) => {
906
955
  const refuse = (code, reason) => {
907
956
  try {
908
957
  socket.close(code, reason);
@@ -920,6 +969,12 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0, selectio
920
969
  return null;
921
970
  }
922
971
  if (atSubscriberLimit(h)) return refuseSubscriberLimit(socket);
972
+ if (expiredAt(h, authorizedAt)) {
973
+ try {
974
+ socket.send(leaseExpiredFin);
975
+ } catch {}
976
+ return refuse(1e3, "lease-expired");
977
+ }
923
978
  const known = h.clients.get(clientId);
924
979
  if (known !== void 0 && !authEquals(known.principal, auth)) {
925
980
  try {
@@ -952,6 +1007,8 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0, selectio
952
1007
  lastSentAt: 0,
953
1008
  lease: null,
954
1009
  auth,
1010
+ authorizedAt,
1011
+ leaseTimer: null,
955
1012
  lanes: /* @__PURE__ */ new Map(),
956
1013
  contexts: /* @__PURE__ */ new Map(),
957
1014
  documents: /* @__PURE__ */ new Map(),
@@ -976,6 +1033,8 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0, selectio
976
1033
  }
977
1034
  gcClients(h);
978
1035
  ensureHeartbeat(h);
1036
+ armLeaseTimer(h, conn);
1037
+ if (!conn.open) return null;
979
1038
  const isEphemeral = (s) => selection.protocols.find((p) => p.name === s.protocol)?.record === "ephemeral";
980
1039
  const early = frame.cmd.filter(isEphemeral);
981
1040
  const late = frame.cmd.filter((s) => !isEphemeral(s));
@@ -993,6 +1052,7 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0, selectio
993
1052
  const earlyAdmission = admit(early);
994
1053
  if (early.length > 0) flush(h);
995
1054
  notifyClientRecords(h);
1055
+ if (expireLease(h, conn)) return null;
996
1056
  const ops = [];
997
1057
  if (selects(conn, h.protocol.name)) {
998
1058
  const { value, meta } = encodeDocument(h.stateHost.snapshot());
@@ -1051,9 +1111,14 @@ const resume = (h) => {
1051
1111
  h.disposed = false;
1052
1112
  h.stateHost?.setOnOp((op) => recordOp(h, op));
1053
1113
  h.stateHost?.setOnDocuments(() => markViews(h));
1114
+ for (const conn of h.conns) armLeaseTimer(h, conn);
1054
1115
  };
1055
1116
  const suspend = (h) => {
1056
1117
  h.disposed = true;
1118
+ for (const conn of h.conns) {
1119
+ if (conn.leaseTimer !== null) clearTimeout(conn.leaseTimer);
1120
+ conn.leaseTimer = null;
1121
+ }
1057
1122
  if (h.heartbeatTimer !== null) {
1058
1123
  clearInterval(h.heartbeatTimer);
1059
1124
  h.heartbeatTimer = null;
@@ -1087,6 +1152,7 @@ const useSocketHost = (transport, options, persistence) => {
1087
1152
  key: void 0,
1088
1153
  wireVersion: { version: WIRE_VERSION },
1089
1154
  heartbeatMs: 15e3,
1155
+ maxLeaseLifetimeMs: 864e5,
1090
1156
  wsInitTimeoutMs: 5e3,
1091
1157
  maxSubscribers: MAX_SUBSCRIBERS,
1092
1158
  maxInflightCommands: MAX_INFLIGHT_COMMANDS_PER_HOST,
@@ -1129,6 +1195,12 @@ const useSocketHost = (transport, options, persistence) => {
1129
1195
  h.key = options?.key;
1130
1196
  h.wireVersion = options?.wireVersion ?? { version: "2026-09-30" };
1131
1197
  setHeartbeatMs(h, options?.heartbeatIntervalMs ?? 15e3);
1198
+ const maxLeaseLifetimeMs = options?.maxLeaseLifetimeMs === void 0 ? 864e5 : options.maxLeaseLifetimeMs;
1199
+ if (maxLeaseLifetimeMs !== null && (!Number.isFinite(maxLeaseLifetimeMs) || maxLeaseLifetimeMs <= 0)) throw new TypeError("maxLeaseLifetimeMs must be finite and positive or null");
1200
+ if (h.maxLeaseLifetimeMs !== maxLeaseLifetimeMs) {
1201
+ h.maxLeaseLifetimeMs = maxLeaseLifetimeMs;
1202
+ for (const conn of [...h.conns]) armLeaseTimer(h, conn);
1203
+ }
1132
1204
  h.pushback = options?.pushback;
1133
1205
  h.wsInitTimeoutMs = options?.wsInitTimeoutMs ?? 5e3;
1134
1206
  const maxSubscribers = options?.maxSubscribers === void 0 ? MAX_SUBSCRIBERS : options.maxSubscribers;
@@ -1187,6 +1259,6 @@ const StatewireSocketHost = (transport, options, persistence) => {
1187
1259
  };
1188
1260
  };
1189
1261
  //#endregion
1190
- export { CLIENT_ID_RE, PRINCIPAL_MISMATCH_MESSAGE, SUBSCRIBER_LIMIT_MESSAGE, StatewireSocketHost, admitFrame, atSubscriberLimit, authEquals, closeConn, flush, parseFrame, parseFrameValue, register, sendTo };
1262
+ export { CLIENT_ID_RE, PRINCIPAL_MISMATCH_MESSAGE, SUBSCRIBER_LIMIT_MESSAGE, StatewireSocketHost, admitFrame, atSubscriberLimit, authEquals, closeConn, expireLease, flush, parseFrame, parseFrameValue, register, sendTo };
1191
1263
 
1192
1264
  //# sourceMappingURL=socket.js.map