statewire 0.16.0 → 0.17.1

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.
Files changed (96) hide show
  1. package/README.md +12 -4
  2. package/dist/client/connection.d.ts +5 -1
  3. package/dist/client/connection.d.ts.map +1 -1
  4. package/dist/client/connection.js +22 -6
  5. package/dist/client/connection.js.map +1 -1
  6. package/dist/client/core.d.ts.map +1 -1
  7. package/dist/client/core.js +2 -0
  8. package/dist/client/core.js.map +1 -1
  9. package/dist/client/session.d.ts +15 -4
  10. package/dist/client/session.d.ts.map +1 -1
  11. package/dist/client/session.js +235 -52
  12. package/dist/client/session.js.map +1 -1
  13. package/dist/client/sessionStore.d.ts +5 -1
  14. package/dist/client/sessionStore.d.ts.map +1 -1
  15. package/dist/client/sessionStore.js +3 -1
  16. package/dist/client/sessionStore.js.map +1 -1
  17. package/dist/document.d.ts +25 -0
  18. package/dist/document.d.ts.map +1 -0
  19. package/dist/document.js +88 -0
  20. package/dist/document.js.map +1 -0
  21. package/dist/host/draft.js +1 -1
  22. package/dist/host/host.d.ts +1 -0
  23. package/dist/host/host.d.ts.map +1 -1
  24. package/dist/host/host.js.map +1 -1
  25. package/dist/host/http.d.ts.map +1 -1
  26. package/dist/host/http.js +27 -9
  27. package/dist/host/http.js.map +1 -1
  28. package/dist/host/socket.d.ts +12 -5
  29. package/dist/host/socket.d.ts.map +1 -1
  30. package/dist/host/socket.js +107 -43
  31. package/dist/host/socket.js.map +1 -1
  32. package/dist/host-internal.d.ts +9 -1
  33. package/dist/host-internal.d.ts.map +1 -1
  34. package/dist/host-internal.js +1 -1
  35. package/dist/index.d.ts +4 -1
  36. package/dist/index.js +2 -1
  37. package/dist/statewire.d.ts +33 -2
  38. package/dist/statewire.d.ts.map +1 -1
  39. package/dist/statewire.js +34 -3
  40. package/dist/statewire.js.map +1 -1
  41. package/dist/transports/fixtures.js +1 -0
  42. package/dist/transports/fixtures.js.map +1 -1
  43. package/dist/transports/remote.d.ts.map +1 -1
  44. package/dist/transports/remote.js +15 -8
  45. package/dist/transports/remote.js.map +1 -1
  46. package/dist/transports/sse.d.ts.map +1 -1
  47. package/dist/transports/sse.js +8 -8
  48. package/dist/transports/sse.js.map +1 -1
  49. package/dist/transports/testing.d.ts +1 -1
  50. package/dist/transports/testing.d.ts.map +1 -1
  51. package/dist/transports/testing.js +12 -4
  52. package/dist/transports/testing.js.map +1 -1
  53. package/dist/transports/ws.js +2 -2
  54. package/dist/transports/ws.js.map +1 -1
  55. package/dist/types.d.ts +7 -0
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/types.js.map +1 -1
  58. package/dist/wire/documents.d.ts +25 -2
  59. package/dist/wire/documents.d.ts.map +1 -1
  60. package/dist/wire/documents.js +28 -8
  61. package/dist/wire/documents.js.map +1 -1
  62. package/dist/wire/meta.d.ts +3 -18
  63. package/dist/wire/meta.d.ts.map +1 -1
  64. package/dist/wire/meta.js +8 -57
  65. package/dist/wire/meta.js.map +1 -1
  66. package/dist/wire/ops.js +1 -1
  67. package/dist/wire/protocol.d.ts +55 -0
  68. package/dist/wire/protocol.d.ts.map +1 -0
  69. package/dist/wire/protocol.js +221 -0
  70. package/dist/wire/protocol.js.map +1 -0
  71. package/dist/wire/version.d.ts +1 -3
  72. package/dist/wire/version.d.ts.map +1 -1
  73. package/dist/wire/version.js +1 -3
  74. package/dist/wire/version.js.map +1 -1
  75. package/package.json +1 -1
  76. package/src/client/connection.ts +43 -13
  77. package/src/client/core.ts +6 -0
  78. package/src/client/session.ts +340 -45
  79. package/src/client/sessionStore.ts +8 -0
  80. package/src/document.ts +185 -0
  81. package/src/host/host.ts +1 -0
  82. package/src/host/http.ts +36 -7
  83. package/src/host/socket.ts +149 -57
  84. package/src/host-internal.ts +12 -1
  85. package/src/index.ts +5 -0
  86. package/src/statewire.ts +109 -6
  87. package/src/transports/fixtures.ts +1 -1
  88. package/src/transports/remote.ts +16 -7
  89. package/src/transports/sse.ts +18 -8
  90. package/src/transports/testing.ts +10 -2
  91. package/src/transports/ws.ts +1 -1
  92. package/src/types.ts +11 -0
  93. package/src/wire/documents.ts +126 -8
  94. package/src/wire/meta.ts +11 -94
  95. package/src/wire/protocol.ts +327 -0
  96. package/src/wire/version.ts +0 -2
