statewire 0.0.5 → 0.5.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/README.md +1 -1
- package/dist/client/connection.d.ts +1 -1
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +1 -0
- package/dist/client/core.js.map +1 -1
- package/dist/client/session.d.ts +1 -5
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +18 -29
- package/dist/client/session.js.map +1 -1
- package/dist/host/host.d.ts +2 -14
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +41 -92
- package/dist/host/host.js.map +1 -1
- package/dist/host.d.ts +11 -17
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +2 -2
- package/dist/statewire.d.ts +8 -14
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js.map +1 -1
- package/dist/transports/remote.js +1 -4
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +4 -1
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +41 -25
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/ws.d.ts +2 -1
- package/dist/transports/ws.d.ts.map +1 -1
- package/dist/transports/ws.js +25 -6
- package/dist/transports/ws.js.map +1 -1
- package/dist/util/statePath.js +8 -6
- package/dist/util/statePath.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +1 -1
- package/src/client/core.ts +1 -0
- package/src/client/session.ts +9 -26
- package/src/host/host.ts +55 -113
- package/src/host.ts +9 -19
- package/src/statewire.ts +8 -14
- package/src/transports/remote.ts +1 -7
- package/src/transports/sse.ts +71 -27
- package/src/transports/ws.ts +35 -4
package/dist/host/host.js
CHANGED
|
@@ -69,64 +69,6 @@ const useStatewireState = (init) => {
|
|
|
69
69
|
}
|
|
70
70
|
return [value, cell.ctl];
|
|
71
71
|
};
|
|
72
|
-
const executionStates = /* @__PURE__ */ new WeakMap();
|
|
73
|
-
/**
|
|
74
|
-
* `CommandExecution(executor)` runs the (possibly async) `executor({ ack })`
|
|
75
|
-
* immediately: its return value resolves the execution, a throw/rejection
|
|
76
|
-
* rejects it; `ack()` releases the client's ack watermark before the executor
|
|
77
|
-
* finishes. `CommandExecution.withResolvers()` returns `[execution,
|
|
78
|
-
* { ack, resolve, reject }]` instead — settle them from your own spawned work.
|
|
79
|
-
*/
|
|
80
|
-
const CommandExecution = Object.assign((executor) => {
|
|
81
|
-
const state = {
|
|
82
|
-
acked: false,
|
|
83
|
-
settled: null,
|
|
84
|
-
bind: null
|
|
85
|
-
};
|
|
86
|
-
const execution = Object.freeze({});
|
|
87
|
-
executionStates.set(execution, state);
|
|
88
|
-
const settle = (settled) => {
|
|
89
|
-
if (state.settled) return;
|
|
90
|
-
state.settled = settled;
|
|
91
|
-
state.acked = true;
|
|
92
|
-
state.bind?.onSettle(settled);
|
|
93
|
-
};
|
|
94
|
-
const ack = () => {
|
|
95
|
-
if (state.acked) return;
|
|
96
|
-
state.acked = true;
|
|
97
|
-
state.bind?.onAck();
|
|
98
|
-
};
|
|
99
|
-
(async () => {
|
|
100
|
-
try {
|
|
101
|
-
settle({
|
|
102
|
-
type: "resolved",
|
|
103
|
-
value: await executor({ ack })
|
|
104
|
-
});
|
|
105
|
-
} catch (error) {
|
|
106
|
-
settle({
|
|
107
|
-
type: "rejected",
|
|
108
|
-
error
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
})();
|
|
112
|
-
return execution;
|
|
113
|
-
}, { withResolvers: () => {
|
|
114
|
-
let resolve;
|
|
115
|
-
let reject;
|
|
116
|
-
const promise = new Promise((res, rej) => {
|
|
117
|
-
resolve = res;
|
|
118
|
-
reject = rej;
|
|
119
|
-
});
|
|
120
|
-
let ack;
|
|
121
|
-
return [CommandExecution(({ ack: a }) => {
|
|
122
|
-
ack = a;
|
|
123
|
-
return promise;
|
|
124
|
-
}), {
|
|
125
|
-
ack: () => ack(),
|
|
126
|
-
resolve,
|
|
127
|
-
reject
|
|
128
|
-
}];
|
|
129
|
-
} });
|
|
130
72
|
const commandsHosts = /* @__PURE__ */ new WeakMap();
|
|
131
73
|
const getCommandsHost = (commands) => commandsHosts.get(commands);
|
|
132
74
|
const bindCommandsHost = (commands, host) => {
|
|
@@ -173,13 +115,8 @@ const settleInvocation = (cell, inv, call, settled) => {
|
|
|
173
115
|
call.reject(error);
|
|
174
116
|
}
|
|
175
117
|
};
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
watchStall(cell);
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
const call = cell.queue.shift();
|
|
182
|
-
if (!call) return;
|
|
118
|
+
const isThenable = (value) => typeof value === "object" && value !== null && typeof value.then === "function";
|
|
119
|
+
const dispatch = (cell, call) => {
|
|
183
120
|
const handler = cell.handlers[call.method];
|
|
184
121
|
if (handler === void 0) {
|
|
185
122
|
const error = new StatewireSendError({
|
|
@@ -191,7 +128,6 @@ const pump = (cell) => {
|
|
|
191
128
|
error
|
|
192
129
|
});
|
|
193
130
|
call.reject(error);
|
|
194
|
-
pump(cell);
|
|
195
131
|
return;
|
|
196
132
|
}
|
|
197
133
|
const inv = {
|
|
@@ -203,35 +139,48 @@ const pump = (cell) => {
|
|
|
203
139
|
...call.origin !== void 0 && { origin: call.origin },
|
|
204
140
|
...call.origin?.clientId !== void 0 && { clientId: call.origin.clientId }
|
|
205
141
|
};
|
|
206
|
-
cell.running = true;
|
|
207
142
|
cell.listener?.onStart(inv);
|
|
208
143
|
cell.listener?.onPosted?.(inv);
|
|
209
144
|
cell.listener?.onCovered?.(inv);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
145
|
+
const ctx = {
|
|
146
|
+
caller: call.origin?.caller ?? LOCAL_CALLER,
|
|
147
|
+
ack: () => ackInvocation(cell, inv)
|
|
148
|
+
};
|
|
149
|
+
try {
|
|
150
|
+
const value = handler.call(ctx, ...call.params);
|
|
151
|
+
if (isThenable(value)) value.then((resolved) => settleInvocation(cell, inv, call, {
|
|
152
|
+
type: "resolved",
|
|
153
|
+
value: resolved
|
|
154
|
+
}), (error) => settleInvocation(cell, inv, call, {
|
|
155
|
+
type: "rejected",
|
|
156
|
+
error
|
|
157
|
+
}));
|
|
158
|
+
else settleInvocation(cell, inv, call, {
|
|
159
|
+
type: "resolved",
|
|
160
|
+
value
|
|
161
|
+
});
|
|
162
|
+
} catch (error) {
|
|
163
|
+
settleInvocation(cell, inv, call, {
|
|
164
|
+
type: "rejected",
|
|
165
|
+
error
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const pump = (cell) => {
|
|
170
|
+
if (cell.running || !cell.handlers) {
|
|
171
|
+
watchStall(cell);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
cell.running = true;
|
|
175
|
+
try {
|
|
176
|
+
while (cell.handlers) {
|
|
177
|
+
const call = cell.queue.shift();
|
|
178
|
+
if (!call) break;
|
|
179
|
+
dispatch(cell, call);
|
|
233
180
|
}
|
|
234
|
-
}
|
|
181
|
+
} finally {
|
|
182
|
+
cell.running = false;
|
|
183
|
+
}
|
|
235
184
|
};
|
|
236
185
|
const watchStall = (cell) => {
|
|
237
186
|
if (!isDev() || cell.stallTimer !== null || cell.queue.length === 0) return;
|
|
@@ -333,6 +282,6 @@ function _temp() {
|
|
|
333
282
|
return emit;
|
|
334
283
|
}
|
|
335
284
|
//#endregion
|
|
336
|
-
export {
|
|
285
|
+
export { bindCommandsHost, getCommandsHost, getEmitHost, getStateHost, useStatewireCommands, useStatewireEmit, useStatewireState };
|
|
337
286
|
|
|
338
287
|
//# 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","ServerOp","StateDraft","StatewireSendError","Statewire","StatewireHost","isDev","process","env","stateHosts","WeakMap","StateHost","getStateHost","state","get","StateCell","current","draft","changed","onOp","op","onChange","ctl","StateCtl","host","bump","useStatewireState","init","TState","setVersion","cell","commit","c","flush","v","apply","ops","result","events","length","Error","invalidate","push","changedPaths","snapshot","setOnOp","listener","setOnChange","takeChanged","value","record","changedPath","set","proxy","const","ExecutionSettled","type","error","ExecutionBind","onAck","onSettle","settled","ExecutionState","acked","bind","executionStates","CommandExecution","Object","assign","executor","resolvers","ack","T","PromiseLike","Execution","execution","freeze","settle","withResolvers","ExecutionResolvers","resolve","reject","promise","Promise","res","rej","a","CommandsInvocation","method","params","ts","seq","clientId","CommandsOutcome","payload","CommandsListener","onStart","inv","onPosted","onCovered","outcome","CommandsSubmission","caller","Caller","CommandsHost","setListener","submit","call","commandsHosts","getCommandsHost","commands","bindCommandsHost","QueuedCall","origin","ActiveInvocation","CommandsCell","handlers","Record","args","queue","running","unmounted","stallTimer","ReturnType","setTimeout","STALL_WARN_MS","LOCAL_CALLER","context","isConnected","toSendError","fate","cause","message","String","undefined","ackInvocation","settleInvocation","pump","watchStall","shift","handler","console","warn","useStatewireCommands","Commands","THandlers","ThisType","CommandsProxy","HandledCommands","built","Date","now","clearTimeout","queueMicrotask","queued","EmitHost","setSink","sink","event","emitHosts","getEmitHost","emit","useStatewireEmit","_temp"],"sources":["../../src/host/host.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { applyOps, sanitizeValue } from \"../wire/ops\";\nimport type { StatewirePath } from \"../wire/ops\";\nimport { StatewireReject } from \"./engine\";\nimport { createStateDraft } from \"./draft\";\nimport { createMethodProxy } from \"../util/methodProxy\";\nimport type { ServerOp, StateDraft } from \"./draft\";\nimport { StatewireSendError } from \"../types\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireHost } from \"../host\";\n\nconst isDev = () =>\n typeof process === \"undefined\" || process.env[\"NODE_ENV\"] !== \"production\";\n\n// ─── state ──────────────────────────────────────────────\n\nconst stateHosts = new WeakMap<object, StatewireHost.StateHost>();\n\n/** Host-side accessor for a `useStatewireState` state object's source (`snapshot()` reads the committed state). */\nexport const getStateHost = (\n state: object,\n): StatewireHost.StateHost | undefined => stateHosts.get(state);\n\ntype StateCell = {\n current: unknown;\n draft: StateDraft<object> | null;\n changed: StatewirePath[];\n onOp: ((op: ServerOp) => void) | null;\n onChange: (() => void) | null;\n ctl: StatewireHost.StateCtl;\n host: StatewireHost.StateHost;\n bump: () => void;\n};\n\nexport const useStatewireState = <TState>(\n init: () => TState,\n): readonly [TState, StatewireHost.StateCtl<TState>] => {\n const [, setVersion] = useState(0);\n const [cell] = useState((): StateCell => {\n const commit = () => {\n if (c.draft) c.current = c.draft.flush();\n return c.current;\n };\n const c: StateCell = {\n current: sanitizeValue(init()),\n draft: null,\n changed: [[]],\n onOp: null,\n onChange: null,\n bump: () => setVersion((v) => v + 1),\n ctl: {\n apply: (ops) => {\n const result = applyOps(commit(), ops);\n if (result.events.length > 0) {\n throw new Error(\n \"statewire: event ops do not apply to state — route them to emit\",\n );\n }\n c.current = result.state;\n c.draft?.invalidate();\n c.changed.push(...result.changedPaths);\n if (c.onOp) for (const op of ops) c.onOp(op as ServerOp);\n c.bump();\n c.onChange?.();\n },\n snapshot: commit,\n },\n host: {\n snapshot: commit,\n setOnOp: (listener) => {\n c.onOp = listener;\n },\n setOnChange: (listener) => {\n c.onChange = listener;\n },\n takeChanged: () => {\n const changed = c.changed;\n c.changed = [];\n return changed;\n },\n },\n };\n return c;\n });\n\n let value = cell.ctl.snapshot();\n if (typeof value === \"object\" && value !== null) {\n cell.draft ??= createStateDraft<object>({\n snapshot: () => cell.current,\n record: (op, changedPath) => {\n cell.changed.push(changedPath);\n cell.onOp?.(op);\n cell.bump();\n cell.onChange?.();\n },\n });\n stateHosts.set(cell.draft.proxy, cell.host);\n value = cell.draft.proxy;\n }\n return [value as TState, cell.ctl as StatewireHost.StateCtl<TState>] as const;\n};\n\n// ─── commands ───────────────────────────────────────────\n\ntype ExecutionSettled =\n | { type: \"resolved\"; value: unknown }\n | { type: \"rejected\"; error: unknown };\n\ntype ExecutionBind = {\n onAck: () => void;\n onSettle: (settled: ExecutionSettled) => void;\n};\n\ntype ExecutionState = {\n acked: boolean;\n settled: ExecutionSettled | null;\n bind: ExecutionBind | null;\n};\n\nconst executionStates = new WeakMap<object, ExecutionState>();\n\n/**\n * `CommandExecution(executor)` runs the (possibly async) `executor({ ack })`\n * immediately: its return value resolves the execution, a throw/rejection\n * rejects it; `ack()` releases the client's ack watermark before the executor\n * finishes. `CommandExecution.withResolvers()` returns `[execution,\n * { ack, resolve, reject }]` instead — settle them from your own spawned work.\n */\nexport const CommandExecution = Object.assign(\n <T = void>(\n executor: (resolvers: {\n ack: () => void;\n }) => T | void | PromiseLike<T | void>,\n ): StatewireHost.Execution<T> => {\n const state: ExecutionState = { acked: false, settled: null, bind: null };\n const execution = Object.freeze(\n {},\n ) as unknown as StatewireHost.Execution<T>;\n executionStates.set(execution, state);\n const settle = (settled: ExecutionSettled) => {\n if (state.settled) return;\n state.settled = settled;\n state.acked = true;\n state.bind?.onSettle(settled);\n };\n const ack = () => {\n if (state.acked) return;\n state.acked = true;\n state.bind?.onAck();\n };\n void (async () => {\n try {\n settle({ type: \"resolved\", value: await executor({ ack }) });\n } catch (error) {\n settle({ type: \"rejected\", error });\n }\n })();\n return execution;\n },\n {\n withResolvers: <T = void>(): [\n StatewireHost.Execution<T>,\n StatewireHost.ExecutionResolvers<T>,\n ] => {\n let resolve!: (value: T) => void;\n let reject!: (error: unknown) => void;\n const promise = new Promise<T>((res, rej) => {\n resolve = res;\n reject = rej;\n });\n let ack!: () => void;\n const execution = CommandExecution<T>(({ ack: a }) => {\n ack = a;\n return promise;\n });\n return [execution, { ack: () => ack(), resolve, reject }];\n },\n },\n);\n\nexport type CommandsInvocation = {\n readonly method: string;\n readonly params: readonly unknown[];\n readonly ts: number;\n readonly seq: number;\n readonly clientId?: string;\n};\n\nexport type CommandsOutcome =\n | { type: \"accepted\"; payload?: unknown }\n | { type: \"rejected\"; error: unknown };\n\nexport type CommandsListener = {\n onStart: (inv: CommandsInvocation) => void;\n onPosted?: (inv: CommandsInvocation) => void;\n onAck: (inv: CommandsInvocation) => void;\n /** Canonical state now covers the invocation's effect — drop any overlay. */\n onCovered?: (inv: CommandsInvocation) => void;\n onSettle: (inv: CommandsInvocation, outcome: CommandsOutcome) => void;\n};\n\nexport type CommandsSubmission = {\n method: string;\n params: readonly unknown[];\n clientId?: string;\n caller?: StatewireHost.Caller;\n onAck?: () => void;\n onSettle?: (outcome: CommandsOutcome) => void;\n};\n\nexport type CommandsHost = {\n setListener: (listener: CommandsListener | null) => void;\n submit?: (call: CommandsSubmission) => Promise<unknown>;\n};\n\nconst commandsHosts = new WeakMap<object, CommandsHost>();\n\nexport const getCommandsHost = (commands: object): CommandsHost | undefined =>\n commandsHosts.get(commands);\n\nexport const bindCommandsHost = (commands: object, host: CommandsHost) => {\n commandsHosts.set(commands, host);\n};\n\ntype QueuedCall = {\n method: string;\n params: readonly unknown[];\n ts: number;\n resolve: (value: unknown) => void;\n reject: (error: unknown) => void;\n origin?: CommandsSubmission;\n};\n\ntype ActiveInvocation = CommandsInvocation & {\n acked: boolean;\n origin?: CommandsSubmission;\n};\n\ntype CommandsCell = {\n handlers: Record<string, (...args: never[]) => unknown> | null;\n queue: QueuedCall[];\n running: boolean;\n listener: CommandsListener | null;\n seq: number;\n unmounted: boolean;\n stallTimer: ReturnType<typeof setTimeout> | null;\n};\n\nconst STALL_WARN_MS = 5000;\n\n// In-process dispatch has no wire client: never any ambient context, trivially attached.\nconst LOCAL_CALLER: StatewireHost.Caller = Object.freeze({\n get clientId(): string {\n throw new Error(\"statewire: in-process dispatch has no client id\");\n },\n context: null,\n isConnected: true,\n});\n\nconst toSendError = (error: unknown): StatewireSendError =>\n error instanceof StatewireSendError\n ? error\n : new StatewireSendError({\n fate: \"rejected\",\n cause: \"server\",\n message: error instanceof Error ? error.message : String(error),\n ...(error instanceof StatewireReject &&\n error.payload !== undefined && { payload: error.payload }),\n });\n\nconst ackInvocation = (cell: CommandsCell, inv: ActiveInvocation) => {\n if (inv.acked) return;\n inv.acked = true;\n cell.listener?.onAck(inv);\n inv.origin?.onAck?.();\n};\n\nconst settleInvocation = (\n cell: CommandsCell,\n inv: ActiveInvocation,\n call: QueuedCall,\n settled: ExecutionSettled,\n) => {\n ackInvocation(cell, inv);\n if (settled.type === \"resolved\") {\n const outcome: CommandsOutcome =\n settled.value === undefined\n ? { type: \"accepted\" }\n : { type: \"accepted\", payload: sanitizeValue(settled.value) };\n cell.listener?.onSettle(inv, outcome);\n inv.origin?.onSettle?.(outcome);\n call.resolve(outcome.payload);\n } else {\n const error = toSendError(settled.error);\n const outcome: CommandsOutcome = { type: \"rejected\", error };\n cell.listener?.onSettle(inv, outcome);\n inv.origin?.onSettle?.(outcome);\n call.reject(error);\n }\n};\n\nconst pump = (cell: CommandsCell) => {\n if (cell.running || !cell.handlers) {\n watchStall(cell);\n return;\n }\n const call = cell.queue.shift();\n if (!call) return;\n const handler = cell.handlers[call.method];\n if (handler === undefined) {\n const error = new StatewireSendError({\n fate: \"unknown-command\",\n cause: \"server\",\n });\n call.origin?.onSettle?.({ type: \"rejected\", error });\n call.reject(error);\n pump(cell);\n return;\n }\n const inv: ActiveInvocation = {\n method: call.method,\n params: call.params,\n ts: call.ts,\n seq: ++cell.seq,\n acked: false,\n ...(call.origin !== undefined && { origin: call.origin }),\n ...(call.origin?.clientId !== undefined && {\n clientId: call.origin.clientId,\n }),\n };\n cell.running = true;\n cell.listener?.onStart(inv);\n cell.listener?.onPosted?.(inv);\n cell.listener?.onCovered?.(inv);\n void (async () => {\n try {\n const value = await (\n handler as (...args: readonly unknown[]) => unknown\n ).call(call.origin?.caller ?? LOCAL_CALLER, ...call.params);\n const execution =\n typeof value === \"object\" && value !== null\n ? executionStates.get(value)\n : undefined;\n if (execution) {\n if (execution.acked) ackInvocation(cell, inv);\n if (execution.settled) {\n settleInvocation(cell, inv, call, execution.settled);\n } else {\n execution.bind = {\n onAck: () => ackInvocation(cell, inv),\n onSettle: (settled) => settleInvocation(cell, inv, call, settled),\n };\n }\n } else {\n settleInvocation(cell, inv, call, { type: \"resolved\", value });\n }\n } catch (error) {\n settleInvocation(cell, inv, call, { type: \"rejected\", error });\n } finally {\n cell.running = false;\n pump(cell);\n }\n })();\n};\n\nconst watchStall = (cell: CommandsCell) => {\n if (!isDev() || cell.stallTimer !== null || cell.queue.length === 0) return;\n if (cell.handlers) return;\n cell.stallTimer = setTimeout(() => {\n cell.stallTimer = null;\n if (cell.handlers === null && cell.queue.length > 0 && !cell.unmounted) {\n console.warn(\n `statewire: ${cell.queue.length} buffered command(s) waiting over ${STALL_WARN_MS}ms — did the host forget to arm useStatewireCommands?`,\n );\n }\n }, STALL_WARN_MS);\n};\n\nexport const useStatewireCommands = <THandlers extends Statewire.Commands>(\n commands:\n | (THandlers & ThisType<StatewireHost.Caller>)\n | undefined\n | null\n | false,\n): Statewire.CommandsProxy<StatewireHost.HandledCommands<THandlers>> => {\n const [built] = useState(() => {\n const cell: CommandsCell = {\n handlers: null,\n queue: [],\n running: false,\n listener: null,\n seq: 0,\n unmounted: false,\n stallTimer: null,\n };\n const proxy = createMethodProxy<\n Statewire.CommandsProxy<StatewireHost.HandledCommands<THandlers>>\n >(\n (method, params) =>\n new Promise((resolve, reject) => {\n if (cell.unmounted) {\n reject(new Error(\"statewire: commands called after unmount\"));\n return;\n }\n cell.queue.push({\n method,\n params,\n ts: Date.now(),\n resolve,\n reject,\n });\n pump(cell);\n }),\n );\n commandsHosts.set(proxy, {\n setListener: (listener) => {\n cell.listener = listener;\n },\n submit: (origin) =>\n new Promise((resolve, reject) => {\n if (cell.unmounted) {\n reject(new Error(\"statewire: commands called after unmount\"));\n return;\n }\n cell.queue.push({\n method: origin.method,\n params: origin.params,\n ts: Date.now(),\n resolve,\n reject,\n origin,\n });\n pump(cell);\n }),\n });\n return { cell, proxy };\n });\n\n useEffect(() => {\n const { cell } = built;\n cell.unmounted = false;\n return () => {\n cell.unmounted = true;\n cell.handlers = null;\n if (cell.stallTimer !== null) {\n clearTimeout(cell.stallTimer);\n cell.stallTimer = null;\n }\n queueMicrotask(() => {\n if (!cell.unmounted) return;\n const queued = cell.queue;\n cell.queue = [];\n for (const call of queued) {\n call.reject(new Error(\"statewire: unmounted\"));\n }\n });\n };\n }, [built]);\n\n useEffect(() => {\n built.cell.handlers = commands || null;\n pump(built.cell);\n });\n\n return built.proxy;\n};\n\n// ─── emit ───────────────────────────────────────────────\n\nexport type EmitHost = {\n setSink: (sink: ((event: unknown) => void) | null) => void;\n};\n\nconst emitHosts = new WeakMap<object, EmitHost>();\n\nexport const getEmitHost = (emit: object | undefined): EmitHost | undefined =>\n emit && emitHosts.get(emit);\n\nexport const useStatewireEmit = <TEvent>(): ((event: TEvent) => void) => {\n const [built] = useState(() => {\n const cell = { sink: null as ((event: unknown) => void) | null };\n const emit = (event: TEvent) => {\n if (!cell.sink) {\n throw new Error(\"statewire: emit before a host bound the event sink\");\n }\n cell.sink(sanitizeValue(event));\n };\n emitHosts.set(emit, {\n setSink: (sink) => {\n cell.sink = sink;\n },\n });\n return emit;\n });\n return built;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"host.js","names":["useEffect","useState","applyOps","sanitizeValue","StatewirePath","StatewireReject","createStateDraft","createMethodProxy","ServerOp","StateDraft","StatewireSendError","Statewire","StatewireHost","isDev","process","env","stateHosts","WeakMap","StateHost","getStateHost","state","get","StateCell","current","draft","changed","onOp","op","onChange","ctl","StateCtl","host","bump","useStatewireState","init","TState","setVersion","cell","commit","c","flush","v","apply","ops","result","events","length","Error","invalidate","push","changedPaths","snapshot","setOnOp","listener","setOnChange","takeChanged","value","record","changedPath","set","proxy","const","Settled","type","error","CommandsInvocation","method","params","ts","seq","clientId","CommandsOutcome","payload","CommandsListener","onStart","inv","onPosted","onAck","onCovered","onSettle","outcome","CommandsSubmission","caller","Caller","CommandsHost","setListener","submit","call","Promise","commandsHosts","getCommandsHost","commands","bindCommandsHost","QueuedCall","resolve","reject","origin","ActiveInvocation","acked","CommandsCell","handlers","Record","args","queue","running","unmounted","stallTimer","ReturnType","setTimeout","STALL_WARN_MS","LOCAL_CALLER","Object","freeze","context","isConnected","toSendError","fate","cause","message","String","undefined","ackInvocation","settleInvocation","settled","isThenable","PromiseLike","then","dispatch","handler","ctx","Ctx","ack","this","resolved","pump","watchStall","shift","console","warn","useStatewireCommands","Commands","THandlers","ThisType","CommandsProxy","HandledCommands","built","Date","now","clearTimeout","queueMicrotask","queued","EmitHost","setSink","sink","event","emitHosts","getEmitHost","emit","useStatewireEmit","_temp"],"sources":["../../src/host/host.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { applyOps, sanitizeValue } from \"../wire/ops\";\nimport type { StatewirePath } from \"../wire/ops\";\nimport { StatewireReject } from \"./engine\";\nimport { createStateDraft } from \"./draft\";\nimport { createMethodProxy } from \"../util/methodProxy\";\nimport type { ServerOp, StateDraft } from \"./draft\";\nimport { StatewireSendError } from \"../types\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireHost } from \"../host\";\n\nconst isDev = () =>\n typeof process === \"undefined\" || process.env[\"NODE_ENV\"] !== \"production\";\n\n// ─── state ──────────────────────────────────────────────\n\nconst stateHosts = new WeakMap<object, StatewireHost.StateHost>();\n\n/** Host-side accessor for a `useStatewireState` state object's source (`snapshot()` reads the committed state). */\nexport const getStateHost = (\n state: object,\n): StatewireHost.StateHost | undefined => stateHosts.get(state);\n\ntype StateCell = {\n current: unknown;\n draft: StateDraft<object> | null;\n changed: StatewirePath[];\n onOp: ((op: ServerOp) => void) | null;\n onChange: (() => void) | null;\n ctl: StatewireHost.StateCtl;\n host: StatewireHost.StateHost;\n bump: () => void;\n};\n\nexport const useStatewireState = <TState>(\n init: () => TState,\n): readonly [TState, StatewireHost.StateCtl<TState>] => {\n const [, setVersion] = useState(0);\n const [cell] = useState((): StateCell => {\n const commit = () => {\n if (c.draft) c.current = c.draft.flush();\n return c.current;\n };\n const c: StateCell = {\n current: sanitizeValue(init()),\n draft: null,\n changed: [[]],\n onOp: null,\n onChange: null,\n bump: () => setVersion((v) => v + 1),\n ctl: {\n apply: (ops) => {\n const result = applyOps(commit(), ops);\n if (result.events.length > 0) {\n throw new Error(\n \"statewire: event ops do not apply to state — route them to emit\",\n );\n }\n c.current = result.state;\n c.draft?.invalidate();\n c.changed.push(...result.changedPaths);\n if (c.onOp) for (const op of ops) c.onOp(op as ServerOp);\n c.bump();\n c.onChange?.();\n },\n snapshot: commit,\n },\n host: {\n snapshot: commit,\n setOnOp: (listener) => {\n c.onOp = listener;\n },\n setOnChange: (listener) => {\n c.onChange = listener;\n },\n takeChanged: () => {\n const changed = c.changed;\n c.changed = [];\n return changed;\n },\n },\n };\n return c;\n });\n\n let value = cell.ctl.snapshot();\n if (typeof value === \"object\" && value !== null) {\n cell.draft ??= createStateDraft<object>({\n snapshot: () => cell.current,\n record: (op, changedPath) => {\n cell.changed.push(changedPath);\n cell.onOp?.(op);\n cell.bump();\n cell.onChange?.();\n },\n });\n stateHosts.set(cell.draft.proxy, cell.host);\n value = cell.draft.proxy;\n }\n return [value as TState, cell.ctl as StatewireHost.StateCtl<TState>] as const;\n};\n\n// ─── commands ───────────────────────────────────────────\n\ntype Settled =\n | { type: \"resolved\"; value: unknown }\n | { type: \"rejected\"; error: unknown };\n\nexport type CommandsInvocation = {\n readonly method: string;\n readonly params: readonly unknown[];\n readonly ts: number;\n readonly seq: number;\n readonly clientId?: string;\n};\n\nexport type CommandsOutcome =\n | { type: \"accepted\"; payload?: unknown }\n | { type: \"rejected\"; error: unknown };\n\nexport type CommandsListener = {\n onStart: (inv: CommandsInvocation) => void;\n onPosted?: (inv: CommandsInvocation) => void;\n onAck: (inv: CommandsInvocation) => void;\n /** Canonical state now covers the invocation's effect — drop any overlay. */\n onCovered?: (inv: CommandsInvocation) => void;\n onSettle: (inv: CommandsInvocation, outcome: CommandsOutcome) => void;\n};\n\nexport type CommandsSubmission = {\n method: string;\n params: readonly unknown[];\n clientId?: string;\n caller?: StatewireHost.Caller;\n onAck?: () => void;\n onSettle?: (outcome: CommandsOutcome) => void;\n};\n\nexport type CommandsHost = {\n setListener: (listener: CommandsListener | null) => void;\n submit?: (call: CommandsSubmission) => Promise<unknown>;\n};\n\nconst commandsHosts = new WeakMap<object, CommandsHost>();\n\nexport const getCommandsHost = (commands: object): CommandsHost | undefined =>\n commandsHosts.get(commands);\n\nexport const bindCommandsHost = (commands: object, host: CommandsHost) => {\n commandsHosts.set(commands, host);\n};\n\ntype QueuedCall = {\n method: string;\n params: readonly unknown[];\n ts: number;\n resolve: (value: unknown) => void;\n reject: (error: unknown) => void;\n origin?: CommandsSubmission;\n};\n\ntype ActiveInvocation = CommandsInvocation & {\n acked: boolean;\n origin?: CommandsSubmission;\n};\n\ntype CommandsCell = {\n handlers: Record<string, (...args: never[]) => unknown> | null;\n queue: QueuedCall[];\n running: boolean;\n listener: CommandsListener | null;\n seq: number;\n unmounted: boolean;\n stallTimer: ReturnType<typeof setTimeout> | null;\n};\n\nconst STALL_WARN_MS = 5000;\n\n// In-process dispatch has no wire client: never any ambient context, trivially attached.\nconst LOCAL_CALLER: StatewireHost.Caller = Object.freeze({\n get clientId(): string {\n throw new Error(\"statewire: in-process dispatch has no client id\");\n },\n context: null,\n isConnected: true,\n});\n\nconst toSendError = (error: unknown): StatewireSendError =>\n error instanceof StatewireSendError\n ? error\n : new StatewireSendError({\n fate: \"rejected\",\n cause: \"server\",\n message: error instanceof Error ? error.message : String(error),\n ...(error instanceof StatewireReject &&\n error.payload !== undefined && { payload: error.payload }),\n });\n\nconst ackInvocation = (cell: CommandsCell, inv: ActiveInvocation) => {\n if (inv.acked) return;\n inv.acked = true;\n cell.listener?.onAck(inv);\n inv.origin?.onAck?.();\n};\n\nconst settleInvocation = (\n cell: CommandsCell,\n inv: ActiveInvocation,\n call: QueuedCall,\n settled: Settled,\n) => {\n ackInvocation(cell, inv);\n if (settled.type === \"resolved\") {\n const outcome: CommandsOutcome =\n settled.value === undefined\n ? { type: \"accepted\" }\n : { type: \"accepted\", payload: sanitizeValue(settled.value) };\n cell.listener?.onSettle(inv, outcome);\n inv.origin?.onSettle?.(outcome);\n call.resolve(outcome.payload);\n } else {\n const error = toSendError(settled.error);\n const outcome: CommandsOutcome = { type: \"rejected\", error };\n cell.listener?.onSettle(inv, outcome);\n inv.origin?.onSettle?.(outcome);\n call.reject(error);\n }\n};\n\nconst isThenable = (value: unknown): value is PromiseLike<unknown> =>\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as { then?: unknown }).then === \"function\";\n\nconst dispatch = (cell: CommandsCell, call: QueuedCall) => {\n const handler = cell.handlers![call.method];\n if (handler === undefined) {\n const error = new StatewireSendError({\n fate: \"unknown-command\",\n cause: \"server\",\n });\n call.origin?.onSettle?.({ type: \"rejected\", error });\n call.reject(error);\n return;\n }\n const inv: ActiveInvocation = {\n method: call.method,\n params: call.params,\n ts: call.ts,\n seq: ++cell.seq,\n acked: false,\n ...(call.origin !== undefined && { origin: call.origin }),\n ...(call.origin?.clientId !== undefined && {\n clientId: call.origin.clientId,\n }),\n };\n cell.listener?.onStart(inv);\n cell.listener?.onPosted?.(inv);\n cell.listener?.onCovered?.(inv);\n const ctx: StatewireHost.Ctx = {\n caller: call.origin?.caller ?? LOCAL_CALLER,\n ack: () => ackInvocation(cell, inv),\n };\n try {\n const value = (\n handler as (\n this: StatewireHost.Ctx,\n ...args: readonly unknown[]\n ) => unknown\n ).call(ctx, ...call.params);\n if (isThenable(value)) {\n value.then(\n (resolved) =>\n settleInvocation(cell, inv, call, {\n type: \"resolved\",\n value: resolved,\n }),\n (error: unknown) =>\n settleInvocation(cell, inv, call, { type: \"rejected\", error }),\n );\n } else {\n settleInvocation(cell, inv, call, { type: \"resolved\", value });\n }\n } catch (error) {\n settleInvocation(cell, inv, call, { type: \"rejected\", error });\n }\n};\n\n// Prefix-only advancement: each handler runs synchronously to its first\n// await, then the pump moves on; settlement rides the returned promise.\nconst pump = (cell: CommandsCell) => {\n if (cell.running || !cell.handlers) {\n watchStall(cell);\n return;\n }\n cell.running = true;\n try {\n while (cell.handlers) {\n const call = cell.queue.shift();\n if (!call) break;\n dispatch(cell, call);\n }\n } finally {\n cell.running = false;\n }\n};\n\nconst watchStall = (cell: CommandsCell) => {\n if (!isDev() || cell.stallTimer !== null || cell.queue.length === 0) return;\n if (cell.handlers) return;\n cell.stallTimer = setTimeout(() => {\n cell.stallTimer = null;\n if (cell.handlers === null && cell.queue.length > 0 && !cell.unmounted) {\n console.warn(\n `statewire: ${cell.queue.length} buffered command(s) waiting over ${STALL_WARN_MS}ms — did the host forget to arm useStatewireCommands?`,\n );\n }\n }, STALL_WARN_MS);\n};\n\nexport const useStatewireCommands = <THandlers extends Statewire.Commands>(\n commands:\n | (THandlers & ThisType<StatewireHost.Ctx>)\n | undefined\n | null\n | false,\n): Statewire.CommandsProxy<StatewireHost.HandledCommands<THandlers>> => {\n const [built] = useState(() => {\n const cell: CommandsCell = {\n handlers: null,\n queue: [],\n running: false,\n listener: null,\n seq: 0,\n unmounted: false,\n stallTimer: null,\n };\n const proxy = createMethodProxy<\n Statewire.CommandsProxy<StatewireHost.HandledCommands<THandlers>>\n >(\n (method, params) =>\n new Promise((resolve, reject) => {\n if (cell.unmounted) {\n reject(new Error(\"statewire: commands called after unmount\"));\n return;\n }\n cell.queue.push({\n method,\n params,\n ts: Date.now(),\n resolve,\n reject,\n });\n pump(cell);\n }),\n );\n commandsHosts.set(proxy, {\n setListener: (listener) => {\n cell.listener = listener;\n },\n submit: (origin) =>\n new Promise((resolve, reject) => {\n if (cell.unmounted) {\n reject(new Error(\"statewire: commands called after unmount\"));\n return;\n }\n cell.queue.push({\n method: origin.method,\n params: origin.params,\n ts: Date.now(),\n resolve,\n reject,\n origin,\n });\n pump(cell);\n }),\n });\n return { cell, proxy };\n });\n\n useEffect(() => {\n const { cell } = built;\n cell.unmounted = false;\n return () => {\n cell.unmounted = true;\n cell.handlers = null;\n if (cell.stallTimer !== null) {\n clearTimeout(cell.stallTimer);\n cell.stallTimer = null;\n }\n queueMicrotask(() => {\n if (!cell.unmounted) return;\n const queued = cell.queue;\n cell.queue = [];\n for (const call of queued) {\n call.reject(new Error(\"statewire: unmounted\"));\n }\n });\n };\n }, [built]);\n\n useEffect(() => {\n built.cell.handlers = commands || null;\n pump(built.cell);\n });\n\n return built.proxy;\n};\n\n// ─── emit ───────────────────────────────────────────────\n\nexport type EmitHost = {\n setSink: (sink: ((event: unknown) => void) | null) => void;\n};\n\nconst emitHosts = new WeakMap<object, EmitHost>();\n\nexport const getEmitHost = (emit: object | undefined): EmitHost | undefined =>\n emit && emitHosts.get(emit);\n\nexport const useStatewireEmit = <TEvent>(): ((event: TEvent) => void) => {\n const [built] = useState(() => {\n const cell = { sink: null as ((event: unknown) => void) | null };\n const emit = (event: TEvent) => {\n if (!cell.sink) {\n throw new Error(\"statewire: emit before a host bound the event sink\");\n }\n cell.sink(sanitizeValue(event));\n };\n emitHosts.set(emit, {\n setSink: (sink) => {\n cell.sink = sink;\n },\n });\n return emit;\n });\n return built;\n};\n"],"mappings":"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"}
|
package/dist/host.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { Statewire } from "./types.js";
|
|
2
2
|
import { ServerOp, append } from "./host/draft.js";
|
|
3
3
|
import { StatewireReject } from "./host/engine.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getStateHost, useStatewireCommands, useStatewireEmit, useStatewireState } from "./host/host.js";
|
|
5
5
|
//#region src/host.d.ts
|
|
6
|
-
declare const executionResult: unique symbol;
|
|
7
|
-
type UnwrapExecution<V> = V extends StatewireHost.Execution<infer T> ? T : V;
|
|
8
6
|
declare namespace StatewireHost {
|
|
9
7
|
type StateCtl<TState = unknown> = {
|
|
10
8
|
/** Apply server-vocabulary ops (root replace = whole-state swap). */
|
|
@@ -18,21 +16,17 @@ declare namespace StatewireHost {
|
|
|
18
16
|
};
|
|
19
17
|
type StateSource = Statewire.StateSource;
|
|
20
18
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
19
|
+
* Per-invocation handle bound as `this` of a command handler: `caller` is
|
|
20
|
+
* the submitting client, `ack()` releases the client's optimistic overlay /
|
|
21
|
+
* ack watermark before the handler settles (idempotent, a no-op after
|
|
22
|
+
* settle). The command settles when the handler's returned promise does.
|
|
25
23
|
*/
|
|
26
|
-
type
|
|
27
|
-
readonly
|
|
28
|
-
|
|
29
|
-
type ExecutionResolvers<T = void> = {
|
|
30
|
-
ack: () => void;
|
|
31
|
-
resolve: (value: T) => void;
|
|
32
|
-
reject: (error: unknown) => void;
|
|
24
|
+
type Ctx = {
|
|
25
|
+
readonly caller: Caller;
|
|
26
|
+
readonly ack: () => void;
|
|
33
27
|
};
|
|
34
28
|
/**
|
|
35
|
-
* The submitting client
|
|
29
|
+
* The submitting client (`this.caller` in a command handler). Holdable
|
|
36
30
|
* across a long run: `clientId` is the client's `Statewire-Client-Id`
|
|
37
31
|
* (in-process dispatch has none — reading it throws), `context` reads the
|
|
38
32
|
* client's current ambient context live (null until the client sends one),
|
|
@@ -43,7 +37,7 @@ declare namespace StatewireHost {
|
|
|
43
37
|
readonly context: Record<string, unknown> | null;
|
|
44
38
|
readonly isConnected: boolean;
|
|
45
39
|
};
|
|
46
|
-
type HandledCommands<THandlers extends Statewire.Commands> = { [K in keyof THandlers]: (...args: Parameters<THandlers[K]>) =>
|
|
40
|
+
type HandledCommands<THandlers extends Statewire.Commands> = { [K in keyof THandlers]: (...args: Parameters<THandlers[K]>) => Awaited<ReturnType<THandlers[K]>>; };
|
|
47
41
|
type Fin = {
|
|
48
42
|
reason: "evicted" | "error" | "gone" | "superseded" | (string & {});
|
|
49
43
|
message?: string;
|
|
@@ -52,5 +46,5 @@ declare namespace StatewireHost {
|
|
|
52
46
|
};
|
|
53
47
|
}
|
|
54
48
|
//#endregion
|
|
55
|
-
export {
|
|
49
|
+
export { StatewireHost, StatewireReject, append, getStateHost, useStatewireCommands, useStatewireEmit, useStatewireState };
|
|
56
50
|
//# sourceMappingURL=host.d.ts.map
|
package/dist/host.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host.d.ts","names":[],"sources":["../src/host.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"host.d.ts","names":[],"sources":["../src/host.ts"],"mappings":";;;;;kBAYiB;OACH,SAAS;;IAEnB,QAAQ,cAAc,UAAU;;IAEhC,gBAAgB;;OAGN,YAAY,UAAU;;IAEhC,UAAU,YAAY,IAAI;;OAGhB,cAAc,UAAU;;;;;;;OAQxB;aACD,QAAQ;aACR;;;;;;;;;OAUC;aACD;aACA,SAAS;aACT;;OAGC,gBAAgB,kBAAkB,UAAU,eACrD,WAAW,gBACP,MAAM,WAAW,UAAU,QAC3B,QAAQ,WAAW,UAAU;OAGxB;IACV;IACA;IACA;IACA"}
|
package/dist/host.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { StatewireReject } from "./host/engine.js";
|
|
2
2
|
import { append } from "./host/draft.js";
|
|
3
|
-
import {
|
|
4
|
-
export {
|
|
3
|
+
import { getStateHost, useStatewireCommands, useStatewireEmit, useStatewireState } from "./host/host.js";
|
|
4
|
+
export { StatewireReject, append, getStateHost, useStatewireCommands, useStatewireEmit, useStatewireState };
|
package/dist/statewire.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ declare namespace StatewireClient {
|
|
|
28
28
|
onCommandChange?: (update: Statewire.CommandUpdate<TCommands>, ctx: {
|
|
29
29
|
state: NonNullable<TState>;
|
|
30
30
|
}) => void;
|
|
31
|
+
onConnectionChange?: (connection: Connection) => void;
|
|
31
32
|
};
|
|
32
33
|
type Helpers<TState, TCommands extends Statewire.Commands = Statewire.Commands> = {
|
|
33
34
|
/** The statewire's state type — remote statewires include `undefined` until the first attach snapshot applies. */
|
|
@@ -45,7 +46,7 @@ declare namespace StatewireClient {
|
|
|
45
46
|
status: "live";
|
|
46
47
|
} | {
|
|
47
48
|
status: "reconnecting";
|
|
48
|
-
cause: "dropped" | "evicted" | "error";
|
|
49
|
+
cause: "dropped" | "evicted" | "error" | "unauthorized";
|
|
49
50
|
attempt: number;
|
|
50
51
|
nextRetryAt: number;
|
|
51
52
|
lastError?: string;
|
|
@@ -104,30 +105,23 @@ declare namespace StatewireClient {
|
|
|
104
105
|
fate: "lost";
|
|
105
106
|
cause: "unmount" | "stream-gone" | "session-lost" | "superseded";
|
|
106
107
|
};
|
|
107
|
-
/**
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
*/
|
|
112
|
-
type SessionLostCommand = {
|
|
113
|
-
method: string;
|
|
114
|
-
params: unknown[];
|
|
115
|
-
seq: number;
|
|
116
|
-
};
|
|
108
|
+
/** Static headers, or a resolver invoked per request/attach; `staleAuth` is true right after an auth rejection. */
|
|
109
|
+
type HeadersOption = HeadersInit | ((ctx: {
|
|
110
|
+
staleAuth: boolean;
|
|
111
|
+
}) => HeadersInit | Promise<HeadersInit>);
|
|
117
112
|
type SSEOptions = StatewireSessionStoreOptions & {
|
|
118
113
|
url: string;
|
|
119
|
-
headers?:
|
|
114
|
+
headers?: HeadersOption;
|
|
120
115
|
fetch?: typeof globalThis.fetch;
|
|
121
116
|
onChange?: (connection: Connection) => void;
|
|
122
|
-
onCommandSessionLost?: (command: SessionLostCommand) => "replay" | void;
|
|
123
117
|
};
|
|
124
118
|
type WSOptions = StatewireSessionStoreOptions & {
|
|
125
119
|
url: string;
|
|
120
|
+
headers?: HeadersOption;
|
|
126
121
|
protocols?: string | string[];
|
|
127
122
|
query?: Record<string, string> | (() => Record<string, string> | Promise<Record<string, string>>);
|
|
128
123
|
webSocket?: new (url: string, protocols?: string | string[]) => WebSocket;
|
|
129
124
|
onChange?: (connection: Connection) => void;
|
|
130
|
-
onCommandSessionLost?: (command: SessionLostCommand) => "replay" | void;
|
|
131
125
|
};
|
|
132
126
|
}
|
|
133
127
|
//#endregion
|
package/dist/statewire.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statewire.d.ts","names":[],"sources":["../src/statewire.ts"],"mappings":";;;;cAYa,eACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW,YACnD,gBAAgB,QAAQ,QAAQ;cAqCtB,oBACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW,YACnD,gBAAgB,gBAAgB,QAAQ,QAAQ;cAGtC,gBACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD;mBAEiB;UACT;EAEI,YAAA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW;MAK5D,SAAS;MAIT,cAAc,gBAAgB;MAI9B,YAAY,UAAU,cAAc;EAIxC,WAAW,OAAO,0BAA0B;EAI5C,UAAU;EAIV;;kBAOe;OACH,QACV,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD;IAEA,WAAW,gBAAgB,UAAU,QAAQ,WAAW;IACxD,aAAa,WAAW,YAAY,SAAS;IAC7C,WAAW,OAAO,QAAQ;MAAO,OAAO,YAAY;;IACpD,iBACE,OAAO,YAAY,SACnB;MAAO,SAAS,UAAU;;IAE5B,mBACE,QAAQ,UAAU,cAAc,YAChC;MAAO,OAAO,YAAY
|
|
1
|
+
{"version":3,"file":"statewire.d.ts","names":[],"sources":["../src/statewire.ts"],"mappings":";;;;cAYa,eACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW,YACnD,gBAAgB,QAAQ,QAAQ;cAqCtB,oBACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW,YACnD,gBAAgB,gBAAgB,QAAQ,QAAQ;cAGtC,gBACX,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD;mBAEiB;UACT;EAEI,YAAA,SAAS,gBAAgB,QAAQ,QAAQ,WAAW;MAK5D,SAAS;MAIT,cAAc,gBAAgB;MAI9B,YAAY,UAAU,cAAc;EAIxC,WAAW,OAAO,0BAA0B;EAI5C,UAAU;EAIV;;kBAOe;OACH,QACV,QACA,kBAAkB,UAAU,WAAW,UAAU,UACjD;IAEA,WAAW,gBAAgB,UAAU,QAAQ,WAAW;IACxD,aAAa,WAAW,YAAY,SAAS;IAC7C,WAAW,OAAO,QAAQ;MAAO,OAAO,YAAY;;IACpD,iBACE,OAAO,YAAY,SACnB;MAAO,SAAS,UAAU;;IAE5B,mBACE,QAAQ,UAAU,cAAc,YAChC;MAAO,OAAO,YAAY;;IAE5B,sBAAsB,YAAY;;OAGxB,QACV,QACA,kBAAkB,UAAU,WAAW,UAAU;;IAGjD,OAAO;;IAEP,gBAAgB;IAChB,YAAY;IACZ,UAAU,UAAU,cAAc;;IAElC,aAAa,OAAO,4BAA4B;;OAGtC;IACN;;IACA;;IAEA;IACA;IACA;IACA;IACA;IACA;;IAEA;IAAgB;IAAkB;;OAC5B,aAAa;IAAoB;;OACjC,mBAAmB;;;;;OAMnB,WACV,QACA,kBAAkB,UAAU,WAAW,UAAU,aAEjD,OAAO,QACP,SAAS,UAAU,QAAQ,YAC3B,KAAK;OAGK;IACV;IACA;IACA;;;;;;;;;;;OAYU;IACN;IAAkB;IAAiB;IAAiB;;IACpD;IAAyB;;IACzB;IAAwB;IAAiB;;IACzC;IAAiB;;IAEjB;IACA;;IAQA;IACA;IACA;;IAGA;IACA;;;OAIM,gBACR,gBACE;IAAO;QAAyB,cAAc,QAAQ;OAEhD,aAAa;IACvB;IACA,UAAU;IACV,eAAe,WAAW;IAC1B,YAAY,YAAY;;OAGd,YAAY;IACtB;IACA,UAAU;IACV;IACA,QACI,gCACO,yBAAyB,QAAQ;IAC5C,iBAAiB,aAAa,kCAAkC;IAChE,YAAY,YAAY"}
|
package/dist/statewire.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"statewire.js","names":["useMemo","useRef","useState","useSyncExternalStore","createTapRoot","flushTapSync","resource","useResource","ResourceElement","LIVE_CONNECTION","useStatewireCore","Statewire","StatewireSessionStoreOptions","useStatewire","Commands","options","StatewireClient","Options","TState","TCommands","TEvent","Helpers","latest","current","value","transport","core","subscribe","listener","state","getState","connection","commands","CommandsProxy","setContext","contextValue","Record","Error","StatewireResource","root","disposed","constructor","ClientRoot","getValue","Connection","Promise","dispose","unmount","optimistic","Optimistic","NonNullable","onEvent","event","ctx","onStateChange","changes","Changes","onCommandChange","update","CommandUpdate","ConnectionState","status","cause","attempt","nextRetryAt","lastError","message","payload","reconnect","ConnectionStatus","draft","command","Command","OptimisticContext","ts","seq","clientId","SendFailure","fate","predecessorLost","SessionLostCommand","method","params","SSEOptions","url","headers","HeadersInit","fetch","globalThis","onChange","onCommandSessionLost","WSOptions","protocols","query","webSocket","WebSocket"],"sources":["../src/statewire.ts"],"sourcesContent":["import { useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport {\n createTapRoot,\n flushTapSync,\n resource,\n useResource,\n} from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { LIVE_CONNECTION, useStatewireCore } from \"./client/core\";\nimport type { Statewire } from \"./types\";\nimport type { StatewireSessionStoreOptions } from \"./client/sessionStore\";\n\nexport const useStatewire = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.Options<TState, TCommands, TEvent>,\n): StatewireClient.Helpers<TState, TCommands> => {\n const latest = useRef(options);\n latest.current = options;\n\n const value = useResource(options.transport) as Statewire<unknown>;\n const core = useStatewireCore(() => latest.current, value);\n const [subscribe] = useState(\n () => (listener: () => void) =>\n core.subscribe(() => flushTapSync(listener)),\n );\n\n const state = useSyncExternalStore(\n subscribe,\n core.getState,\n core.getState,\n ) as TState;\n const connection = value.connection ?? LIVE_CONNECTION;\n\n return useMemo(\n () => ({\n state,\n getState: core.getState as () => TState,\n connection,\n commands: core.commands as Statewire.CommandsProxy<TCommands>,\n setContext: (contextValue: Record<string, unknown>) => {\n if (!value.setContext) {\n throw new Error(\n \"statewire: this transport has no client context channel\",\n );\n }\n return value.setContext(contextValue);\n },\n }),\n [state, connection, core, value],\n );\n};\n\nexport const StatewireResource = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.Options<TState, TCommands, TEvent>,\n): ResourceElement<StatewireClient.Helpers<TState, TCommands>> =>\n resource(useStatewire<TState, TCommands, TEvent>)(options);\n\nexport class StatewireClient<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n> {\n private readonly root;\n private disposed = false;\n\n constructor(options: StatewireClient.Options<TState, TCommands, TEvent>) {\n const ClientRoot = () => useStatewire<TState, TCommands, TEvent>(options);\n this.root = createTapRoot(ClientRoot);\n }\n\n get state(): TState {\n return this.root.getValue().getState();\n }\n\n get connection(): StatewireClient.Connection {\n return this.root.getValue().connection;\n }\n\n get commands(): Statewire.CommandsProxy<TCommands> {\n return this.root.getValue().commands;\n }\n\n setContext(value: Record<string, unknown>): Promise<unknown> {\n return this.root.getValue().setContext(value);\n }\n\n subscribe(listener: () => void): () => void {\n return this.root.subscribe(listener);\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n this.root.unmount();\n }\n}\n\nexport namespace StatewireClient {\n export type Options<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n > = {\n transport: ResourceElement<Statewire<TState, TCommands, TEvent>>;\n optimistic?: Optimistic<NonNullable<TState>, TCommands>;\n onEvent?: (event: TEvent, ctx: { state: NonNullable<TState> }) => void;\n onStateChange?: (\n state: NonNullable<TState>,\n ctx: { changes: Statewire.Changes },\n ) => void;\n onCommandChange?: (\n update: Statewire.CommandUpdate<TCommands>,\n ctx: { state: NonNullable<TState> },\n ) => void;\n };\n\n export type Helpers<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = {\n /** The statewire's state type — remote statewires include `undefined` until the first attach snapshot applies. */\n state: TState;\n /** Identity-stable across renders; reads the current canonical state without subscribing the caller. */\n getState: () => TState;\n connection: Connection;\n commands: Statewire.CommandsProxy<TCommands>;\n /** Replace this client's ambient context on the server; throws on transports without a client context channel. */\n setContext: (value: Record<string, unknown>) => Promise<unknown>;\n };\n\n export type ConnectionState =\n | { status: \"connecting\" }\n | { status: \"live\" }\n | {\n status: \"reconnecting\";\n cause: \"dropped\" | \"evicted\" | \"error\";\n attempt: number;\n nextRetryAt: number;\n lastError?: string;\n message?: string;\n }\n | { status: \"gone\"; message?: string; payload?: unknown };\n export type Connection = ConnectionState & { reconnect: () => void };\n export type ConnectionStatus = Connection[\"status\"];\n\n /**\n * Mutate the draft in place; the client records the next state plus the exact\n * changed paths. Re-run against the new base on every rebase.\n */\n export type Optimistic<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = (\n draft: TState,\n command: Statewire.Command<TCommands>,\n ctx: OptimisticContext,\n ) => void;\n\n export type OptimisticContext = {\n ts: number;\n seq: number;\n clientId?: string;\n };\n\n /**\n * fate: \"aborted\" = never sent, \"rejected\" = the handler refused,\n * \"unknown-command\" = no such command, \"invalid-params\" = the args did not\n * match the command's schema, \"lost\" = sent but never covered by an ack,\n * \"crashed\" = acked but the processing died, \"result-unavailable\" = acked\n * but the result is gone.\n * cause is which channel settled it: \"server\" = the server's answer;\n * the rest are channel deaths the client settled locally.\n */\n export type SendFailure =\n | { fate: \"rejected\"; cause: \"server\"; message: string; payload?: unknown }\n | { fate: \"unknown-command\"; cause: \"server\" }\n | { fate: \"invalid-params\"; cause: \"server\"; message: string }\n | { fate: \"crashed\"; cause: \"server\" }\n | {\n fate: \"result-unavailable\";\n cause:\n | \"server\"\n | \"unmount\"\n | \"stream-gone\"\n | \"session-lost\"\n | \"superseded\";\n }\n | {\n fate: \"aborted\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\" | \"superseded\";\n predecessorLost?: true;\n }\n | {\n fate: \"lost\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\" | \"superseded\";\n };\n\n /**\n * A pending command offered for replay while the session is being evicted.\n * Return \"replay\" to re-enqueue it under the rotated client id instead of\n * settling it as a session-lost failure.\n */\n export type SessionLostCommand = {\n method: string;\n params: unknown[];\n seq: number;\n };\n\n export type SSEOptions = StatewireSessionStoreOptions & {\n url: string;\n headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);\n fetch?: typeof globalThis.fetch;\n onChange?: (connection: Connection) => void;\n onCommandSessionLost?: (command: SessionLostCommand) => \"replay\" | void;\n };\n\n export type WSOptions = StatewireSessionStoreOptions & {\n url: string;\n protocols?: string | string[];\n query?:\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>);\n webSocket?: new (url: string, protocols?: string | string[]) => WebSocket;\n onChange?: (connection: Connection) => void;\n onCommandSessionLost?: (command: SessionLostCommand) => \"replay\" | void;\n };\n}\n"],"mappings":";;;;AAYA,MAAaa,gBAKXE,YAC+C;CAC/C,MAAMO,SAASrB,OAAOc,OAAO;CAC7BO,OAAOC,UAAUR;CAEjB,MAAMS,QAAQjB,YAAYQ,QAAQU,SAAS;CAC3C,MAAMC,OAAOhB,uBAAuBY,OAAOC,SAASC,KAAK;CACzD,MAAM,CAACG,aAAazB,gBACX0B,aACLF,KAAKC,gBAAgBtB,aAAauB,QAAQ,CAAC,CAC/C;CAEA,MAAMC,QAAQ1B,qBACZwB,WACAD,KAAKI,UACLJ,KAAKI,QACP;CACA,MAAMC,aAAaP,MAAMO,cAActB;CAEvC,OAAOT,eACE;EACL6B;EACAC,UAAUJ,KAAKI;EACfC;EACAC,UAAUN,KAAKM;EACfE,aAAaC,iBAA0C;GACrD,IAAI,CAACX,MAAMU,YACT,MAAM,IAAIG,MACR,yDACF;GAEF,OAAOb,MAAMU,WAAWC,YAAY;EACtC;CACF,IACA;EAACN;EAAOE;EAAYL;EAAMF;CAAK,CACjC;AACF;AAEA,MAAac,qBAKXvB,YAEAT,SAASO,YAAuC,CAAC,CAACE,OAAO;AAE3D,IAAaC,kBAAb,MAIE;CACA;CACA,WAAmB;CAEnByB,YAAY1B,SAA6D;EACvE,MAAM2B,mBAAmB7B,aAAwCE,OAAO;EACxE,KAAKwB,OAAOnC,cAAcsC,UAAU;CACtC;CAEA,IAAIb,QAAgB;EAClB,OAAO,KAAKU,KAAKI,SAAS,CAAC,CAACb,SAAS;CACvC;CAEA,IAAIC,aAAyC;EAC3C,OAAO,KAAKQ,KAAKI,SAAS,CAAC,CAACZ;CAC9B;CAEA,IAAIC,WAA+C;EACjD,OAAO,KAAKO,KAAKI,SAAS,CAAC,CAACX;CAC9B;CAEAE,WAAWV,OAAkD;EAC3D,OAAO,KAAKe,KAAKI,SAAS,CAAC,CAACT,WAAWV,KAAK;CAC9C;CAEAG,UAAUC,UAAkC;EAC1C,OAAO,KAAKW,KAAKZ,UAAUC,QAAQ;CACrC;CAEAkB,UAAgB;EACd,IAAI,KAAKN,UAAU;EACnB,KAAKA,WAAW;EAChB,KAAKD,KAAKQ,QAAQ;CACpB;AACF"}
|
|
1
|
+
{"version":3,"file":"statewire.js","names":["useMemo","useRef","useState","useSyncExternalStore","createTapRoot","flushTapSync","resource","useResource","ResourceElement","LIVE_CONNECTION","useStatewireCore","Statewire","StatewireSessionStoreOptions","useStatewire","Commands","options","StatewireClient","Options","TState","TCommands","TEvent","Helpers","latest","current","value","transport","core","subscribe","listener","state","getState","connection","commands","CommandsProxy","setContext","contextValue","Record","Error","StatewireResource","root","disposed","constructor","ClientRoot","getValue","Connection","Promise","dispose","unmount","optimistic","Optimistic","NonNullable","onEvent","event","ctx","onStateChange","changes","Changes","onCommandChange","update","CommandUpdate","onConnectionChange","ConnectionState","status","cause","attempt","nextRetryAt","lastError","message","payload","reconnect","ConnectionStatus","draft","command","Command","OptimisticContext","ts","seq","clientId","SendFailure","fate","predecessorLost","HeadersOption","HeadersInit","staleAuth","SSEOptions","url","headers","fetch","globalThis","onChange","WSOptions","protocols","query","webSocket","WebSocket"],"sources":["../src/statewire.ts"],"sourcesContent":["import { useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport {\n createTapRoot,\n flushTapSync,\n resource,\n useResource,\n} from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { LIVE_CONNECTION, useStatewireCore } from \"./client/core\";\nimport type { Statewire } from \"./types\";\nimport type { StatewireSessionStoreOptions } from \"./client/sessionStore\";\n\nexport const useStatewire = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.Options<TState, TCommands, TEvent>,\n): StatewireClient.Helpers<TState, TCommands> => {\n const latest = useRef(options);\n latest.current = options;\n\n const value = useResource(options.transport) as Statewire<unknown>;\n const core = useStatewireCore(() => latest.current, value);\n const [subscribe] = useState(\n () => (listener: () => void) =>\n core.subscribe(() => flushTapSync(listener)),\n );\n\n const state = useSyncExternalStore(\n subscribe,\n core.getState,\n core.getState,\n ) as TState;\n const connection = value.connection ?? LIVE_CONNECTION;\n\n return useMemo(\n () => ({\n state,\n getState: core.getState as () => TState,\n connection,\n commands: core.commands as Statewire.CommandsProxy<TCommands>,\n setContext: (contextValue: Record<string, unknown>) => {\n if (!value.setContext) {\n throw new Error(\n \"statewire: this transport has no client context channel\",\n );\n }\n return value.setContext(contextValue);\n },\n }),\n [state, connection, core, value],\n );\n};\n\nexport const StatewireResource = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.Options<TState, TCommands, TEvent>,\n): ResourceElement<StatewireClient.Helpers<TState, TCommands>> =>\n resource(useStatewire<TState, TCommands, TEvent>)(options);\n\nexport class StatewireClient<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n> {\n private readonly root;\n private disposed = false;\n\n constructor(options: StatewireClient.Options<TState, TCommands, TEvent>) {\n const ClientRoot = () => useStatewire<TState, TCommands, TEvent>(options);\n this.root = createTapRoot(ClientRoot);\n }\n\n get state(): TState {\n return this.root.getValue().getState();\n }\n\n get connection(): StatewireClient.Connection {\n return this.root.getValue().connection;\n }\n\n get commands(): Statewire.CommandsProxy<TCommands> {\n return this.root.getValue().commands;\n }\n\n setContext(value: Record<string, unknown>): Promise<unknown> {\n return this.root.getValue().setContext(value);\n }\n\n subscribe(listener: () => void): () => void {\n return this.root.subscribe(listener);\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n this.root.unmount();\n }\n}\n\nexport namespace StatewireClient {\n export type Options<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n > = {\n transport: ResourceElement<Statewire<TState, TCommands, TEvent>>;\n optimistic?: Optimistic<NonNullable<TState>, TCommands>;\n onEvent?: (event: TEvent, ctx: { state: NonNullable<TState> }) => void;\n onStateChange?: (\n state: NonNullable<TState>,\n ctx: { changes: Statewire.Changes },\n ) => void;\n onCommandChange?: (\n update: Statewire.CommandUpdate<TCommands>,\n ctx: { state: NonNullable<TState> },\n ) => void;\n onConnectionChange?: (connection: Connection) => void;\n };\n\n export type Helpers<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = {\n /** The statewire's state type — remote statewires include `undefined` until the first attach snapshot applies. */\n state: TState;\n /** Identity-stable across renders; reads the current canonical state without subscribing the caller. */\n getState: () => TState;\n connection: Connection;\n commands: Statewire.CommandsProxy<TCommands>;\n /** Replace this client's ambient context on the server; throws on transports without a client context channel. */\n setContext: (value: Record<string, unknown>) => Promise<unknown>;\n };\n\n export type ConnectionState =\n | { status: \"connecting\" }\n | { status: \"live\" }\n | {\n status: \"reconnecting\";\n cause: \"dropped\" | \"evicted\" | \"error\" | \"unauthorized\";\n attempt: number;\n nextRetryAt: number;\n lastError?: string;\n message?: string;\n }\n | { status: \"gone\"; message?: string; payload?: unknown };\n export type Connection = ConnectionState & { reconnect: () => void };\n export type ConnectionStatus = Connection[\"status\"];\n\n /**\n * Mutate the draft in place; the client records the next state plus the exact\n * changed paths. Re-run against the new base on every rebase.\n */\n export type Optimistic<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = (\n draft: TState,\n command: Statewire.Command<TCommands>,\n ctx: OptimisticContext,\n ) => void;\n\n export type OptimisticContext = {\n ts: number;\n seq: number;\n clientId?: string;\n };\n\n /**\n * fate: \"aborted\" = never sent, \"rejected\" = the handler refused,\n * \"unknown-command\" = no such command, \"invalid-params\" = the args did not\n * match the command's schema, \"lost\" = sent but never covered by an ack,\n * \"crashed\" = acked but the processing died, \"result-unavailable\" = acked\n * but the result is gone.\n * cause is which channel settled it: \"server\" = the server's answer;\n * the rest are channel deaths the client settled locally.\n */\n export type SendFailure =\n | { fate: \"rejected\"; cause: \"server\"; message: string; payload?: unknown }\n | { fate: \"unknown-command\"; cause: \"server\" }\n | { fate: \"invalid-params\"; cause: \"server\"; message: string }\n | { fate: \"crashed\"; cause: \"server\" }\n | {\n fate: \"result-unavailable\";\n cause:\n | \"server\"\n | \"unmount\"\n | \"stream-gone\"\n | \"session-lost\"\n | \"superseded\";\n }\n | {\n fate: \"aborted\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\" | \"superseded\";\n predecessorLost?: true;\n }\n | {\n fate: \"lost\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\" | \"superseded\";\n };\n\n /** Static headers, or a resolver invoked per request/attach; `staleAuth` is true right after an auth rejection. */\n export type HeadersOption =\n | HeadersInit\n | ((ctx: { staleAuth: boolean }) => HeadersInit | Promise<HeadersInit>);\n\n export type SSEOptions = StatewireSessionStoreOptions & {\n url: string;\n headers?: HeadersOption;\n fetch?: typeof globalThis.fetch;\n onChange?: (connection: Connection) => void;\n };\n\n export type WSOptions = StatewireSessionStoreOptions & {\n url: string;\n headers?: HeadersOption;\n protocols?: string | string[];\n query?:\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>);\n webSocket?: new (url: string, protocols?: string | string[]) => WebSocket;\n onChange?: (connection: Connection) => void;\n };\n}\n"],"mappings":";;;;AAYA,MAAaa,gBAKXE,YAC+C;CAC/C,MAAMO,SAASrB,OAAOc,OAAO;CAC7BO,OAAOC,UAAUR;CAEjB,MAAMS,QAAQjB,YAAYQ,QAAQU,SAAS;CAC3C,MAAMC,OAAOhB,uBAAuBY,OAAOC,SAASC,KAAK;CACzD,MAAM,CAACG,aAAazB,gBACX0B,aACLF,KAAKC,gBAAgBtB,aAAauB,QAAQ,CAAC,CAC/C;CAEA,MAAMC,QAAQ1B,qBACZwB,WACAD,KAAKI,UACLJ,KAAKI,QACP;CACA,MAAMC,aAAaP,MAAMO,cAActB;CAEvC,OAAOT,eACE;EACL6B;EACAC,UAAUJ,KAAKI;EACfC;EACAC,UAAUN,KAAKM;EACfE,aAAaC,iBAA0C;GACrD,IAAI,CAACX,MAAMU,YACT,MAAM,IAAIG,MACR,yDACF;GAEF,OAAOb,MAAMU,WAAWC,YAAY;EACtC;CACF,IACA;EAACN;EAAOE;EAAYL;EAAMF;CAAK,CACjC;AACF;AAEA,MAAac,qBAKXvB,YAEAT,SAASO,YAAuC,CAAC,CAACE,OAAO;AAE3D,IAAaC,kBAAb,MAIE;CACA;CACA,WAAmB;CAEnByB,YAAY1B,SAA6D;EACvE,MAAM2B,mBAAmB7B,aAAwCE,OAAO;EACxE,KAAKwB,OAAOnC,cAAcsC,UAAU;CACtC;CAEA,IAAIb,QAAgB;EAClB,OAAO,KAAKU,KAAKI,SAAS,CAAC,CAACb,SAAS;CACvC;CAEA,IAAIC,aAAyC;EAC3C,OAAO,KAAKQ,KAAKI,SAAS,CAAC,CAACZ;CAC9B;CAEA,IAAIC,WAA+C;EACjD,OAAO,KAAKO,KAAKI,SAAS,CAAC,CAACX;CAC9B;CAEAE,WAAWV,OAAkD;EAC3D,OAAO,KAAKe,KAAKI,SAAS,CAAC,CAACT,WAAWV,KAAK;CAC9C;CAEAG,UAAUC,UAAkC;EAC1C,OAAO,KAAKW,KAAKZ,UAAUC,QAAQ;CACrC;CAEAkB,UAAgB;EACd,IAAI,KAAKN,UAAU;EACnB,KAAKA,WAAW;EAChB,KAAKD,KAAKQ,QAAQ;CACpB;AACF"}
|
|
@@ -4,10 +4,7 @@ import { useEffect } from "@assistant-ui/tap/react-shim";
|
|
|
4
4
|
//#region src/transports/remote.ts
|
|
5
5
|
const useStatewireRemote = (options, useLink) => {
|
|
6
6
|
const emit = useStatewireEmit();
|
|
7
|
-
const s = useStatewireSession({
|
|
8
|
-
sessionStore: options.sessionStore,
|
|
9
|
-
onCommandSessionLost: options.onCommandSessionLost
|
|
10
|
-
}, emit);
|
|
7
|
+
const s = useStatewireSession({ sessionStore: options.sessionStore }, emit);
|
|
11
8
|
const conn = useLink(options, s.ctx);
|
|
12
9
|
useEffect(() => setTransport(s, conn), [s, conn]);
|
|
13
10
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote.js","names":["useEffect","useStatewireEmit","Statewire","setContext","setTransport","useStatewireSession","StatewireSessionOptions","StatewireClient","RemoteLink","StatewireRemoteProps","useStatewireRemote","url","onChange","connection","Connection","options","TOptions","useLink","ctx","emit","s","sessionStore","
|
|
1
|
+
{"version":3,"file":"remote.js","names":["useEffect","useStatewireEmit","Statewire","setContext","setTransport","useStatewireSession","StatewireSessionOptions","StatewireClient","RemoteLink","StatewireRemoteProps","useStatewireRemote","url","onChange","connection","Connection","options","TOptions","useLink","ctx","emit","s","sessionStore","conn","state","canonical","commands","source","value"],"sources":["../../src/transports/remote.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport { useStatewireEmit } from \"../host/host\";\nimport type { Statewire } from \"../types\";\nimport {\n setContext,\n setTransport,\n useStatewireSession,\n} from \"../client/session\";\nimport type { StatewireSessionOptions } from \"../client/session\";\nimport type { StatewireClient } from \"../statewire\";\nimport type { RemoteLink, StatewireRemoteProps } from \"./sse\";\n\nexport const useStatewireRemote = <\n TOptions extends {\n url: string;\n onChange?: (connection: StatewireClient.Connection) => void;\n } & StatewireSessionOptions,\n>(\n options: TOptions,\n useLink: (options: TOptions, ctx: StatewireRemoteProps) => RemoteLink,\n): Statewire<any, any, any> => {\n const emit = useStatewireEmit<any>();\n const s = useStatewireSession({ sessionStore: options.sessionStore }, emit);\n const conn = useLink(options, s.ctx);\n useEffect(() => setTransport(s, conn), [s, conn]);\n\n return {\n state: s.canonical,\n commands: s.commands,\n emit,\n connection: conn.connection,\n source: s.source,\n setContext: (value) => setContext(s, value),\n };\n};\n"],"mappings":";;;;AAYA,MAAaU,sBAMXK,SACAE,YAC6B;CAC7B,MAAME,OAAOlB,iBAAsB;CACnC,MAAMmB,IAAIf,oBAAoB,EAAEgB,cAAcN,QAAQM,aAAa,GAAGF,IAAI;CAC1E,MAAMG,OAAOL,QAAQF,SAASK,EAAEF,GAAG;CACnClB,gBAAgBI,aAAagB,GAAGE,IAAI,GAAG,CAACF,GAAGE,IAAI,CAAC;CAEhD,OAAO;EACLC,OAAOH,EAAEI;EACTC,UAAUL,EAAEK;EACZN;EACAN,YAAYS,KAAKT;EACjBa,QAAQN,EAAEM;EACVvB,aAAawB,UAAUxB,WAAWiB,GAAGO,KAAK;CAC5C;AACF"}
|
package/dist/transports/sse.d.ts
CHANGED
|
@@ -65,7 +65,10 @@ type RemoteLink = {
|
|
|
65
65
|
command: StatewireWireCommand;
|
|
66
66
|
}) => Promise<StatewireSubmitResult>;
|
|
67
67
|
};
|
|
68
|
+
type AuthState = {
|
|
69
|
+
stale: boolean;
|
|
70
|
+
};
|
|
68
71
|
declare const StatewireSSE: <TState = any, TCommands extends Statewire.Commands = Statewire.Commands, TEvent = any>(options: StatewireClient.SSEOptions) => ResourceElement<Statewire<TState | undefined, TCommands, TEvent>>;
|
|
69
72
|
//#endregion
|
|
70
|
-
export { ConnectionState, RemoteLink, StatewireRemoteProps, StatewireSSE, StatewireSubmitResult, StatewireWireCommand, contextOps, encodeWireCommand };
|
|
73
|
+
export { AuthState, ConnectionState, RemoteLink, StatewireRemoteProps, StatewireSSE, StatewireSubmitResult, StatewireWireCommand, contextOps, encodeWireCommand };
|
|
71
74
|
//# sourceMappingURL=sse.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sse.d.ts","names":[],"sources":["../../src/transports/sse.ts"],"mappings":";;;;;;;KAuBY;EACV;EACA,aAAa;IAAQ;IAAgB;;EACrC,QAAQ;IACN;IACA,MAAM;IACN;;EAEF,WAAW,MAAM,cAAc;;KAGrB;EACN;EAAgB;EAA4B;;EAC5C;EAAgB;EAAgB,SAAS;;;cAGlC,aAAc,OAAO;;;SAAA;;cAIrB,oBAAqB,SAAS;;;;;;;;;;KAU/B;EACN;EAAiB,WAAW;;EAC5B;EAAkB;;EAClB;;EACA;;EACA;;KAEM,kBAAkB,gBAAgB;;KAGlC;EACV,YAAY,gBAAgB;EAC5B,OAAO;IACL;IACA,SAAS;QACL,QAAQ;;
|
|
1
|
+
{"version":3,"file":"sse.d.ts","names":[],"sources":["../../src/transports/sse.ts"],"mappings":";;;;;;;KAuBY;EACV;EACA,aAAa;IAAQ;IAAgB;;EACrC,QAAQ;IACN;IACA,MAAM;IACN;;EAEF,WAAW,MAAM,cAAc;;KAGrB;EACN;EAAgB;EAA4B;;EAC5C;EAAgB;EAAgB,SAAS;;;cAGlC,aAAc,OAAO;;;SAAA;;cAIrB,oBAAqB,SAAS;;;;;;;;;;KAU/B;EACN;EAAiB,WAAW;;EAC5B;EAAkB;;EAClB;;EACA;;EACA;;KAEM,kBAAkB,gBAAgB;;KAGlC;EACV,YAAY,gBAAgB;EAC5B,OAAO;IACL;IACA,SAAS;QACL,QAAQ;;KAMJ;EAAc;;cAsOb,eACX,cACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,eACtB,gBAAgB,UAAU,oBAAoB,WAAW"}
|