statewire 0.11.0 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +2 -2
  2. package/dist/client/connection.d.ts +6 -4
  3. package/dist/client/connection.d.ts.map +1 -1
  4. package/dist/client/connection.js +10 -2
  5. package/dist/client/connection.js.map +1 -1
  6. package/dist/client/core.d.ts +1 -1
  7. package/dist/client/session.d.ts +4 -2
  8. package/dist/client/session.d.ts.map +1 -1
  9. package/dist/client/session.js +102 -102
  10. package/dist/client/session.js.map +1 -1
  11. package/dist/host/draft.d.ts.map +1 -1
  12. package/dist/host/draft.js +9 -1
  13. package/dist/host/draft.js.map +1 -1
  14. package/dist/host/host.d.ts +1 -1
  15. package/dist/host/http.js +4 -3
  16. package/dist/host/http.js.map +1 -1
  17. package/dist/host/socket.d.ts +6 -5
  18. package/dist/host/socket.d.ts.map +1 -1
  19. package/dist/host/socket.js +12 -20
  20. package/dist/host/socket.js.map +1 -1
  21. package/dist/host-internal.d.ts +2 -1
  22. package/dist/host-internal.d.ts.map +1 -1
  23. package/dist/host.d.ts +2 -2
  24. package/dist/host.d.ts.map +1 -1
  25. package/dist/index.d.ts +1 -1
  26. package/dist/statewire.d.ts +15 -4
  27. package/dist/statewire.d.ts.map +1 -1
  28. package/dist/statewire.js.map +1 -1
  29. package/dist/transports/fake-sse.js +4 -4
  30. package/dist/transports/fake-sse.js.map +1 -1
  31. package/dist/transports/fixtures.d.ts +1 -1
  32. package/dist/transports/remote.d.ts +1 -1
  33. package/dist/transports/remote.js +1 -0
  34. package/dist/transports/remote.js.map +1 -1
  35. package/dist/transports/sse.d.ts +2 -2
  36. package/dist/transports/testing.d.ts +1 -1
  37. package/dist/transports/testing.js +2 -4
  38. package/dist/transports/testing.js.map +1 -1
  39. package/dist/wire/meta.d.ts +5 -3
  40. package/dist/wire/meta.d.ts.map +1 -1
  41. package/dist/wire/meta.js +7 -4
  42. package/dist/wire/meta.js.map +1 -1
  43. package/dist/wire/ops.d.ts.map +1 -1
  44. package/dist/wire/ops.js +26 -2
  45. package/dist/wire/ops.js.map +1 -1
  46. package/package.json +1 -1
  47. package/src/client/connection.ts +12 -6
  48. package/src/client/session.ts +145 -139
  49. package/src/host/draft.ts +15 -1
  50. package/src/host/http.ts +10 -9
  51. package/src/host/socket.ts +18 -28
  52. package/src/host-internal.ts +2 -1
  53. package/src/host.ts +0 -1
  54. package/src/statewire.ts +18 -4
  55. package/src/transports/fake-sse.ts +6 -6
  56. package/src/transports/remote.ts +1 -0
  57. package/src/transports/testing.ts +2 -7
  58. package/src/wire/meta.ts +12 -4
  59. package/src/wire/ops.ts +30 -3
@@ -1 +1 @@
1
- {"version":3,"file":"remote.js","names":["useEffect","Statewire","setContext","setTransport","useStatewireSession","StatewireSessionOptions","StatewireClient","RemoteLink","StatewireRemoteProps","useStatewireRemote","url","onChange","connection","Connection","onDone","options","TOptions","useLink","ctx","s","sessionStore","conn","state","canonical","commands","source","durableSeq","isBusy","idle","value"],"sources":["../../src/transports/remote.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport type { Statewire } from \"../types\";\nimport {\n setContext,\n setTransport,\n useStatewireSession,\n} from \"../client/session\";\nimport type { StatewireSessionOptions } from \"../client/session\";\nimport type { StatewireClient } from \"../statewire\";\nimport type { RemoteLink, StatewireRemoteProps } from \"./sse\";\n\nexport const useStatewireRemote = <\n TOptions extends {\n url: string;\n onChange?: (connection: StatewireClient.Connection) => void;\n onDone?: () => void;\n } & StatewireSessionOptions,\n>(\n options: TOptions,\n useLink: (options: TOptions, ctx: StatewireRemoteProps) => RemoteLink,\n): Statewire<any, any> => {\n const s = useStatewireSession({\n sessionStore: options.sessionStore,\n onDone: options.onDone,\n });\n const conn = useLink(options, s.ctx);\n useEffect(() => setTransport(s, conn), [s, conn]);\n\n return {\n state: s.canonical,\n commands: s.commands,\n connection: conn.connection,\n source: s.source,\n get durableSeq() {\n return s.durableSeq;\n },\n get isBusy() {\n return !s.idle;\n },\n setContext: (value) => setContext(s, value),\n };\n};\n"],"mappings":";;;AAWA,MAAaS,sBAOXM,SACAE,YACwB;CACxB,MAAME,IAAIf,oBAAoB;EAC5BgB,cAAcL,QAAQK;EACtBN,QAAQC,QAAQD;CAClB,CAAC;CACD,MAAMO,OAAOJ,QAAQF,SAASI,EAAED,GAAG;CACnClB,gBAAgBG,aAAagB,GAAGE,IAAI,GAAG,CAACF,GAAGE,IAAI,CAAC;CAEhD,OAAO;EACLC,OAAOH,EAAEI;EACTC,UAAUL,EAAEK;EACZZ,YAAYS,KAAKT;EACjBa,QAAQN,EAAEM;EACV,IAAIC,aAAa;GACf,OAAOP,EAAEO;EACX;EACA,IAAIC,SAAS;GACX,OAAO,CAACR,EAAES;EACZ;EACA1B,aAAa2B,UAAU3B,WAAWiB,GAAGU,KAAK;CAC5C;AACF"}
1
+ {"version":3,"file":"remote.js","names":["useEffect","Statewire","setContext","setTransport","useStatewireSession","StatewireSessionOptions","StatewireClient","RemoteLink","StatewireRemoteProps","useStatewireRemote","url","onChange","connection","Connection","onDone","options","TOptions","useLink","ctx","s","sessionStore","onSessionLost","conn","state","canonical","commands","source","durableSeq","isBusy","idle","value"],"sources":["../../src/transports/remote.ts"],"sourcesContent":["import { useEffect } from \"react\";\nimport type { Statewire } from \"../types\";\nimport {\n setContext,\n setTransport,\n useStatewireSession,\n} from \"../client/session\";\nimport type { StatewireSessionOptions } from \"../client/session\";\nimport type { StatewireClient } from \"../statewire\";\nimport type { RemoteLink, StatewireRemoteProps } from \"./sse\";\n\nexport const useStatewireRemote = <\n TOptions extends {\n url: string;\n onChange?: (connection: StatewireClient.Connection) => void;\n onDone?: () => void;\n } & StatewireSessionOptions,\n>(\n options: TOptions,\n useLink: (options: TOptions, ctx: StatewireRemoteProps) => RemoteLink,\n): Statewire<any, any> => {\n const s = useStatewireSession({\n sessionStore: options.sessionStore,\n onSessionLost: options.onSessionLost,\n onDone: options.onDone,\n });\n const conn = useLink(options, s.ctx);\n useEffect(() => setTransport(s, conn), [s, conn]);\n\n return {\n state: s.canonical,\n commands: s.commands,\n connection: conn.connection,\n source: s.source,\n get durableSeq() {\n return s.durableSeq;\n },\n get isBusy() {\n return !s.idle;\n },\n setContext: (value) => setContext(s, value),\n };\n};\n"],"mappings":";;;AAWA,MAAaS,sBAOXM,SACAE,YACwB;CACxB,MAAME,IAAIf,oBAAoB;EAC5BgB,cAAcL,QAAQK;EACtBC,eAAeN,QAAQM;EACvBP,QAAQC,QAAQD;CAClB,CAAC;CACD,MAAMQ,OAAOL,QAAQF,SAASI,EAAED,GAAG;CACnClB,gBAAgBG,aAAagB,GAAGG,IAAI,GAAG,CAACH,GAAGG,IAAI,CAAC;CAEhD,OAAO;EACLC,OAAOJ,EAAEK;EACTC,UAAUN,EAAEM;EACZb,YAAYU,KAAKV;EACjBc,QAAQP,EAAEO;EACV,IAAIC,aAAa;GACf,OAAOR,EAAEQ;EACX;EACA,IAAIC,SAAS;GACX,OAAO,CAACT,EAAEU;EACZ;EACA3B,aAAa4B,UAAU5B,WAAWiB,GAAGW,KAAK;CAC5C;AACF"}
@@ -1,7 +1,7 @@
1
- import { StatewireClient } from "../statewire.js";
2
- import { Statewire } from "../types.js";
3
1
  import { StatewireHost } from "../host.js";