@@ -1 +1 @@
1
- {"version":3,"file":"host.js","names":["useEffect","useState","applyOps","sanitizeValue","StatewirePath","StatewireReject","createStateDraft","createMethodProxy","StateDraft","StatewireSendError","Statewire","StatewireHost","isDev","process","env","stateHosts","WeakMap","StateHost","getStateHost","state","get","StateCell","current","draft","changed","onOp","op","Op","onChange","ctl","StateCtl","host","bump","useStatewireState","init","TState","setVersion","cell","commit","c","flush","v","apply","ops","result","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","onApplied","onCovered","onSettle","outcome","CommandsSubmission","caller","Caller","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","Map","running","unmounted","stallTimer","ReturnType","setTimeout","STALL_WARN_MS","LOCAL_CALLER","Object","freeze","Error","auth","undefined","context","isConnected","toSendError","fate","cause","message","String","applyInvocation","settleInvocation","delete","abortInvocations","clear","isThenable","PromiseLike","then","handlerOf","prototype","propertyIsEnumerable","handler","dispatch","code","ctx","Ctx","signal","this","resolved","pump","watchStall","shift","length","console","warn","useStatewireCommands","Commands","THandlers","ThisType","CommandsProxy","HandledCommands","built","Date","now","keys","filter","clearTimeout","queueMicrotask","queued","startsWith"],"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 { 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: Statewire.Op) => 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 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 },\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 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 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 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 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: 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 auth: undefined,\n context: null,\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.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 applyInvocation(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 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 ctx: StatewireHost.Ctx = {\n caller: call.origin?.caller ?? LOCAL_CALLER,\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): Statewire.CommandsProxy<StatewireHost.HandledCommands<THandlers>> => {\n const [built] = useState(() => {\n const cell: CommandsCell = {\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 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 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":["useEffect","useState","applyOps","sanitizeValue","StatewirePath","StatewireReject","createStateDraft","createMethodProxy","StateDraft","StatewireSendError","Statewire","StatewireHost","isDev","process","env","stateHosts","WeakMap","StateHost","getStateHost","state","get","StateCell","current","draft","changed","onOp","op","Op","onChange","ctl","StateCtl","host","bump","useStatewireState","init","TState","setVersion","cell","commit","c","flush","v","apply","ops","result","invalidate","push","changedPaths","snapshot","setOnOp","listener","setOnChange","takeChanged","value","record","changedPath","set","proxy","const","Settled","type","error","CommandsInvocation","protocol","method","params","ts","seq","clientId","CommandsOutcome","payload","CommandsListener","onStart","inv","onPosted","onApplied","onCovered","onSettle","outcome","CommandsSubmission","caller","Caller","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","Map","running","unmounted","stallTimer","ReturnType","setTimeout","STALL_WARN_MS","LOCAL_CALLER","Object","freeze","Error","auth","undefined","context","isConnected","toSendError","fate","cause","message","String","applyInvocation","settleInvocation","delete","abortInvocations","clear","isThenable","PromiseLike","then","handlerOf","prototype","propertyIsEnumerable","handler","dispatch","code","ctx","Ctx","signal","this","resolved","pump","watchStall","shift","length","console","warn","useStatewireCommands","Commands","THandlers","ThisType","CommandsProxy","HandledCommands","built","Date","now","keys","filter","clearTimeout","queueMicrotask","queued","startsWith"],"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 { 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: Statewire.Op) => 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 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 },\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 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 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 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: 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 auth: undefined,\n context: null,\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.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 applyInvocation(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 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 ctx: StatewireHost.Ctx = {\n caller: call.origin?.caller ?? LOCAL_CALLER,\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): Statewire.CommandsProxy<StatewireHost.HandledCommands<THandlers>> => {\n const [built] = useState(() => {\n const cell: CommandsCell = {\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 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 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":";;;cAiTa,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":";;;cAuUa,SACX,GAAG,iBACH,SAAS,YACR,QAAQ;cAkCE,SACX,GAAG,iBACH,SAAS,YACR,QAAQ"}
package/dist/host/http.js CHANGED
@@ -1,5 +1,5 @@
1
+ import { negotiateProtocols } from "../wire/protocol.js";
1
2
  import { MAX_FRAME_BODY_BYTES } from "../wire/limits.js";
