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.
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +18 -4
- package/dist/client/connection.js.map +1 -1
- package/dist/client/session.d.ts +3 -1
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +41 -16
- package/dist/client/session.js.map +1 -1
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +14 -11
- package/dist/host/host.js.map +1 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +13 -8
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +8 -2
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +83 -11
- package/dist/host/socket.js.map +1 -1
- package/dist/host-internal.d.ts +4 -1
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +6 -5
- package/dist/statewire.js.map +1 -1
- package/dist/transports/sse.d.ts +1 -0
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +7 -0
- package/dist/transports/sse.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +16 -4
- package/src/client/session.ts +54 -15
- package/src/host/host.ts +9 -3
- package/src/host/http.ts +37 -5
- package/src/host/socket.ts +111 -6
- package/src/host-internal.ts +4 -0
- package/src/statewire.ts +11 -6
- package/src/transports/sse.ts +12 -0
package/dist/host/host.js.map
CHANGED
|
@@ -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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"}
|
package/dist/host/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","names":[],"sources":["../../src/host/http.ts"],"mappings":";;;
|
|
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 {
|
|
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, {
|
package/dist/host/http.js.map
CHANGED
|
@@ -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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"}
|
package/dist/host/socket.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/host/socket.js
CHANGED
|
@@ -149,11 +149,13 @@ const cmdOf = (adm, outcome) => {
|
|
|
149
149
|
...encodePayload(failure.payload)
|
|
150
150
|
};
|
|
151
151
|
}
|
|
152
|
-
default:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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)
|
|
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
|