4
2
  import { StatewireCmd, StatewireSyn } from "../client/connection.js";
3
+ import { StatewireClient } from "../statewire.js";
4
+ import { Statewire } from "../types.js";
5
5
  import { ResourceElement } from "@assistant-ui/tap";
6
6
  //#region src/transports/sse.d.ts
7
7
  /** A client frame's lanes: stamped statements, strictly consecutive per lane. */
@@ -1,5 +1,5 @@
1
- import { Statewire } from "../types.js";
2
1
  import { StatewireEnvelope } from "../client/connection.js";
2
+ import { Statewire } from "../types.js";
3
3
  import { SocketRecorder, commandFrame, createCounterElement, createSocketRecorder, settle } from "./fixtures.js";
4
4
  import { applyOptimistic } from "../util/recordMutations.js";
5
5
  import { ResourceElement } from "@assistant-ui/tap";
@@ -43,8 +43,7 @@ const createMockTransport = (initialState) => {
43
43
  const id = new Headers(init?.headers).get("Statewire-Client-Id") ?? "";
44
44
  if (url.endsWith("/stream")) {
45
45
  clientId = id;
46
- const known = clients.has(id);
47
- if (!known) clients.set(id, {
46
+ if (!clients.has(id)) clients.set(id, {
48
47
  seq: 0,
49
48
  ctxSeq: 0
50
49
  });
@@ -60,8 +59,7 @@ const createMockTransport = (initialState) => {
60
59
  }],
61
60
  syn: {
62
61
  seq: rec.seq,
63
- ctxSeq: rec.ctxSeq,
64
- record: known ? "resumed" : "fresh"
62
+ ctxSeq: rec.ctxSeq
65
63
  }
66
64
  }));