2
- import { VERSION_HEADER } from "../wire/version.js";
3
3
  import { CLIENT_ID_RE, PRINCIPAL_MISMATCH_MESSAGE, SUBSCRIBER_LIMIT_MESSAGE, admitFrame, atSubscriberLimit, authEquals, closeConn, flush, parseFrame, parseFrameValue, register, sendTo } from "./socket.js";
4
4
  //#region src/host/http.ts
5
5
  const CLIENT_ID_HEADER = "Statewire-Client-Id";
@@ -66,7 +66,7 @@ const mintLease = () => {
66
66
  const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
67
67
  return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
68
68
  };
69
- const attachSse = (h, clientId, auth) => {
69
+ const attachSse = (h, clientId, auth, selection) => {
70
70
  let controller;
71
71
  let connection = null;
72
72
  let open = true;
@@ -109,7 +109,7 @@ const attachSse = (h, clientId, auth) => {
109
109
  return;
110
110
  }
111
111
  }
112
- }, false, auth);
112
+ }, false, auth, selection);
113
113
  connection = registration?.connection ?? null;
114
114
  if (!open) connection?.close();
115
115
  return {
@@ -126,14 +126,13 @@ const streamResponse = (attachment, lease) => {
126
126
  status: 200,
127
127
  headers: {
128
128
  "Content-Type": "text/event-stream",
129
- [VERSION_HEADER]: "1",
130
129
  ...lease !== null && { [LEASE_HEADER]: lease }
131
130
  }
132
131
  });
133
132
  };
134
133
  const admitAttachFrame = (h, registration, frame) => {
135
134
  if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
136
- const admission = admitFrame(h, registration.rec, frame, registration.conn.auth);
135
+ const admission = admitFrame(h, registration.rec, frame, registration.conn.auth, registration.conn.selection);
137
136
  if (admission.type === "refused") sendTo(h, registration.conn, JSON.stringify(admission.syn));
138
137
  };
