statewire 0.0.0-0 → 0.0.2
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 +84 -1
- package/dist/client/changes.d.ts +7 -0
- package/dist/client/changes.d.ts.map +1 -0
- package/dist/client/changes.js +25 -0
- package/dist/client/changes.js.map +1 -0
- package/dist/client/connection.d.ts +63 -0
- package/dist/client/connection.d.ts.map +1 -0
- package/dist/client/connection.js +211 -0
- package/dist/client/connection.js.map +1 -0
- package/dist/client/core.d.ts +14 -0
- package/dist/client/core.d.ts.map +1 -0
- package/dist/client/core.js +272 -0
- package/dist/client/core.js.map +1 -0
- package/dist/client/session.d.ts +71 -0
- package/dist/client/session.d.ts.map +1 -0
- package/dist/client/session.js +529 -0
- package/dist/client/session.js.map +1 -0
- package/dist/client/sessionStore.d.ts +16 -0
- package/dist/client/sessionStore.d.ts.map +1 -0
- package/dist/client/sessionStore.js +14 -0
- package/dist/client/sessionStore.js.map +1 -0
- package/dist/client/timing.d.ts +11 -0
- package/dist/client/timing.d.ts.map +1 -0
- package/dist/client/timing.js +15 -0
- package/dist/client/timing.js.map +1 -0
- package/dist/host/adapter.d.ts +8 -0
- package/dist/host/adapter.d.ts.map +1 -0
- package/dist/host/adapter.js +23 -0
- package/dist/host/adapter.js.map +1 -0
- package/dist/host/draft.d.ts +28 -0
- package/dist/host/draft.d.ts.map +1 -0
- package/dist/host/draft.js +211 -0
- package/dist/host/draft.js.map +1 -0
- package/dist/host/engine.d.ts +14 -0
- package/dist/host/engine.d.ts.map +1 -0
- package/dist/host/engine.js +17 -0
- package/dist/host/engine.js.map +1 -0
- package/dist/host/host.d.ts +62 -0
- package/dist/host/host.d.ts.map +1 -0
- package/dist/host/host.js +331 -0
- package/dist/host/host.js.map +1 -0
- package/dist/host/socket.d.ts +14 -0
- package/dist/host/socket.d.ts.map +1 -0
- package/dist/host/socket.js +459 -0
- package/dist/host/socket.js.map +1 -0
- package/dist/host-internal.d.ts +27 -0
- package/dist/host-internal.d.ts.map +1 -0
- package/dist/host-internal.js +3 -0
- package/dist/host.d.ts +44 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +4 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/statewire.d.ts +104 -0
- package/dist/statewire.d.ts.map +1 -0
- package/dist/statewire.js +53 -0
- package/dist/statewire.js.map +1 -0
- package/dist/transports/fake-sse.d.ts +12 -0
- package/dist/transports/fake-sse.d.ts.map +1 -0
- package/dist/transports/fake-sse.js +39 -0
- package/dist/transports/fake-sse.js.map +1 -0
- package/dist/transports/fixtures.d.ts +33 -0
- package/dist/transports/fixtures.d.ts.map +1 -0
- package/dist/transports/fixtures.js +50 -0
- package/dist/transports/fixtures.js.map +1 -0
- package/dist/transports/remote.d.ts +12 -0
- package/dist/transports/remote.d.ts.map +1 -0
- package/dist/transports/remote.js +21 -0
- package/dist/transports/remote.js.map +1 -0
- package/dist/transports/sse-parser.d.ts +5 -0
- package/dist/transports/sse-parser.d.ts.map +1 -0
- package/dist/transports/sse-parser.js +26 -0
- package/dist/transports/sse-parser.js.map +1 -0
- package/dist/transports/sse.d.ts +64 -0
- package/dist/transports/sse.d.ts.map +1 -0
- package/dist/transports/sse.js +137 -0
- package/dist/transports/sse.js.map +1 -0
- package/dist/transports/testing.d.ts +32 -0
- package/dist/transports/testing.d.ts.map +1 -0
- package/dist/transports/testing.js +94 -0
- package/dist/transports/testing.js.map +1 -0
- package/dist/transports/ws.d.ts +9 -0
- package/dist/transports/ws.d.ts.map +1 -0
- package/dist/transports/ws.js +169 -0
- package/dist/transports/ws.js.map +1 -0
- package/dist/types.d.ts +60 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +24 -0
- package/dist/types.js.map +1 -0
- package/dist/util/draftTraps.d.ts +6 -0
- package/dist/util/draftTraps.d.ts.map +1 -0
- package/dist/util/draftTraps.js +25 -0
- package/dist/util/draftTraps.js.map +1 -0
- package/dist/util/macrotask.d.ts +5 -0
- package/dist/util/macrotask.d.ts.map +1 -0
- package/dist/util/macrotask.js +25 -0
- package/dist/util/macrotask.js.map +1 -0
- package/dist/util/methodProxy.d.ts +5 -0
- package/dist/util/methodProxy.d.ts.map +1 -0
- package/dist/util/methodProxy.js +17 -0
- package/dist/util/methodProxy.js.map +1 -0
- package/dist/util/randomId.d.ts +5 -0
- package/dist/util/randomId.d.ts.map +1 -0
- package/dist/util/randomId.js +6 -0
- package/dist/util/randomId.js.map +1 -0
- package/dist/util/recordMutations.d.ts +10 -0
- package/dist/util/recordMutations.d.ts.map +1 -0
- package/dist/util/recordMutations.js +109 -0
- package/dist/util/recordMutations.js.map +1 -0
- package/dist/util/statePath.d.ts +14 -0
- package/dist/util/statePath.d.ts.map +1 -0
- package/dist/util/statePath.js +31 -0
- package/dist/util/statePath.js.map +1 -0
- package/dist/wire/meta.d.ts +27 -0
- package/dist/wire/meta.d.ts.map +1 -0
- package/dist/wire/meta.js +75 -0
- package/dist/wire/meta.js.map +1 -0
- package/dist/wire/ops.d.ts +19 -0
- package/dist/wire/ops.d.ts.map +1 -0
- package/dist/wire/ops.js +112 -0
- package/dist/wire/ops.js.map +1 -0
- package/package.json +52 -5
- package/src/client/changes.ts +38 -0
- package/src/client/connection.ts +301 -0
- package/src/client/core.ts +312 -0
- package/src/client/session.ts +696 -0
- package/src/client/sessionStore.ts +23 -0
- package/src/client/timing.ts +14 -0
- package/src/host/adapter.ts +33 -0
- package/src/host/draft.ts +323 -0
- package/src/host/engine.ts +13 -0
- package/src/host/host.ts +482 -0
- package/src/host/socket.ts +542 -0
- package/src/host-internal.ts +28 -0
- package/src/host.ts +61 -0
- package/src/index.ts +5 -0
- package/src/statewire.ts +200 -0
- package/src/transports/fake-sse.ts +51 -0
- package/src/transports/fixtures.ts +60 -0
- package/src/transports/remote.ts +30 -0
- package/src/transports/sse-parser.ts +24 -0
- package/src/transports/sse.ts +267 -0
- package/src/transports/testing.ts +123 -0
- package/src/transports/ws.ts +256 -0
- package/src/types.ts +88 -0
- package/src/util/draftTraps.ts +32 -0
- package/src/util/macrotask.ts +25 -0
- package/src/util/methodProxy.ts +16 -0
- package/src/util/randomId.ts +4 -0
- package/src/util/recordMutations.ts +0 -0
- package/src/util/statePath.ts +61 -0
- package/src/wire/meta.ts +101 -0
- package/src/wire/ops.ts +202 -0
- package/index.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.js","names":["useRef","useState","flushTapSync","resource","ResourceElement","encodeParams","reviveResponse","createSseParser","timing","classify","handleEnvelope","overloadedError","parseRetryAfter","useStatewireConn","Conn","StatewireEnvelope","StatewireResponse","Statewire","StatewireClient","StatewireHost","useStatewireRemote","StatewireRemoteProps","clientId","onSnapshot","args","ops","lastSeq","onOps","res","ack","onFinish","Fin","StatewireWireCommand","method","params","StatewireSubmitResult","type","response","message","ConnectionState","status","cause","attempt","nextRetryAt","lastError","payload","RemoteLink","connection","Connection","send","seq","command","Promise","baseUrl","options","SSEOptions","url","replace","requestHeaders","extra","Record","headers","merged","Headers","name","value","Object","entries","set","timedFetch","input","init","RequestInit","signal","AbortSignal","timeout","Response","fetch","globalThis","consume","c","reader","ReadableStreamDefaultReader","Uint8Array","heartbeat","AbortController","dead","decoder","TextDecoder","parse","timer","setTimeout","abort","heartbeatTimeoutMs","result","ReadableStreamReadResult","read","clearTimeout","done","data","decode","stream","env","JSON","attemptStream","ctx","accept","headerTimer","cache","any","overloaded","ok","body","Error","getReader","aborted","cancel","catch","get","sendCommand","String","lease","stringify","lifetime","json","undefined","verdict","const","error","useSseLink","latest","current","onChange","attemptOnce","link","useStatewireSSE","StatewireSSE","Commands","TState","TCommands","TEvent"],"sources":["../../src/transports/sse.ts"],"sourcesContent":["import { useRef, useState } from \"react\";\nimport { flushTapSync, resource } from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { encodeParams, reviveResponse } from \"../wire/meta\";\nimport { createSseParser } from \"./sse-parser\";\nimport { timing } from \"../client/timing\";\nimport {\n classify,\n handleEnvelope,\n overloadedError,\n parseRetryAfter,\n useStatewireConn,\n} from \"../client/connection\";\nimport type {\n Conn,\n StatewireEnvelope,\n StatewireResponse,\n} from \"../client/connection\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireClient } from \"../statewire\";\nimport type { StatewireHost } from \"../host\";\nimport { useStatewireRemote } from \"./remote\";\n\nexport type StatewireRemoteProps = {\n clientId: string;\n onSnapshot: (args: { ops: unknown[]; lastSeq: number }) => void;\n onOps: (args: {\n ops: unknown[];\n res?: StatewireResponse[];\n ack?: number;\n }) => void;\n onFinish: (args: StatewireHost.Fin) => void;\n};\n\nexport type StatewireWireCommand = {\n method: string;\n params: readonly unknown[];\n};\n\nexport type StatewireSubmitResult =\n | { type: \"receipt\"; response?: StatewireResponse }\n | { type: \"rejected\"; message: string }\n | { type: \"seq-gap\" }\n | { type: \"unknown-client\" }\n | { type: \"stale-lease\" };\n\nexport 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\n/** The live wire a remote session drives: connection status plus command POST. */\nexport type RemoteLink = {\n connection: StatewireClient.Connection;\n send: (args: {\n seq: number;\n command: StatewireWireCommand;\n }) => Promise<StatewireSubmitResult>;\n};\n\nconst baseUrl = (options: StatewireClient.SSEOptions) =>\n options.url.replace(/\\/$/, \"\");\n\nconst requestHeaders = async (\n options: StatewireClient.SSEOptions,\n extra: Record<string, string>,\n) => {\n const { headers } = options;\n const merged = new Headers(\n typeof headers === \"function\" ? await headers() : headers,\n );\n for (const [name, value] of Object.entries(extra)) merged.set(name, value);\n return merged;\n};\n\nconst timedFetch = async (\n options: StatewireClient.SSEOptions,\n input: string,\n init: RequestInit & { signal: AbortSignal },\n timeout: AbortSignal,\n): Promise<Response> => {\n try {\n return await (options.fetch ?? globalThis.fetch)(input, init);\n } catch {\n throw classify(timeout);\n }\n};\n\nconst consume = async (\n c: Conn,\n reader: ReadableStreamDefaultReader<Uint8Array>,\n heartbeat: AbortController,\n dead: () => boolean,\n): Promise<\"stop\" | \"reattach\" | \"ended\"> => {\n const decoder = new TextDecoder();\n const parse = createSseParser();\n for (;;) {\n const timer = setTimeout(\n () => heartbeat.abort(),\n timing.heartbeatTimeoutMs,\n );\n let result: ReadableStreamReadResult<Uint8Array>;\n try {\n result = await reader.read();\n } catch {\n throw classify(heartbeat.signal);\n } finally {\n clearTimeout(timer);\n }\n if (result.done) return \"ended\";\n for (const data of parse(decoder.decode(result.value, { stream: true }))) {\n if (dead()) return \"ended\";\n const env = JSON.parse(data) as StatewireEnvelope;\n const done = flushTapSync(() => handleEnvelope(c, env));\n if (done) return done;\n }\n }\n};\n\nconst attemptStream = async (\n c: Conn,\n options: StatewireClient.SSEOptions,\n abort: AbortController,\n): Promise<\"stop\" | \"reattach\" | \"ended\"> => {\n const headers = await requestHeaders(options, {\n \"Statewire-Client-Id\": c.ctx.clientId,\n accept: \"text/event-stream\",\n });\n const heartbeat = new AbortController();\n const headerTimer = setTimeout(\n () => heartbeat.abort(),\n timing.heartbeatTimeoutMs,\n );\n let res: Response;\n try {\n res = await timedFetch(\n options,\n `${baseUrl(options)}/stream`,\n {\n headers,\n cache: \"no-store\",\n signal: AbortSignal.any([abort.signal, heartbeat.signal]),\n },\n heartbeat.signal,\n );\n } finally {\n clearTimeout(headerTimer);\n }\n if (res.status === 429 || res.status === 503) throw overloaded(res);\n if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`);\n const reader = res.body.getReader();\n try {\n return await consume(c, reader, heartbeat, () => abort.signal.aborted);\n } finally {\n reader.cancel().catch(() => {});\n }\n};\n\nconst overloaded = (res: Response) =>\n overloadedError(parseRetryAfter(res.headers.get(\"Retry-After\")));\n\nconst sendCommand = async (\n c: Conn,\n options: StatewireClient.SSEOptions,\n { seq, command }: { seq: number; command: StatewireWireCommand },\n): Promise<StatewireSubmitResult> => {\n const timeout = AbortSignal.timeout(timing.heartbeatTimeoutMs);\n const res = await timedFetch(\n options,\n `${baseUrl(options)}/commands`,\n {\n method: \"POST\",\n headers: await requestHeaders(options, {\n \"content-type\": \"application/json\",\n \"Statewire-Client-Id\": c.ctx.clientId,\n \"Statewire-Command-Seq\": String(seq),\n ...(c.lease !== null && { \"Statewire-Lease\": c.lease }),\n }),\n body: JSON.stringify({\n method: command.method,\n ...encodeParams(command.params),\n }),\n signal: AbortSignal.any([\n c.lifetime?.signal ?? AbortSignal.abort(),\n timeout,\n ]),\n },\n timeout,\n );\n if (res.ok) {\n let body: { res?: StatewireResponse } | null;\n try {\n body = (await res.json()) as { res?: StatewireResponse } | null;\n } catch {\n return {\n type: \"rejected\",\n message: \"statewire: protocol error: malformed /commands receipt body\",\n };\n }\n return body?.res !== undefined\n ? { type: \"receipt\", response: reviveResponse(body.res) }\n : { type: \"receipt\" };\n }\n if (res.status === 409 || res.status === 412 || res.status === 423) {\n // Only a body naming the same verdict as the status is statewire's own;\n // a bare status is middleware noise — a transport fault, retried.\n const verdict =\n res.status === 409\n ? (\"seq-gap\" as const)\n : res.status === 412\n ? (\"unknown-client\" as const)\n : (\"stale-lease\" as const);\n const body = (await res.json().catch(() => null)) as {\n error?: string;\n } | null;\n if (body?.error === verdict) return { type: verdict };\n throw new Error(`HTTP ${res.status}`);\n }\n if (res.status === 429 || res.status === 503) throw overloaded(res);\n if (res.status >= 400 && res.status < 500) {\n return { type: \"rejected\", message: `HTTP ${res.status}` };\n }\n throw new Error(`HTTP ${res.status}`);\n};\n\nconst useSseLink = (\n options: StatewireClient.SSEOptions,\n ctx: StatewireRemoteProps,\n): RemoteLink => {\n const latest = useRef(options);\n latest.current = options;\n const c = useStatewireConn({\n url: options.url,\n onChange: options.onChange,\n ctx,\n attemptOnce: (c, abort) => attemptStream(c, options, abort),\n });\n const [link] = useState(\n (): RemoteLink => ({\n get connection() {\n return c.connection;\n },\n send: (args) => sendCommand(c, latest.current, args),\n }),\n );\n return link;\n};\n\nconst useStatewireSSE = (\n options: StatewireClient.SSEOptions,\n): Statewire<any, any, any> => useStatewireRemote(options, useSseLink);\n\nexport const StatewireSSE = resource(useStatewireSSE) as <\n TState = any,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.SSEOptions,\n) => ResourceElement<Statewire<TState | undefined, TCommands, TEvent>>;\n"],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { StatewireEnvelope } from "../client/connection.js";
|
|
2
|
+
import { Statewire } from "../types.js";
|
|
3
|
+
import { SocketRecorder, commandFrame, createCounterElement, createSocketRecorder, settle } from "./fixtures.js";
|
|
4
|
+
import { ResourceElement } from "@assistant-ui/tap";
|
|
5
|
+
//#region src/transports/testing.d.ts
|
|
6
|
+
/** Let queued microtasks and op batches drain through the statewire client. */
|
|
7
|
+
declare const tick: () => Promise<void>;
|
|
8
|
+
/** Run a mutator against a recording draft, as the client does for `optimistic`. */
|
|
9
|
+
declare const applyOptimistic: <TState>(state: TState, mutate: (draft: TState) => void) => TState;
|
|
10
|
+
type MockTransport<TState> = {
|
|
11
|
+
/** Pass as the `transport:` option of useStatewire / Harness. */
|
|
12
|
+
transport: ResourceElement<Statewire<TState | undefined, any, any>>;
|
|
13
|
+
/** Captured `{ method, params }` command bodies, in POST order. */
|
|
14
|
+
commands: {
|
|
15
|
+
method: string;
|
|
16
|
+
params: unknown[];
|
|
17
|
+
}[];
|
|
18
|
+
/** Replace the replicated state and broadcast it as a root snapshot op. */
|
|
19
|
+
setState: (state: TState) => Promise<void>;
|
|
20
|
+
/** Deliver an ack watermark on the stream. */
|
|
21
|
+
ack: (seq: number) => Promise<void>;
|
|
22
|
+
/** Deliver an arbitrary envelope on the stream. */
|
|
23
|
+
sendEnvelope: (envelope: StatewireEnvelope) => Promise<void>;
|
|
24
|
+
/** End the stream with a fin; further attaches fail like a dead server. */
|
|
25
|
+
finish: (reason: string, message?: string) => Promise<void>;
|
|
26
|
+
readonly clientId: string;
|
|
27
|
+
readonly attached: boolean;
|
|
28
|
+
};
|
|
29
|
+
declare const createMockTransport: <TState>(initialState: TState) => MockTransport<TState>;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { MockTransport, SocketRecorder, applyOptimistic, commandFrame, createCounterElement, createMockTransport, createSocketRecorder, settle, tick };
|
|
32
|
+
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.d.ts","names":[],"sources":["../../src/transports/testing.ts"],"mappings":";;;;;;cAWa,YAAI;;cAGJ,kBAAmB,QAC9B,OAAO,QACP,SAAS,OAAO,oBACf;KAES,cAAc;;EAExB,WAAW,gBAAgB,UAAU;;EAErC;IAAY;IAAgB;;;EAE5B,WAAW,OAAO,WAAW;;EAE7B,MAAM,gBAAgB;;EAEtB,eAAe,UAAU,sBAAsB;;EAE/C,SAAS,gBAAgB,qBAAqB;WACrC;WACA;;cAUE,sBAAuB,QAClC,cAAc,WACb,cAAc"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { recordMutations } from "../util/recordMutations.js";
|
|
2
|
+
import { StatewireSSE } from "./sse.js";
|
|
3
|
+
import { openSSEStream, sseFrame } from "./fake-sse.js";
|
|
4
|
+
import { commandFrame, createCounterElement, createSocketRecorder, settle } from "./fixtures.js";
|
|
5
|
+
//#region src/transports/testing.ts
|
|
6
|
+
/** Let queued microtasks and op batches drain through the statewire client. */
|
|
7
|
+
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
8
|
+
/** Run a mutator against a recording draft, as the client does for `optimistic`. */
|
|
9
|
+
const applyOptimistic = (state, mutate) => recordMutations(state, mutate).state;
|
|
10
|
+
/**
|
|
11
|
+
* A mock statewire backend: serves `initialState` as the attach snapshot,
|
|
12
|
+
* records command POSTs (receipt only — acks and results are delivered from
|
|
13
|
+
* test code), and exposes the wire as a transport ResourceElement.
|
|
14
|
+
*/
|
|
15
|
+
let mockCount = 0;
|
|
16
|
+
const createMockTransport = (initialState) => {
|
|
17
|
+
let state = initialState;
|
|
18
|
+
let clientId = "";
|
|
19
|
+
let finished = false;
|
|
20
|
+
const clients = /* @__PURE__ */ new Map();
|
|
21
|
+
const streams = /* @__PURE__ */ new Set();
|
|
22
|
+
const commands = [];
|
|
23
|
+
const sendEnvelope = (envelope) => {
|
|
24
|
+
for (const stream of streams) stream.send(sseFrame(envelope));
|
|
25
|
+
return tick();
|
|
26
|
+
};
|
|
27
|
+
const fakeFetch = (async (input, init) => {
|
|
28
|
+
if (finished) throw new TypeError("fetch failed (finished)");
|
|
29
|
+
const url = String(input);
|
|
30
|
+
const headers = new Headers(init?.headers);
|
|
31
|
+
const id = headers.get("Statewire-Client-Id") ?? "";
|
|
32
|
+
if (url.endsWith("/stream")) {
|
|
33
|
+
clientId = id;
|
|
34
|
+
const lastSeq = clients.get(id) ?? 0;
|
|
35
|
+
clients.set(id, lastSeq);
|
|
36
|
+
return openSSEStream(streams, id, (handle) => {
|
|
37
|
+
handle.send(sseFrame({
|
|
38
|
+
ops: [{
|
|
39
|
+
op: "replace",
|
|
40
|
+
path: [],
|
|
41
|
+
value: state
|
|
42
|
+
}],
|
|
43
|
+
syn: {
|
|
44
|
+
lastSeq,
|
|
45
|
+
lease: "mock-lease"
|
|
46
|
+
}
|
|
47
|
+
}));
|
|
48
|
+
}, init?.signal ?? void 0);
|
|
49
|
+
}
|
|
50
|
+
if (url.endsWith("/commands")) {
|
|
51
|
+
const seq = Number(headers.get("Statewire-Command-Seq"));
|
|
52
|
+
clients.set(id, Math.max(clients.get(id) ?? 0, seq));
|
|
53
|
+
commands.push(JSON.parse(String(init?.body)));
|
|
54
|
+
return Response.json({});
|
|
55
|
+
}
|
|
56
|
+
return new Response(null, { status: 404 });
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
transport: StatewireSSE({
|
|
60
|
+
url: `http://mock-${++mockCount}/api`,
|
|
61
|
+
fetch: fakeFetch
|
|
62
|
+
}),
|
|
63
|
+
commands,
|
|
64
|
+
setState: (next) => {
|
|
65
|
+
state = next;
|
|
66
|
+
return sendEnvelope({ ops: [{
|
|
67
|
+
op: "replace",
|
|
68
|
+
path: [],
|
|
69
|
+
value: next
|
|
70
|
+
}] });
|
|
71
|
+
},
|
|
72
|
+
ack: (seq) => sendEnvelope({ ack: seq }),
|
|
73
|
+
sendEnvelope,
|
|
74
|
+
finish: async (reason, message) => {
|
|
75
|
+
await sendEnvelope({ fin: {
|
|
76
|
+
reason,
|
|
77
|
+
...message !== void 0 && { message }
|
|
78
|
+
} });
|
|
79
|
+
finished = true;
|
|
80
|
+
for (const stream of [...streams]) stream.close();
|
|
81
|
+
await tick();
|
|
82
|
+
},
|
|
83
|
+
get clientId() {
|
|
84
|
+
return clientId;
|
|
85
|
+
},
|
|
86
|
+
get attached() {
|
|
87
|
+
return streams.size > 0;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
export { applyOptimistic, commandFrame, createCounterElement, createMockTransport, createSocketRecorder, settle, tick };
|
|
93
|
+
|
|
94
|
+
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.js","names":["ResourceElement","StatewireEnvelope","recordMutations","StatewireSSE","Statewire","openSSEStream","sseFrame","FakeStreamHandle","tick","Promise","r","setTimeout","applyOptimistic","state","TState","mutate","draft","MockTransport","transport","commands","method","params","setState","ack","seq","sendEnvelope","envelope","finish","reason","message","clientId","attached","mockCount","createMockTransport","initialState","finished","clients","Map","streams","Set","stream","send","fakeFetch","input","RequestInfo","URL","init","RequestInit","Response","TypeError","url","String","headers","Headers","id","get","endsWith","lastSeq","set","handle","ops","op","path","value","syn","lease","signal","undefined","Number","Math","max","push","JSON","parse","body","json","status","fetch","next","fin","close","size"],"sources":["../../src/transports/testing.ts"],"sourcesContent":["import type { ResourceElement } from \"@assistant-ui/tap\";\nimport type { StatewireEnvelope } from \"../client/connection\";\nimport { recordMutations } from \"../util/recordMutations\";\nimport { StatewireSSE } from \"./sse\";\nimport type { Statewire } from \"../types\";\nimport { openSSEStream, sseFrame } from \"./fake-sse\";\nimport type { FakeStreamHandle } from \"./fake-sse\";\n\nexport * from \"./fixtures\";\n\n/** Let queued microtasks and op batches drain through the statewire client. */\nexport const tick = () => new Promise<void>((r) => setTimeout(r, 0));\n\n/** Run a mutator against a recording draft, as the client does for `optimistic`. */\nexport const applyOptimistic = <TState>(\n state: TState,\n mutate: (draft: TState) => void,\n): TState => recordMutations(state, mutate).state;\n\nexport type MockTransport<TState> = {\n /** Pass as the `transport:` option of useStatewire / Harness. */\n transport: ResourceElement<Statewire<TState | undefined, any, any>>;\n /** Captured `{ method, params }` command bodies, in POST order. */\n commands: { method: string; params: unknown[] }[];\n /** Replace the replicated state and broadcast it as a root snapshot op. */\n setState: (state: TState) => Promise<void>;\n /** Deliver an ack watermark on the stream. */\n ack: (seq: number) => Promise<void>;\n /** Deliver an arbitrary envelope on the stream. */\n sendEnvelope: (envelope: StatewireEnvelope) => Promise<void>;\n /** End the stream with a fin; further attaches fail like a dead server. */\n finish: (reason: string, message?: string) => Promise<void>;\n readonly clientId: string;\n readonly attached: boolean;\n};\n\n/**\n * A mock statewire backend: serves `initialState` as the attach snapshot,\n * records command POSTs (receipt only — acks and results are delivered from\n * test code), and exposes the wire as a transport ResourceElement.\n */\nlet mockCount = 0;\n\nexport const createMockTransport = <TState>(\n initialState: TState,\n): MockTransport<TState> => {\n let state: unknown = initialState;\n let clientId = \"\";\n let finished = false;\n const clients = new Map<string, number>();\n const streams = new Set<FakeStreamHandle>();\n const commands: { method: string; params: unknown[] }[] = [];\n\n const sendEnvelope = (envelope: StatewireEnvelope) => {\n for (const stream of streams) stream.send(sseFrame(envelope));\n return tick();\n };\n\n const fakeFetch = (async (\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> => {\n if (finished) throw new TypeError(\"fetch failed (finished)\");\n const url = String(input);\n const headers = new Headers(init?.headers);\n const id = headers.get(\"Statewire-Client-Id\") ?? \"\";\n if (url.endsWith(\"/stream\")) {\n clientId = id;\n const lastSeq = clients.get(id) ?? 0;\n clients.set(id, lastSeq);\n return openSSEStream(\n streams,\n id,\n (handle) => {\n handle.send(\n sseFrame({\n ops: [{ op: \"replace\", path: [], value: state }],\n syn: { lastSeq, lease: \"mock-lease\" },\n }),\n );\n },\n init?.signal ?? undefined,\n );\n }\n if (url.endsWith(\"/commands\")) {\n const seq = Number(headers.get(\"Statewire-Command-Seq\"));\n clients.set(id, Math.max(clients.get(id) ?? 0, seq));\n commands.push(\n JSON.parse(String(init?.body)) as { method: string; params: unknown[] },\n );\n return Response.json({});\n }\n return new Response(null, { status: 404 });\n }) as typeof fetch;\n\n return {\n transport: StatewireSSE({\n url: `http://mock-${++mockCount}/api`,\n fetch: fakeFetch,\n }),\n commands,\n setState: (next) => {\n state = next;\n return sendEnvelope({ ops: [{ op: \"replace\", path: [], value: next }] });\n },\n ack: (seq) => sendEnvelope({ ack: seq }),\n sendEnvelope,\n finish: async (reason, message) => {\n await sendEnvelope({\n fin: { reason, ...(message !== undefined && { message }) },\n });\n finished = true;\n for (const stream of [...streams]) stream.close();\n await tick();\n },\n get clientId() {\n return clientId;\n },\n get attached() {\n return streams.size > 0;\n },\n };\n};\n"],"mappings":";;;;;;AAWA,MAAaQ,aAAa,IAAIC,SAAeC,MAAMC,WAAWD,GAAG,CAAC,CAAC;;AAGnE,MAAaE,mBACXC,OACAE,WACWb,gBAAgBW,OAAOE,MAAM,CAAC,CAACF;;;;;;AAwB5C,IAAImB,YAAY;AAEhB,MAAaC,uBACXC,iBAC0B;CAC1B,IAAIrB,QAAiBqB;CACrB,IAAIJ,WAAW;CACf,IAAIK,WAAW;CACf,MAAMC,0BAAU,IAAIC,IAAoB;CACxC,MAAMC,0BAAU,IAAIC,IAAsB;CAC1C,MAAMpB,WAAoD,CAAA;CAE1D,MAAMM,gBAAgBC,aAAgC;EACpD,KAAK,MAAMc,UAAUF,SAASE,OAAOC,KAAKnC,SAASoB,QAAQ,CAAC;EAC5D,OAAOlB,KAAK;CACd;CAEA,MAAMkC,aAAa,OACjBC,OACAG,SACsB;EACtB,IAAIX,UAAU,MAAM,IAAIc,UAAU,yBAAyB;EAC3D,MAAMC,MAAMC,OAAOR,KAAK;EACxB,MAAMS,UAAU,IAAIC,QAAQP,MAAMM,OAAO;EACzC,MAAME,KAAKF,QAAQG,IAAI,qBAAqB,KAAK;EACjD,IAAIL,IAAIM,SAAS,SAAS,GAAG;GAC3B1B,WAAWwB;GACX,MAAMG,UAAUrB,QAAQmB,IAAID,EAAE,KAAK;GACnClB,QAAQsB,IAAIJ,IAAIG,OAAO;GACvB,OAAOpD,cACLiC,SACAgB,KACCK,WAAW;IACVA,OAAOlB,KACLnC,SAAS;KACPsD,KAAK,CAAC;MAAEC,IAAI;MAAWC,MAAM,CAAA;MAAIC,OAAOlD;KAAM,CAAC;KAC/CmD,KAAK;MAAEP;MAASQ,OAAO;KAAa;IACtC,CAAC,CACH;GACF,GACAnB,MAAMoB,UAAUC,KAAAA,CAClB;EACF;EACA,IAAIjB,IAAIM,SAAS,WAAW,GAAG;GAC7B,MAAMhC,MAAM4C,OAAOhB,QAAQG,IAAI,uBAAuB,CAAC;GACvDnB,QAAQsB,IAAIJ,IAAIe,KAAKC,IAAIlC,QAAQmB,IAAID,EAAE,KAAK,GAAG9B,GAAG,CAAC;GACnDL,SAASoD,KACPC,KAAKC,MAAMtB,OAAOL,MAAM4B,IAAI,CAAC,CAC/B;GACA,OAAO1B,SAAS2B,KAAK,CAAC,CAAC;EACzB;EACA,OAAO,IAAI3B,SAAS,MAAM,EAAE4B,QAAQ,IAAI,CAAC;CAC3C;CAEA,OAAO;EACL1D,WAAWf,aAAa;GACtB+C,KAAK,eAAe,EAAElB,UAAS;GAC/B6C,OAAOnC;EACT,CAAC;EACDvB;EACAG,WAAWwD,SAAS;GAClBjE,QAAQiE;GACR,OAAOrD,aAAa,EAAEmC,KAAK,CAAC;IAAEC,IAAI;IAAWC,MAAM,CAAA;IAAIC,OAAOe;GAAK,CAAC,EAAE,CAAC;EACzE;EACAvD,MAAMC,QAAQC,aAAa,EAAEF,KAAKC,IAAI,CAAC;EACvCC;EACAE,QAAQ,OAAOC,QAAQC,YAAY;GACjC,MAAMJ,aAAa,EACjBsD,KAAK;IAAEnD;IAAQ,GAAIC,YAAYsC,KAAAA,KAAa,EAAEtC,QAAQ;GAAG,EAC3D,CAAC;GACDM,WAAW;GACX,KAAK,MAAMK,UAAU,CAAC,GAAGF,OAAO,GAAGE,OAAOwC,MAAM;GAChD,MAAMxE,KAAK;EACb;EACA,IAAIsB,WAAW;GACb,OAAOA;EACT;EACA,IAAIC,WAAW;GACb,OAAOO,QAAQ2C,OAAO;EACxB;CACF;AACF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { StatewireClient } from "../statewire.js";
|
|
2
|
+
import { Statewire } from "../types.js";
|
|
3
|
+
import { ResourceElement } from "@assistant-ui/tap";
|
|
4
|
+
//#region src/transports/ws.d.ts
|
|
5
|
+
declare const WS_SUBPROTOCOL = "statewire.v1";
|
|
6
|
+
declare const StatewireWS: <TState = any, TCommands extends Statewire.Commands = Statewire.Commands, TEvent = any>(options: StatewireClient.WSOptions) => ResourceElement<Statewire<TState | undefined, TCommands, TEvent>>;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { StatewireWS, WS_SUBPROTOCOL };
|
|
9
|
+
//# sourceMappingURL=ws.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws.d.ts","names":[],"sources":["../../src/transports/ws.ts"],"mappings":";;;;cAmCa;cAsNA,cACX,cACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,cACtB,gBAAgB,UAAU,oBAAoB,WAAW"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { encodeParams } from "../wire/meta.js";
|
|
2
|
+
import { CLIENT_QUERY_PARAM } from "../host/adapter.js";
|
|
3
|
+
import { timing } from "../client/timing.js";
|
|
4
|
+
import { handleEnvelope, overloadedError, useStatewireConn } from "../client/connection.js";
|
|
5
|
+
import { useStatewireRemote } from "./remote.js";
|
|
6
|
+
import { useState } from "@assistant-ui/tap/react-shim";
|
|
7
|
+
import { flushTapSync, resource } from "@assistant-ui/tap";
|
|
8
|
+
//#region src/transports/ws.ts
|
|
9
|
+
const OPEN = 1;
|
|
10
|
+
const WS_SUBPROTOCOL = "statewire.v1";
|
|
11
|
+
const socketUrl = async (options, clientId) => {
|
|
12
|
+
const { url, query } = options;
|
|
13
|
+
const params = new URLSearchParams(typeof query === "function" ? await query() : query);
|
|
14
|
+
params.set(CLIENT_QUERY_PARAM, clientId);
|
|
15
|
+
return `${url.replace(/\/$/, "").replace(/^http/, "ws")}/ws?${params.toString()}`;
|
|
16
|
+
};
|
|
17
|
+
const settleOverloaded = (w, { seq, retryAfterMs }) => {
|
|
18
|
+
if (seq === void 0) throw overloadedError(retryAfterMs);
|
|
19
|
+
const entry = w.pending.find((p) => p.seq === seq);
|
|
20
|
+
if (!entry) return;
|
|
21
|
+
w.pending = w.pending.filter((p) => p !== entry);
|
|
22
|
+
entry.reject(overloadedError(retryAfterMs));
|
|
23
|
+
};
|
|
24
|
+
const resolvePending = (w, matches) => {
|
|
25
|
+
const remaining = [];
|
|
26
|
+
for (const entry of w.pending) if (matches(entry)) entry.resolve({ type: "receipt" });
|
|
27
|
+
else remaining.push(entry);
|
|
28
|
+
w.pending = remaining;
|
|
29
|
+
};
|
|
30
|
+
const failPending = (w, error) => {
|
|
31
|
+
const pending = w.pending;
|
|
32
|
+
w.pending = [];
|
|
33
|
+
for (const entry of pending) entry.reject(error);
|
|
34
|
+
};
|
|
35
|
+
const runSocket = (c, w, ws, abort) => new Promise((resolve, reject) => {
|
|
36
|
+
let timer;
|
|
37
|
+
let timedOut = false;
|
|
38
|
+
let outcome = null;
|
|
39
|
+
let closed = false;
|
|
40
|
+
const arm = () => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
timer = setTimeout(() => {
|
|
43
|
+
timedOut = true;
|
|
44
|
+
ws.close();
|
|
45
|
+
}, timing.heartbeatTimeoutMs);
|
|
46
|
+
};
|
|
47
|
+
arm();
|
|
48
|
+
abort.signal.addEventListener("abort", () => ws.close());
|
|
49
|
+
ws.onerror = () => {};
|
|
50
|
+
const stale = () => Boolean(outcome) || closed || abort.signal.aborted;
|
|
51
|
+
const fail = (error) => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
reject(error instanceof Error ? error : /* @__PURE__ */ new Error("protocol"));
|
|
54
|
+
ws.close();
|
|
55
|
+
};
|
|
56
|
+
const processEnv = (env) => {
|
|
57
|
+
const done = flushTapSync(() => handleEnvelope(c, env));
|
|
58
|
+
if (env.ack !== void 0) {
|
|
59
|
+
const ack = env.ack;
|
|
60
|
+
resolvePending(w, (entry) => entry.seq <= ack);
|
|
61
|
+
}
|
|
62
|
+
if (env.res !== void 0) {
|
|
63
|
+
const seqs = new Set(env.res.map((r) => r.seq));
|
|
64
|
+
resolvePending(w, (entry) => seqs.has(entry.seq));
|
|
65
|
+
}
|
|
66
|
+
if (done) {
|
|
67
|
+
outcome = done;
|
|
68
|
+
ws.close();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
ws.onmessage = (event) => {
|
|
72
|
+
if (stale()) return;
|
|
73
|
+
arm();
|
|
74
|
+
let frame;
|
|
75
|
+
try {
|
|
76
|
+
frame = JSON.parse(String(event.data));
|
|
77
|
+
} catch {
|
|
78
|
+
fail(/* @__PURE__ */ new Error("protocol"));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
if (frame.overloaded !== void 0) settleOverloaded(w, frame.overloaded);
|
|
83
|
+
else processEnv(frame);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
fail(error);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
ws.onclose = () => {
|
|
89
|
+
closed = true;
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
if (w.socket === ws) failPending(w, /* @__PURE__ */ new Error("network"));
|
|
92
|
+
if (outcome) resolve(outcome);
|
|
93
|
+
else reject(/* @__PURE__ */ new Error(timedOut ? "timeout" : "network"));
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
const attemptSocket = async (c, w, options, abort) => {
|
|
97
|
+
let ws = null;
|
|
98
|
+
try {
|
|
99
|
+
const url = await socketUrl(options, c.ctx.clientId);
|
|
100
|
+
abort.signal.throwIfAborted();
|
|
101
|
+
const { protocols, webSocket } = options;
|
|
102
|
+
const WS = webSocket ?? globalThis.WebSocket;
|
|
103
|
+
if (!WS) throw new Error("no WebSocket implementation");
|
|
104
|
+
ws = new WS(url, [WS_SUBPROTOCOL, ...protocols === void 0 ? [] : typeof protocols === "string" ? [protocols] : protocols]);
|
|
105
|
+
w.socket = ws;
|
|
106
|
+
return await runSocket(c, w, ws, abort);
|
|
107
|
+
} finally {
|
|
108
|
+
if (w.socket === ws) w.socket = null;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const sendCommand = (w, { seq, command }) => {
|
|
112
|
+
const ws = w.socket;
|
|
113
|
+
if (!ws || ws.readyState !== OPEN) return Promise.reject(/* @__PURE__ */ new Error("network"));
|
|
114
|
+
return new Promise((resolve, reject) => {
|
|
115
|
+
const entry = {
|
|
116
|
+
seq,
|
|
117
|
+
resolve: (result) => {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
resolve(result);
|
|
120
|
+
},
|
|
121
|
+
reject: (error) => {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
reject(error);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const timer = setTimeout(() => {
|
|
127
|
+
w.pending = w.pending.filter((p) => p !== entry);
|
|
128
|
+
reject(/* @__PURE__ */ new Error("timeout"));
|
|
129
|
+
ws.close();
|
|
130
|
+
}, timing.heartbeatTimeoutMs);
|
|
131
|
+
w.pending.push(entry);
|
|
132
|
+
try {
|
|
133
|
+
ws.send(JSON.stringify({
|
|
134
|
+
method: command.method,
|
|
135
|
+
...encodeParams(command.params),
|
|
136
|
+
seq
|
|
137
|
+
}));
|
|
138
|
+
} catch {
|
|
139
|
+
clearTimeout(timer);
|
|
140
|
+
w.pending = w.pending.filter((p) => p !== entry);
|
|
141
|
+
reject(/* @__PURE__ */ new Error("network"));
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
};
|
|
145
|
+
const useWsLink = (options, ctx) => {
|
|
146
|
+
const [w] = useState(() => ({
|
|
147
|
+
socket: null,
|
|
148
|
+
pending: []
|
|
149
|
+
}));
|
|
150
|
+
const c_0 = useStatewireConn({
|
|
151
|
+
url: options.url,
|
|
152
|
+
onChange: options.onChange,
|
|
153
|
+
ctx,
|
|
154
|
+
attemptOnce: (c, abort) => attemptSocket(c, w, options, abort)
|
|
155
|
+
});
|
|
156
|
+
const [link] = useState(() => ({
|
|
157
|
+
get connection() {
|
|
158
|
+
return c_0.connection;
|
|
159
|
+
},
|
|
160
|
+
send: (args) => sendCommand(w, args)
|
|
161
|
+
}));
|
|
162
|
+
return link;
|
|
163
|
+
};
|
|
164
|
+
const useStatewireWS = (options) => useStatewireRemote(options, useWsLink);
|
|
165
|
+
const StatewireWS = resource(useStatewireWS);
|
|
166
|
+
//#endregion
|
|
167
|
+
export { StatewireWS, WS_SUBPROTOCOL };
|
|
168
|
+
|
|
169
|
+
//# sourceMappingURL=ws.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws.js","names":["useState","flushTapSync","resource","ResourceElement","encodeParams","timing","handleEnvelope","overloadedError","useStatewireConn","Conn","StatewireEnvelope","Statewire","StatewireClient","useStatewireRemote","CLIENT_QUERY_PARAM","RemoteLink","StatewireSubmitResult","StatewireRemoteProps","StatewireWireCommand","PendingCommand","seq","resolve","result","reject","error","Error","WsChannel","socket","WebSocket","pending","OPEN","WS_SUBPROTOCOL","socketUrl","options","WSOptions","clientId","url","query","params","URLSearchParams","set","base","replace","toString","settleOverloaded","w","retryAfterMs","undefined","entry","find","p","filter","resolvePending","matches","remaining","type","push","failPending","runSocket","c","ws","abort","AbortController","Promise","timer","ReturnType","setTimeout","timedOut","outcome","closed","arm","clearTimeout","close","heartbeatTimeoutMs","signal","addEventListener","onerror","stale","Boolean","aborted","fail","processEnv","env","done","ack","res","seqs","Set","map","r","has","onmessage","event","frame","overloaded","JSON","parse","String","data","onclose","attemptSocket","ctx","throwIfAborted","protocols","webSocket","WS","globalThis","extra","sendCommand","command","readyState","send","stringify","method","useWsLink","onChange","attemptOnce","link","connection","args","useStatewireWS","StatewireWS","Commands","TState","TCommands","TEvent"],"sources":["../../src/transports/ws.ts"],"sourcesContent":["import { useState } from \"react\";\nimport { flushTapSync, resource } from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { encodeParams } from \"../wire/meta\";\nimport { timing } from \"../client/timing\";\nimport {\n handleEnvelope,\n overloadedError,\n useStatewireConn,\n} from \"../client/connection\";\nimport type { Conn, StatewireEnvelope } from \"../client/connection\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireClient } from \"../statewire\";\nimport { useStatewireRemote } from \"./remote\";\nimport { CLIENT_QUERY_PARAM } from \"../host/adapter\";\nimport type {\n RemoteLink,\n StatewireSubmitResult,\n StatewireRemoteProps,\n StatewireWireCommand,\n} from \"./sse\";\n\ntype PendingCommand = {\n seq: number;\n resolve: (result: StatewireSubmitResult) => void;\n reject: (error: Error) => void;\n};\n\ntype WsChannel = {\n socket: WebSocket | null;\n pending: PendingCommand[];\n};\n\nconst OPEN = 1;\n\nexport const WS_SUBPROTOCOL = \"statewire.v1\";\n\nconst socketUrl = async (\n options: StatewireClient.WSOptions,\n clientId: string,\n) => {\n const { url, query } = options;\n const params = new URLSearchParams(\n typeof query === \"function\" ? await query() : query,\n );\n params.set(CLIENT_QUERY_PARAM, clientId);\n const base = url.replace(/\\/$/, \"\").replace(/^http/, \"ws\");\n return `${base}/ws?${params.toString()}`;\n};\n\nconst settleOverloaded = (\n w: WsChannel,\n { seq, retryAfterMs }: { seq?: number; retryAfterMs?: number },\n) => {\n if (seq === undefined) throw overloadedError(retryAfterMs);\n const entry = w.pending.find((p) => p.seq === seq);\n if (!entry) return;\n w.pending = w.pending.filter((p) => p !== entry);\n entry.reject(overloadedError(retryAfterMs));\n};\n\nconst resolvePending = (\n w: WsChannel,\n matches: (entry: PendingCommand) => boolean,\n) => {\n const remaining: PendingCommand[] = [];\n for (const entry of w.pending) {\n if (matches(entry)) entry.resolve({ type: \"receipt\" });\n else remaining.push(entry);\n }\n w.pending = remaining;\n};\n\nconst failPending = (w: WsChannel, error: Error) => {\n const pending = w.pending;\n w.pending = [];\n for (const entry of pending) entry.reject(error);\n};\n\nconst runSocket = (\n c: Conn,\n w: WsChannel,\n ws: WebSocket,\n abort: AbortController,\n): Promise<\"stop\" | \"reattach\"> =>\n new Promise((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let timedOut = false;\n let outcome: \"stop\" | \"reattach\" | null = null;\n let closed = false;\n const arm = () => {\n clearTimeout(timer);\n timer = setTimeout(() => {\n timedOut = true;\n ws.close();\n }, timing.heartbeatTimeoutMs);\n };\n arm();\n abort.signal.addEventListener(\"abort\", () => ws.close());\n ws.onerror = () => {};\n const stale = () => Boolean(outcome) || closed || abort.signal.aborted;\n const fail = (error: unknown) => {\n clearTimeout(timer);\n reject(error instanceof Error ? error : new Error(\"protocol\"));\n ws.close();\n };\n const processEnv = (env: StatewireEnvelope) => {\n const done = flushTapSync(() => handleEnvelope(c, env));\n if (env.ack !== undefined) {\n const ack = env.ack;\n resolvePending(w, (entry) => entry.seq <= ack);\n }\n if (env.res !== undefined) {\n const seqs = new Set(env.res.map((r) => r.seq));\n resolvePending(w, (entry) => seqs.has(entry.seq));\n }\n if (done) {\n outcome = done;\n ws.close();\n }\n };\n ws.onmessage = (event) => {\n if (stale()) return;\n arm();\n let frame: StatewireEnvelope & {\n overloaded?: { seq?: number; retryAfterMs?: number };\n };\n try {\n frame = JSON.parse(String(event.data)) as typeof frame;\n } catch {\n fail(new Error(\"protocol\"));\n return;\n }\n try {\n if (frame.overloaded !== undefined) {\n settleOverloaded(w, frame.overloaded);\n } else {\n processEnv(frame);\n }\n } catch (error) {\n fail(error);\n }\n };\n ws.onclose = () => {\n closed = true;\n clearTimeout(timer);\n if (w.socket === ws) failPending(w, new Error(\"network\"));\n if (outcome) resolve(outcome);\n else reject(new Error(timedOut ? \"timeout\" : \"network\"));\n };\n });\n\nconst attemptSocket = async (\n c: Conn,\n w: WsChannel,\n options: StatewireClient.WSOptions,\n abort: AbortController,\n): Promise<\"stop\" | \"reattach\"> => {\n let ws: WebSocket | null = null;\n try {\n const url = await socketUrl(options, c.ctx.clientId);\n abort.signal.throwIfAborted();\n const { protocols, webSocket } = options;\n const WS = webSocket ?? globalThis.WebSocket;\n if (!WS) throw new Error(\"no WebSocket implementation\");\n const extra =\n protocols === undefined\n ? []\n : typeof protocols === \"string\"\n ? [protocols]\n : protocols;\n ws = new WS(url, [WS_SUBPROTOCOL, ...extra]);\n w.socket = ws;\n return await runSocket(c, w, ws, abort);\n } finally {\n if (w.socket === ws) w.socket = null;\n }\n};\n\nconst sendCommand = (\n w: WsChannel,\n { seq, command }: { seq: number; command: StatewireWireCommand },\n): Promise<StatewireSubmitResult> => {\n const ws = w.socket;\n if (!ws || ws.readyState !== OPEN) {\n return Promise.reject(new Error(\"network\"));\n }\n return new Promise((resolve, reject) => {\n const entry: PendingCommand = {\n seq,\n resolve: (result) => {\n clearTimeout(timer);\n resolve(result);\n },\n reject: (error) => {\n clearTimeout(timer);\n reject(error);\n },\n };\n // A timed-out command fails the socket: the retry then rides reconnect +\n // attach-seq arbitration.\n const timer = setTimeout(() => {\n w.pending = w.pending.filter((p) => p !== entry);\n reject(new Error(\"timeout\"));\n ws.close();\n }, timing.heartbeatTimeoutMs);\n w.pending.push(entry);\n try {\n ws.send(\n JSON.stringify({\n method: command.method,\n ...encodeParams(command.params),\n seq,\n }),\n );\n } catch {\n clearTimeout(timer);\n w.pending = w.pending.filter((p) => p !== entry);\n reject(new Error(\"network\"));\n }\n });\n};\n\nconst useWsLink = (\n options: StatewireClient.WSOptions,\n ctx: StatewireRemoteProps,\n): RemoteLink => {\n const [w] = useState((): WsChannel => ({ socket: null, pending: [] }));\n const c = useStatewireConn({\n url: options.url,\n onChange: options.onChange,\n ctx,\n attemptOnce: (c, abort) => attemptSocket(c, w, options, abort),\n });\n const [link] = useState(\n (): RemoteLink => ({\n get connection() {\n return c.connection;\n },\n send: (args) => sendCommand(w, args),\n }),\n );\n return link;\n};\n\nconst useStatewireWS = (\n options: StatewireClient.WSOptions,\n): Statewire<any, any, any> => useStatewireRemote(options, useWsLink);\n\nexport const StatewireWS = resource(useStatewireWS) as <\n TState = any,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.WSOptions,\n) => ResourceElement<Statewire<TState | undefined, TCommands, TEvent>>;\n"],"mappings":";;;;;;;;AAiCA,MAAM8B,OAAO;AAEb,MAAaC,iBAAiB;AAE9B,MAAMC,YAAY,OAChBC,SACAE,aACG;CACH,MAAM,EAAEC,KAAKC,UAAUJ;CACvB,MAAMK,SAAS,IAAIC,gBACjB,OAAOF,UAAU,aAAa,MAAMA,MAAM,IAAIA,KAChD;CACAC,OAAOE,IAAI1B,oBAAoBqB,QAAQ;CAEvC,OAAO,GADMC,IAAIM,QAAQ,OAAO,EAAE,CAAC,CAACA,QAAQ,SAAS,IAC3CD,EAAI,MAAOH,OAAOK,SAAS;AACvC;AAEA,MAAMC,oBACJC,GACA,EAAEzB,KAAK0B,mBACJ;CACH,IAAI1B,QAAQ2B,KAAAA,GAAW,MAAMxC,gBAAgBuC,YAAY;CACzD,MAAME,QAAQH,EAAEhB,QAAQoB,MAAMC,MAAMA,EAAE9B,QAAQA,GAAG;CACjD,IAAI,CAAC4B,OAAO;CACZH,EAAEhB,UAAUgB,EAAEhB,QAAQsB,QAAQD,MAAMA,MAAMF,KAAK;CAC/CA,MAAMzB,OAAOhB,gBAAgBuC,YAAY,CAAC;AAC5C;AAEA,MAAMM,kBACJP,GACAQ,YACG;CACH,MAAMC,YAA8B,CAAA;CACpC,KAAK,MAAMN,SAASH,EAAEhB,SACpB,IAAIwB,QAAQL,KAAK,GAAGA,MAAM3B,QAAQ,EAAEkC,MAAM,UAAU,CAAC;MAChDD,UAAUE,KAAKR,KAAK;CAE3BH,EAAEhB,UAAUyB;AACd;AAEA,MAAMG,eAAeZ,GAAcrB,UAAiB;CAClD,MAAMK,UAAUgB,EAAEhB;CAClBgB,EAAEhB,UAAU,CAAA;CACZ,KAAK,MAAMmB,SAASnB,SAASmB,MAAMzB,OAAOC,KAAK;AACjD;AAEA,MAAMkC,aACJC,GACAd,GACAe,IACAC,UAEA,IAAIE,SAAS1C,SAASE,WAAW;CAC/B,IAAIyC;CACJ,IAAIG,WAAW;CACf,IAAIC,UAAsC;CAC1C,IAAIC,SAAS;CACb,MAAMC,YAAY;EAChBC,aAAaP,KAAK;EAClBA,QAAQE,iBAAiB;GACvBC,WAAW;GACXP,GAAGY,MAAM;EACX,GAAGnE,OAAOoE,kBAAkB;CAC9B;CACAH,IAAI;CACJT,MAAMa,OAAOC,iBAAiB,eAAef,GAAGY,MAAM,CAAC;CACvDZ,GAAGgB,gBAAgB,CAAC;CACpB,MAAMC,cAAcC,QAAQV,OAAO,KAAKC,UAAUR,MAAMa,OAAOK;CAC/D,MAAMC,QAAQxD,UAAmB;EAC/B+C,aAAaP,KAAK;EAClBzC,OAAOC,iBAAiBC,QAAQD,wBAAQ,IAAIC,MAAM,UAAU,CAAC;EAC7DmC,GAAGY,MAAM;CACX;CACA,MAAMS,cAAcC,QAA2B;EAC7C,MAAMC,OAAOlF,mBAAmBK,eAAeqD,GAAGuB,GAAG,CAAC;EACtD,IAAIA,IAAIE,QAAQrC,KAAAA,GAAW;GACzB,MAAMqC,MAAMF,IAAIE;GAChBhC,eAAeP,IAAIG,UAAUA,MAAM5B,OAAOgE,GAAG;EAC/C;EACA,IAAIF,IAAIG,QAAQtC,KAAAA,GAAW;GACzB,MAAMuC,OAAO,IAAIC,IAAIL,IAAIG,IAAIG,KAAKC,MAAMA,EAAErE,GAAG,CAAC;GAC9CgC,eAAeP,IAAIG,UAAUsC,KAAKI,IAAI1C,MAAM5B,GAAG,CAAC;EAClD;EACA,IAAI+D,MAAM;GACRf,UAAUe;GACVvB,GAAGY,MAAM;EACX;CACF;CACAZ,GAAG+B,aAAaC,UAAU;EACxB,IAAIf,MAAM,GAAG;EACbP,IAAI;EACJ,IAAIuB;EAGJ,IAAI;GACFA,QAAQE,KAAKC,MAAMC,OAAOL,MAAMM,IAAI,CAAC;EACvC,QAAQ;GACNlB,qBAAK,IAAIvD,MAAM,UAAU,CAAC;GAC1B;EACF;EACA,IAAI;GACF,IAAIoE,MAAMC,eAAe/C,KAAAA,GACvBH,iBAAiBC,GAAGgD,MAAMC,UAAU;QAEpCb,WAAWY,KAAK;EAEpB,SAASrE,OAAO;GACdwD,KAAKxD,KAAK;EACZ;CACF;CACAoC,GAAGuC,gBAAgB;EACjB9B,SAAS;EACTE,aAAaP,KAAK;EAClB,IAAInB,EAAElB,WAAWiC,IAAIH,YAAYZ,mBAAG,IAAIpB,MAAM,SAAS,CAAC;EACxD,IAAI2C,SAAS/C,QAAQ+C,OAAO;OACvB7C,uBAAO,IAAIE,MAAM0C,WAAW,YAAY,SAAS,CAAC;CACzD;AACF,CAAC;AAEH,MAAMiC,gBAAgB,OACpBzC,GACAd,GACAZ,SACA4B,UACiC;CACjC,IAAID,KAAuB;CAC3B,IAAI;EACF,MAAMxB,MAAM,MAAMJ,UAAUC,SAAS0B,EAAE0C,IAAIlE,QAAQ;EACnD0B,MAAMa,OAAO4B,eAAe;EAC5B,MAAM,EAAEC,WAAWC,cAAcvE;EACjC,MAAMwE,KAAKD,aAAaE,WAAW9E;EACnC,IAAI,CAAC6E,IAAI,MAAM,IAAIhF,MAAM,6BAA6B;EAOtDmC,KAAK,IAAI6C,GAAGrE,KAAK,CAACL,gBAAgB,GALhCwE,cAAcxD,KAAAA,IACV,CAAA,IACA,OAAOwD,cAAc,WACnB,CAACA,SAAS,IACVA,SACkC,CAAC;EAC3C1D,EAAElB,SAASiC;EACX,OAAO,MAAMF,UAAUC,GAAGd,GAAGe,IAAIC,KAAK;CACxC,UAAU;EACR,IAAIhB,EAAElB,WAAWiC,IAAIf,EAAElB,SAAS;CAClC;AACF;AAEA,MAAMiF,eACJ/D,GACA,EAAEzB,KAAKyF,cAC4B;CACnC,MAAMjD,KAAKf,EAAElB;CACb,IAAI,CAACiC,MAAMA,GAAGkD,eAAehF,MAC3B,OAAOiC,QAAQxC,uBAAO,IAAIE,MAAM,SAAS,CAAC;CAE5C,OAAO,IAAIsC,SAAS1C,SAASE,WAAW;EACtC,MAAMyB,QAAwB;GAC5B5B;GACAC,UAAUC,WAAW;IACnBiD,aAAaP,KAAK;IAClB3C,QAAQC,MAAM;GAChB;GACAC,SAASC,UAAU;IACjB+C,aAAaP,KAAK;IAClBzC,OAAOC,KAAK;GACd;EACF;EAGA,MAAMwC,QAAQE,iBAAiB;GAC7BrB,EAAEhB,UAAUgB,EAAEhB,QAAQsB,QAAQD,MAAMA,MAAMF,KAAK;GAC/CzB,uBAAO,IAAIE,MAAM,SAAS,CAAC;GAC3BmC,GAAGY,MAAM;EACX,GAAGnE,OAAOoE,kBAAkB;EAC5B5B,EAAEhB,QAAQ2B,KAAKR,KAAK;EACpB,IAAI;GACFY,GAAGmD,KACDhB,KAAKiB,UAAU;IACbC,QAAQJ,QAAQI;IAChB,GAAG7G,aAAayG,QAAQvE,MAAM;IAC9BlB;GACF,CAAC,CACH;EACF,QAAQ;GACNmD,aAAaP,KAAK;GAClBnB,EAAEhB,UAAUgB,EAAEhB,QAAQsB,QAAQD,MAAMA,MAAMF,KAAK;GAC/CzB,uBAAO,IAAIE,MAAM,SAAS,CAAC;EAC7B;CACF,CAAC;AACH;AAEA,MAAMyF,aACJjF,SACAoE,QACe;CACf,MAAM,CAACxD,KAAK7C,gBAA2B;EAAE2B,QAAQ;EAAME,SAAS,CAAA;CAAG,EAAE;CACrE,MAAM8B,MAAInD,iBAAiB;EACzB4B,KAAKH,QAAQG;EACb+E,UAAUlF,QAAQkF;EAClBd;EACAe,cAAczD,GAAGE,UAAUuC,cAAczC,GAAGd,GAAGZ,SAAS4B,KAAK;CAC/D,CAAC;CACD,MAAM,CAACwD,QAAQrH,gBACM;EACjB,IAAIsH,aAAa;GACf,OAAO3D,IAAE2D;EACX;EACAP,OAAOQ,SAASX,YAAY/D,GAAG0E,IAAI;CACrC,EACF;CACA,OAAOF;AACT;AAEA,MAAMG,kBACJvF,YAC6BpB,mBAAmBoB,SAASiF,SAAS;AAEpE,MAAaO,cAAcvH,SAASsH,cAAc"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { StatewireClient } from "./statewire.js";
|
|
2
|
+
import { StatewirePath } from "./wire/ops.js";
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
declare namespace Statewire {
|
|
5
|
+
type Commands = Record<string, (...args: never[]) => unknown>;
|
|
6
|
+
type Command<TCommands extends Commands = Commands> = { [K in keyof TCommands]: {
|
|
7
|
+
method: K;
|
|
8
|
+
params: Parameters<TCommands[K]>;
|
|
9
|
+
}; }[keyof TCommands];
|
|
10
|
+
type CommandsProxy<TCommands extends Commands> = { [K in keyof TCommands]: (...args: Parameters<TCommands[K]>) => Promise<Awaited<ReturnType<TCommands[K]>>>; };
|
|
11
|
+
type CommandUpdate<TCommands extends Commands = Commands> = { [K in keyof TCommands]: {
|
|
12
|
+
method: K;
|
|
13
|
+
params: Parameters<TCommands[K]>;
|
|
14
|
+
seq: number;
|
|
15
|
+
status: "queued" | "posted" | "acked" | "settled";
|
|
16
|
+
result?: unknown;
|
|
17
|
+
failure?: StatewireClient.SendFailure;
|
|
18
|
+
}; }[keyof TCommands];
|
|
19
|
+
/** Push-based canonical state feed a statewire hands to the client core. */
|
|
20
|
+
type StateSource = {
|
|
21
|
+
snapshot: () => unknown;
|
|
22
|
+
/** Drain the paths changed since the last take (`[]` entry = whole state). */
|
|
23
|
+
takeChanged: () => StatewirePath[];
|
|
24
|
+
/** Synchronous, per change; the consumer owns coalescing. */
|
|
25
|
+
setOnChange: (listener: (() => void) | null) => void;
|
|
26
|
+
};
|
|
27
|
+
type Op = {
|
|
28
|
+
op: "replace";
|
|
29
|
+
path: StatewirePath;
|
|
30
|
+
value: unknown;
|
|
31
|
+
} | {
|
|
32
|
+
op: "add";
|
|
33
|
+
path: StatewirePath;
|
|
34
|
+
value: unknown;
|
|
35
|
+
} | {
|
|
36
|
+
op: "remove";
|
|
37
|
+
path: StatewirePath;
|
|
38
|
+
} | {
|
|
39
|
+
op: "event";
|
|
40
|
+
value: object;
|
|
41
|
+
};
|
|
42
|
+
type Changes = {
|
|
43
|
+
isChangedAt(path: readonly (string | number)[]): boolean;
|
|
44
|
+
getChangedKeys(path: readonly (string | number)[]): string[];
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
interface Statewire<TState = any, TCommands extends Statewire.Commands = Statewire.Commands, TEvent = any> {
|
|
48
|
+
state: TState;
|
|
49
|
+
commands: Statewire.CommandsProxy<TCommands>;
|
|
50
|
+
emit?: (event: TEvent) => void;
|
|
51
|
+
connection?: StatewireClient.Connection;
|
|
52
|
+
source?: Statewire.StateSource;
|
|
53
|
+
}
|
|
54
|
+
declare class StatewireSendError extends Error {
|
|
55
|
+
readonly failure: StatewireClient.SendFailure;
|
|
56
|
+
constructor(failure: StatewireClient.SendFailure);
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { Statewire, StatewireSendError };
|
|
60
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;kBAGiB;OACH,WAAW,mBAAmB;OAE9B,QAAQ,kBAAkB,WAAW,eAC9C,WAAW;IAAc,QAAQ;IAAG,QAAQ,WAAW,UAAU;aAC5D;OAEI,cAAc,kBAAkB,eACzC,WAAW,gBACP,MAAM,WAAW,UAAU,QAC3B,QAAQ,QAAQ,WAAW,UAAU;OAGhC,cAAc,kBAAkB,WAAW,eACpD,WAAW;IACV,QAAQ;IACR,QAAQ,WAAW,UAAU;IAC7B;IACA;IACA;IACA,UAAU,gBAAgB;aAEtB;;OAGI;IACV;;IAEA,mBAAmB;;IAEnB,cAAc;;OAGJ;IACN;IAAe,MAAM;IAAe;;IACpC;IAAW,MAAM;IAAe;;IAChC;IAAc,MAAM;;IACpB;IAAa;;OAEP;IACV,YAAY;IACZ,eAAe;;;UAIF,UACf,cACA,kBAAkB,UAAU,WAAW,UAAU,UACjD;EAEA,OAAO;EACP,UAAU,UAAU,cAAc;EAClC,QAAQ,OAAO;EACf,aAAa,gBAAgB;EAC7B,SAAS,UAAU;;cAsBR,2BAA2B;WAC7B,SAAS,gBAAgB;EAEtB,YAAA,SAAS,gBAAgB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/types.ts
|
|
2
|
+
const describe = (failure) => {
|
|
3
|
+
switch (failure.fate) {
|
|
4
|
+
case "rejected": return `statewire: command rejected: ${failure.message}`;
|
|
5
|
+
case "unknown-command": return "statewire: unknown command";
|
|
6
|
+
case "invalid-params": return `statewire: invalid params: ${failure.message}`;
|
|
7
|
+
case "aborted": return `statewire: command aborted (${failure.cause})`;
|
|
8
|
+
case "lost": return `statewire: command lost (${failure.cause})`;
|
|
9
|
+
case "crashed": return "statewire: command crashed";
|
|
10
|
+
case "result-unavailable": return `statewire: command result unavailable (${failure.cause})`;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
var StatewireSendError = class extends Error {
|
|
14
|
+
failure;
|
|
15
|
+
constructor(failure) {
|
|
16
|
+
super(describe(failure));
|
|
17
|
+
this.name = "StatewireSendError";
|
|
18
|
+
this.failure = failure;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
//#endregion
|
|
22
|
+
export { StatewireSendError };
|
|
23
|
+
|
|
24
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","names":["StatewireClient","StatewirePath","Statewire","Commands","Record","args","Command","TCommands","method","K","params","Parameters","CommandsProxy","Promise","Awaited","ReturnType","CommandUpdate","seq","status","result","failure","SendFailure","StateSource","snapshot","takeChanged","setOnChange","listener","Op","op","path","value","Changes","isChangedAt","getChangedKeys","state","TState","commands","emit","event","TEvent","connection","Connection","source","describe","fate","message","cause","StatewireSendError","Error","constructor","name"],"sources":["../src/types.ts"],"sourcesContent":["import type { StatewireClient } from \"./statewire\";\nimport type { StatewirePath } from \"./wire/ops\";\n\nexport namespace Statewire {\n export type Commands = Record<string, (...args: never[]) => unknown>;\n\n export type Command<TCommands extends Commands = Commands> = {\n [K in keyof TCommands]: { method: K; params: Parameters<TCommands[K]> };\n }[keyof TCommands];\n\n export type CommandsProxy<TCommands extends Commands> = {\n [K in keyof TCommands]: (\n ...args: Parameters<TCommands[K]>\n ) => Promise<Awaited<ReturnType<TCommands[K]>>>;\n };\n\n export type CommandUpdate<TCommands extends Commands = Commands> = {\n [K in keyof TCommands]: {\n method: K;\n params: Parameters<TCommands[K]>;\n seq: number;\n status: \"queued\" | \"posted\" | \"acked\" | \"settled\";\n result?: unknown;\n failure?: StatewireClient.SendFailure;\n };\n }[keyof TCommands];\n\n /** Push-based canonical state feed a statewire hands to the client core. */\n export type StateSource = {\n snapshot: () => unknown;\n /** Drain the paths changed since the last take (`[]` entry = whole state). */\n takeChanged: () => StatewirePath[];\n /** Synchronous, per change; the consumer owns coalescing. */\n setOnChange: (listener: (() => void) | null) => void;\n };\n\n export type Op =\n | { op: \"replace\"; path: StatewirePath; value: unknown }\n | { op: \"add\"; path: StatewirePath; value: unknown }\n | { op: \"remove\"; path: StatewirePath }\n | { op: \"event\"; value: object };\n\n export type Changes = {\n isChangedAt(path: readonly (string | number)[]): boolean;\n getChangedKeys(path: readonly (string | number)[]): string[];\n };\n}\n\nexport interface Statewire<\n TState = any,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n> {\n state: TState;\n commands: Statewire.CommandsProxy<TCommands>;\n emit?: (event: TEvent) => void;\n connection?: StatewireClient.Connection;\n source?: Statewire.StateSource;\n}\n\nconst describe = (failure: StatewireClient.SendFailure): string => {\n switch (failure.fate) {\n case \"rejected\":\n return `statewire: command rejected: ${failure.message}`;\n case \"unknown-command\":\n return \"statewire: unknown command\";\n case \"invalid-params\":\n return `statewire: invalid params: ${failure.message}`;\n case \"aborted\":\n return `statewire: command aborted (${failure.cause})`;\n case \"lost\":\n return `statewire: command lost (${failure.cause})`;\n case \"crashed\":\n return \"statewire: command crashed\";\n case \"result-unavailable\":\n return `statewire: command result unavailable (${failure.cause})`;\n }\n};\n\nexport class StatewireSendError extends Error {\n readonly failure: StatewireClient.SendFailure;\n\n constructor(failure: StatewireClient.SendFailure) {\n super(describe(failure));\n this.name = \"StatewireSendError\";\n this.failure = failure;\n }\n}\n"],"mappings":";AA4DA,MAAM2C,YAAYvB,YAAiD;CACjE,QAAQA,QAAQwB,MAAhB;EACE,KAAK,YACH,OAAO,gCAAgCxB,QAAQyB;EACjD,KAAK,mBACH,OAAO;EACT,KAAK,kBACH,OAAO,8BAA8BzB,QAAQyB;EAC/C,KAAK,WACH,OAAO,+BAA+BzB,QAAQ0B,MAAK;EACrD,KAAK,QACH,OAAO,4BAA4B1B,QAAQ0B,MAAK;EAClD,KAAK,WACH,OAAO;EACT,KAAK,sBACH,OAAO,0CAA0C1B,QAAQ0B,MAAK;CAClE;AACF;AAEA,IAAaC,qBAAb,cAAwCC,MAAM;CAC5C;CAEAC,YAAY7B,SAAsC;EAChD,MAAMuB,SAASvB,OAAO,CAAC;EACvB,KAAK8B,OAAO;EACZ,KAAK9B,UAAUA;CACjB;AACF"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
//#region src/util/draftTraps.d.ts
|
|
2
|
+
type SharedTraps = Pick<ProxyHandler<object>, "defineProperty" | "setPrototypeOf" | "preventExtensions" | "ownKeys" | "getOwnPropertyDescriptor">;
|
|
3
|
+
declare const sharedDraftTraps: (current: () => object, context: string) => SharedTraps;
|
|
4
|
+
//#endregion
|
|
5
|
+
export { sharedDraftTraps };
|
|
6
|
+
//# sourceMappingURL=draftTraps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draftTraps.d.ts","names":[],"sources":["../../src/util/draftTraps.ts"],"mappings":";KAAK,cAAc,KACjB;cAQW,mBACX,uBACA,oBACC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/util/draftTraps.ts
|
|
2
|
+
const sharedDraftTraps = (current, context) => {
|
|
3
|
+
const unsupported = (op) => {
|
|
4
|
+
throw new Error(`statewire: ${op} is not supported ${context}`);
|
|
5
|
+
};
|
|
6
|
+
return {
|
|
7
|
+
defineProperty: () => unsupported("defineProperty"),
|
|
8
|
+
setPrototypeOf: () => unsupported("setPrototypeOf"),
|
|
9
|
+
preventExtensions: () => unsupported("preventExtensions"),
|
|
10
|
+
ownKeys: () => Reflect.ownKeys(current()),
|
|
11
|
+
getOwnPropertyDescriptor: (_target, prop) => {
|
|
12
|
+
const node = current();
|
|
13
|
+
const desc = Reflect.getOwnPropertyDescriptor(node, prop);
|
|
14
|
+
if (!desc) return void 0;
|
|
15
|
+
return {
|
|
16
|
+
...desc,
|
|
17
|
+
configurable: !Array.isArray(node) || prop !== "length"
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { sharedDraftTraps };
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=draftTraps.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draftTraps.js","names":["SharedTraps","Pick","ProxyHandler","sharedDraftTraps","current","context","unsupported","op","Error","defineProperty","setPrototypeOf","preventExtensions","ownKeys","Reflect","getOwnPropertyDescriptor","_target","prop","node","desc","undefined","configurable","Array","isArray"],"sources":["../../src/util/draftTraps.ts"],"sourcesContent":["type SharedTraps = Pick<\n ProxyHandler<object>,\n | \"defineProperty\"\n | \"setPrototypeOf\"\n | \"preventExtensions\"\n | \"ownKeys\"\n | \"getOwnPropertyDescriptor\"\n>;\n\nexport const sharedDraftTraps = (\n current: () => object,\n context: string,\n): SharedTraps => {\n const unsupported = (op: string): never => {\n throw new Error(`statewire: ${op} is not supported ${context}`);\n };\n return {\n defineProperty: () => unsupported(\"defineProperty\"),\n setPrototypeOf: () => unsupported(\"setPrototypeOf\"),\n preventExtensions: () => unsupported(\"preventExtensions\"),\n ownKeys: () => Reflect.ownKeys(current()),\n getOwnPropertyDescriptor: (_target, prop) => {\n const node = current();\n const desc = Reflect.getOwnPropertyDescriptor(node, prop);\n if (!desc) return undefined;\n return {\n ...desc,\n configurable: !Array.isArray(node) || prop !== \"length\",\n };\n },\n };\n};\n"],"mappings":";AASA,MAAaG,oBACXC,SACAC,YACgB;CAChB,MAAMC,eAAeC,OAAsB;EACzC,MAAM,IAAIC,MAAM,cAAcD,GAAE,oBAAqBF,SAAS;CAChE;CACA,OAAO;EACLI,sBAAsBH,YAAY,gBAAgB;EAClDI,sBAAsBJ,YAAY,gBAAgB;EAClDK,yBAAyBL,YAAY,mBAAmB;EACxDM,eAAeC,QAAQD,QAAQR,QAAQ,CAAC;EACxCU,2BAA2BC,SAASC,SAAS;GAC3C,MAAMC,OAAOb,QAAQ;GACrB,MAAMc,OAAOL,QAAQC,yBAAyBG,MAAMD,IAAI;GACxD,IAAI,CAACE,MAAM,OAAOC,KAAAA;GAClB,OAAO;IACL,GAAGD;IACHE,cAAc,CAACC,MAAMC,QAAQL,IAAI,KAAKD,SAAS;GACjD;EACF;CACF;AACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"macrotask.d.ts","names":[],"sources":["../../src/util/macrotask.ts"],"mappings":";cAIa,wBAAyB"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/util/macrotask.ts
|
|
2
|
+
const createMacrotaskPoster = (callback) => {
|
|
3
|
+
if (typeof MessageChannel !== "undefined") {
|
|
4
|
+
let port1 = null;
|
|
5
|
+
let port2;
|
|
6
|
+
return () => {
|
|
7
|
+
if (!port1) {
|
|
8
|
+
const channel = new MessageChannel();
|
|
9
|
+
channel.port1.onmessage = () => {
|
|
10
|
+
port1?.unref?.();
|
|
11
|
+
callback();
|
|
12
|
+
};
|
|
13
|
+
port1 = channel.port1;
|
|
14
|
+
port2 = channel.port2;
|
|
15
|
+
}
|
|
16
|
+
port1.ref?.();
|
|
17
|
+
port2.postMessage(null);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return () => setTimeout(callback, 0);
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
23
|
+
export { createMacrotaskPoster };
|
|
24
|
+
|
|
25
|
+
//# sourceMappingURL=macrotask.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"macrotask.js","names":["createMacrotaskPoster","callback","MessageChannel","port1","MessagePort","ref","unref","port2","channel","onmessage","postMessage","setTimeout"],"sources":["../../src/util/macrotask.ts"],"sourcesContent":["// MessageChannel schedules the flush as a macrotask (tap's pattern): it runs\n// after the whole microtask drain, so every producer in the current turn\n// coalesces. The port is ref'd only while a flush is pending — an active\n// MessagePort holds the Node event loop open. ref/unref are Node-only.\nexport const createMacrotaskPoster = (callback: () => void): (() => void) => {\n if (typeof MessageChannel !== \"undefined\") {\n let port1: (MessagePort & { ref?: () => void; unref?: () => void }) | null =\n null;\n let port2: MessagePort;\n return () => {\n if (!port1) {\n const channel = new MessageChannel();\n channel.port1.onmessage = () => {\n port1?.unref?.();\n callback();\n };\n port1 = channel.port1;\n port2 = channel.port2;\n }\n port1.ref?.();\n port2!.postMessage(null);\n };\n }\n return () => setTimeout(callback, 0);\n};\n"],"mappings":";AAIA,MAAaA,yBAAyBC,aAAuC;CAC3E,IAAI,OAAOC,mBAAmB,aAAa;EACzC,IAAIC,QACF;EACF,IAAII;EACJ,aAAa;GACX,IAAI,CAACJ,OAAO;IACV,MAAMK,UAAU,IAAIN,eAAe;IACnCM,QAAQL,MAAMM,kBAAkB;KAC9BN,OAAOG,QAAQ;KACfL,SAAS;IACX;IACAE,QAAQK,QAAQL;IAChBI,QAAQC,QAAQD;GAClB;GACAJ,MAAME,MAAM;GACZE,MAAOG,YAAY,IAAI;EACzB;CACF;CACA,aAAaC,WAAWV,UAAU,CAAC;AACrC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"methodProxy.d.ts","names":[],"sources":["../../src/util/methodProxy.ts"],"mappings":";cAAa,oBAAqB,kBAChC,WAAW,gBAAgB,sBAAsB,qBAChD"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/util/methodProxy.ts
|
|
2
|
+
const createMethodProxy = (dispatch) => {
|
|
3
|
+
const memo = /* @__PURE__ */ new Map();
|
|
4
|
+
return new Proxy({}, { get: (_target, method) => {
|
|
5
|
+
if (typeof method !== "string") return void 0;
|
|
6
|
+
let fn = memo.get(method);
|
|
7
|
+
if (fn === void 0) {
|
|
8
|
+
fn = (...params) => dispatch(method, params);
|
|
9
|
+
memo.set(method, fn);
|
|
10
|
+
}
|
|
11
|
+
return fn;
|
|
12
|
+
} });
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { createMethodProxy };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=methodProxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"methodProxy.js","names":["createMethodProxy","dispatch","method","params","Promise","T","memo","Map","Proxy","get","_target","undefined","fn","set"],"sources":["../../src/util/methodProxy.ts"],"sourcesContent":["export const createMethodProxy = <T extends object>(\n dispatch: (method: string, params: unknown[]) => Promise<unknown>,\n): T => {\n const memo = new Map<string, (...params: unknown[]) => Promise<unknown>>();\n return new Proxy({} as T, {\n get: (_target, method) => {\n if (typeof method !== \"string\") return undefined;\n let fn = memo.get(method);\n if (fn === undefined) {\n fn = (...params: unknown[]) => dispatch(method, params);\n memo.set(method, fn);\n }\n return fn;\n },\n });\n};\n"],"mappings":";AAAA,MAAaA,qBACXC,aACM;CACN,MAAMK,uBAAO,IAAIC,IAAwD;CACzE,OAAO,IAAIC,MAAM,CAAC,GAAQ,EACxBC,MAAMC,SAASR,WAAW;EACxB,IAAI,OAAOA,WAAW,UAAU,OAAOS,KAAAA;EACvC,IAAIC,KAAKN,KAAKG,IAAIP,MAAM;EACxB,IAAIU,OAAOD,KAAAA,GAAW;GACpBC,MAAM,GAAGT,WAAsBF,SAASC,QAAQC,MAAM;GACtDG,KAAKO,IAAIX,QAAQU,EAAE;EACrB;EACA,OAAOA;CACT,EACF,CAAC;AACH"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"randomId.d.ts","names":[],"sources":["../../src/util/randomId.ts"],"mappings":";cAAa"}
|