statewire 0.18.1 → 0.19.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 +8 -5
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +24 -13
- package/dist/client/connection.js.map +1 -1
- package/dist/client/session.d.ts +13 -15
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +296 -401
- package/dist/client/session.js.map +1 -1
- package/dist/client/timing.d.ts +2 -1
- package/dist/client/timing.d.ts.map +1 -1
- package/dist/client/timing.js +2 -1
- package/dist/client/timing.js.map +1 -1
- package/dist/host/engine.d.ts +2 -0
- package/dist/host/engine.d.ts.map +1 -1
- package/dist/host/engine.js +2 -0
- package/dist/host/engine.js.map +1 -1
- package/dist/host/host.d.ts +3 -1
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +38 -7
- package/dist/host/host.js.map +1 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +9 -19
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +48 -25
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +399 -266
- package/dist/host/socket.js.map +1 -1
- package/dist/host/synced.d.ts +9 -0
- package/dist/host/synced.d.ts.map +1 -0
- package/dist/host/synced.js +45 -0
- package/dist/host/synced.js.map +1 -0
- package/dist/host-internal.d.ts +20 -12
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host.d.ts +33 -9
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/statewire.d.ts +8 -20
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +0 -14
- package/dist/statewire.js.map +1 -1
- package/dist/storage.d.ts +2 -5
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +54 -89
- package/dist/storage.js.map +1 -1
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +1 -6
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +2 -5
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +7 -5
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +19 -18
- package/dist/transports/testing.js.map +1 -1
- package/dist/types.d.ts +3 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wire/limits.d.ts +2 -2
- package/dist/wire/limits.js +2 -2
- package/dist/wire/limits.js.map +1 -1
- package/dist/wire/protocol.d.ts +6 -1
- package/dist/wire/protocol.d.ts.map +1 -1
- package/dist/wire/protocol.js +33 -12
- package/dist/wire/protocol.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +46 -33
- package/src/client/session.ts +442 -517
- package/src/client/timing.ts +2 -0
- package/src/host/engine.ts +3 -1
- package/src/host/host.ts +50 -3
- package/src/host/http.ts +23 -31
- package/src/host/socket.ts +560 -339
- package/src/host/synced.ts +60 -0
- package/src/host-internal.ts +21 -7
- package/src/host.ts +40 -8
- package/src/index.ts +1 -0
- package/src/statewire.ts +8 -37
- package/src/storage.ts +90 -157
- package/src/transports/remote.ts +0 -7
- package/src/transports/sse.ts +8 -3
- package/src/transports/testing.ts +34 -18
- package/src/types.ts +3 -5
- package/src/wire/limits.ts +1 -1
- package/src/wire/protocol.ts +40 -11
package/dist/host/host.js
CHANGED
|
@@ -7,6 +7,7 @@ import { useEffect, useState } from "@assistant-ui/tap/react-shim";
|
|
|
7
7
|
//#region src/host/host.ts
|
|
8
8
|
const isDev = () => typeof process === "undefined" || process.env["NODE_ENV"] !== "production";
|
|
9
9
|
const stateHosts = /* @__PURE__ */ new WeakMap();
|
|
10
|
+
const documentIdentity = (protocol, key) => JSON.stringify([protocol, key ?? null]);
|
|
10
11
|
/** Host-side accessor for a `useStatewireState` state object's source (`snapshot()` reads the committed state). */
|
|
11
12
|
const getStateHost = (state) => stateHosts.get(state);
|
|
12
13
|
const useStatewireState = (init) => {
|
|
@@ -20,8 +21,10 @@ const useStatewireState = (init) => {
|
|
|
20
21
|
current: sanitizeValue(init()),
|
|
21
22
|
draft: null,
|
|
22
23
|
changed: [[]],
|
|
24
|
+
documents: /* @__PURE__ */ new Map(),
|
|
23
25
|
onOp: null,
|
|
24
26
|
onChange: null,
|
|
27
|
+
onDocuments: null,
|
|
25
28
|
bump: () => setVersion((v) => v + 1),
|
|
26
29
|
ctl: {
|
|
27
30
|
apply: (ops) => {
|
|
@@ -47,13 +50,35 @@ const useStatewireState = (init) => {
|
|
|
47
50
|
const changed = c.changed;
|
|
48
51
|
c.changed = [];
|
|
49
52
|
return changed;
|
|
53
|
+
},
|
|
54
|
+
mount: (protocol, value, options) => {
|
|
55
|
+
const key = options?.key;
|
|
56
|
+
const main = options?.main ?? key === void 0;
|
|
57
|
+
if (main) {
|
|
58
|
+
for (const [id, doc] of c.documents) if (doc.protocol === protocol && doc.main && id !== documentIdentity(protocol, key)) throw new Error(`statewire: ${protocol} already has a main document`);
|
|
59
|
+
}
|
|
60
|
+
c.documents.set(documentIdentity(protocol, key), {
|
|
61
|
+
protocol,
|
|
62
|
+
key: key === void 0 ? null : [...key],
|
|
63
|
+
main,
|
|
64
|
+
value: sanitizeValue(value)
|
|
65
|
+
});
|
|
66
|
+
c.onDocuments?.();
|
|
67
|
+
},
|
|
68
|
+
unmount: (protocol_0, key_0) => {
|
|
69
|
+
if (!c.documents.delete(documentIdentity(protocol_0, key_0))) throw new Error("statewire: document is not mounted");
|
|
70
|
+
c.onDocuments?.();
|
|
71
|
+
},
|
|
72
|
+
documents: () => c.documents,
|
|
73
|
+
setOnDocuments: (listener_1) => {
|
|
74
|
+
c.onDocuments = listener_1;
|
|
50
75
|
}
|
|
51
76
|
}
|
|
52
77
|
};
|
|
53
78
|
return c;
|
|
54
79
|
});
|
|
55
|
-
let
|
|
56
|
-
if (typeof
|
|
80
|
+
let value_0 = cell.ctl.snapshot();
|
|
81
|
+
if (typeof value_0 === "object" && value_0 !== null) {
|
|
57
82
|
cell.draft ??= createStateDraft({
|
|
58
83
|
snapshot: () => cell.current,
|
|
59
84
|
record: (op, changedPath) => {
|
|
@@ -64,9 +89,9 @@ const useStatewireState = (init) => {
|
|
|
64
89
|
}
|
|
65
90
|
});
|
|
66
91
|
stateHosts.set(cell.draft.proxy, cell.host);
|
|
67
|
-
|
|
92
|
+
value_0 = cell.draft.proxy;
|
|
68
93
|
}
|
|
69
|
-
return [
|
|
94
|
+
return [value_0, cell.ctl];
|
|
70
95
|
};
|
|
71
96
|
const commandsHosts = /* @__PURE__ */ new WeakMap();
|
|
72
97
|
const getCommandsHost = (commands) => commandsHosts.get(commands);
|
|
@@ -79,13 +104,13 @@ const LOCAL_CALLER = Object.freeze({
|
|
|
79
104
|
throw new Error("statewire: in-process dispatch has no client id");
|
|
80
105
|
},
|
|
81
106
|
auth: void 0,
|
|
82
|
-
context: null,
|
|
83
107
|
isConnected: true
|
|
84
108
|
});
|
|
85
109
|
const toSendError = (error) => error instanceof StatewireSendError ? error : error instanceof StatewireReject ? new StatewireSendError({
|
|
86
110
|
fate: "rejected",
|
|
87
111
|
cause: "server",
|
|
88
112
|
message: error.message,
|
|
113
|
+
...error.code !== void 0 && { code: error.code },
|
|
89
114
|
...error.payload !== void 0 && { payload: error.payload }
|
|
90
115
|
}) : new StatewireSendError({
|
|
91
116
|
fate: "failed",
|
|
@@ -103,8 +128,8 @@ const settleInvocation = (cell, inv, call, settled) => {
|
|
|
103
128
|
if (inv.settled) return;
|
|
104
129
|
inv.settled = true;
|
|
105
130
|
cell.active.delete(inv);
|
|
106
|
-
applyInvocation(cell, inv);
|
|
107
131
|
if (settled.type === "resolved") {
|
|
132
|
+
applyInvocation(cell, inv);
|
|
108
133
|
const outcome = settled.value === void 0 ? { type: "accepted" } : {
|
|
109
134
|
type: "accepted",
|
|
110
135
|
payload: sanitizeValue(settled.value)
|
|
@@ -113,6 +138,7 @@ const settleInvocation = (cell, inv, call, settled) => {
|
|
|
113
138
|
inv.origin?.onSettle?.(outcome);
|
|
114
139
|
call.resolve(outcome.payload);
|
|
115
140
|
} else {
|
|
141
|
+
cell.listener?.onCovered?.(inv);
|
|
116
142
|
const error = toSendError(settled.error);
|
|
117
143
|
const outcome = {
|
|
118
144
|
type: "rejected",
|
|
@@ -176,8 +202,13 @@ const dispatch = (cell, call) => {
|
|
|
176
202
|
cell.active.set(inv, call);
|
|
177
203
|
cell.listener?.onStart(inv);
|
|
178
204
|
cell.listener?.onPosted?.(inv);
|
|
205
|
+
const attach = call.origin?.attach;
|
|
179
206
|
const ctx = {
|
|
180
207
|
caller: call.origin?.caller ?? LOCAL_CALLER,
|
|
208
|
+
get attach() {
|
|
209
|
+
if (attach === void 0) throw new Error("statewire: in-process dispatch has no attach");
|
|
210
|
+
return attach;
|
|
211
|
+
},
|
|
181
212
|
signal: inv.controller.signal,
|
|
182
213
|
applied: () => {
|
|
183
214
|
if (!inv.settled) applyInvocation(cell, inv);
|
|
@@ -313,6 +344,6 @@ const useStatewireCommands = (commands) => {
|
|
|
313
344
|
return built.proxy;
|
|
314
345
|
};
|
|
315
346
|
//#endregion
|
|
316
|
-
export { bindCommandsHost, getCommandsHost, getStateHost, useStatewireCommands, useStatewireState };
|
|
347
|
+
export { bindCommandsHost, documentIdentity, getCommandsHost, getStateHost, useStatewireCommands, useStatewireState };
|
|
317
348
|
|
|
318
349
|
//# sourceMappingURL=host.js.map
|
package/dist/host/host.js.map
CHANGED
|
@@ -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","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
|
+
{"version":3,"file":"host.js","names":["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","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","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\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 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 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: 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 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): 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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"}
|
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":";;;cAgUa,SACX,GAAG,iBACH,SAAS,YACR,QAAQ;cAkCE,SACX,GAAG,iBACH,SAAS,YACR,QAAQ"}
|
package/dist/host/http.js
CHANGED
|
@@ -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, selection) => {
|
|
69
|
+
const attachSse = (h, clientId, auth, selection, frame) => {
|
|
70
70
|
let controller;
|
|
71
71
|
let connection = null;
|
|
72
72
|
let open = true;
|
|
@@ -109,7 +109,7 @@ const attachSse = (h, clientId, auth, selection) => {
|
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
-
}, false, auth, selection);
|
|
112
|
+
}, false, auth, selection, frame);
|
|
113
113
|
connection = registration?.connection ?? null;
|
|
114
114
|
if (!open) connection?.close();
|
|
115
115
|
return {
|
|
@@ -130,11 +130,6 @@ const streamResponse = (attachment, lease) => {
|
|
|
130
130
|
}
|
|
131
131
|
});
|
|
132
132
|
};
|
|
133
|
-
const admitAttachFrame = (h, registration, frame) => {
|
|
134
|
-
if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
|
|
135
|
-
const admission = admitFrame(h, registration.rec, frame, registration.conn.auth, registration.conn.selection);
|
|
136
|
-
if (admission.type === "refused") sendTo(h, registration.conn, JSON.stringify(admission.syn));
|
|
137
|
-
};
|
|
138
133
|
const getStream = async (h, request) => {
|
|
139
134
|
const authorized = await authorize(h, headersOf(request));
|
|
140
135
|
if ("refused" in authorized) return authorized.refused;
|
|
@@ -163,10 +158,7 @@ const postStream = async (h, request) => {
|
|
|
163
158
|
}
|
|
164
159
|
let frame;
|
|
165
160
|
try {
|
|
166
|
-
frame = body === null && !raw.trim() ? {
|
|
167
|
-
ctx: [],
|
|
168
|
-
cmd: []
|
|
169
|
-
} : parseFrameValue(body);
|
|
161
|
+
frame = body === null && !raw.trim() ? { cmd: [] } : parseFrameValue(body);
|
|
170
162
|
} catch (error) {
|
|
171
163
|
if (!(error instanceof Error) || error.name !== "StatewireViolation") throw error;
|
|
172
164
|
return Response.json({ detail: error.message }, { status: 400 });
|
|
@@ -178,20 +170,19 @@ const postStream = async (h, request) => {
|
|
|
178
170
|
if (atSubscriberLimit(h)) return subscriberLimitResponse();
|
|
179
171
|
const selection = negotiateProtocols(request.headers, h.wireVersion, [h.protocol]);
|
|
180
172
|
if ("fin" in selection) return Response.json(selection, { status: selection.fin.status });
|
|
181
|
-
|
|
182
|
-
if (attachment.registration === null) return streamResponse(attachment, null);
|
|
183
|
-
const lease = mintLease();
|
|
184
|
-
attachment.registration.conn.lease = lease;
|
|
173
|
+
let attachment;
|
|
185
174
|
try {
|
|
186
|
-
|
|
175
|
+
attachment = attachSse(h, client.clientId, authorized.auth, selection, frame);
|
|
187
176
|
} catch (error) {
|
|
188
|
-
closeConn(h, attachment.registration.conn, 1008, "invalid-context");
|
|
189
177
|
if (!(error instanceof Error) || error.name !== "StatewireViolation") throw error;
|
|
190
178
|
return Response.json({ fin: {
|
|
191
179
|
reason: "protocol-error",
|
|
192
180
|
message: error.message
|
|
193
181
|
} }, { status: 400 });
|
|
194
182
|
}
|
|
183
|
+
if (attachment.registration === null) return streamResponse(attachment, null);
|
|
184
|
+
const lease = mintLease();
|
|
185
|
+
attachment.registration.conn.lease = lease;
|
|
195
186
|
return streamResponse(attachment, lease);
|
|
196
187
|
};
|
|
197
188
|
const stream = async (h, request) => {
|
|
@@ -222,7 +213,6 @@ const frames = async (h, request) => {
|
|
|
222
213
|
const leased = lease === null ? void 0 : [...h.conns].find((conn) => conn.open && conn.lease === lease);
|
|
223
214
|
if (h.disposed || h.finished !== null || leased === void 0) return Response.json({ error: "stale-lease" }, { status: 423 });
|
|
224
215
|
const clientId = leased.clientId;
|
|
225
|
-
const rec = h.clients.get(clientId);
|
|
226
216
|
const raw = await readFrameBody(request);
|
|
227
217
|
if (raw === null) return Response.json({ error: "frame body too large" }, { status: 413 });
|
|
228
218
|
let frame;
|
|
@@ -235,7 +225,7 @@ const frames = async (h, request) => {
|
|
|
235
225
|
if (!leased.open || h.disposed || h.finished !== null) return Response.json({ error: "stale-lease" }, { status: 423 });
|
|
236
226
|
let admission;
|
|
237
227
|
try {
|
|
238
|
-
admission = admitFrame(h,
|
|
228
|
+
admission = admitFrame(h, leased, frame);
|
|
239
229
|
} catch (error) {
|
|
240
230
|
if (!(error instanceof Error) || error.name !== "StatewireViolation") throw error;
|
|
241
231
|
return violationResponse(h, clientId, error.message);
|
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","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
|
+
{"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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"}
|
package/dist/host/socket.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import { StatewireDocuments } from "../wire/documents.js";
|
|
2
|
-
import { StatewireProtocol } from "../wire/protocol.js";
|
|
3
1
|
import { CommandsHost } from "./host.js";
|
|
4
2
|
import { StatewireHost } from "../host.js";
|
|
5
|
-
import {
|
|
3
|
+
import { StatewireProtocol } from "../wire/protocol.js";
|
|
6
4
|
import { Statewire } from "../types.js";
|
|
7
5
|
import { StatewirePath } from "../wire/ops.js";
|
|
8
6
|
import { StatewireDocument } from "../document.js";
|
|
@@ -15,31 +13,41 @@ declare const PRINCIPAL_MISMATCH_MESSAGE = "client id is bound to another princi
|
|
|
15
13
|
* objects compare by value; anything else by identity. */
|
|
16
14
|
declare const authEquals: (a: unknown, b: unknown) => boolean;
|
|
17
15
|
declare const SUBSCRIBER_LIMIT_MESSAGE = "stream subscriber limit reached";
|
|
16
|
+
type Cmd = StatewireHostInternal.Cmd;
|
|
18
17
|
type WireOp = {
|
|
19
18
|
op: string;
|
|
19
|
+
doc?: number;
|
|
20
20
|
path?: StatewirePath;
|
|
21
21
|
value?: unknown;
|
|
22
22
|
meta?: StatewireDocument.MetaEntry[];
|
|
23
|
+
protocol?: string;
|
|
24
|
+
key?: readonly string[];
|
|
25
|
+
main?: boolean;
|
|
23
26
|
};
|
|
24
27
|
type Admission = {
|
|
28
|
+
protocol: string;
|
|
25
29
|
seq: number;
|
|
26
30
|
method: string;
|
|
27
31
|
applied: boolean;
|
|
28
32
|
appliedSent: boolean;
|
|
29
33
|
settled: boolean;
|
|
30
34
|
};
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
/** One statement lane: durable lanes live on the client record, ephemeral lanes on the attach. */
|
|
36
|
+
type Lane = {
|
|
37
|
+
protocol: string;
|
|
38
|
+
clientId: string;
|
|
39
|
+
conn: Conn | null;
|
|
33
40
|
lastSeq: number;
|
|
34
|
-
ctxSeq: number;
|
|
35
|
-
touched: number;
|
|
36
|
-
context: Record<string, unknown> | null;
|
|
37
|
-
contextDocuments: StatewireDocuments.State;
|
|
38
41
|
results: Map<number, {
|
|
39
42
|
ts: number;
|
|
40
|
-
cmd:
|
|
43
|
+
cmd: Cmd;
|
|
41
44
|
}>;
|
|
42
45
|
inflight: Map<number, Admission>;
|
|
46
|
+
};
|
|
47
|
+
type ClientRec = {
|
|
48
|
+
id: string;
|
|
49
|
+
touched: number;
|
|
50
|
+
lanes: Map<string, Lane>;
|
|
43
51
|
/** The auth captured at the record's first attach; later attaches must match. */
|
|
44
52
|
principal: unknown;
|
|
45
53
|
};
|
|
@@ -48,11 +56,23 @@ type Conn = {
|
|
|
48
56
|
clientId: string;
|
|
49
57
|
socket: StatewireHostInternal.Socket;
|
|
50
58
|
open: boolean;
|
|
59
|
+
ready: boolean;
|
|
51
60
|
sawFrame: boolean;
|
|
52
61
|
lastSentAt: number;
|
|
53
62
|
lease: string | null;
|
|
54
63
|
/** The awaited `authorize` return captured at this attach's authorization. */
|
|
55
64
|
auth: unknown;
|
|
65
|
+
lanes: Map<string, Lane>;
|
|
66
|
+
documents: Map<string, StatewireHost.Document>;
|
|
67
|
+
included: Set<string>;
|
|
68
|
+
view: Map<string, {
|
|
69
|
+
doc: number;
|
|
70
|
+
value: unknown;
|
|
71
|
+
}>;
|
|
72
|
+
nextDoc: number;
|
|
73
|
+
pendingApplied: Cmd[];
|
|
74
|
+
pendingCmds: Cmd[];
|
|
75
|
+
attach: StatewireHost.Attach;
|
|
56
76
|
};
|
|
57
77
|
type PendingConn = {
|
|
58
78
|
socket: StatewireHostInternal.Socket;
|
|
@@ -62,10 +82,6 @@ type PendingConn = {
|
|
|
62
82
|
timer: ReturnType<typeof setTimeout> | null;
|
|
63
83
|
connection: StatewireHostInternal.SocketConnection | null;
|
|
64
84
|
};
|
|
65
|
-
type CtxStatement = {
|
|
66
|
-
seq: number;
|
|
67
|
-
ops: unknown[];
|
|
68
|
-
};
|
|
69
85
|
type CmdStatement = {
|
|
70
86
|
protocol: string;
|
|
71
87
|
seq: number;
|
|
@@ -74,7 +90,6 @@ type CmdStatement = {
|
|
|
74
90
|
meta?: unknown;
|
|
75
91
|
};
|
|
76
92
|
type ParsedFrame = {
|
|
77
|
-
ctx: CtxStatement[];
|
|
78
93
|
cmd: CmdStatement[];
|
|
79
94
|
};
|
|
80
95
|
declare const parseFrameValue: (frame: unknown) => ParsedFrame;
|
|
@@ -95,8 +110,9 @@ type SocketHostState = {
|
|
|
95
110
|
conns: Set<Conn>;
|
|
96
111
|
pendingConns: Set<PendingConn>;
|
|
97
112
|
pendingOps: WireOp[];
|
|
98
|
-
pendingApplied: Map<string,
|
|
99
|
-
pendingCmds: Map<string,
|
|
113
|
+
pendingApplied: Map<string, Cmd[]>;
|
|
114
|
+
pendingCmds: Map<string, Cmd[]>;
|
|
115
|
+
viewsDirty: boolean;
|
|
100
116
|
idleWire: boolean;
|
|
101
117
|
finished: StatewireHost.Fin | null;
|
|
102
118
|
disposed: boolean;
|
|
@@ -122,26 +138,33 @@ type FrameAdmission = {
|
|
|
122
138
|
type: "refused";
|
|
123
139
|
syn: {
|
|
124
140
|
syn: {
|
|
125
|
-
|
|
126
|
-
|
|
141
|
+
protocols: {
|
|
142
|
+
name: string;
|
|
143
|
+
seq: number;
|
|
144
|
+
}[];
|
|
127
145
|
retry?: number;
|
|
128
146
|
};
|
|
129
147
|
};
|
|
130
148
|
};
|
|
131
|
-
declare const admitFrame: (h: SocketHostState,
|
|
149
|
+
declare const admitFrame: (h: SocketHostState, conn: Conn, frame: ParsedFrame) => FrameAdmission;
|
|
132
150
|
type Registration = {
|
|
133
|
-
rec: ClientRec;
|
|
134
151
|
conn: Conn;
|
|
135
152
|
connection: StatewireHostInternal.SocketConnection;
|
|
136
153
|
};
|
|
137
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Attach a socket: ephemeral-lane statements of the attach frame are admitted
|
|
156
|
+
* before the snapshot (their documents and answers ride the hello), durable
|
|
157
|
+
* ones after it. A violation in the attach frame closes the attach and throws.
|
|
158
|
+
*/
|
|
159
|
+
declare const register: (h: SocketHostState, clientId: string, socket: StatewireHostInternal.Socket, sawFrame?: boolean, auth?: unknown, selection?: StatewireProtocol.Selection, frame?: ParsedFrame) => Registration | null;
|
|
138
160
|
/**
|
|
139
161
|
* Transport-agnostic WS binding over a statewire/host transport:
|
|
140
162
|
* feed it sockets (anything with `send`/`close`) and raw text frames, and it
|
|
141
163
|
* speaks the Statewire packet protocol — snapshot-first hello with `syn`,
|
|
142
|
-
*
|
|
143
|
-
* arrays fanned out to every attach of a client id
|
|
144
|
-
*
|
|
164
|
+
* per-protocol statement lanes under one admission law, `{cmd}` answer
|
|
165
|
+
* arrays fanned out to every attach of a client id (durable lanes) or the
|
|
166
|
+
* admitting attach (ephemeral lanes), per-attach document views, idle edges,
|
|
167
|
+
* and `fin` on finish.
|
|
145
168
|
*/
|
|
146
169
|
declare const StatewireSocketHost: (transport: ResourceElement<Statewire>, options?: StatewireHostInternal.SocketHostOptions, persistence?: StatewireHostInternal.SocketHostPersistence) => StatewireHostInternal.SocketHost;
|
|
147
170
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket.d.ts","names":[],"sources":["../../src/host/socket.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"socket.d.ts","names":[],"sources":["../../src/host/socket.ts"],"mappings":";;;;;;;;;cA+Ba,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,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;EAAgB,KAAK;;cAOpB,kBAAmB,mBAAiB;cA4CpC,aAAc,gBAAc;KAgG7B;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,YAAY;EAC5B,aAAa,YAAY;EACzB;EACA;EACA,UAAU,cAAc;EACxB;EACA;EACA,gBAAgB,kBAAkB;EAClC,WAAW,cAAc;EACzB,cAAc;EACd,SAAS;EACT,aAAa,sBAAsB;EACnC;EACA,UACE,kBACA,QAAQ,sBAAsB,QAC9B,UAAU,sBAAsB,yBAC7B,sBAAsB;EAC3B,SAAS,KAAK,cAAc;EAC5B,SAAS,SAAS,YAAY,QAAQ;EACtC,SAAS,SAAS,YAAY,QAAQ;;cAsD3B,YACX,GAAG,iBACH,MAAM,MACN,eACA;cAiBW,SAAU,GAAG,iBAAiB,MAAM,MAAM;cAmB1C,oBAAqB,GAAG;cA+KxB,QAAS,GAAG,iBAAiB,MAAM,cAAc;KA0UlD;EACN;;EAEA;EACA;IACE;MAAO;QAAa;QAAc;;MAAiB;;;;cAqB9C,aACX,GAAG,iBACH,MAAM,MACN,OAAO,gBACN;KAwYS;EACV,MAAM;EACN,YAAY,sBAAsB;;;;;;;cAQvB,WACX,GAAG,iBACH,kBACA,QAAQ,sBAAsB,QAC9B,oBACA,gBACA,YAAW,kBAAkB,WAI7B,QAAO,gBACN;;;;;;;;;;cA4TU,sBACX,WAAW,gBAAgB,YAC3B,UAAU,sBAAsB,mBAChC,cAAc,sBAAsB,0BACnC,sBAAsB"}
|