statewire 0.7.0 → 0.8.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.
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"core.d.ts","names":[],"sources":["../../src/client/core.ts"],"mappings":";;;cAYa,iBAAiB,gBAAgB;cA2TjC,mBACX,kBAAkB,gBAAgB,wBAClC,OAAO;EAjFe,SAAA,MAAA;;;;EAsEE,YAAA"}
@@ -30,6 +30,7 @@ const createCore = (getOptions) => {
30
30
  let notifiedOverlayPaths = [];
31
31
  let notifiedConnection = null;
32
32
  let stale = false;
33
+ let everAttached = false;
33
34
  let overlaysDirty = false;
34
35
  let flushing = false;
35
36
  const listeners = /* @__PURE__ */ new Set();
@@ -50,10 +51,22 @@ const createCore = (getOptions) => {
50
51
  const drainCanonical = () => {
51
52
  if (source) {
52
53
  pendingChanged.push(...source.takeChanged());
53
- canonical = source.snapshot();
54
- } else if (value && value.state !== canonical) {
55
- pendingChanged.push([]);
56
- canonical = value.state;
54
+ const snapshot = source.snapshot();
55
+ if (snapshot !== void 0) {
56
+ everAttached = true;
57
+ canonical = snapshot;
58
+ return;
59
+ }
60
+ const next = everAttached ? void 0 : getOptions().initialState;
61
+ if (next !== canonical) pendingChanged.push([]);
62
+ canonical = next;
63
+ } else if (value) {
64
+ if (value.state !== void 0) everAttached = true;
65
+ const next = everAttached ? value.state : getOptions().initialState;
66
+ if (next !== canonical) {
67
+ pendingChanged.push([]);
68
+ canonical = next;
69
+ }
57
70
  }
58
71
  };
59
72
  const recompute = () => {
@@ -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","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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"}
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","everAttached","overlaysDirty","flushing","listeners","Set","post","scheduled","flush","schedule","markStale","drainCanonical","push","takeChanged","snapshot","undefined","next","initialState","state","recompute","prevCanonical","optimistic","materialize","paths","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 everAttached = 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 // initialState stands in for the canonical state only before the first\n // attach snapshot; replay boundaries after that read as loading, not preload.\n const drainCanonical = () => {\n if (source) {\n pendingChanged.push(...source.takeChanged());\n const snapshot = source.snapshot();\n if (snapshot !== undefined) {\n everAttached = true;\n canonical = snapshot;\n return;\n }\n const next = everAttached ? undefined : getOptions().initialState;\n if (next !== canonical) pendingChanged.push([]);\n canonical = next;\n } else if (value) {\n if (value.state !== undefined) everAttached = true;\n const next = everAttached ? value.state : getOptions().initialState;\n if (next !== canonical) {\n pendingChanged.push([]);\n canonical = next;\n }\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":";;;;;;;;;AAYA,MAAae,kBAA8C;CACzDE,QAAQ;CACRC,iBAAiB,CAAC;AACpB;AAEA,MAAMC,aAAaC,UACjBA,iBAAiBR,qBACbQ,MAAME,UACN;CACEC,MAAM;CACNC,OAAO;CACPC,SAASL,iBAAiBM,QAAQN,MAAMK,UAAUE,OAAOP,KAAK;AAChE;AAIN,MAAMW,cACJC,eACG;CACH,IAAIE,QAAmC;CACvC,IAAIC,SAAuC;CAC3C,IAAIE,WAAsB,CAAA;CAC1B,IAAIC,UAAqC,CAAA;CACzC,IAAIE,SAAoB,CAAA;CACxB,IAAIC,iBAAkC,CAAA;CACtC,IAAIC;CACJ,IAAIC;CACJ,IAAIC,eAAyC,CAAA;CAC7C,IAAIC;CACJ,IAAIC,uBAAiD,CAAA;CACrD,IAAIC,qBAAwD;CAC5D,IAAIC,QAAQ;CACZ,IAAIC,eAAe;CACnB,IAAIC,gBAAgB;CACpB,IAAIC,WAAW;CACf,MAAMC,4BAAY,IAAIC,IAAgB;CAEtC,MAAMC,OAAO7C,4BAA4B;EACvC8C,YAAY;EACZC,MAAM;CACR,CAAC;CACD,IAAID,YAAY;CAChB,MAAME,iBAAiB;EACrB,IAAIF,WAAW;EACfA,YAAY;EACZD,KAAK;CACP;CAEA,MAAMI,kBAAkB;EACtBV,QAAQ;EACRS,SAAS;CACX;CAIA,MAAME,uBAAuB;EAC3B,IAAIxB,QAAQ;GACVM,eAAemB,KAAK,GAAGzB,OAAO0B,YAAY,CAAC;GAC3C,MAAMC,WAAW3B,OAAO2B,SAAS;GACjC,IAAIA,aAAaC,KAAAA,GAAW;IAC1Bd,eAAe;IACfP,YAAYoB;IACZ;GACF;GACA,MAAME,OAAOf,eAAec,KAAAA,IAAY/B,WAAW,CAAC,CAACiC;GACrD,IAAID,SAAStB,WAAWD,eAAemB,KAAK,CAAA,CAAE;GAC9ClB,YAAYsB;EACd,OAAO,IAAI9B,OAAO;GAChB,IAAIA,MAAMgC,UAAUH,KAAAA,GAAWd,eAAe;GAC9C,MAAMe,OAAOf,eAAef,MAAMgC,QAAQlC,WAAW,CAAC,CAACiC;GACvD,IAAID,SAAStB,WAAW;IACtBD,eAAemB,KAAK,CAAA,CAAE;IACtBlB,YAAYsB;GACd;EACF;CACF;CAEA,MAAMG,kBAAkB;EACtB,IAAI,CAACnB,SAAS,CAACd,OAAO;EACtBc,QAAQ;EACR,MAAMoB,gBAAgB1B;EACtBiB,eAAe;EACf,IAAIjB,cAAc0B,iBAAiB,CAAClB,eAAe;EACnDA,gBAAgB;EAChB,MAAM,EAAEmB,YAAYC,gBAAgBtC,WAAW;EAC/C,IAAIgC,OAAOtB;EACX,IAAI6B,QAAkC,CAAA;EACtC,IAAIF,cAAc3B,cAAcqB,KAAAA,GAC9B,KAAK,MAAMS,WAAWnC,UAAU;GAC9B,IAAImC,QAAQ1C,SAAS;GACrB,MAAM,EAAED,QAAQ2C;GAChB,MAAMC,MAAyC;IAC7CE,IAAI9C,IAAI8C;IACRC,KAAK/C,IAAI+C;IACT,GAAI/C,IAAIgD,aAAad,KAAAA,KAAa,EAAEc,UAAUhD,IAAIgD,SAAS;GAC7D;GACA,IAAI;IACF,MAAMC,WAAWtE,gBAAgBwD,OAAOe,UACtCV,WACEU,OACA;KAAEC,QAAQnD,IAAImD;KAAQC,QAAQpD,IAAIoD;IAAO,GACzCR,GACF,CACF;IACAT,OAAOc,SAASZ;IAChBK,QAAQ,CAAC,GAAGA,OAAO,GAAGO,SAASK,YAAY;GAC7C,SAAS/D,OAAO;IACdoD,QAAQ1C,UAAU;IAClBsD,QAAQhE,MACN,4CAA4CS,IAAImD,OAAM,oCACtD5D,KACF;GACF;EACF;EAEF,IAAIkD,eAAeN,SAASD,KAAAA,GAC1B,IAAI;GACF,MAAMe,WAAWtE,gBAAgBwD,OAAOe,UACtCT,YAAYS,KAAc,CAC5B;GACAf,OAAOc,SAASZ;GAChBK,QAAQ,CAAC,GAAGA,OAAO,GAAGO,SAASK,YAAY;EAC7C,SAAS/D,OAAO;GACdgE,QAAQhE,MACN,+DACAA,KACF;EACF;EAEFuB,YAAYqB;EACZpB,eAAe2B;CACjB;CAEA,MAAMc,sBACJnD,OAAOoD,cAAcvE;CAEvB,MAAMyC,cAAc;EAClB,IAAI,CAACtB,OAAO;EACZ,IAAIiB,UAAU;GACZM,SAAS;GACT;EACF;EACAN,WAAW;EACX,IAAI;GACFgB,UAAU;GACV,MAAMoB,UAAUvD,WAAW;GAC3B,MAAMwD,eAAe7C,cAAcE;GACnC,IAAI2C,cAAc;IAChB,MAAMC,OAAO5C;IACb,MAAM6C,UAAU;KACd,GAAGjD;KACH,GAAGK;KACH,GAAGF;IAAY;IAEjBH,iBAAiB,CAAA;IACjBI,oBAAoBF;IACpBG,uBAAuBF;IACvB2C,QAAQI,gBAAgBhD,WAAoB,EAC1CiD,SAAS1F,cAAcuF,MAAM9C,WAAW+C,OAAO,EACjD,CAAC;GACH;GACA,IAAIpD,QAAQuD,SAAS,KAAKlD,cAAcoB,KAAAA,GAAW;IACjD,MAAM+B,QAAQxD;IACdA,UAAU,CAAA;IACV,KAAK,MAAMyD,UAAUD,OACnBP,QAAQS,kBAAkBD,QAAQ,EAAE7B,OAAOvB,UAAmB,CAAC;GAEnE;GACA,IAAIH,OAAOqD,SAAS,KAAKlD,cAAcoB,KAAAA,GAAW;IAChD,MAAM+B,QAAQtD;IACdA,SAAS,CAAA;IACT,KAAK,MAAMyD,SAASH,OAClBP,QAAQW,UAAUD,OAAO,EAAE/B,OAAOvB,UAAmB,CAAC;GAE1D;GACA,MAAM2C,aAAaD,cAAc;GACjC,MAAMc,oBAAoBb,eAAevC;GACzCA,qBAAqBuC;GACrB,IAAIa,mBAAmBZ,QAAQa,qBAAqBd,UAAU;GAC9D,IAAIE,gBAAgBW,mBAClB,KAAK,MAAME,YAAY,CAAC,GAAGjD,SAAS,GAAGiD,SAAS;EAEpD,UAAU;GACRlD,WAAW;EACb;CACF;CAEA,MAAMmD,QACJzE,KACAZ,QACAsF,UACG;EACH,IAAI,CAACvE,WAAW,CAAC,CAACgE,iBAAiB;EACnC1D,QAAQsB,KAAK;GACXoB,QAAQnD,IAAImD;GACZC,QAAQpD,IAAIoD;GACZL,KAAK/C,IAAI+C;GACT3D;GACA,GAAGsF;EACL,CAAC;EACD9C,SAAS;CACX;CAEA,MAAMgD,mBAAqC;EACzCC,UAAU7E,QAAQ;GAChBQ,SAASuB,KAAK;IAAE/B;IAAKC,SAAS;GAAM,CAAC;GACrCoB,gBAAgB;GAChBQ,UAAU;GACV4C,KAAKzE,KAAK,QAAQ;EACpB;EACA8E,WAAW9E,QAAQyE,KAAKzE,KAAK,QAAQ;EACrC+E,QAAQ/E,QAAQyE,KAAKzE,KAAK,OAAO;EACjCgF,YAAYhF,QAAQ;GAClB,IAAI,CAACQ,SAASyE,MAAMC,MAAMA,EAAElF,QAAQA,GAAG,GAAG;GAC1CQ,WAAWA,SAAS2E,QAAQD,MAAMA,EAAElF,QAAQA,GAAG;GAC/CqB,gBAAgB;GAChBQ,UAAU;EACZ;EACAuD,WAAWpF,KAAKqF,YAAY;GAC1BZ,KACEzE,KACA,WACAqF,QAAQC,SAAS,aACb,EAAEX,QAAQU,QAAQE,QAAQ,IAC1B,EAAE9F,SAASH,UAAU+F,QAAQ9F,KAAK,EAAE,CAC1C;GACAoC,MAAM;EACR;CACF;CAEA,MAAM6D,aAAapB,UAAmB;EACpC,IAAI,CAACjE,WAAW,CAAC,CAACkE,SAAS;EAC3B1D,OAAOoB,KAAKqC,KAAK;EACjBxC,SAAS;CACX;CAEA,MAAM6D,UAAUtD,SAA6B;EAC3C,IAAIA,SAAS9B,OAAO;EACpB,MAAMuD,OAAOvD;EACbA,QAAQ8B;EACR,MAAMuD,aACJvD,KAAK7B,WACJ,OAAO6B,KAAKE,UAAU,YAAYF,KAAKE,UAAU,OAC9C7D,aAAa2D,KAAKE,KAAK,IACvBH,KAAAA,MACJ;EACF,IAAIwD,eAAepF,QAAQ;GACzBA,QAAQqF,YAAY,IAAI;GACxBrF,SAASoF;GACTpF,QAAQqF,YAAY9D,SAAS;GAC7BA,UAAU;EACZ;EACA,IAAIM,KAAKyD,aAAahC,MAAMgC,UAAU;GACpC,IAAIhC,MAAMtF,gBAAgBsF,KAAKgC,QAAQ,CAAC,EAAEC,YAAY,IAAI;GAC1DvH,gBAAgB6D,KAAKyD,QAAQ,CAAC,EAAEC,YAAYjB,gBAAgB;GAC5D,IAAIpE,SAASwD,SAAS,GAAG;IACvBxD,WAAW,CAAA;IACXa,gBAAgB;GAClB;GACAQ,UAAU;EACZ;EACA,IAAIM,KAAK2D,SAASlC,MAAMkC,MAAM;GAC5B,IAAIlC,MAAMrF,YAAYqF,KAAKkC,IAAI,CAAC,EAAEC,QAAQ,IAAI;GAC9CxH,YAAY4D,KAAK2D,IAAI,CAAC,EAAEC,QAAQP,SAAS;EAC3C;EACA,IAAI,CAAClF,QAAQuB,UAAU;EACvBD,SAAS;CACX;CAEA,MAAMoE,eAAe;EACnB1F,QAAQqF,YAAY,IAAI;EACxBrF,SAAS;EACT,IAAID,OAAO;GACT/B,gBAAgB+B,MAAMuF,QAAQ,CAAC,EAAEC,YAAY,IAAI;GACjDtH,YAAY8B,MAAMyF,IAAI,CAAC,EAAEC,QAAQ,IAAI;GACrC1F,QAAQ;EACV;CACF;CAqBA,OAAO;EACLoF;EACAO;EACAQ,gBAAgB;GACdlE,UAAU;GACV,OAAOxB;EACT;EACA8E,UA1Be/G,mBACdsE,QAAQC,WAAW;GAClB,IAAI,CAAC/C,OACH,OAAO6F,QAAQC,OACb,IAAIpH,mBAAmB;IAAEW,MAAM;IAAWC,OAAO;GAAU,CAAC,CAC9D;GAEF,MAAMyG,OACJ/F,MAAMuF,SAINzC;GACF,MAAMoD,UAAUH,KAAK,GAAGhD,MAAM;GAC9BzB,MAAM;GACN,OAAO4E;EACT,CAUAX;EACAa,YAAYjC,aAAyB;GACnCjD,UAAUmF,IAAIlC,QAAQ;GACtB,aAAa;IACXjD,UAAUoF,OAAOnC,QAAQ;GAC3B;EACF;CACF;AACF;AAEA,MAAaoC,oBAAmBzG,YAAAE,UAAA;CAAA,MAAAwG,IAAAC,EAAA,CAAA;CAAA,IAAAC;CAAA,IAAAF,EAAA,OAAA1G,YAAA;EAIN4G,WAAM7G,WAAWC,UAAU;EAAC0G,EAAA,KAAA1G;EAAA0G,EAAA,KAAAE;CAAA,OAAAA,KAAAF,EAAA;CAApD,MAAA,CAAAG,QAAe5I,SAAS2I,EAA4B;CAIpDC,KAAIvB,OAAQpF,KAAK;CAAC,IAAA4G;CAAA,IAAAJ,EAAA,OAAAG,QAAAH,EAAA,OAAAxG,OAAA;EACR4G,WAAA;GACRD,KAAIvB,OAAQpF,KAAK;EAAC;EACnBwG,EAAA,KAAAG;EAAAH,EAAA,KAAAxG;EAAAwG,EAAA,KAAAI;CAAA,OAAAA,KAAAJ,EAAA;CAFD1I,UAAU8I,EAET;CAAC,IAAAC;CAAA,IAAAC;CAAA,IAAAN,EAAA,OAAAG,MAAA;EACQE,iBAAYF,KAAIhB,OAAQ;EAAGmB,KAAA,CAACH,IAAI;EAACH,EAAA,KAAAG;EAAAH,EAAA,KAAAK;EAAAL,EAAA,KAAAM;CAAA,OAAA;EAAAD,KAAAL,EAAA;EAAAM,KAAAN,EAAA;CAAA;CAA3C1I,UAAU+I,IAA2BC,EAAM;CAAC,OACrCH;AAAI"}
@@ -18,6 +18,8 @@ declare class StatewireClient<TState, TCommands extends Statewire.Commands = Sta
18
18
  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
+ /** Served as the canonical state until the attach snapshot; own commands fold over it. */
22
+ initialState?: TState;
21
23
  optimistic?: Optimistic<NonNullable<TState>, TCommands>;
22
24
  materialize?: Materialize<NonNullable<TState>>;
23
25
  onEvent?: (event: TEvent, ctx: {
@@ -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,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"}
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;;IAExD,eAAe;IACf,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"}
@@ -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","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
+ {"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","initialState","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 /** Served as the canonical state until the attach snapshot; own commands fold over it. */\n initialState?: TState;\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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "statewire",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "State over the wire: snapshot-first delta streaming and command submission for any state and any command",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,6 +42,7 @@ const createCore = (
42
42
  let notifiedOverlayPaths: readonly StatewirePath[] = [];
43
43
  let notifiedConnection: StatewireClient.Connection | null = null;
44
44
  let stale = false;
45
+ let everAttached = false;
45
46
  let overlaysDirty = false;
46
47
  let flushing = false;
47
48
  const listeners = new Set<() => void>();
@@ -62,13 +63,27 @@ const createCore = (
62
63
  schedule();
63
64
  };
64
65
 
66
+ // initialState stands in for the canonical state only before the first
67
+ // attach snapshot; replay boundaries after that read as loading, not preload.
65
68
  const drainCanonical = () => {
66
69
  if (source) {
67
70
  pendingChanged.push(...source.takeChanged());
68
- canonical = source.snapshot();
69
- } else if (value && value.state !== canonical) {
70
- pendingChanged.push([]);
71
- canonical = value.state;
71
+ const snapshot = source.snapshot();
72
+ if (snapshot !== undefined) {
73
+ everAttached = true;
74
+ canonical = snapshot;
75
+ return;
76
+ }
77
+ const next = everAttached ? undefined : getOptions().initialState;
78
+ if (next !== canonical) pendingChanged.push([]);
79
+ canonical = next;
80
+ } else if (value) {
81
+ if (value.state !== undefined) everAttached = true;
82
+ const next = everAttached ? value.state : getOptions().initialState;
83
+ if (next !== canonical) {
84
+ pendingChanged.push([]);
85
+ canonical = next;
86
+ }
72
87
  }
73
88
  };
74
89
 
package/src/statewire.ts CHANGED
@@ -109,6 +109,8 @@ export namespace StatewireClient {
109
109
  TEvent = any,
110
110
  > = {
111
111
  transport: ResourceElement<Statewire<TState, TCommands, TEvent>>;
112
+ /** Served as the canonical state until the attach snapshot; own commands fold over it. */
113
+ initialState?: TState;
112
114
  optimistic?: Optimistic<NonNullable<TState>, TCommands>;
113
115
  materialize?: Materialize<NonNullable<TState>>;
114
116
  onEvent?: (event: TEvent, ctx: { state: NonNullable<TState> }) => void;