139
138
  const getStream = async (h, request) => {
@@ -144,7 +143,9 @@ const getStream = async (h, request) => {
144
143
  const mismatch = principalMismatchResponse(h, client.clientId, authorized.auth);
145
144
  if (mismatch !== null) return mismatch;
146
145
  if (atSubscriberLimit(h)) return subscriberLimitResponse();
147
- const attachment = attachSse(h, client.clientId, authorized.auth);
146
+ const selection = negotiateProtocols(request.headers, h.wireVersion, [h.protocol]);
147
+ if ("fin" in selection) return Response.json(selection, { status: selection.fin.status });
148
+ const attachment = attachSse(h, client.clientId, authorized.auth, selection);
148
149
  const lease = attachment.registration === null ? null : mintLease();
149
150
  if (attachment.registration !== null) attachment.registration.conn.lease = lease;
150
151
  return streamResponse(attachment, lease);
@@ -175,11 +176,22 @@ const postStream = async (h, request) => {
175
176
  const mismatch = principalMismatchResponse(h, client.clientId, authorized.auth);
176
177
  if (mismatch !== null) return mismatch;
177
178
  if (atSubscriberLimit(h)) return subscriberLimitResponse();
178
- const attachment = attachSse(h, client.clientId, authorized.auth);
179
+ const selection = negotiateProtocols(request.headers, h.wireVersion, [h.protocol]);
180
+ if ("fin" in selection) return Response.json(selection, { status: selection.fin.status });
181
+ const attachment = attachSse(h, client.clientId, authorized.auth, selection);
179
182
  if (attachment.registration === null) return streamResponse(attachment, null);
180
183
  const lease = mintLease();
181
184
  attachment.registration.conn.lease = lease;
182
- admitAttachFrame(h, attachment.registration, frame);
185
+ try {
186
+ admitAttachFrame(h, attachment.registration, frame);
187
+ } catch (error) {
188
+ closeConn(h, attachment.registration.conn, 1008, "invalid-context");
189
+ if (!(error instanceof Error) || error.name !== "StatewireViolation") throw error;
190
+ return Response.json({ fin: {
191
+ reason: "protocol-error",
192
+ message: error.message
193
+ } }, { status: 400 });
194
+ }
183
195
  return streamResponse(attachment, lease);
184
196
  };
185
197
  const stream = async (h, request) => {
@@ -221,7 +233,13 @@ const frames = async (h, request) => {
221
233
  return violationResponse(h, clientId, error.message);
222
234
  }
223
235
  if (!leased.open || h.disposed || h.finished !== null) return Response.json({ error: "stale-lease" }, { status: 423 });
224
- const admission = admitFrame(h, rec, frame, leased.auth);
236
+ let admission;
237
+ try {
238
+ admission = admitFrame(h, rec, frame, leased.auth, leased.selection);
239
+ } catch (error) {
240
+ if (!(error instanceof Error) || error.name !== "StatewireViolation") throw error;
241
+ return violationResponse(h, clientId, error.message);
242
+ }
225
243
  if (admission.type === "refused") {
226
244
  const retry = admission.syn.syn.retry;
227
245
  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","PROTOCOL_VERSION","VERSION_HEADER","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","controller","ReadableStreamDefaultController","connection","open","backpressureActive","start","next","close","highWaterMark","streamQueueSize","socket","send","data","desiredSize","enqueue","frame","streamResponse","attachment","lease","admitAttachFrame","ctx","cmd","admission","conn","type","JSON","stringify","syn","getStream","authorized","client","mismatch","postStream","raw","trim","parse","Error","name","message","stream","method","Allow","finClient","fin","Fin","conns","violationResponse","frames","leased","find","disposed","finished","retry","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 { PROTOCOL_VERSION, VERSION_HEADER } from \"../wire/version\";\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): 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(h, clientId, socket, false, auth);\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 [VERSION_HEADER]: PROTOCOL_VERSION,\n ...(lease !== null && { [LEASE_HEADER]: lease }),\n },\n });\n};\n\nconst admitAttachFrame = (\n h: SocketHostState,\n registration: Registration,\n frame: ParsedFrame,\n) => {\n if (frame.ctx.length === 0 && frame.cmd.length === 0) return;\n const admission = admitFrame(\n h,\n registration.rec,\n frame,\n registration.conn.auth,\n );\n if (admission.type === \"refused\") {\n sendTo(h, registration.conn, JSON.stringify(admission.syn));\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 attachment = attachSse(h, client.clientId, authorized.auth);\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 =\n body === null && !raw.trim()\n ? { ctx: [], cmd: [] }\n : 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 attachment = attachSse(h, client.clientId, authorized.auth);\n if (attachment.registration === null) return streamResponse(attachment, null);\n const lease = mintLease();\n attachment.registration.conn.lease = lease;\n admitAttachFrame(h, attachment.registration, frame);\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 rec = h.clients.get(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 const admission = admitFrame(h, rec, frame, leased.auth);\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","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","controller","ReadableStreamDefaultController","connection","open","backpressureActive","start","next","close","highWaterMark","streamQueueSize","socket","send","data","desiredSize","enqueue","frame","streamResponse","attachment","lease","admitAttachFrame","ctx","cmd","admission","conn","type","JSON","stringify","syn","getStream","authorized","client","mismatch","wireVersion","protocol","fin","postStream","raw","trim","parse","Error","name","message","stream","method","Allow","finClient","Fin","conns","violationResponse","frames","leased","find","disposed","finished","ReturnType","retry","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): 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(h, clientId, socket, false, auth, selection);\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 admitAttachFrame = (\n h: SocketHostState,\n registration: Registration,\n frame: ParsedFrame,\n) => {\n if (frame.ctx.length === 0 && frame.cmd.length === 0) return;\n const admission = admitFrame(\n h,\n registration.rec,\n frame,\n registration.conn.auth,\n registration.conn.selection,\n );\n if (admission.type === \"refused\") {\n sendTo(h, registration.conn, JSON.stringify(admission.syn));\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 =\n body === null && !raw.trim()\n ? { ctx: [], cmd: [] }\n : 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 const attachment = attachSse(h, client.clientId, authorized.auth, selection);\n if (attachment.registration === null) return streamResponse(attachment, null);\n const lease = mintLease();\n attachment.registration.conn.lease = lease;\n try {\n admitAttachFrame(h, attachment.registration, frame);\n } catch (error) {\n closeConn(h, attachment.registration.conn, 1008, \"invalid-context\");\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 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 rec = h.clients.get(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, rec, frame, leased.auth, leased.selection);\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,9 +1,11 @@
1
+ import { StatewireDocuments } from "../wire/documents.js";
2
+ import { StatewireProtocol } from "../wire/protocol.js";
1
3
  import { CommandsHost } from "./host.js";
2
4
  import { StatewireHost } from "../host.js";
3
5
  import { StatewireCmd } from "../client/connection.js";
4
6
  import { Statewire } from "../types.js";
5
7
  import { StatewirePath } from "../wire/ops.js";
6
- import { StatewireMetaEntry } from "../wire/meta.js";
8
+ import { StatewireDocument } from "../document.js";
7
9
  import { StatewireHostInternal } from "../host-internal.js";
8
10
  import { ResourceElement } from "@assistant-ui/tap";
9
11
  //#region src/host/socket.d.ts
@@ -17,7 +19,7 @@ type WireOp = {
17
19
  op: string;
18
20
  path?: StatewirePath;
19
21
  value?: unknown;
20
- meta?: StatewireMetaEntry[];
22
+ meta?: StatewireDocument.MetaEntry[];
21
23
  };
22
24
  type Admission = {
23
25
  seq: number;
@@ -32,6 +34,7 @@ type ClientRec = {
32
34
  ctxSeq: number;
33
35
  touched: number;
34
36
  context: Record<string, unknown> | null;
37
+ contextDocuments: StatewireDocuments.State;
35
38
  results: Map<number, {
36
39
  ts: number;
37
40
  cmd: StatewireCmd;
@@ -41,6 +44,7 @@ type ClientRec = {
41
44
  principal: unknown;
42
45
  };
43
46
  type Conn = {
47
+ selection: StatewireProtocol.Selection;
44
48
  clientId: string;
45
49
  socket: StatewireHostInternal.Socket;
46
50
  open: boolean;
@@ -60,9 +64,10 @@ type PendingConn = {
60
64
  };
61
65
  type CtxStatement = {
62
66
  seq: number;
63
- value: Record<string, unknown>;
67
+ ops: unknown[];
64
68
  };
65
69
  type CmdStatement = {
70
+ protocol: string;
66
71
  seq: number;
67
72
  method: string;
68
73
  params: unknown;
@@ -75,6 +80,8 @@ type ParsedFrame = {
75
80
  declare const parseFrameValue: (frame: unknown) => ParsedFrame;
76
81
  declare const parseFrame: (raw: string) => ParsedFrame;
77
82
  type SocketHostState = {
83
+ protocol: StatewireProtocol.Definition;
84
+ wireVersion: StatewireProtocol.Range;
78
85
  heartbeatMs: number;
79
86
  wsInitTimeoutMs: number;
80
87
  maxSubscribers: number;
@@ -121,13 +128,13 @@ type FrameAdmission = {
121
128
  };
122
129
  };
123
130
  };
124
- declare const admitFrame: (h: SocketHostState, rec: ClientRec, frame: ParsedFrame, auth: unknown) => FrameAdmission;
131
+ declare const admitFrame: (h: SocketHostState, rec: ClientRec, frame: ParsedFrame, auth: unknown, selection?: StatewireProtocol.Selection) => FrameAdmission;
125
132
  type Registration = {
126
133
  rec: ClientRec;
127
134
  conn: Conn;
128
135
  connection: StatewireHostInternal.SocketConnection;
129
136
  };
130
- declare const register: (h: SocketHostState, clientId: string, socket: StatewireHostInternal.Socket, sawFrame?: boolean, auth?: unknown) => Registration | null;
137
+ declare const register: (h: SocketHostState, clientId: string, socket: StatewireHostInternal.Socket, sawFrame?: boolean, auth?: unknown, selection?: StatewireProtocol.Selection) => Registration | null;
131
138
  /**
132
139
  * Transport-agnostic WS binding over a statewire/host transport:
133
140
  * feed it sockets (anything with `send`/`close`) and raw text frames, and it
@@ -1 +1 @@
1
- {"version":3,"file":"socket.d.ts","names":[],"sources":["../../src/host/socket.ts"],"mappings":";;;;;;;;;cAwBa,cAAY;cACZ;;;cAKA,aAAc,YAAY;cAoC1B;KAGR;EACH;EACA,OAAO;EACP;EACA,OAAO;;KAGJ;EACH;EACA;EACA;EACA;EACA;;KAGU;EACV;EACA;EACA;EACA;EACA,SAAS;EACT,SAAS;IAAc;IAAY,KAAK;;EACxC,UAAU,YAAY;;EAEtB;;KAGU;EACV;EACA,QAAQ,sBAAsB;EAC9B;EACA;EACA;EACA;;EAEA;;KAGG;EACH,QAAQ,sBAAsB;EAC9B;EACA;EACA;EACA,OAAO,kBAAkB;EACzB,YAAY,sBAAsB;;KAc/B;EAAiB;EAAa,OAAO;;KACrC;EACH;EACA;EACA;EACA;;KAGU;EACV,KAAK;EACL,KAAK;;cAkDM,kBAAmB,mBAAiB;cAmDpC,aAAc,gBAAc;KAiG7B;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,WAAW,sBAAsB;EACjC,SAAS,YAAY;EACrB,OAAO,IAAI;EACX,cAAc,IAAI;EAClB,YAAY;EACZ,gBAAgB;EAChB,aAAa,YAAY;EACzB;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;;cA+C3B,YACX,GAAG,iBACH,MAAM,MACN,eACA;cAgBW,SAAU,GAAG,iBAAiB,MAAM,MAAM;cAU1C,oBAAqB,GAAG;cAkCxB,QAAS,GAAG,iBAAiB,MAAM,cAAc;KAkQlD;EACN;;EAEA;EACA;IACE;MAAO;MAAa;MAAgB;;;;cAI/B,aACX,GAAG,iBACH,KAAK,WACL,OAAO,aACP,kBACC;KA8XS;EACV,KAAK;EACL,MAAM;EACN,YAAY,sBAAsB;;cAGvB,WACX,GAAG,iBACH,kBACA,QAAQ,sBAAsB,QAC9B,oBACA,mBACC;;;;;;;;;cAyPU,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;EACH;EACA,OAAO;EACP;EACA,OAAO,kBAAkB;;KAGtB;EACH;EACA;EACA;EACA;EACA;;KAGU;EACV;EACA;EACA;EACA;EACA,SAAS;EACT,kBAAkB,mBAAmB;EACrC,SAAS;IAAc;IAAY,KAAK;;EACxC,UAAU,YAAY;;EAEtB;;KAGU;EACV,WAAW,kBAAkB;EAC7B;EACA,QAAQ,sBAAsB;EAC9B;EACA;EACA;EACA;;EAEA;;KAGG;EACH,QAAQ,sBAAsB;EAC9B;EACA;EACA;EACA,OAAO,kBAAkB;EACzB,YAAY,sBAAsB;;KAc/B;EAAiB;EAAa;;KAC9B;EACH;EACA;EACA;EACA;EACA;;KAGU;EACV,KAAK;EACL,KAAK;;cA0BM,kBAAmB,mBAAiB;cAoDpC,aAAc,gBAAc;KAiG7B;EACV,UAAU,kBAAkB;EAC5B,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;EAChB,aAAa,YAAY;EACzB;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;;cA+C3B,YACX,GAAG,iBACH,MAAM,MACN,eACA;cAgBW,SAAU,GAAG,iBAAiB,MAAM,MAAM;cAU1C,oBAAqB,GAAG;cAkCxB,QAAS,GAAG,iBAAiB,MAAM,cAAc;KAsRlD;EACN;;EAEA;EACA;IACE;MAAO;MAAa;MAAgB;;;;cAI/B,aACX,GAAG,iBACH,KAAK,WACL,OAAO,aACP,eACA,YAAW,kBAAkB,cAI5B;KAsbS;EACV,KAAK;EACL,MAAM;EACN,YAAY,sBAAsB;;cAGvB,WACX,GAAG,iBACH,kBACA,QAAQ,sBAAsB,QAC9B,oBACA,gBACA,YAAW,kBAAkB,cAI5B;;;;;;;;;cA0QU,sBACX,WAAW,gBAAgB,YAC3B,UAAU,sBAAsB,mBAChC,cAAc,sBAAsB,0BACnC,sBAAsB"}