67
65
  }, init?.signal ?? void 0, { "Statewire-Lease": "mock-lease" });
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","names":["ResourceElement","StatewireEnvelope","StatewireSSE","StatewireFrame","Statewire","openSSEStream","sseFrame","FakeStreamHandle","tick","Promise","r","setTimeout","applyOptimistic","MockTransport","transport","TState","commands","method","params","context","setState","state","applied","seq","settle","payload","sendEnvelope","envelope","finish","reason","message","clientId","attached","mockCount","createMockTransport","initialState","finished","clients","Map","ctxSeq","streams","Set","stream","send","admit","id","frame","rec","get","statement","ctx","Math","max","root","ops","value","push","cmd","fakeFetch","input","RequestInfo","URL","init","RequestInit","Response","TypeError","url","String","headers","Headers","endsWith","known","has","set","JSON","parse","body","handle","op","path","syn","record","signal","undefined","json","status","fetch","next","type","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 { StatewireFrame } 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>>;\n /** Captured statements in arrival order: cmd as `{method, params}`, ctx as `{context}`. */\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 a bare ack cmd packet on the stream. */\n applied: (seq: number) => Promise<void>;\n /** Deliver a terminal result cmd packet on the stream. */\n settle: (seq: number, payload?: unknown) => 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 submitted frames (receipt only — acks and results are delivered\n * from 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, { seq: number; ctxSeq: 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 admit = (id: string, frame: StatewireFrame) => {\n const rec = clients.get(id)!;\n for (const statement of frame.ctx ?? []) {\n rec.ctxSeq = Math.max(rec.ctxSeq, statement.seq);\n const root = statement.ops[0] as { value?: unknown } | undefined;\n commands.push({ context: root?.value });\n }\n for (const statement of frame.cmd ?? []) {\n rec.seq = Math.max(rec.seq, statement.seq);\n commands.push({ method: statement.method, params: statement.params });\n }\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 known = clients.has(id);\n if (!known) clients.set(id, { seq: 0, ctxSeq: 0 });\n const rec = clients.get(id)!;\n const frame = JSON.parse(String(init?.body)) as StatewireFrame;\n admit(id, frame);\n return openSSEStream(\n streams,\n id,\n (handle) => {\n handle.send(\n sseFrame({\n ops: [{ op: \"replace\", path: [], value: state }],\n syn: {\n seq: rec.seq,\n ctxSeq: rec.ctxSeq,\n record: known ? \"resumed\" : \"fresh\",\n },\n }),\n );\n },\n init?.signal ?? undefined,\n { \"Statewire-Lease\": \"mock-lease\" },\n );\n }\n if (url.endsWith(\"/frames\")) {\n admit(id, JSON.parse(String(init?.body)) as StatewireFrame);\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 applied: (seq) => sendEnvelope({ cmd: [{ seq, type: \"applied\" }] }),\n settle: (seq, payload) =>\n sendEnvelope({\n cmd: [\n {\n seq,\n type: \"result\",\n ...(payload !== undefined && { payload }),\n },\n ],\n }),\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":"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"}
1
+ {"version":3,"file":"testing.js","names":["ResourceElement","StatewireEnvelope","StatewireSSE","StatewireFrame","Statewire","openSSEStream","sseFrame","FakeStreamHandle","tick","Promise","r","setTimeout","applyOptimistic","MockTransport","transport","TState","commands","method","params","context","setState","state","applied","seq","settle","payload","sendEnvelope","envelope","finish","reason","message","clientId","attached","mockCount","createMockTransport","initialState","finished","clients","Map","ctxSeq","streams","Set","stream","send","admit","id","frame","rec","get","statement","ctx","Math","max","root","ops","value","push","cmd","fakeFetch","input","RequestInfo","URL","init","RequestInit","Response","TypeError","url","String","headers","Headers","endsWith","has","set","JSON","parse","body","handle","op","path","syn","signal","undefined","json","status","fetch","next","type","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 { StatewireFrame } 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>>;\n /** Captured statements in arrival order: cmd as `{method, params}`, ctx as `{context}`. */\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 a bare ack cmd packet on the stream. */\n applied: (seq: number) => Promise<void>;\n /** Deliver a terminal result cmd packet on the stream. */\n settle: (seq: number, payload?: unknown) => 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 submitted frames (receipt only — acks and results are delivered\n * from 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, { seq: number; ctxSeq: 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 admit = (id: string, frame: StatewireFrame) => {\n const rec = clients.get(id)!;\n for (const statement of frame.ctx ?? []) {\n rec.ctxSeq = Math.max(rec.ctxSeq, statement.seq);\n const root = statement.ops[0] as { value?: unknown } | undefined;\n commands.push({ context: root?.value });\n }\n for (const statement of frame.cmd ?? []) {\n rec.seq = Math.max(rec.seq, statement.seq);\n commands.push({ method: statement.method, params: statement.params });\n }\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 if (!clients.has(id)) clients.set(id, { seq: 0, ctxSeq: 0 });\n const rec = clients.get(id)!;\n const frame = JSON.parse(String(init?.body)) as StatewireFrame;\n admit(id, frame);\n return openSSEStream(\n streams,\n id,\n (handle) => {\n handle.send(\n sseFrame({\n ops: [{ op: \"replace\", path: [], value: state }],\n syn: { seq: rec.seq, ctxSeq: rec.ctxSeq },\n }),\n );\n },\n init?.signal ?? undefined,\n { \"Statewire-Lease\": \"mock-lease\" },\n );\n }\n if (url.endsWith(\"/frames\")) {\n admit(id, JSON.parse(String(init?.body)) as StatewireFrame);\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 applied: (seq) => sendEnvelope({ cmd: [{ seq, type: \"applied\" }] }),\n settle: (seq, payload) =>\n sendEnvelope({\n cmd: [\n {\n seq,\n type: \"result\",\n ...(payload !== undefined && { payload }),\n },\n ],\n }),\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":"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"}
@@ -3,9 +3,11 @@ type StatewireMetaEntry = {
3
3
  path: (string | number)[];
4
4
  type: string;
5
5
  };
6
- /** Revive meta-typed locations inside a wire value (in place). Unknown types
7
- * are skipped; a path that does not resolve to a valid target faults. */
8
- declare const reviveValue: (value: unknown, meta: unknown) => unknown;
6
+ /** Revive meta-typed locations inside a wire value (in place). Clients skip
7
+ * unknown types; hosts pass `rejectUnknown` to fault on them instead. */
8
+ declare const reviveValue: (value: unknown, meta: unknown, { rejectUnknown }?: {
9
+ rejectUnknown?: boolean | undefined;
10
+ }) => unknown;
9
11
  declare const reviveOps: (ops: unknown[]) => unknown[];
10
12
  declare const reviveResponse: <T extends {
11
13
  payload?: unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"meta.d.ts","names":[],"sources":["../../src/wire/meta.ts"],"mappings":";KAEY;EACV;EACA;;;;cAiBW,cAAe,gBAAgB;cAwB/B,YAAa;cAOb,iBAAkB;EAAY;EAAmB;GAC5D,UAAU,MACT;;;cA+BU,eACX;EACG;EAAmB,OAAO;;;cAOlB,cACX;EACG;EAAgB,OAAO"}
1
+ {"version":3,"file":"meta.d.ts","names":[],"sources":["../../src/wire/meta.ts"],"mappings":";KAEY;EACV;EACA;;;;cAiBW,cACX,gBACA,iBACA;;;cA6BW,YAAa;cAOb,iBAAkB;EAAY;EAAmB;GAC5D,UAAU,MACT;;;cA+BU,eACX;EACG;EAAmB,OAAO;;;cAOlB,cACX;EACG;EAAgB,OAAO"}
package/dist/wire/meta.js CHANGED
@@ -4,14 +4,17 @@ const fault = (message) => {
4
4
  throw new Error(`statewire: protocol error: ${message}`);
5
5
  };
6
6
  const isPath = (path) => Array.isArray(path) && path.every((seg) => typeof seg === "string" || typeof seg === "number" && Number.isInteger(seg));
7
- /** Revive meta-typed locations inside a wire value (in place). Unknown types
8
- * are skipped; a path that does not resolve to a valid target faults. */
9
- const reviveValue = (value, meta) => {
7
+ /** Revive meta-typed locations inside a wire value (in place). Clients skip
8
+ * unknown types; hosts pass `rejectUnknown` to fault on them instead. */
9
+ const reviveValue = (value, meta, { rejectUnknown = false } = {}) => {
10
10
  if (!Array.isArray(meta)) return fault("meta must be an array");
11
11
  for (const entry of meta) {
12
12
  const path = typeof entry === "object" && entry !== null ? entry.path : null;
13
13
  if (!isPath(path)) return fault("meta entry must carry a path array");
14
- if (entry.type !== "date") continue;
14
+ if (entry.type !== "date") {
15
+ if (rejectUnknown) return fault(`unknown meta type: ${String(entry.type)}`);
16
+ continue;
17
+ }
15
18
  const walked = walkStatePath(value, path, "resolve");
16
19
  if (walked === void 0) return fault(`meta path ${path.join(".")} does not resolve`);
17
20
  if (typeof walked.leaf !== "string") return fault(`meta path ${path.join(".")} must name a string`);
@@ -1 +1 @@
1
- {"version":3,"file":"meta.js","names":["walkStatePath","StatewireMetaEntry","path","type","fault","message","Error","isPath","Array","isArray","every","seg","Number","isInteger","reviveValue","value","meta","entry","walked","undefined","join","leaf","date","Date","isNaN","getTime","parent","Record","key","reviveOps","ops","map","op","rest","reviveResponse","payload","response","T","walkDates","node","push","toISOString","item","i","Object","fromEntries","keys","encodeParams","params","encoded","param","length","encodeValue"],"sources":["../../src/wire/meta.ts"],"sourcesContent":["import { walkStatePath } from \"../util/statePath\";\n\nexport type StatewireMetaEntry = {\n path: (string | number)[];\n type: string;\n};\n\nconst fault = (message: string): never => {\n throw new Error(`statewire: protocol error: ${message}`);\n};\n\nconst isPath = (path: unknown): path is (string | number)[] =>\n Array.isArray(path) &&\n path.every(\n (seg) =>\n typeof seg === \"string\" ||\n (typeof seg === \"number\" && Number.isInteger(seg)),\n );\n\n/** Revive meta-typed locations inside a wire value (in place). Unknown types\n * are skipped; a path that does not resolve to a valid target faults. */\nexport const reviveValue = (value: unknown, meta: unknown): unknown => {\n if (!Array.isArray(meta)) return fault(\"meta must be an array\");\n for (const entry of meta as { path?: unknown; type?: unknown }[]) {\n const path =\n typeof entry === \"object\" && entry !== null ? entry.path : null;\n if (!isPath(path)) return fault(\"meta entry must carry a path array\");\n if (entry.type !== \"date\") continue;\n const walked = walkStatePath(value, path, \"resolve\");\n if (walked === undefined) {\n return fault(`meta path ${path.join(\".\")} does not resolve`);\n }\n if (typeof walked.leaf !== \"string\") {\n return fault(`meta path ${path.join(\".\")} must name a string`);\n }\n const date = new Date(walked.leaf);\n if (Number.isNaN(date.getTime())) {\n return fault(`invalid date at meta path ${path.join(\".\")}`);\n }\n if (walked.parent === undefined) return date;\n (walked.parent as Record<string | number, unknown>)[walked.key] = date;\n }\n return value;\n};\n\nexport const reviveOps = (ops: unknown[]): unknown[] =>\n ops.map((op) => {\n if (typeof op !== \"object\" || op === null || !(\"meta\" in op)) return op;\n const { meta, ...rest } = op as { meta: unknown; value?: unknown };\n return { ...rest, value: reviveValue(rest.value, meta) };\n });\n\nexport const reviveResponse = <T extends { payload?: unknown; meta?: unknown }>(\n response: T,\n): T => {\n if (response.meta === undefined) return response;\n const { meta, ...rest } = response;\n return { ...rest, payload: reviveValue(rest.payload, meta) } as T;\n};\n\nconst walkDates = (\n node: unknown,\n path: (string | number)[],\n meta: StatewireMetaEntry[],\n): unknown => {\n if (node instanceof Date) {\n meta.push({ path, type: \"date\" });\n return node.toISOString();\n }\n if (Array.isArray(node)) {\n return node.map((item, i) => walkDates(item, [...path, i], meta));\n }\n if (typeof node === \"object\" && node !== null) {\n return Object.fromEntries(\n Object.keys(node).map((key) => [\n key,\n walkDates((node as Record<string, unknown>)[key], [...path, key], meta),\n ]),\n );\n }\n return node;\n};\n\n/** Encode Date instances inside command params as canonical ISO strings,\n * recording their paths (starting at the param index) in meta. */\nexport const encodeParams = (\n params: readonly unknown[],\n): { params: unknown[]; meta?: StatewireMetaEntry[] } => {\n const meta: StatewireMetaEntry[] = [];\n const encoded = params.map((param, i) => walkDates(param, [i], meta));\n return meta.length > 0 ? { params: encoded, meta } : { params: encoded };\n};\n\n/** Encode Date instances inside a wire value, recording their paths in meta. */\nexport const encodeValue = (\n value: unknown,\n): { value: unknown; meta?: StatewireMetaEntry[] } => {\n const meta: StatewireMetaEntry[] = [];\n const encoded = walkDates(value, [], meta);\n return meta.length > 0 ? { value: encoded, meta } : { value: encoded };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"meta.js","names":["walkStatePath","StatewireMetaEntry","path","type","fault","message","Error","isPath","Array","isArray","every","seg","Number","isInteger","reviveValue","value","meta","rejectUnknown","entry","String","walked","undefined","join","leaf","date","Date","isNaN","getTime","parent","Record","key","reviveOps","ops","map","op","rest","reviveResponse","payload","response","T","walkDates","node","push","toISOString","item","i","Object","fromEntries","keys","encodeParams","params","encoded","param","length","encodeValue"],"sources":["../../src/wire/meta.ts"],"sourcesContent":["import { walkStatePath } from \"../util/statePath\";\n\nexport type StatewireMetaEntry = {\n path: (string | number)[];\n type: string;\n};\n\nconst fault = (message: string): never => {\n throw new Error(`statewire: protocol error: ${message}`);\n};\n\nconst isPath = (path: unknown): path is (string | number)[] =>\n Array.isArray(path) &&\n path.every(\n (seg) =>\n typeof seg === \"string\" ||\n (typeof seg === \"number\" && Number.isInteger(seg)),\n );\n\n/** Revive meta-typed locations inside a wire value (in place). Clients skip\n * unknown types; hosts pass `rejectUnknown` to fault on them instead. */\nexport const reviveValue = (\n value: unknown,\n meta: unknown,\n { rejectUnknown = false } = {},\n): unknown => {\n if (!Array.isArray(meta)) return fault(\"meta must be an array\");\n for (const entry of meta as { path?: unknown; type?: unknown }[]) {\n const path =\n typeof entry === \"object\" && entry !== null ? entry.path : null;\n if (!isPath(path)) return fault(\"meta entry must carry a path array\");\n if (entry.type !== \"date\") {\n if (rejectUnknown)\n return fault(`unknown meta type: ${String(entry.type)}`);\n continue;\n }\n const walked = walkStatePath(value, path, \"resolve\");\n if (walked === undefined) {\n return fault(`meta path ${path.join(\".\")} does not resolve`);\n }\n if (typeof walked.leaf !== \"string\") {\n return fault(`meta path ${path.join(\".\")} must name a string`);\n }\n const date = new Date(walked.leaf);\n if (Number.isNaN(date.getTime())) {\n return fault(`invalid date at meta path ${path.join(\".\")}`);\n }\n if (walked.parent === undefined) return date;\n (walked.parent as Record<string | number, unknown>)[walked.key] = date;\n }\n return value;\n};\n\nexport const reviveOps = (ops: unknown[]): unknown[] =>\n ops.map((op) => {\n if (typeof op !== \"object\" || op === null || !(\"meta\" in op)) return op;\n const { meta, ...rest } = op as { meta: unknown; value?: unknown };\n return { ...rest, value: reviveValue(rest.value, meta) };\n });\n\nexport const reviveResponse = <T extends { payload?: unknown; meta?: unknown }>(\n response: T,\n): T => {\n if (response.meta === undefined) return response;\n const { meta, ...rest } = response;\n return { ...rest, payload: reviveValue(rest.payload, meta) } as T;\n};\n\nconst walkDates = (\n node: unknown,\n path: (string | number)[],\n meta: StatewireMetaEntry[],\n): unknown => {\n if (node instanceof Date) {\n meta.push({ path, type: \"date\" });\n return node.toISOString();\n }\n if (Array.isArray(node)) {\n return node.map((item, i) => walkDates(item, [...path, i], meta));\n }\n if (typeof node === \"object\" && node !== null) {\n return Object.fromEntries(\n Object.keys(node).map((key) => [\n key,\n walkDates((node as Record<string, unknown>)[key], [...path, key], meta),\n ]),\n );\n }\n return node;\n};\n\n/** Encode Date instances inside command params as canonical ISO strings,\n * recording their paths (starting at the param index) in meta. */\nexport const encodeParams = (\n params: readonly unknown[],\n): { params: unknown[]; meta?: StatewireMetaEntry[] } => {\n const meta: StatewireMetaEntry[] = [];\n const encoded = params.map((param, i) => walkDates(param, [i], meta));\n return meta.length > 0 ? { params: encoded, meta } : { params: encoded };\n};\n\n/** Encode Date instances inside a wire value, recording their paths in meta. */\nexport const encodeValue = (\n value: unknown,\n): { value: unknown; meta?: StatewireMetaEntry[] } => {\n const meta: StatewireMetaEntry[] = [];\n const encoded = walkDates(value, [], meta);\n return meta.length > 0 ? { value: encoded, meta } : { value: encoded };\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"ops.d.ts","names":[],"sources":["../../src/wire/ops.ts"],"mappings":";;KAIY;cAwDC,gBAAiB,gBAAgB;cAsGjC,UACX,eACA,IAAI,UAAU;EACX;EAAgB,cAAc;;cAUtB,WACX,gBACA;EAEA;EACA,cAAc"}
1
+ {"version":3,"file":"ops.d.ts","names":[],"sources":["../../src/wire/ops.ts"],"mappings":";;KAIY;cAwDC,gBAAiB,gBAAgB;cAiIjC,UACX,eACA,IAAI,UAAU;EACX;EAAgB,cAAc;;cAUtB,WACX,gBACA;EAEA;EACA,cAAc"}
package/dist/wire/ops.js CHANGED
@@ -31,9 +31,33 @@ const sanitizeAt = (value, parent, path) => {
31
31
  };
32
32
  const sanitizeValue = (value, parent) => sanitizeAt(value, parent, parent === void 0 ? [] : [parent]);
33
33
  const invalid = (op) => /* @__PURE__ */ new Error(`Statewire cannot apply ${op.op} at path: ${op.path.join(".")}`);
34
+ const utf16Index = (text, offset) => {
35
+ let index = 0;
36
+ for (let remaining = offset; remaining > 0; remaining -= 1) {
37
+ if (index >= text.length) return -1;
38
+ index += text.codePointAt(index) > 65535 ? 2 : 1;
39
+ }
40
+ return index;
41
+ };
42
+ const countCodePoints = (text) => {
43
+ let count = 0;
44
+ for (let index = 0; index < text.length; index += 1) {
45
+ if (text.codePointAt(index) > 65535) index += 1;
46
+ count += 1;
47
+ }
48
+ return count;
49
+ };
50
+ let last = null;
34
51
  const insertedText = (current, op, offset) => {
35
- if (typeof op.value !== "string" || offset > current.length) throw invalid(op);
36
- return current.slice(0, offset) + op.value + current.slice(offset);
52
+ if (typeof op.value !== "string") throw invalid(op);
53
+ const index = last !== null && last.str === current && last.count === offset ? current.length : utf16Index(current, offset);
54
+ if (index < 0) throw invalid(op);
55
+ const next = current.slice(0, index) + op.value + current.slice(index);
56
+ if (index === current.length) last = {
57
+ str: next,
58
+ count: offset + countCodePoints(op.value)
59
+ };
60
+ return next;
37
61
  };
38
62
  const applyAdd = (box, op) => {
39
63
  const offset = op.path[op.path.length - 1];
@@ -1 +1 @@
1
- {"version":3,"file":"ops.js","names":["recordMutations","isUnsafeKey","walkStatePath","Statewire","StatewirePath","warnStripped","key","parent","process","env","console","warn","undefined","unserializable","kind","path","Error","length","join","sanitizeAt","value","Date","getTime","Array","isArray","map","item","index","out","Record","Object","keys","sanitizeValue","invalid","op","Op","insertedText","current","Extract","offset","slice","applyAdd","box","state","walked","Number","isInteger","target","leaf","splice","next","applyInList","list","applyInObject","obj","hasOwn","applyOpToDraft","isMountPlaneOp","doc","warnedOps","Set","warnUnknownOp","type","has","add","JSON","stringify","unboxPaths","paths","applyOp","root","changedPaths","recorded","draft","applyOps","ops","mutable","push"],"sources":["../../src/wire/ops.ts"],"sourcesContent":["import { recordMutations } from \"../util/recordMutations\";\nimport { isUnsafeKey, walkStatePath } from \"../util/statePath\";\nimport type { Statewire } from \"../types\";\n\nexport type StatewirePath = readonly (string | number)[];\n\nconst warnStripped = (key: string, parent: string | undefined) => {\n if (\n typeof process === \"undefined\" ||\n process.env[\"NODE_ENV\"] !== \"production\"\n ) {\n console.warn(\n `statewire: stripped unsafe key \"${key}\"` +\n (parent === undefined ? \"\" : ` under \"${parent}\"`),\n );\n }\n};\n\nconst unserializable = (\n kind: \"function\" | \"undefined\",\n path: readonly (string | number)[],\n): never => {\n throw new Error(\n `statewire: cannot serialize ${kind}` +\n (path.length === 0 ? \"\" : ` at ${path.join(\".\")}`),\n );\n};\n\nconst sanitizeAt = (\n value: unknown,\n parent: string | undefined,\n path: readonly (string | number)[],\n): unknown => {\n if (value instanceof Date) return new Date(value.getTime());\n if (Array.isArray(value)) {\n return value.map((item, index) => {\n if (item === undefined) unserializable(\"undefined\", [...path, index]);\n return sanitizeAt(item, undefined, [...path, index]);\n });\n }\n if (typeof value === \"object\" && value !== null) {\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(value)) {\n if (isUnsafeKey(key, parent)) {\n warnStripped(key, parent);\n continue;\n }\n const item = (value as Record<string, unknown>)[key];\n if (item === undefined) continue;\n out[key] = sanitizeAt(item, key, [...path, key]);\n }\n return out;\n }\n if (typeof value === \"function\") unserializable(\"function\", path);\n return value;\n};\n\n// One clone pass: strips unsafe keys (with a warning); explicit-undefined\n// object entries are omitted (delete semantics), functions and undefined\n// list items throw.\nexport const sanitizeValue = (value: unknown, parent?: string): unknown =>\n sanitizeAt(value, parent, parent === undefined ? [] : [parent]);\n\nconst invalid = (op: Statewire.Op) =>\n new Error(`Statewire cannot apply ${op.op} at path: ${op.path.join(\".\")}`);\n\nconst insertedText = (\n current: string,\n op: Extract<Statewire.Op, { op: \"add\" }>,\n offset: number,\n): string => {\n if (typeof op.value !== \"string\" || offset > current.length) {\n throw invalid(op);\n }\n return current.slice(0, offset) + op.value + current.slice(offset);\n};\n\nconst applyAdd = (\n box: { state: unknown },\n op: Extract<Statewire.Op, { op: \"add\" }>,\n) => {\n const offset = op.path[op.path.length - 1];\n const walked = walkStatePath(box.state, op.path.slice(0, -1), \"resolve\");\n if (\n walked === undefined ||\n typeof offset !== \"number\" ||\n !Number.isInteger(offset) ||\n offset < 0\n ) {\n throw invalid(op);\n }\n const target = walked.leaf;\n if (Array.isArray(target)) {\n if (offset > target.length) throw invalid(op);\n target.splice(offset, 0, sanitizeValue(op.value));\n } else if (typeof target === \"string\") {\n const next = insertedText(target, op, offset);\n if (op.path.length === 1) {\n box.state = next;\n } else {\n (walked.parent as Record<string | number, unknown>)[walked.key] = next;\n }\n } else {\n throw invalid(op);\n }\n};\n\nconst applyInList = (list: unknown[], index: number, op: Statewire.Op) => {\n if (index >= list.length) throw invalid(op);\n if (op.op === \"replace\") {\n list[index] = sanitizeValue(op.value);\n } else {\n list.splice(index, 1);\n }\n};\n\nconst applyInObject = (\n obj: Record<string, unknown>,\n key: string,\n op: Statewire.Op,\n) => {\n if (op.op === \"replace\") {\n obj[key] = sanitizeValue(op.value, key);\n } else {\n if (!Object.hasOwn(obj, key)) throw invalid(op);\n delete obj[key];\n }\n};\n\nconst applyOpToDraft = (box: { state: unknown }, op: Statewire.Op): void => {\n if (op.op === \"add\") {\n if (op.path.length === 0) throw invalid(op);\n applyAdd(box, op);\n return;\n }\n if (op.path.length === 0) {\n if (op.op !== \"replace\") throw invalid(op);\n box.state = sanitizeValue(op.value);\n return;\n }\n const walked = walkStatePath(box.state, op.path, \"parent\");\n if (walked === undefined) throw invalid(op);\n if (typeof walked.key === \"number\") {\n applyInList(walked.parent as unknown[], walked.key, op);\n } else {\n applyInObject(walked.parent as Record<string, unknown>, walked.key, op);\n }\n};\n\nconst isMountPlaneOp = (op: { op?: unknown; doc?: unknown }) =>\n \"doc\" in op || op.op === \"mount\" || op.op === \"unmount\";\n\nconst warnedOps = new Set<unknown>();\nconst warnUnknownOp = (type: unknown) => {\n if (warnedOps.has(type)) return;\n warnedOps.add(type);\n console.warn(`statewire: ignoring unknown op type ${JSON.stringify(type)}`);\n};\n\nconst unboxPaths = (paths: readonly StatewirePath[]): StatewirePath[] =>\n paths.map((path) => path.slice(1));\n\nexport const applyOp = (\n root: unknown,\n op: Statewire.Op,\n): { state: unknown; changedPaths: StatewirePath[] } => {\n const recorded = recordMutations({ state: root }, (draft) =>\n applyOpToDraft(draft, op),\n );\n return {\n state: recorded.state.state,\n changedPaths: unboxPaths(recorded.changedPaths),\n };\n};\n\nexport const applyOps = (\n state: unknown,\n ops: readonly unknown[],\n): {\n state: unknown;\n changedPaths: StatewirePath[];\n} => {\n const mutable: Statewire.Op[] = [];\n for (const op of ops as Statewire.Op[]) {\n if (isMountPlaneOp(op)) continue;\n if (op.op === \"replace\" || op.op === \"add\" || op.op === \"remove\") {\n mutable.push(op);\n } else {\n warnUnknownOp((op as { op: unknown }).op);\n }\n }\n const recorded = recordMutations({ state }, (draft) => {\n for (const op of mutable) applyOpToDraft(draft, op);\n });\n return {\n state: recorded.state.state,\n changedPaths: unboxPaths(recorded.changedPaths),\n };\n};\n"],"mappings":";;;AAMA,MAAMK,gBAAgBC,KAAaC,WAA+B;CAChE,IACE,OAAOC,YAAY,eACnBA,QAAQC,IAAI,gBAAgB,cAE5BC,QAAQC,KACN,mCAAmCL,IAAG,MACnCC,WAAWK,KAAAA,IAAY,KAAK,WAAWL,OAAM,GAClD;AAEJ;AAEA,MAAMM,kBACJC,MACAC,SACU;CACV,MAAM,IAAIC,MACR,+BAA+BF,UAC5BC,KAAKE,WAAW,IAAI,KAAK,OAAOF,KAAKG,KAAK,GAAG,IAClD;AACF;AAEA,MAAMC,cACJC,OACAb,QACAQ,SACY;CACZ,IAAIK,iBAAiBC,MAAM,OAAO,IAAIA,KAAKD,MAAME,QAAQ,CAAC;CAC1D,IAAIC,MAAMC,QAAQJ,KAAK,GACrB,OAAOA,MAAMK,KAAKC,MAAMC,UAAU;EAChC,IAAID,SAASd,KAAAA,GAAWC,eAAe,aAAa,CAAC,GAAGE,MAAMY,KAAK,CAAC;EACpE,OAAOR,WAAWO,MAAMd,KAAAA,GAAW,CAAC,GAAGG,MAAMY,KAAK,CAAC;CACrD,CAAC;CAEH,IAAI,OAAOP,UAAU,YAAYA,UAAU,MAAM;EAC/C,MAAMQ,MAA+B,CAAC;EACtC,KAAK,MAAMtB,OAAOwB,OAAOC,KAAKX,KAAK,GAAG;GACpC,IAAInB,YAAYK,KAAKC,MAAM,GAAG;IAC5BF,aAAaC,KAAKC,MAAM;IACxB;GACF;GACA,MAAMmB,OAAQN,MAAkCd;GAChD,IAAIoB,SAASd,KAAAA,GAAW;GACxBgB,IAAItB,OAAOa,WAAWO,MAAMpB,KAAK,CAAC,GAAGS,MAAMT,GAAG,CAAC;EACjD;EACA,OAAOsB;CACT;CACA,IAAI,OAAOR,UAAU,YAAYP,eAAe,YAAYE,IAAI;CAChE,OAAOK;AACT;AAKA,MAAaY,iBAAiBZ,OAAgBb,WAC5CY,WAAWC,OAAOb,QAAQA,WAAWK,KAAAA,IAAY,CAAA,IAAK,CAACL,MAAM,CAAC;AAEhE,MAAM0B,WAAWC,uBACf,IAAIlB,MAAM,0BAA0BkB,GAAGA,GAAE,YAAaA,GAAGnB,KAAKG,KAAK,GAAG,GAAG;AAE3E,MAAMkB,gBACJC,SACAH,IACAK,WACW;CACX,IAAI,OAAOL,GAAGd,UAAU,YAAYmB,SAASF,QAAQpB,QACnD,MAAMgB,QAAQC,EAAE;CAElB,OAAOG,QAAQG,MAAM,GAAGD,MAAM,IAAIL,GAAGd,QAAQiB,QAAQG,MAAMD,MAAM;AACnE;AAEA,MAAME,YACJC,KACAR,OACG;CACH,MAAMK,SAASL,GAAGnB,KAAKmB,GAAGnB,KAAKE,SAAS;CACxC,MAAM2B,SAAS1C,cAAcwC,IAAIC,OAAOT,GAAGnB,KAAKyB,MAAM,GAAG,EAAE,GAAG,SAAS;CACvE,IACEI,WAAWhC,KAAAA,KACX,OAAO2B,WAAW,YAClB,CAACM,OAAOC,UAAUP,MAAM,KACxBA,SAAS,GAET,MAAMN,QAAQC,EAAE;CAElB,MAAMa,SAASH,OAAOI;CACtB,IAAIzB,MAAMC,QAAQuB,MAAM,GAAG;EACzB,IAAIR,SAASQ,OAAO9B,QAAQ,MAAMgB,QAAQC,EAAE;EAC5Ca,OAAOE,OAAOV,QAAQ,GAAGP,cAAcE,GAAGd,KAAK,CAAC;CAClD,OAAO,IAAI,OAAO2B,WAAW,UAAU;EACrC,MAAMG,OAAOd,aAAaW,QAAQb,IAAIK,MAAM;EAC5C,IAAIL,GAAGnB,KAAKE,WAAW,GACrByB,IAAIC,QAAQO;OAEZ,OAAQ3C,OAA4CqC,OAAOtC,OAAO4C;CAEtE,OACE,MAAMjB,QAAQC,EAAE;AAEpB;AAEA,MAAMiB,eAAeC,MAAiBzB,OAAeO,OAAqB;CACxE,IAAIP,SAASyB,KAAKnC,QAAQ,MAAMgB,QAAQC,EAAE;CAC1C,IAAIA,GAAGA,OAAO,WACZkB,KAAKzB,SAASK,cAAcE,GAAGd,KAAK;MAEpCgC,KAAKH,OAAOtB,OAAO,CAAC;AAExB;AAEA,MAAM0B,iBACJC,KACAhD,KACA4B,OACG;CACH,IAAIA,GAAGA,OAAO,WACZoB,IAAIhD,OAAO0B,cAAcE,GAAGd,OAAOd,GAAG;MACjC;EACL,IAAI,CAACwB,OAAOyB,OAAOD,KAAKhD,GAAG,GAAG,MAAM2B,QAAQC,EAAE;EAC9C,OAAOoB,IAAIhD;CACb;AACF;AAEA,MAAMkD,kBAAkBd,KAAyBR,OAA2B;CAC1E,IAAIA,GAAGA,OAAO,OAAO;EACnB,IAAIA,GAAGnB,KAAKE,WAAW,GAAG,MAAMgB,QAAQC,EAAE;EAC1CO,SAASC,KAAKR,EAAE;EAChB;CACF;CACA,IAAIA,GAAGnB,KAAKE,WAAW,GAAG;EACxB,IAAIiB,GAAGA,OAAO,WAAW,MAAMD,QAAQC,EAAE;EACzCQ,IAAIC,QAAQX,cAAcE,GAAGd,KAAK;EAClC;CACF;CACA,MAAMwB,SAAS1C,cAAcwC,IAAIC,OAAOT,GAAGnB,MAAM,QAAQ;CACzD,IAAI6B,WAAWhC,KAAAA,GAAW,MAAMqB,QAAQC,EAAE;CAC1C,IAAI,OAAOU,OAAOtC,QAAQ,UACxB6C,YAAYP,OAAOrC,QAAqBqC,OAAOtC,KAAK4B,EAAE;MAEtDmB,cAAcT,OAAOrC,QAAmCqC,OAAOtC,KAAK4B,EAAE;AAE1E;AAEA,MAAMuB,kBAAkBvB,OACtB,SAASA,MAAMA,GAAGA,OAAO,WAAWA,GAAGA,OAAO;AAEhD,MAAMyB,4BAAY,IAAIC,IAAa;AACnC,MAAMC,iBAAiBC,SAAkB;CACvC,IAAIH,UAAUI,IAAID,IAAI,GAAG;CACzBH,UAAUK,IAAIF,IAAI;CAClBpD,QAAQC,KAAK,uCAAuCsD,KAAKC,UAAUJ,IAAI,GAAG;AAC5E;AAEA,MAAMK,cAAcC,UAClBA,MAAM3C,KAAKV,SAASA,KAAKyB,MAAM,CAAC,CAAC;AAEnC,MAAa6B,WACXC,MACApC,OACsD;CACtD,MAAMsC,WAAWxE,gBAAgB,EAAE2C,OAAO2B,KAAK,IAAIG,UACjDjB,eAAeiB,OAAOvC,EAAE,CAC1B;CACA,OAAO;EACLS,OAAO6B,SAAS7B,MAAMA;EACtB4B,cAAcJ,WAAWK,SAASD,YAAY;CAChD;AACF;AAEA,MAAaG,YACX/B,OACAgC,QAIG;CACH,MAAMC,UAA0B,CAAA;CAChC,KAAK,MAAM1C,MAAMyC,KAAuB;EACtC,IAAIlB,eAAevB,EAAE,GAAG;EACxB,IAAIA,GAAGA,OAAO,aAAaA,GAAGA,OAAO,SAASA,GAAGA,OAAO,UACtD0C,QAAQC,KAAK3C,EAAE;OAEf2B,cAAe3B,GAAuBA,EAAE;CAE5C;CACA,MAAMsC,WAAWxE,gBAAgB,EAAE2C,MAAM,IAAI8B,UAAU;EACrD,KAAK,MAAMvC,MAAM0C,SAASpB,eAAeiB,OAAOvC,EAAE;CACpD,CAAC;CACD,OAAO;EACLS,OAAO6B,SAAS7B,MAAMA;EACtB4B,cAAcJ,WAAWK,SAASD,YAAY;CAChD;AACF"}
1
+ {"version":3,"file":"ops.js","names":["recordMutations","isUnsafeKey","walkStatePath","Statewire","StatewirePath","warnStripped","key","parent","process","env","console","warn","undefined","unserializable","kind","path","Error","length","join","sanitizeAt","value","Date","getTime","Array","isArray","map","item","index","out","Record","Object","keys","sanitizeValue","invalid","op","Op","utf16Index","text","offset","remaining","codePointAt","countCodePoints","count","last","str","insertedText","current","Extract","memoized","next","slice","applyAdd","box","state","walked","Number","isInteger","target","leaf","splice","applyInList","list","applyInObject","obj","hasOwn","applyOpToDraft","isMountPlaneOp","doc","warnedOps","Set","warnUnknownOp","type","has","add","JSON","stringify","unboxPaths","paths","applyOp","root","changedPaths","recorded","draft","applyOps","ops","mutable","push"],"sources":["../../src/wire/ops.ts"],"sourcesContent":["import { recordMutations } from \"../util/recordMutations\";\nimport { isUnsafeKey, walkStatePath } from \"../util/statePath\";\nimport type { Statewire } from \"../types\";\n\nexport type StatewirePath = readonly (string | number)[];\n\nconst warnStripped = (key: string, parent: string | undefined) => {\n if (\n typeof process === \"undefined\" ||\n process.env[\"NODE_ENV\"] !== \"production\"\n ) {\n console.warn(\n `statewire: stripped unsafe key \"${key}\"` +\n (parent === undefined ? \"\" : ` under \"${parent}\"`),\n );\n }\n};\n\nconst unserializable = (\n kind: \"function\" | \"undefined\",\n path: readonly (string | number)[],\n): never => {\n throw new Error(\n `statewire: cannot serialize ${kind}` +\n (path.length === 0 ? \"\" : ` at ${path.join(\".\")}`),\n );\n};\n\nconst sanitizeAt = (\n value: unknown,\n parent: string | undefined,\n path: readonly (string | number)[],\n): unknown => {\n if (value instanceof Date) return new Date(value.getTime());\n if (Array.isArray(value)) {\n return value.map((item, index) => {\n if (item === undefined) unserializable(\"undefined\", [...path, index]);\n return sanitizeAt(item, undefined, [...path, index]);\n });\n }\n if (typeof value === \"object\" && value !== null) {\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(value)) {\n if (isUnsafeKey(key, parent)) {\n warnStripped(key, parent);\n continue;\n }\n const item = (value as Record<string, unknown>)[key];\n if (item === undefined) continue;\n out[key] = sanitizeAt(item, key, [...path, key]);\n }\n return out;\n }\n if (typeof value === \"function\") unserializable(\"function\", path);\n return value;\n};\n\n// One clone pass: strips unsafe keys (with a warning); explicit-undefined\n// object entries are omitted (delete semantics), functions and undefined\n// list items throw.\nexport const sanitizeValue = (value: unknown, parent?: string): unknown =>\n sanitizeAt(value, parent, parent === undefined ? [] : [parent]);\n\nconst invalid = (op: Statewire.Op) =>\n new Error(`Statewire cannot apply ${op.op} at path: ${op.path.join(\".\")}`);\n\n// Text-insert offsets are Unicode code points on the wire, not UTF-16 units.\nconst utf16Index = (text: string, offset: number): number => {\n let index = 0;\n for (let remaining = offset; remaining > 0; remaining -= 1) {\n if (index >= text.length) return -1;\n index += text.codePointAt(index)! > 0xffff ? 2 : 1;\n }\n return index;\n};\n\nconst countCodePoints = (text: string): number => {\n let count = 0;\n for (let index = 0; index < text.length; index += 1) {\n if (text.codePointAt(index)! > 0xffff) index += 1;\n count += 1;\n }\n return count;\n};\n\nlet last: { str: string; count: number } | null = null;\n\nconst insertedText = (\n current: string,\n op: Extract<Statewire.Op, { op: \"add\" }>,\n offset: number,\n): string => {\n if (typeof op.value !== \"string\") throw invalid(op);\n const memoized =\n last !== null && last.str === current && last.count === offset;\n const index = memoized ? current.length : utf16Index(current, offset);\n if (index < 0) throw invalid(op);\n const next = current.slice(0, index) + op.value + current.slice(index);\n if (index === current.length) {\n last = { str: next, count: offset + countCodePoints(op.value) };\n }\n return next;\n};\n\nconst applyAdd = (\n box: { state: unknown },\n op: Extract<Statewire.Op, { op: \"add\" }>,\n) => {\n const offset = op.path[op.path.length - 1];\n const walked = walkStatePath(box.state, op.path.slice(0, -1), \"resolve\");\n if (\n walked === undefined ||\n typeof offset !== \"number\" ||\n !Number.isInteger(offset) ||\n offset < 0\n ) {\n throw invalid(op);\n }\n const target = walked.leaf;\n if (Array.isArray(target)) {\n if (offset > target.length) throw invalid(op);\n target.splice(offset, 0, sanitizeValue(op.value));\n } else if (typeof target === \"string\") {\n const next = insertedText(target, op, offset);\n if (op.path.length === 1) {\n box.state = next;\n } else {\n (walked.parent as Record<string | number, unknown>)[walked.key] = next;\n }\n } else {\n throw invalid(op);\n }\n};\n\nconst applyInList = (list: unknown[], index: number, op: Statewire.Op) => {\n if (index >= list.length) throw invalid(op);\n if (op.op === \"replace\") {\n list[index] = sanitizeValue(op.value);\n } else {\n list.splice(index, 1);\n }\n};\n\nconst applyInObject = (\n obj: Record<string, unknown>,\n key: string,\n op: Statewire.Op,\n) => {\n if (op.op === \"replace\") {\n obj[key] = sanitizeValue(op.value, key);\n } else {\n if (!Object.hasOwn(obj, key)) throw invalid(op);\n delete obj[key];\n }\n};\n\nconst applyOpToDraft = (box: { state: unknown }, op: Statewire.Op): void => {\n if (op.op === \"add\") {\n if (op.path.length === 0) throw invalid(op);\n applyAdd(box, op);\n return;\n }\n if (op.path.length === 0) {\n if (op.op !== \"replace\") throw invalid(op);\n box.state = sanitizeValue(op.value);\n return;\n }\n const walked = walkStatePath(box.state, op.path, \"parent\");\n if (walked === undefined) throw invalid(op);\n if (typeof walked.key === \"number\") {\n applyInList(walked.parent as unknown[], walked.key, op);\n } else {\n applyInObject(walked.parent as Record<string, unknown>, walked.key, op);\n }\n};\n\nconst isMountPlaneOp = (op: { op?: unknown; doc?: unknown }) =>\n \"doc\" in op || op.op === \"mount\" || op.op === \"unmount\";\n\nconst warnedOps = new Set<unknown>();\nconst warnUnknownOp = (type: unknown) => {\n if (warnedOps.has(type)) return;\n warnedOps.add(type);\n console.warn(`statewire: ignoring unknown op type ${JSON.stringify(type)}`);\n};\n\nconst unboxPaths = (paths: readonly StatewirePath[]): StatewirePath[] =>\n paths.map((path) => path.slice(1));\n\nexport const applyOp = (\n root: unknown,\n op: Statewire.Op,\n): { state: unknown; changedPaths: StatewirePath[] } => {\n const recorded = recordMutations({ state: root }, (draft) =>\n applyOpToDraft(draft, op),\n );\n return {\n state: recorded.state.state,\n changedPaths: unboxPaths(recorded.changedPaths),\n };\n};\n\nexport const applyOps = (\n state: unknown,\n ops: readonly unknown[],\n): {\n state: unknown;\n changedPaths: StatewirePath[];\n} => {\n const mutable: Statewire.Op[] = [];\n for (const op of ops as Statewire.Op[]) {\n if (isMountPlaneOp(op)) continue;\n if (op.op === \"replace\" || op.op === \"add\" || op.op === \"remove\") {\n mutable.push(op);\n } else {\n warnUnknownOp((op as { op: unknown }).op);\n }\n }\n const recorded = recordMutations({ state }, (draft) => {\n for (const op of mutable) applyOpToDraft(draft, op);\n });\n return {\n state: recorded.state.state,\n changedPaths: unboxPaths(recorded.changedPaths),\n };\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "statewire",
3
- "version": "0.11.0",
3
+ "version": "0.12.2",
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": {
@@ -11,9 +11,18 @@ export type Episode = {
11
11
  message?: string;
12
12
  };
13
13
 
14
+ /** The terminal cmd verdict types; "applied" is the sole non-terminal type. */
15
+ export const TERMINAL = [
16
+ "result",
17
+ "rejected",
18
+ "failed",
19
+ "crashed",
20
+ "expired",
21
+ ] as const;
22
+
14
23
  export type StatewireCmd = {
15
24
  seq: number;
16
- type?: "applied" | "result" | "rejected" | "failed" | "crashed" | "expired";
25
+ type?: "applied" | (typeof TERMINAL)[number];
17
26
  code?: string;
18
27
  message?: string;
19
28
  payload?: unknown;
@@ -25,7 +34,7 @@ export type StatewireCmd = {
25
34
  export type StatewireSyn = {
26
35
  seq?: number;
27
36
  ctxSeq?: number;
28
- record?: "fresh" | "resumed" | "ephemeral";
37
+ record?: "ephemeral";
29
38
  id?: string;
30
39
  idle?: boolean;
31
40
  retry?: number;
@@ -344,10 +353,7 @@ const settleFin = (c: Conn, fin: StatewireHost.Fin): AttemptOutcome => {
344
353
  }
345
354
  // An auth denial gets one immediate redial; a consecutive repeat parks as gone.
346
355
  const denial = finish.reason === "unauthorized";
347
- const terminal =
348
- finish.reason === "gone" ||
349
- finish.reason === "superseded" ||
350
- (denial && c.authDenials > 0);
356
+ const terminal = finish.reason === "gone" || (denial && c.authDenials > 0);
351
357
  if (denial) c.authDenials += 1;
352
358
  c.ctx.onFinish(terminal ? finish : { ...finish, reason: "error" });
353
359
  if (!terminal) {