statewire 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/core.d.ts.map +1 -1
- package/dist/client/core.js +8 -1
- package/dist/client/core.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/statewire.d.ts +7 -0
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js.map +1 -1
- package/dist/transports/testing.d.ts +1 -2
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +1 -3
- package/dist/transports/testing.js.map +1 -1
- package/dist/util/recordMutations.d.ts +3 -1
- package/dist/util/recordMutations.d.ts.map +1 -1
- package/dist/util/recordMutations.js +3 -1
- package/dist/util/recordMutations.js.map +1 -1
- package/package.json +1 -1
- package/src/client/core.ts +15 -1
- package/src/index.ts +1 -0
- package/src/statewire.ts +8 -0
- package/src/transports/testing.ts +1 -6
- package/src/util/recordMutations.ts +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","names":[],"sources":["../../src/client/core.ts"],"mappings":";;;cAYa,iBAAiB,gBAAgB;
|
|
1
|
+
{"version":3,"file":"core.d.ts","names":[],"sources":["../../src/client/core.ts"],"mappings":";;;cAYa,iBAAiB,gBAAgB;cA4SjC,mBACX,kBAAkB,gBAAgB,wBAClC,OAAO;EAjFe,SAAA,MAAA;;;;EAsEE,YAAA"}
|
package/dist/client/core.js
CHANGED
|
@@ -63,7 +63,7 @@ const createCore = (getOptions) => {
|
|
|
63
63
|
drainCanonical();
|
|
64
64
|
if (canonical === prevCanonical && !overlaysDirty) return;
|
|
65
65
|
overlaysDirty = false;
|
|
66
|
-
const { optimistic } = getOptions();
|
|
66
|
+
const { optimistic, materialize } = getOptions();
|
|
67
67
|
let next = canonical;
|
|
68
68
|
let paths = [];
|
|
69
69
|
if (optimistic && canonical !== void 0) for (const overlay of overlays) {
|
|
@@ -86,6 +86,13 @@ const createCore = (getOptions) => {
|
|
|
86
86
|
console.error(`statewire: optimistic reducer threw for "${inv.method}"; dropping its optimistic update`, error);
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
if (materialize && next !== void 0) try {
|
|
90
|
+
const recorded = recordMutations(next, (draft) => materialize(draft));
|
|
91
|
+
next = recorded.state;
|
|
92
|
+
paths = [...paths, ...recorded.changedPaths];
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error("statewire: materialize threw; publishing without its update", error);
|
|
95
|
+
}
|
|
89
96
|
published = next;
|
|
90
97
|
overlayPaths = paths;
|
|
91
98
|
};
|
package/dist/client/core.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.js","names":["useEffect","useState","createChanges","getCommandsHost","getEmitHost","getStateHost","CommandsInvocation","CommandsListener","recordMutations","createMacrotaskPoster","createMethodProxy","StatewirePath","StatewireSendError","Statewire","StatewireClient","LIVE_CONNECTION","Connection","status","reconnect","failureOf","error","SendFailure","failure","fate","cause","message","Error","String","Overlay","inv","dropped","createCore","getOptions","Options","value","source","StateSource","overlays","updates","CommandUpdate","events","pendingChanged","canonical","published","overlayPaths","notifiedPublished","notifiedOverlayPaths","notifiedConnection","stale","overlaysDirty","flushing","listeners","Set","post","scheduled","flush","schedule","markStale","drainCanonical","push","takeChanged","snapshot","state","recompute","prevCanonical","optimistic","next","paths","undefined","overlay","ctx","OptimisticContext","ts","seq","clientId","recorded","draft","method","params","Command","changedPaths","console","getConnection","connection","options","stateChanged","prev","changed","onStateChange","changes","length","batch","update","onCommandChange","event","onEvent","connectionChanged","onConnectionChange","listener","note","extra","result","commandsListener","onStart","onPosted","onAck","onCovered","some","o","filter","onSettle","outcome","type","payload","eventSink","attach","nextSource","setOnChange","commands","setListener","emit","setSink","detach","CommandsProxy","Promise","reject","send","Record","args","promise","getState","subscribe","add","delete","useStatewireCore","$","_c","t0","core","t1","t2","t3"],"sources":["../../src/client/core.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { createChanges } from \"./changes\";\nimport { getCommandsHost, getEmitHost, getStateHost } from \"../host/host\";\nimport type { CommandsInvocation, CommandsListener } from \"../host/host\";\nimport { recordMutations } from \"../util/recordMutations\";\nimport { createMacrotaskPoster } from \"../util/macrotask\";\nimport { createMethodProxy } from \"../util/methodProxy\";\nimport type { StatewirePath } from \"../wire/ops\";\nimport { StatewireSendError } from \"../types\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireClient } from \"../statewire\";\n\nexport const LIVE_CONNECTION: StatewireClient.Connection = {\n status: \"live\",\n reconnect: () => {},\n};\n\nconst failureOf = (error: unknown): StatewireClient.SendFailure =>\n error instanceof StatewireSendError\n ? error.failure\n : {\n fate: \"rejected\",\n cause: \"server\",\n message: error instanceof Error ? error.message : String(error),\n };\n\ntype Overlay = { inv: CommandsInvocation; dropped: boolean };\n\nconst createCore = (\n getOptions: () => StatewireClient.Options<any, any, any>,\n) => {\n let value: Statewire<unknown> | null = null;\n let source: Statewire.StateSource | null = null;\n let overlays: Overlay[] = [];\n let updates: Statewire.CommandUpdate[] = [];\n let events: unknown[] = [];\n let pendingChanged: StatewirePath[] = [];\n let canonical: unknown;\n let published: unknown;\n let overlayPaths: readonly StatewirePath[] = [];\n let notifiedPublished: unknown;\n let notifiedOverlayPaths: readonly StatewirePath[] = [];\n let notifiedConnection: StatewireClient.Connection | null = null;\n let stale = false;\n let overlaysDirty = false;\n let flushing = false;\n const listeners = new Set<() => void>();\n\n const post = createMacrotaskPoster(() => {\n scheduled = false;\n flush();\n });\n let scheduled = false;\n const schedule = () => {\n if (scheduled) return;\n scheduled = true;\n post();\n };\n\n const markStale = () => {\n stale = true;\n schedule();\n };\n\n const drainCanonical = () => {\n if (source) {\n pendingChanged.push(...source.takeChanged());\n canonical = source.snapshot();\n } else if (value && value.state !== canonical) {\n pendingChanged.push([]);\n canonical = value.state;\n }\n };\n\n const recompute = () => {\n if (!stale || !value) return;\n stale = false;\n const prevCanonical = canonical;\n drainCanonical();\n if (canonical === prevCanonical && !overlaysDirty) return;\n overlaysDirty = false;\n const { optimistic } = getOptions();\n let next = canonical;\n let paths: readonly StatewirePath[] = [];\n if (optimistic && canonical !== undefined) {\n for (const overlay of overlays) {\n if (overlay.dropped) continue;\n const { inv } = overlay;\n const ctx: StatewireClient.OptimisticContext = {\n ts: inv.ts,\n seq: inv.seq,\n ...(inv.clientId !== undefined && { clientId: inv.clientId }),\n };\n try {\n const recorded = recordMutations(next, (draft) =>\n optimistic(\n draft as never,\n { method: inv.method, params: inv.params } as Statewire.Command,\n ctx,\n ),\n );\n next = recorded.state;\n paths = [...paths, ...recorded.changedPaths];\n } catch (error) {\n overlay.dropped = true;\n console.error(\n `statewire: optimistic reducer threw for \"${inv.method}\"; dropping its optimistic update`,\n error,\n );\n }\n }\n }\n published = next;\n overlayPaths = paths;\n };\n\n const getConnection = (): StatewireClient.Connection =>\n value?.connection ?? LIVE_CONNECTION;\n\n const flush = () => {\n if (!value) return;\n if (flushing) {\n schedule();\n return;\n }\n flushing = true;\n try {\n recompute();\n const options = getOptions();\n const stateChanged = published !== notifiedPublished;\n if (stateChanged) {\n const prev = notifiedPublished;\n const changed = [\n ...pendingChanged,\n ...notifiedOverlayPaths,\n ...overlayPaths,\n ];\n pendingChanged = [];\n notifiedPublished = published;\n notifiedOverlayPaths = overlayPaths;\n options.onStateChange?.(published as never, {\n changes: createChanges(prev, published, changed),\n });\n }\n if (updates.length > 0 && published !== undefined) {\n const batch = updates;\n updates = [];\n for (const update of batch) {\n options.onCommandChange?.(update, { state: published as never });\n }\n }\n if (events.length > 0 && published !== undefined) {\n const batch = events;\n events = [];\n for (const event of batch) {\n options.onEvent?.(event, { state: published as never });\n }\n }\n const connection = getConnection();\n const connectionChanged = connection !== notifiedConnection;\n notifiedConnection = connection;\n if (connectionChanged) options.onConnectionChange?.(connection);\n if (stateChanged || connectionChanged) {\n for (const listener of [...listeners]) listener();\n }\n } finally {\n flushing = false;\n }\n };\n\n const note = (\n inv: CommandsInvocation,\n status: Statewire.CommandUpdate[\"status\"],\n extra?: { result?: unknown; failure?: StatewireClient.SendFailure },\n ) => {\n if (!getOptions().onCommandChange) return;\n updates.push({\n method: inv.method,\n params: inv.params as never[],\n seq: inv.seq,\n status,\n ...extra,\n });\n schedule();\n };\n\n const commandsListener: CommandsListener = {\n onStart: (inv) => {\n overlays.push({ inv, dropped: false });\n overlaysDirty = true;\n markStale();\n note(inv, \"queued\");\n },\n onPosted: (inv) => note(inv, \"posted\"),\n onAck: (inv) => note(inv, \"acked\"),\n onCovered: (inv) => {\n if (!overlays.some((o) => o.inv === inv)) return;\n overlays = overlays.filter((o) => o.inv !== inv);\n overlaysDirty = true;\n markStale();\n },\n onSettle: (inv, outcome) => {\n note(\n inv,\n \"settled\",\n outcome.type === \"accepted\"\n ? { result: outcome.payload }\n : { failure: failureOf(outcome.error) },\n );\n flush();\n },\n };\n\n const eventSink = (event: unknown) => {\n if (!getOptions().onEvent) return;\n events.push(event);\n schedule();\n };\n\n const attach = (next: Statewire<unknown>) => {\n if (next === value) return;\n const prev = value;\n value = next;\n const nextSource =\n next.source ??\n (typeof next.state === \"object\" && next.state !== null\n ? getStateHost(next.state)\n : undefined) ??\n null;\n if (nextSource !== source) {\n source?.setOnChange(null);\n source = nextSource;\n source?.setOnChange(markStale);\n markStale();\n }\n if (next.commands !== prev?.commands) {\n if (prev) getCommandsHost(prev.commands)?.setListener(null);\n getCommandsHost(next.commands)?.setListener(commandsListener);\n if (overlays.length > 0) {\n overlays = [];\n overlaysDirty = true;\n }\n markStale();\n }\n if (next.emit !== prev?.emit) {\n if (prev) getEmitHost(prev.emit)?.setSink(null);\n getEmitHost(next.emit)?.setSink(eventSink);\n }\n if (!source) markStale();\n schedule();\n };\n\n const detach = () => {\n source?.setOnChange(null);\n source = null;\n if (value) {\n getCommandsHost(value.commands)?.setListener(null);\n getEmitHost(value.emit)?.setSink(null);\n value = null;\n }\n };\n\n const commands = createMethodProxy<Statewire.CommandsProxy<any>>(\n (method, params) => {\n if (!value) {\n return Promise.reject(\n new StatewireSendError({ fate: \"aborted\", cause: \"unmount\" }),\n );\n }\n const send = (\n value.commands as Record<\n string,\n (...args: unknown[]) => Promise<unknown>\n >\n )[method]!;\n const promise = send(...params);\n flush();\n return promise;\n },\n );\n\n return {\n attach,\n detach,\n getState: () => {\n recompute();\n return published;\n },\n commands,\n subscribe: (listener: () => void) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n};\n\nexport const useStatewireCore = (\n getOptions: () => StatewireClient.Options<any, any, any>,\n value: Statewire<unknown>,\n) => {\n const [core] = useState(() => createCore(getOptions));\n // Attach during render so the first snapshot reads the transport's state;\n // re-attaching the same transport is a no-op and the single-slot host\n // listeners make a swap land on the last writer.\n core.attach(value);\n useEffect(() => {\n core.attach(value);\n });\n useEffect(() => () => core.detach(), [core]);\n return core;\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"core.js","names":["useEffect","useState","createChanges","getCommandsHost","getEmitHost","getStateHost","CommandsInvocation","CommandsListener","recordMutations","createMacrotaskPoster","createMethodProxy","StatewirePath","StatewireSendError","Statewire","StatewireClient","LIVE_CONNECTION","Connection","status","reconnect","failureOf","error","SendFailure","failure","fate","cause","message","Error","String","Overlay","inv","dropped","createCore","getOptions","Options","value","source","StateSource","overlays","updates","CommandUpdate","events","pendingChanged","canonical","published","overlayPaths","notifiedPublished","notifiedOverlayPaths","notifiedConnection","stale","overlaysDirty","flushing","listeners","Set","post","scheduled","flush","schedule","markStale","drainCanonical","push","takeChanged","snapshot","state","recompute","prevCanonical","optimistic","materialize","next","paths","undefined","overlay","ctx","OptimisticContext","ts","seq","clientId","recorded","draft","method","params","Command","changedPaths","console","getConnection","connection","options","stateChanged","prev","changed","onStateChange","changes","length","batch","update","onCommandChange","event","onEvent","connectionChanged","onConnectionChange","listener","note","extra","result","commandsListener","onStart","onPosted","onAck","onCovered","some","o","filter","onSettle","outcome","type","payload","eventSink","attach","nextSource","setOnChange","commands","setListener","emit","setSink","detach","CommandsProxy","Promise","reject","send","Record","args","promise","getState","subscribe","add","delete","useStatewireCore","$","_c","t0","core","t1","t2","t3"],"sources":["../../src/client/core.ts"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { createChanges } from \"./changes\";\nimport { getCommandsHost, getEmitHost, getStateHost } from \"../host/host\";\nimport type { CommandsInvocation, CommandsListener } from \"../host/host\";\nimport { recordMutations } from \"../util/recordMutations\";\nimport { createMacrotaskPoster } from \"../util/macrotask\";\nimport { createMethodProxy } from \"../util/methodProxy\";\nimport type { StatewirePath } from \"../wire/ops\";\nimport { StatewireSendError } from \"../types\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireClient } from \"../statewire\";\n\nexport const LIVE_CONNECTION: StatewireClient.Connection = {\n status: \"live\",\n reconnect: () => {},\n};\n\nconst failureOf = (error: unknown): StatewireClient.SendFailure =>\n error instanceof StatewireSendError\n ? error.failure\n : {\n fate: \"rejected\",\n cause: \"server\",\n message: error instanceof Error ? error.message : String(error),\n };\n\ntype Overlay = { inv: CommandsInvocation; dropped: boolean };\n\nconst createCore = (\n getOptions: () => StatewireClient.Options<any, any, any>,\n) => {\n let value: Statewire<unknown> | null = null;\n let source: Statewire.StateSource | null = null;\n let overlays: Overlay[] = [];\n let updates: Statewire.CommandUpdate[] = [];\n let events: unknown[] = [];\n let pendingChanged: StatewirePath[] = [];\n let canonical: unknown;\n let published: unknown;\n let overlayPaths: readonly StatewirePath[] = [];\n let notifiedPublished: unknown;\n let notifiedOverlayPaths: readonly StatewirePath[] = [];\n let notifiedConnection: StatewireClient.Connection | null = null;\n let stale = false;\n let overlaysDirty = false;\n let flushing = false;\n const listeners = new Set<() => void>();\n\n const post = createMacrotaskPoster(() => {\n scheduled = false;\n flush();\n });\n let scheduled = false;\n const schedule = () => {\n if (scheduled) return;\n scheduled = true;\n post();\n };\n\n const markStale = () => {\n stale = true;\n schedule();\n };\n\n const drainCanonical = () => {\n if (source) {\n pendingChanged.push(...source.takeChanged());\n canonical = source.snapshot();\n } else if (value && value.state !== canonical) {\n pendingChanged.push([]);\n canonical = value.state;\n }\n };\n\n const recompute = () => {\n if (!stale || !value) return;\n stale = false;\n const prevCanonical = canonical;\n drainCanonical();\n if (canonical === prevCanonical && !overlaysDirty) return;\n overlaysDirty = false;\n const { optimistic, materialize } = getOptions();\n let next = canonical;\n let paths: readonly StatewirePath[] = [];\n if (optimistic && canonical !== undefined) {\n for (const overlay of overlays) {\n if (overlay.dropped) continue;\n const { inv } = overlay;\n const ctx: StatewireClient.OptimisticContext = {\n ts: inv.ts,\n seq: inv.seq,\n ...(inv.clientId !== undefined && { clientId: inv.clientId }),\n };\n try {\n const recorded = recordMutations(next, (draft) =>\n optimistic(\n draft as never,\n { method: inv.method, params: inv.params } as Statewire.Command,\n ctx,\n ),\n );\n next = recorded.state;\n paths = [...paths, ...recorded.changedPaths];\n } catch (error) {\n overlay.dropped = true;\n console.error(\n `statewire: optimistic reducer threw for \"${inv.method}\"; dropping its optimistic update`,\n error,\n );\n }\n }\n }\n if (materialize && next !== undefined) {\n try {\n const recorded = recordMutations(next, (draft) =>\n materialize(draft as never),\n );\n next = recorded.state;\n paths = [...paths, ...recorded.changedPaths];\n } catch (error) {\n console.error(\n \"statewire: materialize threw; publishing without its update\",\n error,\n );\n }\n }\n published = next;\n overlayPaths = paths;\n };\n\n const getConnection = (): StatewireClient.Connection =>\n value?.connection ?? LIVE_CONNECTION;\n\n const flush = () => {\n if (!value) return;\n if (flushing) {\n schedule();\n return;\n }\n flushing = true;\n try {\n recompute();\n const options = getOptions();\n const stateChanged = published !== notifiedPublished;\n if (stateChanged) {\n const prev = notifiedPublished;\n const changed = [\n ...pendingChanged,\n ...notifiedOverlayPaths,\n ...overlayPaths,\n ];\n pendingChanged = [];\n notifiedPublished = published;\n notifiedOverlayPaths = overlayPaths;\n options.onStateChange?.(published as never, {\n changes: createChanges(prev, published, changed),\n });\n }\n if (updates.length > 0 && published !== undefined) {\n const batch = updates;\n updates = [];\n for (const update of batch) {\n options.onCommandChange?.(update, { state: published as never });\n }\n }\n if (events.length > 0 && published !== undefined) {\n const batch = events;\n events = [];\n for (const event of batch) {\n options.onEvent?.(event, { state: published as never });\n }\n }\n const connection = getConnection();\n const connectionChanged = connection !== notifiedConnection;\n notifiedConnection = connection;\n if (connectionChanged) options.onConnectionChange?.(connection);\n if (stateChanged || connectionChanged) {\n for (const listener of [...listeners]) listener();\n }\n } finally {\n flushing = false;\n }\n };\n\n const note = (\n inv: CommandsInvocation,\n status: Statewire.CommandUpdate[\"status\"],\n extra?: { result?: unknown; failure?: StatewireClient.SendFailure },\n ) => {\n if (!getOptions().onCommandChange) return;\n updates.push({\n method: inv.method,\n params: inv.params as never[],\n seq: inv.seq,\n status,\n ...extra,\n });\n schedule();\n };\n\n const commandsListener: CommandsListener = {\n onStart: (inv) => {\n overlays.push({ inv, dropped: false });\n overlaysDirty = true;\n markStale();\n note(inv, \"queued\");\n },\n onPosted: (inv) => note(inv, \"posted\"),\n onAck: (inv) => note(inv, \"acked\"),\n onCovered: (inv) => {\n if (!overlays.some((o) => o.inv === inv)) return;\n overlays = overlays.filter((o) => o.inv !== inv);\n overlaysDirty = true;\n markStale();\n },\n onSettle: (inv, outcome) => {\n note(\n inv,\n \"settled\",\n outcome.type === \"accepted\"\n ? { result: outcome.payload }\n : { failure: failureOf(outcome.error) },\n );\n flush();\n },\n };\n\n const eventSink = (event: unknown) => {\n if (!getOptions().onEvent) return;\n events.push(event);\n schedule();\n };\n\n const attach = (next: Statewire<unknown>) => {\n if (next === value) return;\n const prev = value;\n value = next;\n const nextSource =\n next.source ??\n (typeof next.state === \"object\" && next.state !== null\n ? getStateHost(next.state)\n : undefined) ??\n null;\n if (nextSource !== source) {\n source?.setOnChange(null);\n source = nextSource;\n source?.setOnChange(markStale);\n markStale();\n }\n if (next.commands !== prev?.commands) {\n if (prev) getCommandsHost(prev.commands)?.setListener(null);\n getCommandsHost(next.commands)?.setListener(commandsListener);\n if (overlays.length > 0) {\n overlays = [];\n overlaysDirty = true;\n }\n markStale();\n }\n if (next.emit !== prev?.emit) {\n if (prev) getEmitHost(prev.emit)?.setSink(null);\n getEmitHost(next.emit)?.setSink(eventSink);\n }\n if (!source) markStale();\n schedule();\n };\n\n const detach = () => {\n source?.setOnChange(null);\n source = null;\n if (value) {\n getCommandsHost(value.commands)?.setListener(null);\n getEmitHost(value.emit)?.setSink(null);\n value = null;\n }\n };\n\n const commands = createMethodProxy<Statewire.CommandsProxy<any>>(\n (method, params) => {\n if (!value) {\n return Promise.reject(\n new StatewireSendError({ fate: \"aborted\", cause: \"unmount\" }),\n );\n }\n const send = (\n value.commands as Record<\n string,\n (...args: unknown[]) => Promise<unknown>\n >\n )[method]!;\n const promise = send(...params);\n flush();\n return promise;\n },\n );\n\n return {\n attach,\n detach,\n getState: () => {\n recompute();\n return published;\n },\n commands,\n subscribe: (listener: () => void) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n};\n\nexport const useStatewireCore = (\n getOptions: () => StatewireClient.Options<any, any, any>,\n value: Statewire<unknown>,\n) => {\n const [core] = useState(() => createCore(getOptions));\n // Attach during render so the first snapshot reads the transport's state;\n // re-attaching the same transport is a no-op and the single-slot host\n // listeners make a swap land on the last writer.\n core.attach(value);\n useEffect(() => {\n core.attach(value);\n });\n useEffect(() => () => core.detach(), [core]);\n return core;\n};\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ import { StatewireClient, StatewireResource, useStatewire } from "./statewire.js
|
|
|
2
2
|
import { Statewire, StatewireSendError } from "./types.js";
|
|
3
3
|
import { StatewireSSE } from "./transports/sse.js";
|
|
4
4
|
import { StatewireWS } from "./transports/ws.js";
|
|
5
|
-
|
|
5
|
+
import { applyOptimistic } from "./util/recordMutations.js";
|
|
6
|
+
export { type Statewire, StatewireClient, StatewireResource, StatewireSSE, StatewireSendError, StatewireWS, applyOptimistic, useStatewire };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { applyOptimistic } from "./util/recordMutations.js";
|
|
1
2
|
import { StatewireSendError } from "./types.js";
|
|
2
3
|
import { StatewireClient, StatewireResource, useStatewire } from "./statewire.js";
|
|
3
4
|
import { StatewireSSE } from "./transports/sse.js";
|
|
4
5
|
import { StatewireWS } from "./transports/ws.js";
|
|
5
|
-
export { StatewireClient, StatewireResource, StatewireSSE, StatewireSendError, StatewireWS, useStatewire };
|
|
6
|
+
export { StatewireClient, StatewireResource, StatewireSSE, StatewireSendError, StatewireWS, applyOptimistic, useStatewire };
|
package/dist/statewire.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ declare namespace StatewireClient {
|
|
|
19
19
|
type Options<TState, TCommands extends Statewire.Commands = Statewire.Commands, TEvent = any> = {
|
|
20
20
|
transport: ResourceElement<Statewire<TState, TCommands, TEvent>>;
|
|
21
21
|
optimistic?: Optimistic<NonNullable<TState>, TCommands>;
|
|
22
|
+
materialize?: Materialize<NonNullable<TState>>;
|
|
22
23
|
onEvent?: (event: TEvent, ctx: {
|
|
23
24
|
state: NonNullable<TState>;
|
|
24
25
|
}) => void;
|
|
@@ -70,6 +71,12 @@ declare namespace StatewireClient {
|
|
|
70
71
|
seq: number;
|
|
71
72
|
clientId?: string;
|
|
72
73
|
};
|
|
74
|
+
/**
|
|
75
|
+
* Mutate the draft to make server-accepted, still-uncommitted intents in
|
|
76
|
+
* the state concrete (e.g. dispatch records into messages). Runs after the
|
|
77
|
+
* optimistic folds on every published state.
|
|
78
|
+
*/
|
|
79
|
+
type Materialize<TState> = (draft: TState) => void;
|
|
73
80
|
/**
|
|
74
81
|
* fate: "aborted" = never sent, "rejected" = the handler refused,
|
|
75
82
|
* "unknown-command" = no such command, "invalid-params" = the args did not
|
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;;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
|
|
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,cAAc,YAAY,YAAY;IACtC,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;;;;;;;OAQU,YAAY,WAAW,OAAO;;;;;;;;;;OAW9B;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","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"}
|
|
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","materialize","Materialize","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 materialize?: Materialize<NonNullable<TState>>;\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 * Mutate the draft to make server-accepted, still-uncommitted intents in\n * the state concrete (e.g. dispatch records into messages). Runs after the\n * optimistic folds on every published state.\n */\n export type Materialize<TState> = (draft: TState) => void;\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"}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { Statewire } from "../types.js";
|
|
2
2
|
import { StatewireEnvelope } from "../client/connection.js";
|
|
3
|
+
import { applyOptimistic } from "../util/recordMutations.js";
|
|
3
4
|
import { SocketRecorder, commandFrame, createCounterElement, createSocketRecorder, settle } from "./fixtures.js";
|
|
4
5
|
import { ResourceElement } from "@assistant-ui/tap";
|
|
5
6
|
//#region src/transports/testing.d.ts
|
|
6
7
|
/** Let queued microtasks and op batches drain through the statewire client. */
|
|
7
8
|
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
9
|
type MockTransport<TState> = {
|
|
11
10
|
/** Pass as the `transport:` option of useStatewire / Harness. */
|
|
12
11
|
transport: ResourceElement<Statewire<TState | undefined, any, any>>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.d.ts","names":[],"sources":["../../src/transports/testing.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"testing.d.ts","names":[],"sources":["../../src/transports/testing.ts"],"mappings":";;;;;;;cAUa,YAAI;KAIL,cAAc;;EAExB,WAAW,gBAAgB,UAAU;;EAErC;IAAY;IAAiB;IAAoB;;;EAEjD,WAAW,OAAO,WAAW;;EAE7B,MAAM,gBAAgB;;EAEtB,eAAe,UAAU,sBAAsB;;EAE/C,SAAS,gBAAgB,qBAAqB;WACrC;WACA;;cAUE,sBAAuB,QAClC,cAAc,WACb,cAAc"}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { applyOptimistic } from "../util/recordMutations.js";
|
|
2
2
|
import { StatewireSSE } from "./sse.js";
|
|
3
3
|
import { openSSEStream, sseFrame } from "./fake-sse.js";
|
|
4
4
|
import { commandFrame, createCounterElement, createSocketRecorder, settle } from "./fixtures.js";
|
|
5
5
|
//#region src/transports/testing.ts
|
|
6
6
|
/** Let queued microtasks and op batches drain through the statewire client. */
|
|
7
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
8
|
/**
|
|
11
9
|
* A mock statewire backend: serves `initialState` as the attach snapshot,
|
|
12
10
|
* records command POSTs (receipt only — acks and results are delivered from
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","names":["ResourceElement","StatewireEnvelope","
|
|
1
|
+
{"version":3,"file":"testing.js","names":["ResourceElement","StatewireEnvelope","StatewireSSE","Statewire","openSSEStream","sseFrame","FakeStreamHandle","tick","Promise","r","setTimeout","applyOptimistic","MockTransport","transport","TState","commands","method","params","context","setState","state","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 { 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\nexport { applyOptimistic } from \"../util/recordMutations\";\n\nexport type MockTransport<TState> = {\n /** Pass as the `transport:` option of useStatewire / Harness. */\n transport: ResourceElement<Statewire<TState | undefined, any, any>>;\n /** Captured `/commands` POST bodies, in POST order. */\n commands: { method?: string; params?: unknown[]; context?: 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: MockTransport<unknown>[\"commands\"] = [];\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(\n String(init?.body),\n ) as MockTransport<unknown>[\"commands\"][number],\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":";;;;;;AAUA,MAAaO,aAAa,IAAIC,SAAeC,MAAMC,WAAWD,GAAG,CAAC,CAAC;;;;;;AA0BnE,IAAIqB,YAAY;AAEhB,MAAaC,uBACXC,iBAC0B;CAC1B,IAAIZ,QAAiBY;CACrB,IAAIJ,WAAW;CACf,IAAIK,WAAW;CACf,MAAMC,0BAAU,IAAIC,IAAoB;CACxC,MAAMC,0BAAU,IAAIC,IAAsB;CAC1C,MAAMtB,WAA+C,CAAA;CAErD,MAAMQ,gBAAgBC,aAAgC;EACpD,KAAK,MAAMc,UAAUF,SAASE,OAAOC,KAAKlC,SAASmB,QAAQ,CAAC;EAC5D,OAAOjB,KAAK;CACd;CAEA,MAAMiC,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,OAAOnD,cACLgC,SACAgB,KACCK,WAAW;IACVA,OAAOlB,KACLlC,SAAS;KACPqD,KAAK,CAAC;MAAEC,IAAI;MAAWC,MAAM,CAAA;MAAIC,OAAOzC;KAAM,CAAC;KAC/C0C,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;GACnDP,SAASsD,KACPC,KAAKC,MACHtB,OAAOL,MAAM4B,IAAI,CACnB,CACF;GACA,OAAO1B,SAAS2B,KAAK,CAAC,CAAC;EACzB;EACA,OAAO,IAAI3B,SAAS,MAAM,EAAE4B,QAAQ,IAAI,CAAC;CAC3C;CAEA,OAAO;EACL7D,WAAWX,aAAa;GACtB8C,KAAK,eAAe,EAAElB,UAAS;GAC/B6C,OAAOnC;EACT,CAAC;EACDzB;EACAI,WAAWyD,SAAS;GAClBxD,QAAQwD;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,MAAMvE,KAAK;EACb;EACA,IAAIqB,WAAW;GACb,OAAOA;EACT;EACA,IAAIC,WAAW;GACb,OAAOO,QAAQ2C,OAAO;EACxB;CACF;AACF"}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
//#region src/util/recordMutations.d.ts
|
|
2
2
|
type PathKey = string | number;
|
|
3
3
|
type Path = readonly PathKey[];
|
|
4
|
+
/** Run a mutator against a recording draft, as the client does for `optimistic`. */
|
|
5
|
+
declare const applyOptimistic: <TState>(state: TState, mutate: (draft: TState) => void) => TState;
|
|
4
6
|
declare const recordMutations: <T>(base: T, mutate: (draft: T) => void) => {
|
|
5
7
|
state: T;
|
|
6
8
|
changedPaths: Path[];
|
|
7
9
|
};
|
|
8
10
|
//#endregion
|
|
9
|
-
export { recordMutations };
|
|
11
|
+
export { applyOptimistic, recordMutations };
|
|
10
12
|
//# sourceMappingURL=recordMutations.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recordMutations.d.ts","names":[],"sources":["../../src/util/recordMutations.ts"],"mappings":";KAEK;KACA,gBAAgB;
|
|
1
|
+
{"version":3,"file":"recordMutations.d.ts","names":[],"sources":["../../src/util/recordMutations.ts"],"mappings":";KAEK;KACA,gBAAgB;;cAwER,kBAAmB,QAC9B,OAAO,QACP,SAAS,OAAO,oBACf;cAEU,kBAAmB,GAC9B,MAAM,GACN,SAAS,OAAO;EACb,OAAO;EAAG,cAAc"}
|
|
@@ -34,6 +34,8 @@ const finalize = (node) => {
|
|
|
34
34
|
}
|
|
35
35
|
return result;
|
|
36
36
|
};
|
|
37
|
+
/** Run a mutator against a recording draft, as the client does for `optimistic`. */
|
|
38
|
+
const applyOptimistic = (state, mutate) => recordMutations(state, mutate).state;
|
|
37
39
|
const recordMutations = (base, mutate) => {
|
|
38
40
|
if (!isDraftable(base)) throw new Error("statewire: cannot record mutations on primitive state");
|
|
39
41
|
const changedPaths = [];
|
|
@@ -104,6 +106,6 @@ const recordMutations = (base, mutate) => {
|
|
|
104
106
|
};
|
|
105
107
|
};
|
|
106
108
|
//#endregion
|
|
107
|
-
export { recordMutations };
|
|
109
|
+
export { applyOptimistic, recordMutations };
|
|
108
110
|
|
|
109
111
|
//# sourceMappingURL=recordMutations.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recordMutations.js","names":["sharedDraftTraps","PathKey","Path","DRAFT","Symbol","Node","base","Record","PropertyKey","copy","modified","isArray","parent","path","proxy","assigned","Set","isDraftable","value","Array","proto","Object","getPrototypeOf","prototype","nodeOf","undefined","latest","node","prepareCopy","slice","markModified","n","resolveAssigned","container","key","keys","child","finalize","result","has","
|
|
1
|
+
{"version":3,"file":"recordMutations.js","names":["sharedDraftTraps","PathKey","Path","DRAFT","Symbol","Node","base","Record","PropertyKey","copy","modified","isArray","parent","path","proxy","assigned","Set","isDraftable","value","Array","proto","Object","getPrototypeOf","prototype","nodeOf","undefined","latest","node","prepareCopy","slice","markModified","n","resolveAssigned","container","key","keys","child","finalize","result","has","applyOptimistic","state","TState","mutate","draft","recordMutations","T","changedPaths","Error","seen","record","join","add","push","createNode","touched","prop","Proxy","get","_target","current","Number","set","next","source","hasOwn","deleteProperty","Reflect","root"],"sources":["../../src/util/recordMutations.ts"],"sourcesContent":["import { sharedDraftTraps } from \"./draftTraps\";\n\ntype PathKey = string | number;\ntype Path = readonly PathKey[];\n\nconst DRAFT = Symbol(\"statewire.draft\");\n\ntype Node = {\n base: Record<PropertyKey, unknown>;\n copy: Record<PropertyKey, unknown> | null;\n modified: boolean;\n isArray: boolean;\n parent: Node | null;\n path: Path;\n proxy: unknown;\n assigned: Set<PropertyKey> | null;\n};\n\nconst isDraftable = (value: unknown): value is object => {\n if (typeof value !== \"object\" || value === null) return false;\n if (Array.isArray(value)) return true;\n const proto: unknown = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n};\n\nconst nodeOf = (value: unknown): Node | undefined =>\n typeof value === \"object\" && value !== null\n ? (value as { [DRAFT]?: Node })[DRAFT]\n : undefined;\n\nconst latest = (node: Node) => node.copy ?? node.base;\n\nconst prepareCopy = (node: Node) => {\n node.copy ??= node.isArray\n ? ((node.base as unknown as unknown[]).slice() as unknown as Record<\n PropertyKey,\n unknown\n >)\n : { ...node.base };\n};\n\nconst markModified = (node: Node) => {\n for (let n: Node | null = node; n && !n.modified; n = n.parent) {\n n.modified = true;\n }\n};\n\nconst resolveAssigned = (container: object) => {\n for (const key of Object.keys(container)) {\n const value = (container as Record<string, unknown>)[key];\n const child = nodeOf(value);\n if (child) {\n (container as Record<string, unknown>)[key] = finalize(child);\n } else if (isDraftable(value)) {\n resolveAssigned(value);\n }\n }\n};\n\nconst finalize = (node: Node): unknown => {\n if (!node.modified) return node.base;\n const result = node.copy!;\n for (const key of Object.keys(result)) {\n const value = result[key];\n const child = nodeOf(value);\n if (child) {\n result[key] = finalize(child);\n } else if (node.assigned?.has(key) && isDraftable(value)) {\n resolveAssigned(value);\n }\n }\n return result;\n};\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 const recordMutations = <T>(\n base: T,\n mutate: (draft: T) => void,\n): { state: T; changedPaths: Path[] } => {\n if (!isDraftable(base)) {\n throw new Error(\"statewire: cannot record mutations on primitive state\");\n }\n\n const changedPaths: Path[] = [];\n const seen = new Set<string>();\n const record = (path: Path) => {\n const key = path.join(\"\u0000\");\n if (seen.has(key)) return;\n seen.add(key);\n changedPaths.push(path);\n };\n\n const createNode = (value: object, parent: Node | null, path: Path): Node => {\n const node: Node = {\n base: value as Record<PropertyKey, unknown>,\n copy: null,\n modified: false,\n isArray: Array.isArray(value),\n parent,\n path,\n proxy: null,\n assigned: null,\n };\n const touched = (prop: PropertyKey) => {\n if (typeof prop !== \"string\") return;\n record(node.isArray ? node.path : [...node.path, prop]);\n };\n node.proxy = new Proxy(value, {\n get: (_target, prop) => {\n if (prop === DRAFT) return node;\n const current = latest(node)[prop];\n if (typeof prop === \"symbol\") return current;\n if (nodeOf(current)) return current;\n if (isDraftable(current) && current === node.base[prop]) {\n prepareCopy(node);\n const child = createNode(current, node, [\n ...path,\n node.isArray ? Number(prop) : prop,\n ]);\n node.copy![prop] = child.proxy;\n return child.proxy;\n }\n return current;\n },\n set: (_target, prop, next) => {\n const source = latest(node);\n if (Object.hasOwn(source, prop) && source[prop] === next) return true;\n prepareCopy(node);\n markModified(node);\n touched(prop);\n node.copy![prop] = next;\n (node.assigned ??= new Set()).add(prop);\n return true;\n },\n deleteProperty: (_target, prop) => {\n if (!Object.hasOwn(latest(node), prop)) return true;\n prepareCopy(node);\n markModified(node);\n touched(prop);\n delete node.copy![prop];\n return true;\n },\n has: (_target, prop) => prop === DRAFT || Reflect.has(latest(node), prop),\n ...sharedDraftTraps(() => latest(node), \"inside a mutator\"),\n });\n return node;\n };\n\n const root = createNode(base as unknown as object, null, []);\n mutate(root.proxy as T);\n return { state: finalize(root) as T, changedPaths };\n};\n"],"mappings":";;AAKA,MAAMG,QAAQC,OAAO,iBAAiB;AAatC,MAAMa,eAAeC,UAAoC;CACvD,IAAI,OAAOA,UAAU,YAAYA,UAAU,MAAM,OAAO;CACxD,IAAIC,MAAMR,QAAQO,KAAK,GAAG,OAAO;CACjC,MAAME,QAAiBC,OAAOC,eAAeJ,KAAK;CAClD,OAAOE,UAAUC,OAAOE,aAAaH,UAAU;AACjD;AAEA,MAAMI,UAAUN,UACd,OAAOA,UAAU,YAAYA,UAAU,OAClCA,MAA6Bf,SAC9BsB,KAAAA;AAEN,MAAMC,UAAUC,SAAeA,KAAKlB,QAAQkB,KAAKrB;AAEjD,MAAMsB,eAAeD,SAAe;CAClCA,KAAKlB,SAASkB,KAAKhB,UACbgB,KAAKrB,KAA8BuB,MAAM,IAI3C,EAAE,GAAGF,KAAKrB,KAAK;AACrB;AAEA,MAAMwB,gBAAgBH,SAAe;CACnC,KAAK,IAAII,IAAiBJ,MAAMI,KAAK,CAACA,EAAErB,UAAUqB,IAAIA,EAAEnB,QACtDmB,EAAErB,WAAW;AAEjB;AAEA,MAAMsB,mBAAmBC,cAAsB;CAC7C,KAAK,MAAMC,OAAOb,OAAOc,KAAKF,SAAS,GAAG;EACxC,MAAMf,QAASe,UAAsCC;EACrD,MAAME,QAAQZ,OAAON,KAAK;EAC1B,IAAIkB,OACF,UAAuCF,OAAOG,SAASD,KAAK;OACvD,IAAInB,YAAYC,KAAK,GAC1Bc,gBAAgBd,KAAK;CAEzB;AACF;AAEA,MAAMmB,YAAYV,SAAwB;CACxC,IAAI,CAACA,KAAKjB,UAAU,OAAOiB,KAAKrB;CAChC,MAAMgC,SAASX,KAAKlB;CACpB,KAAK,MAAMyB,OAAOb,OAAOc,KAAKG,MAAM,GAAG;EACrC,MAAMpB,QAAQoB,OAAOJ;EACrB,MAAME,QAAQZ,OAAON,KAAK;EAC1B,IAAIkB,OACFE,OAAOJ,OAAOG,SAASD,KAAK;OACvB,IAAIT,KAAKZ,UAAUwB,IAAIL,GAAG,KAAKjB,YAAYC,KAAK,GACrDc,gBAAgBd,KAAK;CAEzB;CACA,OAAOoB;AACT;;AAGA,MAAaE,mBACXC,OACAE,WACWE,gBAAgBJ,OAAOE,MAAM,CAAC,CAACF;AAE5C,MAAaI,mBACXvC,MACAqC,WACuC;CACvC,IAAI,CAAC1B,YAAYX,IAAI,GACnB,MAAM,IAAI0C,MAAM,uDAAuD;CAGzE,MAAMD,eAAuB,CAAA;CAC7B,MAAME,uBAAO,IAAIjC,IAAY;CAC7B,MAAMkC,UAAUrC,SAAe;EAC7B,MAAMqB,MAAMrB,KAAKsC,KAAK,IAAG;EACzB,IAAIF,KAAKV,IAAIL,GAAG,GAAG;EACnBe,KAAKG,IAAIlB,GAAG;EACZa,aAAaM,KAAKxC,IAAI;CACxB;CAEA,MAAMyC,cAAcpC,OAAeN,QAAqBC,SAAqB;EAC3E,MAAMc,OAAa;GACjBrB,MAAMY;GACNT,MAAM;GACNC,UAAU;GACVC,SAASQ,MAAMR,QAAQO,KAAK;GAC5BN;GACAC;GACAC,OAAO;GACPC,UAAU;EACZ;EACA,MAAMwC,WAAWC,SAAsB;GACrC,IAAI,OAAOA,SAAS,UAAU;GAC9BN,OAAOvB,KAAKhB,UAAUgB,KAAKd,OAAO,CAAC,GAAGc,KAAKd,MAAM2C,IAAI,CAAC;EACxD;EACA7B,KAAKb,QAAQ,IAAI2C,MAAMvC,OAAO;GAC5BwC,MAAMC,SAASH,SAAS;IACtB,IAAIA,SAASrD,OAAO,OAAOwB;IAC3B,MAAMiC,UAAUlC,OAAOC,IAAI,CAAC,CAAC6B;IAC7B,IAAI,OAAOA,SAAS,UAAU,OAAOI;IACrC,IAAIpC,OAAOoC,OAAO,GAAG,OAAOA;IAC5B,IAAI3C,YAAY2C,OAAO,KAAKA,YAAYjC,KAAKrB,KAAKkD,OAAO;KACvD5B,YAAYD,IAAI;KAChB,MAAMS,QAAQkB,WAAWM,SAASjC,MAAM,CACtC,GAAGd,MACHc,KAAKhB,UAAUkD,OAAOL,IAAI,IAAIA,IAAI,CACnC;KACD7B,KAAKlB,KAAM+C,QAAQpB,MAAMtB;KACzB,OAAOsB,MAAMtB;IACf;IACA,OAAO8C;GACT;GACAE,MAAMH,SAASH,MAAMO,SAAS;IAC5B,MAAMC,SAAStC,OAAOC,IAAI;IAC1B,IAAIN,OAAO4C,OAAOD,QAAQR,IAAI,KAAKQ,OAAOR,UAAUO,MAAM,OAAO;IACjEnC,YAAYD,IAAI;IAChBG,aAAaH,IAAI;IACjB4B,QAAQC,IAAI;IACZ7B,KAAKlB,KAAM+C,QAAQO;IACnB,CAACpC,KAAKZ,6BAAa,IAAIC,IAAI,EAAA,CAAGoC,IAAII,IAAI;IACtC,OAAO;GACT;GACAU,iBAAiBP,SAASH,SAAS;IACjC,IAAI,CAACnC,OAAO4C,OAAOvC,OAAOC,IAAI,GAAG6B,IAAI,GAAG,OAAO;IAC/C5B,YAAYD,IAAI;IAChBG,aAAaH,IAAI;IACjB4B,QAAQC,IAAI;IACZ,OAAO7B,KAAKlB,KAAM+C;IAClB,OAAO;GACT;GACAjB,MAAMoB,SAASH,SAASA,SAASrD,SAASgE,QAAQ5B,IAAIb,OAAOC,IAAI,GAAG6B,IAAI;GACxE,GAAGxD,uBAAuB0B,OAAOC,IAAI,GAAG,kBAAkB;EAC5D,CAAC;EACD,OAAOA;CACT;CAEA,MAAMyC,OAAOd,WAAWhD,MAA2B,MAAM,CAAA,CAAE;CAC3DqC,OAAOyB,KAAKtD,KAAU;CACtB,OAAO;EAAE2B,OAAOJ,SAAS+B,IAAI;EAAQrB;CAAa;AACpD"}
|
package/package.json
CHANGED
package/src/client/core.ts
CHANGED
|
@@ -79,7 +79,7 @@ const createCore = (
|
|
|
79
79
|
drainCanonical();
|
|
80
80
|
if (canonical === prevCanonical && !overlaysDirty) return;
|
|
81
81
|
overlaysDirty = false;
|
|
82
|
-
const { optimistic } = getOptions();
|
|
82
|
+
const { optimistic, materialize } = getOptions();
|
|
83
83
|
let next = canonical;
|
|
84
84
|
let paths: readonly StatewirePath[] = [];
|
|
85
85
|
if (optimistic && canonical !== undefined) {
|
|
@@ -110,6 +110,20 @@ const createCore = (
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
+
if (materialize && next !== undefined) {
|
|
114
|
+
try {
|
|
115
|
+
const recorded = recordMutations(next, (draft) =>
|
|
116
|
+
materialize(draft as never),
|
|
117
|
+
);
|
|
118
|
+
next = recorded.state;
|
|
119
|
+
paths = [...paths, ...recorded.changedPaths];
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(
|
|
122
|
+
"statewire: materialize threw; publishing without its update",
|
|
123
|
+
error,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
113
127
|
published = next;
|
|
114
128
|
overlayPaths = paths;
|
|
115
129
|
};
|
package/src/index.ts
CHANGED
|
@@ -3,3 +3,4 @@ export { StatewireClient, useStatewire, StatewireResource } from "./statewire";
|
|
|
3
3
|
export { StatewireSSE } from "./transports/sse";
|
|
4
4
|
export { StatewireWS } from "./transports/ws";
|
|
5
5
|
export { StatewireSendError } from "./types";
|
|
6
|
+
export { applyOptimistic } from "./util/recordMutations";
|
package/src/statewire.ts
CHANGED
|
@@ -110,6 +110,7 @@ export namespace StatewireClient {
|
|
|
110
110
|
> = {
|
|
111
111
|
transport: ResourceElement<Statewire<TState, TCommands, TEvent>>;
|
|
112
112
|
optimistic?: Optimistic<NonNullable<TState>, TCommands>;
|
|
113
|
+
materialize?: Materialize<NonNullable<TState>>;
|
|
113
114
|
onEvent?: (event: TEvent, ctx: { state: NonNullable<TState> }) => void;
|
|
114
115
|
onStateChange?: (
|
|
115
116
|
state: NonNullable<TState>,
|
|
@@ -170,6 +171,13 @@ export namespace StatewireClient {
|
|
|
170
171
|
clientId?: string;
|
|
171
172
|
};
|
|
172
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Mutate the draft to make server-accepted, still-uncommitted intents in
|
|
176
|
+
* the state concrete (e.g. dispatch records into messages). Runs after the
|
|
177
|
+
* optimistic folds on every published state.
|
|
178
|
+
*/
|
|
179
|
+
export type Materialize<TState> = (draft: TState) => void;
|
|
180
|
+
|
|
173
181
|
/**
|
|
174
182
|
* fate: "aborted" = never sent, "rejected" = the handler refused,
|
|
175
183
|
* "unknown-command" = no such command, "invalid-params" = the args did not
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ResourceElement } from "@assistant-ui/tap";
|
|
2
2
|
import type { StatewireEnvelope } from "../client/connection";
|
|
3
|
-
import { recordMutations } from "../util/recordMutations";
|
|
4
3
|
import { StatewireSSE } from "./sse";
|
|
5
4
|
import type { Statewire } from "../types";
|
|
6
5
|
import { openSSEStream, sseFrame } from "./fake-sse";
|
|
@@ -11,11 +10,7 @@ export * from "./fixtures";
|
|
|
11
10
|
/** Let queued microtasks and op batches drain through the statewire client. */
|
|
12
11
|
export const tick = () => new Promise<void>((r) => setTimeout(r, 0));
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
export const applyOptimistic = <TState>(
|
|
16
|
-
state: TState,
|
|
17
|
-
mutate: (draft: TState) => void,
|
|
18
|
-
): TState => recordMutations(state, mutate).state;
|
|
13
|
+
export { applyOptimistic } from "../util/recordMutations";
|
|
19
14
|
|
|
20
15
|
export type MockTransport<TState> = {
|
|
21
16
|
/** Pass as the `transport:` option of useStatewire / Harness. */
|
|
Binary file
|