statewire 0.8.1 → 0.9.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.
Files changed (95) hide show
  1. package/README.md +2 -2
  2. package/dist/client/connection.d.ts +23 -19
  3. package/dist/client/connection.d.ts.map +1 -1
  4. package/dist/client/connection.js +143 -103
  5. package/dist/client/connection.js.map +1 -1
  6. package/dist/client/core.d.ts +30 -3
  7. package/dist/client/core.d.ts.map +1 -1
  8. package/dist/client/core.js +222 -260
  9. package/dist/client/core.js.map +1 -1
  10. package/dist/client/session.d.ts +34 -39
  11. package/dist/client/session.d.ts.map +1 -1
  12. package/dist/client/session.js +380 -373
  13. package/dist/client/session.js.map +1 -1
  14. package/dist/client/sessionStore.d.ts +1 -0
  15. package/dist/client/sessionStore.d.ts.map +1 -1
  16. package/dist/client/sessionStore.js.map +1 -1
  17. package/dist/client/timing.d.ts +1 -2
  18. package/dist/client/timing.d.ts.map +1 -1
  19. package/dist/client/timing.js +1 -5
  20. package/dist/client/timing.js.map +1 -1
  21. package/dist/host/draft.d.ts +2 -5
  22. package/dist/host/draft.d.ts.map +1 -1
  23. package/dist/host/draft.js.map +1 -1
  24. package/dist/host/host.d.ts +5 -8
  25. package/dist/host/host.d.ts.map +1 -1
  26. package/dist/host/host.js +23 -32
  27. package/dist/host/host.js.map +1 -1
  28. package/dist/host/socket.d.ts +3 -2
  29. package/dist/host/socket.d.ts.map +1 -1
  30. package/dist/host/socket.js +484 -424
  31. package/dist/host/socket.js.map +1 -1
  32. package/dist/host-internal.d.ts +4 -0
  33. package/dist/host-internal.d.ts.map +1 -1
  34. package/dist/host.d.ts +10 -10
  35. package/dist/host.d.ts.map +1 -1
  36. package/dist/host.js +2 -2
  37. package/dist/index.d.ts +1 -2
  38. package/dist/index.js +1 -2
  39. package/dist/statewire.d.ts +29 -24
  40. package/dist/statewire.d.ts.map +1 -1
  41. package/dist/statewire.js +8 -2
  42. package/dist/statewire.js.map +1 -1
  43. package/dist/transports/fake-sse.d.ts +1 -1
  44. package/dist/transports/fake-sse.d.ts.map +1 -1
  45. package/dist/transports/fake-sse.js +5 -2
  46. package/dist/transports/fake-sse.js.map +1 -1
  47. package/dist/transports/fixtures.d.ts +1 -2
  48. package/dist/transports/fixtures.d.ts.map +1 -1
  49. package/dist/transports/fixtures.js +5 -7
  50. package/dist/transports/fixtures.js.map +1 -1
  51. package/dist/transports/remote.d.ts +2 -1
  52. package/dist/transports/remote.d.ts.map +1 -1
  53. package/dist/transports/remote.js +10 -4
  54. package/dist/transports/remote.js.map +1 -1
  55. package/dist/transports/sse.d.ts +32 -46
  56. package/dist/transports/sse.d.ts.map +1 -1
  57. package/dist/transports/sse.js +56 -59
  58. package/dist/transports/sse.js.map +1 -1
  59. package/dist/transports/testing.d.ts +7 -5
  60. package/dist/transports/testing.d.ts.map +1 -1
  61. package/dist/transports/testing.js +41 -14
  62. package/dist/transports/testing.js.map +1 -1
  63. package/dist/transports/ws.d.ts +2 -3
  64. package/dist/transports/ws.d.ts.map +1 -1
  65. package/dist/transports/ws.js +25 -80
  66. package/dist/transports/ws.js.map +1 -1
  67. package/dist/types.d.ts +8 -8
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/types.js +3 -4
  70. package/dist/types.js.map +1 -1
  71. package/dist/wire/ops.d.ts +1 -5
  72. package/dist/wire/ops.d.ts.map +1 -1
  73. package/dist/wire/ops.js +1 -4
  74. package/dist/wire/ops.js.map +1 -1
  75. package/package.json +1 -1
  76. package/src/client/connection.ts +175 -128
  77. package/src/client/core.ts +287 -278
  78. package/src/client/session.ts +474 -480
  79. package/src/client/sessionStore.ts +1 -0
  80. package/src/client/timing.ts +0 -6
  81. package/src/host/draft.ts +1 -3
  82. package/src/host/host.ts +41 -58
  83. package/src/host/socket.ts +653 -448
  84. package/src/host-internal.ts +4 -0
  85. package/src/host.ts +13 -10
  86. package/src/index.ts +0 -1
  87. package/src/statewire.ts +38 -32
  88. package/src/transports/fake-sse.ts +2 -1
  89. package/src/transports/fixtures.ts +4 -9
  90. package/src/transports/remote.ts +12 -5
  91. package/src/transports/sse.ts +99 -115
  92. package/src/transports/testing.ts +47 -19
  93. package/src/transports/ws.ts +44 -115
  94. package/src/types.ts +15 -14
  95. package/src/wire/ops.ts +9 -16
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","names":["ResourceElement","StatewireEnvelope","StatewireSSE","Statewire","openSSEStream","sseFrame","FakeStreamHandle","tick","Promise","r","setTimeout","applyOptimistic","MockTransport","transport","TState","commands","method","params","context","setState","state","ack","seq","sendEnvelope","envelope","finish","reason","message","clientId","attached","mockCount","createMockTransport","initialState","finished","clients","Map","streams","Set","stream","send","fakeFetch","input","RequestInfo","URL","init","RequestInit","Response","TypeError","url","String","headers","Headers","id","get","endsWith","lastSeq","set","handle","ops","op","path","value","syn","lease","signal","undefined","Number","Math","max","push","JSON","parse","body","json","status","fetch","next","fin","close","size"],"sources":["../../src/transports/testing.ts"],"sourcesContent":["import type { ResourceElement } from \"@assistant-ui/tap\";\nimport type { StatewireEnvelope } from \"../client/connection\";\nimport { StatewireSSE } from \"./sse\";\nimport type { Statewire } from \"../types\";\nimport { openSSEStream, sseFrame } from \"./fake-sse\";\nimport type { FakeStreamHandle } from \"./fake-sse\";\n\nexport * from \"./fixtures\";\n\n/** Let queued microtasks and op batches drain through the statewire client. */\nexport const tick = () => new Promise<void>((r) => setTimeout(r, 0));\n\nexport { applyOptimistic } from \"../util/recordMutations\";\n\nexport type MockTransport<TState> = {\n /** Pass as the `transport:` option of useStatewire / Harness. */\n transport: ResourceElement<Statewire<TState | undefined, any, any>>;\n /** Captured `/commands` POST bodies, in POST order. */\n commands: { method?: string; params?: unknown[]; context?: unknown }[];\n /** Replace the replicated state and broadcast it as a root snapshot op. */\n setState: (state: TState) => Promise<void>;\n /** Deliver an ack watermark on the stream. */\n ack: (seq: number) => Promise<void>;\n /** Deliver an arbitrary envelope on the stream. */\n sendEnvelope: (envelope: StatewireEnvelope) => Promise<void>;\n /** End the stream with a fin; further attaches fail like a dead server. */\n finish: (reason: string, message?: string) => Promise<void>;\n readonly clientId: string;\n readonly attached: boolean;\n};\n\n/**\n * A mock statewire backend: serves `initialState` as the attach snapshot,\n * records command POSTs (receipt only — acks and results are delivered from\n * test code), and exposes the wire as a transport ResourceElement.\n */\nlet mockCount = 0;\n\nexport const createMockTransport = <TState>(\n initialState: TState,\n): MockTransport<TState> => {\n let state: unknown = initialState;\n let clientId = \"\";\n let finished = false;\n const clients = new Map<string, number>();\n const streams = new Set<FakeStreamHandle>();\n const commands: MockTransport<unknown>[\"commands\"] = [];\n\n const sendEnvelope = (envelope: StatewireEnvelope) => {\n for (const stream of streams) stream.send(sseFrame(envelope));\n return tick();\n };\n\n const fakeFetch = (async (\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> => {\n if (finished) throw new TypeError(\"fetch failed (finished)\");\n const url = String(input);\n const headers = new Headers(init?.headers);\n const id = headers.get(\"Statewire-Client-Id\") ?? \"\";\n if (url.endsWith(\"/stream\")) {\n clientId = id;\n const lastSeq = clients.get(id) ?? 0;\n clients.set(id, lastSeq);\n return openSSEStream(\n streams,\n id,\n (handle) => {\n handle.send(\n sseFrame({\n ops: [{ op: \"replace\", path: [], value: state }],\n syn: { lastSeq, lease: \"mock-lease\" },\n }),\n );\n },\n init?.signal ?? undefined,\n );\n }\n if (url.endsWith(\"/commands\")) {\n const seq = Number(headers.get(\"Statewire-Command-Seq\"));\n clients.set(id, Math.max(clients.get(id) ?? 0, seq));\n commands.push(\n JSON.parse(\n String(init?.body),\n ) as MockTransport<unknown>[\"commands\"][number],\n );\n return Response.json({});\n }\n return new Response(null, { status: 404 });\n }) as typeof fetch;\n\n return {\n transport: StatewireSSE({\n url: `http://mock-${++mockCount}/api`,\n fetch: fakeFetch,\n }),\n commands,\n setState: (next) => {\n state = next;\n return sendEnvelope({ ops: [{ op: \"replace\", path: [], value: next }] });\n },\n ack: (seq) => sendEnvelope({ ack: seq }),\n sendEnvelope,\n finish: async (reason, message) => {\n await sendEnvelope({\n fin: { reason, ...(message !== undefined && { message }) },\n });\n finished = true;\n for (const stream of [...streams]) stream.close();\n await tick();\n },\n get clientId() {\n return clientId;\n },\n get attached() {\n return streams.size > 0;\n },\n };\n};\n"],"mappings":";;;;;;AAUA,MAAaO,aAAa,IAAIC,SAAeC,MAAMC,WAAWD,GAAG,CAAC,CAAC;;;;;;AA0BnE,IAAIqB,YAAY;AAEhB,MAAaC,uBACXC,iBAC0B;CAC1B,IAAIZ,QAAiBY;CACrB,IAAIJ,WAAW;CACf,IAAIK,WAAW;CACf,MAAMC,0BAAU,IAAIC,IAAoB;CACxC,MAAMC,0BAAU,IAAIC,IAAsB;CAC1C,MAAMtB,WAA+C,CAAA;CAErD,MAAMQ,gBAAgBC,aAAgC;EACpD,KAAK,MAAMc,UAAUF,SAASE,OAAOC,KAAKlC,SAASmB,QAAQ,CAAC;EAC5D,OAAOjB,KAAK;CACd;CAEA,MAAMiC,aAAa,OACjBC,OACAG,SACsB;EACtB,IAAIX,UAAU,MAAM,IAAIc,UAAU,yBAAyB;EAC3D,MAAMC,MAAMC,OAAOR,KAAK;EACxB,MAAMS,UAAU,IAAIC,QAAQP,MAAMM,OAAO;EACzC,MAAME,KAAKF,QAAQG,IAAI,qBAAqB,KAAK;EACjD,IAAIL,IAAIM,SAAS,SAAS,GAAG;GAC3B1B,WAAWwB;GACX,MAAMG,UAAUrB,QAAQmB,IAAID,EAAE,KAAK;GACnClB,QAAQsB,IAAIJ,IAAIG,OAAO;GACvB,OAAOnD,cACLgC,SACAgB,KACCK,WAAW;IACVA,OAAOlB,KACLlC,SAAS;KACPqD,KAAK,CAAC;MAAEC,IAAI;MAAWC,MAAM,CAAA;MAAIC,OAAOzC;KAAM,CAAC;KAC/C0C,KAAK;MAAEP;MAASQ,OAAO;KAAa;IACtC,CAAC,CACH;GACF,GACAnB,MAAMoB,UAAUC,KAAAA,CAClB;EACF;EACA,IAAIjB,IAAIM,SAAS,WAAW,GAAG;GAC7B,MAAMhC,MAAM4C,OAAOhB,QAAQG,IAAI,uBAAuB,CAAC;GACvDnB,QAAQsB,IAAIJ,IAAIe,KAAKC,IAAIlC,QAAQmB,IAAID,EAAE,KAAK,GAAG9B,GAAG,CAAC;GACnDP,SAASsD,KACPC,KAAKC,MACHtB,OAAOL,MAAM4B,IAAI,CACnB,CACF;GACA,OAAO1B,SAAS2B,KAAK,CAAC,CAAC;EACzB;EACA,OAAO,IAAI3B,SAAS,MAAM,EAAE4B,QAAQ,IAAI,CAAC;CAC3C;CAEA,OAAO;EACL7D,WAAWX,aAAa;GACtB8C,KAAK,eAAe,EAAElB,UAAS;GAC/B6C,OAAOnC;EACT,CAAC;EACDzB;EACAI,WAAWyD,SAAS;GAClBxD,QAAQwD;GACR,OAAOrD,aAAa,EAAEmC,KAAK,CAAC;IAAEC,IAAI;IAAWC,MAAM,CAAA;IAAIC,OAAOe;GAAK,CAAC,EAAE,CAAC;EACzE;EACAvD,MAAMC,QAAQC,aAAa,EAAEF,KAAKC,IAAI,CAAC;EACvCC;EACAE,QAAQ,OAAOC,QAAQC,YAAY;GACjC,MAAMJ,aAAa,EACjBsD,KAAK;IAAEnD;IAAQ,GAAIC,YAAYsC,KAAAA,KAAa,EAAEtC,QAAQ;GAAG,EAC3D,CAAC;GACDM,WAAW;GACX,KAAK,MAAMK,UAAU,CAAC,GAAGF,OAAO,GAAGE,OAAOwC,MAAM;GAChD,MAAMvE,KAAK;EACb;EACA,IAAIqB,WAAW;GACb,OAAOA;EACT;EACA,IAAIC,WAAW;GACb,OAAOO,QAAQ2C,OAAO;EACxB;CACF;AACF"}
1
+ {"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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"}
@@ -3,8 +3,7 @@ import { Statewire } from "../types.js";
3
3
  import { ResourceElement } from "@assistant-ui/tap";
4
4
  //#region src/transports/ws.d.ts
5
5
  declare const WS_SUBPROTOCOL = "statewire.v1";
6
- declare const UNAUTHORIZED_CLOSE = 4401;
7
- declare const StatewireWS: <TState = any, TCommands extends Statewire.Commands = Statewire.Commands, TEvent = any>(options: StatewireClient.WSOptions) => ResourceElement<Statewire<TState | undefined, TCommands, TEvent>>;
6
+ declare const StatewireWS: <TState = any, TCommands extends Statewire.Commands = Statewire.Commands>(options: StatewireClient.WSOptions) => ResourceElement<Statewire<TState | undefined, TCommands>>;
8
7
  //#endregion
9
- export { StatewireWS, UNAUTHORIZED_CLOSE, WS_SUBPROTOCOL };
8
+ export { StatewireWS, WS_SUBPROTOCOL };
10
9
  //# sourceMappingURL=ws.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ws.d.ts","names":[],"sources":["../../src/transports/ws.ts"],"mappings":";;;;cAoCa;cACA;cA6OA,cACX,cACA,kBAAkB,UAAU,WAAW,UAAU,UACjD,cAEA,SAAS,gBAAgB,cACtB,gBAAgB,UAAU,oBAAoB,WAAW"}
1
+ {"version":3,"file":"ws.d.ts","names":[],"sources":["../../src/transports/ws.ts"],"mappings":";;;;cA4Ba;cAgLA,cACX,cACA,kBAAkB,UAAU,WAAW,UAAU,UAEjD,SAAS,gBAAgB,cACtB,gBAAgB,UAAU,oBAAoB"}
@@ -1,8 +1,7 @@
1
1
  import { CLIENT_QUERY_PARAM } from "../host/adapter.js";
2
2
  import { timing } from "../client/timing.js";
3
- import { handleEnvelope, overloadedError, useStatewireConn } from "../client/connection.js";
3
+ import { handleEnvelope, useStatewireConn } from "../client/connection.js";
4
4
  import { useStatewireRemote } from "./remote.js";
5
- import { encodeWireCommand } from "./sse.js";
6
5
  import { useState } from "@assistant-ui/tap/react-shim";
7
6
  import { flushTapSync, resource } from "@assistant-ui/tap";
8
7
  //#region src/transports/ws.ts
@@ -15,33 +14,14 @@ const socketUrl = async (options, clientId) => {
15
14
  params.set(CLIENT_QUERY_PARAM, clientId);
16
15
  return `${url.replace(/\/$/, "").replace(/^http/, "ws")}/ws?${params.toString()}`;
17
16
  };
18
- const headersFrame = async (options, stale) => {
17
+ const firstFrame = async (options, stale, lanes) => {
19
18
  const { headers } = options;
20
- if (headers === void 0) return null;
21
19
  const resolved = new Headers(typeof headers === "function" ? await headers({ staleAuth: stale }) : headers);
22
20
  return JSON.stringify({
23
- type: "headers",
24
- headers: Object.fromEntries(resolved)
21
+ headers: Object.fromEntries(resolved),
22
+ ...lanes ?? {}
25
23
  });
26
24
  };
27
- const settleOverloaded = (w, { seq, retryAfterMs }) => {
28
- if (seq === void 0) throw overloadedError(retryAfterMs);
29
- const entry = w.pending.find((p) => p.seq === seq);
30
- if (!entry) return;
31
- w.pending = w.pending.filter((p) => p !== entry);
32
- entry.reject(overloadedError(retryAfterMs));
33
- };
34
- const resolvePending = (w, matches) => {
35
- const remaining = [];
36
- for (const entry of w.pending) if (matches(entry)) entry.resolve({ type: "receipt" });
37
- else remaining.push(entry);
38
- w.pending = remaining;
39
- };
40
- const failPending = (w, error) => {
41
- const pending = w.pending;
42
- w.pending = [];
43
- for (const entry of pending) entry.reject(error);
44
- };
45
25
  const runSocket = (c, w, ws, frame, staleAuth, abort) => new Promise((resolve, reject) => {
46
26
  let timer;
47
27
  let timedOut = false;
@@ -57,41 +37,30 @@ const runSocket = (c, w, ws, frame, staleAuth, abort) => new Promise((resolve, r
57
37
  arm();
58
38
  abort.signal.addEventListener("abort", () => ws.close());
59
39
  ws.onerror = () => {};
60
- if (frame !== null) ws.onopen = () => ws.send(frame);
40
+ ws.onopen = () => ws.send(frame);
61
41
  const stale = () => Boolean(outcome) || closed || abort.signal.aborted;
62
42
  const fail = (error) => {
63
43
  clearTimeout(timer);
64
44
  reject(error instanceof Error ? error : /* @__PURE__ */ new Error("protocol"));
65
45
  ws.close();
66
46
  };
67
- const processEnv = (env) => {
68
- const done = flushTapSync(() => handleEnvelope(c, env));
69
- if (env.ack !== void 0) {
70
- const ack = env.ack;
71
- resolvePending(w, (entry) => entry.seq <= ack);
72
- }
73
- if (env.res !== void 0) {
74
- const seqs = new Set(env.res.map((r) => r.seq));
75
- resolvePending(w, (entry) => seqs.has(entry.seq));
76
- }
77
- if (done) {
78
- outcome = done;
79
- ws.close();
80
- }
81
- };
82
47
  ws.onmessage = (event) => {
83
48
  if (stale()) return;
84
49
  arm();
85
- let frame;
50
+ let env;
86
51
  try {
87
- frame = JSON.parse(String(event.data));
52
+ env = JSON.parse(String(event.data));
88
53
  } catch {
89
54
  fail(/* @__PURE__ */ new Error("protocol"));
90
55
  return;
91
56
  }
57
+ if (typeof env === "object" && env !== null && Object.keys(env).length === 0) return;
92
58
  try {
93
- if (frame.overloaded !== void 0) settleOverloaded(w, frame.overloaded);
94
- else processEnv(frame);
59
+ const done = flushTapSync(() => handleEnvelope(c, env));
60
+ if (done) {
61
+ outcome = done;
62
+ ws.close();
63
+ }
95
64
  } catch (error) {
96
65
  fail(error);
97
66
  }
@@ -99,9 +68,8 @@ const runSocket = (c, w, ws, frame, staleAuth, abort) => new Promise((resolve, r
99
68
  ws.onclose = (event) => {
100
69
  closed = true;
101
70
  clearTimeout(timer);
102
- if (w.socket === ws) failPending(w, /* @__PURE__ */ new Error("network"));
103
71
  if (outcome) resolve(outcome);
104
- else if (event?.code === 4401) {
72
+ else if (event?.code === UNAUTHORIZED_CLOSE) {
105
73
  if (staleAuth) c.episode = { cause: "unauthorized" };
106
74
  w.authStale = true;
107
75
  reject(/* @__PURE__ */ new Error("unauthorized"));
@@ -114,7 +82,8 @@ const attemptSocket = async (c, w, options, abort) => {
114
82
  const url = await socketUrl(options, c.ctx.clientId);
115
83
  const stale = w.authStale;
116
84
  w.authStale = false;
117
- const frame = await headersFrame(options, stale);
85
+ const lanes = flushTapSync(() => c.ctx.takeAttachLanes());
86
+ const frame = await firstFrame(options, stale, lanes);
118
87
  abort.signal.throwIfAborted();
119
88
  const { protocols, webSocket } = options;
120
89
  const WS = webSocket ?? globalThis.WebSocket;
@@ -126,43 +95,19 @@ const attemptSocket = async (c, w, options, abort) => {
126
95
  if (w.socket === ws) w.socket = null;
127
96
  }
128
97
  };
129
- const sendCommand = (w, { seq, command }) => {
98
+ const sendFrame = (w, frame) => {
130
99
  const ws = w.socket;
131
100
  if (!ws || ws.readyState !== OPEN) return Promise.reject(/* @__PURE__ */ new Error("network"));
132
- return new Promise((resolve, reject) => {
133
- const entry = {
134
- seq,
135
- resolve: (result) => {
136
- clearTimeout(timer);
137
- resolve(result);
138
- },
139
- reject: (error) => {
140
- clearTimeout(timer);
141
- reject(error);
142
- }
143
- };
144
- const timer = setTimeout(() => {
145
- w.pending = w.pending.filter((p) => p !== entry);
146
- reject(/* @__PURE__ */ new Error("timeout"));
147
- ws.close();
148
- }, timing.heartbeatTimeoutMs);
149
- w.pending.push(entry);
150
- try {
151
- ws.send(JSON.stringify({
152
- ...encodeWireCommand(command),
153
- seq
154
- }));
155
- } catch {
156
- clearTimeout(timer);
157
- w.pending = w.pending.filter((p) => p !== entry);
158
- reject(/* @__PURE__ */ new Error("network"));
159
- }
160
- });
101
+ try {
102
+ ws.send(JSON.stringify(frame));
103
+ } catch {
104
+ return Promise.reject(/* @__PURE__ */ new Error("network"));
105
+ }
106
+ return Promise.resolve({ type: "ok" });
161
107
  };
162
108
  const useWsLink = (options, ctx) => {
163
109
  const [w] = useState(() => ({
164
110
  socket: null,
165
- pending: [],
166
111
  authStale: false
167
112
  }));
168
113
  const c_0 = useStatewireConn({
@@ -175,13 +120,13 @@ const useWsLink = (options, ctx) => {
175
120
  get connection() {
176
121
  return c_0.connection;
177
122
  },
178
- send: (args) => sendCommand(w, args)
123
+ send: (frame) => sendFrame(w, frame)
179
124
  }));
180
125
  return link;
181
126
  };
182
127
  const useStatewireWS = (options) => useStatewireRemote(options, useWsLink);
183
128
  const StatewireWS = resource(useStatewireWS);
184
129
  //#endregion
185
- export { StatewireWS, UNAUTHORIZED_CLOSE, WS_SUBPROTOCOL };
130
+ export { StatewireWS, WS_SUBPROTOCOL };
186
131
 
187
132
  //# sourceMappingURL=ws.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ws.js","names":["useState","flushTapSync","resource","ResourceElement","timing","handleEnvelope","overloadedError","useStatewireConn","Conn","StatewireEnvelope","Statewire","StatewireClient","useStatewireRemote","CLIENT_QUERY_PARAM","encodeWireCommand","RemoteLink","StatewireSubmitResult","StatewireRemoteProps","StatewireWireCommand","PendingCommand","seq","resolve","result","reject","error","Error","WsChannel","socket","WebSocket","pending","authStale","OPEN","WS_SUBPROTOCOL","UNAUTHORIZED_CLOSE","socketUrl","options","WSOptions","clientId","url","query","params","URLSearchParams","set","base","replace","toString","headersFrame","stale","Promise","headers","undefined","resolved","Headers","staleAuth","JSON","stringify","type","Object","fromEntries","settleOverloaded","w","retryAfterMs","entry","find","p","filter","resolvePending","matches","remaining","push","failPending","runSocket","c","ws","frame","abort","AbortController","timer","ReturnType","setTimeout","timedOut","outcome","closed","arm","clearTimeout","close","heartbeatTimeoutMs","signal","addEventListener","onerror","onopen","send","Boolean","aborted","fail","processEnv","env","done","ack","res","seqs","Set","map","r","has","onmessage","event","overloaded","parse","String","data","onclose","code","episode","cause","attemptSocket","ctx","throwIfAborted","protocols","webSocket","WS","globalThis","extra","sendCommand","command","readyState","useWsLink","onChange","attemptOnce","link","connection","args","useStatewireWS","StatewireWS","Commands","TState","TCommands","TEvent"],"sources":["../../src/transports/ws.ts"],"sourcesContent":["import { useState } from \"react\";\nimport { flushTapSync, resource } from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { timing } from \"../client/timing\";\nimport {\n handleEnvelope,\n overloadedError,\n useStatewireConn,\n} from \"../client/connection\";\nimport type { Conn, StatewireEnvelope } from \"../client/connection\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireClient } from \"../statewire\";\nimport { useStatewireRemote } from \"./remote\";\nimport { CLIENT_QUERY_PARAM } from \"../host/adapter\";\nimport { encodeWireCommand } from \"./sse\";\nimport type {\n RemoteLink,\n StatewireSubmitResult,\n StatewireRemoteProps,\n StatewireWireCommand,\n} from \"./sse\";\n\ntype PendingCommand = {\n seq: number;\n resolve: (result: StatewireSubmitResult) => void;\n reject: (error: Error) => void;\n};\n\ntype WsChannel = {\n socket: WebSocket | null;\n pending: PendingCommand[];\n authStale: boolean;\n};\n\nconst OPEN = 1;\n\nexport const WS_SUBPROTOCOL = \"statewire.v1\";\nexport const UNAUTHORIZED_CLOSE = 4401;\n\nconst socketUrl = async (\n options: StatewireClient.WSOptions,\n clientId: string,\n) => {\n const { url, query } = options;\n const params = new URLSearchParams(\n typeof query === \"function\" ? await query() : query,\n );\n params.set(CLIENT_QUERY_PARAM, clientId);\n const base = url.replace(/\\/$/, \"\").replace(/^http/, \"ws\");\n return `${base}/ws?${params.toString()}`;\n};\n\nconst headersFrame = async (\n options: StatewireClient.WSOptions,\n stale: boolean,\n): Promise<string | null> => {\n const { headers } = options;\n if (headers === undefined) return null;\n const resolved = new Headers(\n typeof headers === \"function\"\n ? await headers({ staleAuth: stale })\n : headers,\n );\n return JSON.stringify({\n type: \"headers\",\n headers: Object.fromEntries(resolved),\n });\n};\n\nconst settleOverloaded = (\n w: WsChannel,\n { seq, retryAfterMs }: { seq?: number; retryAfterMs?: number },\n) => {\n if (seq === undefined) throw overloadedError(retryAfterMs);\n const entry = w.pending.find((p) => p.seq === seq);\n if (!entry) return;\n w.pending = w.pending.filter((p) => p !== entry);\n entry.reject(overloadedError(retryAfterMs));\n};\n\nconst resolvePending = (\n w: WsChannel,\n matches: (entry: PendingCommand) => boolean,\n) => {\n const remaining: PendingCommand[] = [];\n for (const entry of w.pending) {\n if (matches(entry)) entry.resolve({ type: \"receipt\" });\n else remaining.push(entry);\n }\n w.pending = remaining;\n};\n\nconst failPending = (w: WsChannel, error: Error) => {\n const pending = w.pending;\n w.pending = [];\n for (const entry of pending) entry.reject(error);\n};\n\nconst runSocket = (\n c: Conn,\n w: WsChannel,\n ws: WebSocket,\n frame: string | null,\n staleAuth: boolean,\n abort: AbortController,\n): Promise<\"stop\" | \"reattach\"> =>\n new Promise((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let timedOut = false;\n let outcome: \"stop\" | \"reattach\" | null = null;\n let closed = false;\n const arm = () => {\n clearTimeout(timer);\n timer = setTimeout(() => {\n timedOut = true;\n ws.close();\n }, timing.heartbeatTimeoutMs);\n };\n arm();\n abort.signal.addEventListener(\"abort\", () => ws.close());\n ws.onerror = () => {};\n if (frame !== null) ws.onopen = () => ws.send(frame);\n const stale = () => Boolean(outcome) || closed || abort.signal.aborted;\n const fail = (error: unknown) => {\n clearTimeout(timer);\n reject(error instanceof Error ? error : new Error(\"protocol\"));\n ws.close();\n };\n const processEnv = (env: StatewireEnvelope) => {\n const done = flushTapSync(() => handleEnvelope(c, env));\n if (env.ack !== undefined) {\n const ack = env.ack;\n resolvePending(w, (entry) => entry.seq <= ack);\n }\n if (env.res !== undefined) {\n const seqs = new Set(env.res.map((r) => r.seq));\n resolvePending(w, (entry) => seqs.has(entry.seq));\n }\n if (done) {\n outcome = done;\n ws.close();\n }\n };\n ws.onmessage = (event) => {\n if (stale()) return;\n arm();\n let frame: StatewireEnvelope & {\n overloaded?: { seq?: number; retryAfterMs?: number };\n };\n try {\n frame = JSON.parse(String(event.data)) as typeof frame;\n } catch {\n fail(new Error(\"protocol\"));\n return;\n }\n try {\n if (frame.overloaded !== undefined) {\n settleOverloaded(w, frame.overloaded);\n } else {\n processEnv(frame);\n }\n } catch (error) {\n fail(error);\n }\n };\n ws.onclose = (event?: { code?: number }) => {\n closed = true;\n clearTimeout(timer);\n if (w.socket === ws) failPending(w, new Error(\"network\"));\n if (outcome) resolve(outcome);\n else if (event?.code === UNAUTHORIZED_CLOSE) {\n if (staleAuth) c.episode = { cause: \"unauthorized\" };\n w.authStale = true;\n reject(new Error(\"unauthorized\"));\n } else reject(new Error(timedOut ? \"timeout\" : \"network\"));\n };\n });\n\nconst attemptSocket = async (\n c: Conn,\n w: WsChannel,\n options: StatewireClient.WSOptions,\n abort: AbortController,\n): Promise<\"stop\" | \"reattach\"> => {\n let ws: WebSocket | null = null;\n try {\n const url = await socketUrl(options, c.ctx.clientId);\n const stale = w.authStale;\n w.authStale = false;\n const frame = await headersFrame(options, stale);\n abort.signal.throwIfAborted();\n const { protocols, webSocket } = options;\n const WS = webSocket ?? globalThis.WebSocket;\n if (!WS) throw new Error(\"no WebSocket implementation\");\n const extra =\n protocols === undefined\n ? []\n : typeof protocols === \"string\"\n ? [protocols]\n : protocols;\n ws = new WS(url, [WS_SUBPROTOCOL, ...extra]);\n w.socket = ws;\n return await runSocket(c, w, ws, frame, stale, abort);\n } finally {\n if (w.socket === ws) w.socket = null;\n }\n};\n\nconst sendCommand = (\n w: WsChannel,\n { seq, command }: { seq: number; command: StatewireWireCommand },\n): Promise<StatewireSubmitResult> => {\n const ws = w.socket;\n if (!ws || ws.readyState !== OPEN) {\n return Promise.reject(new Error(\"network\"));\n }\n return new Promise((resolve, reject) => {\n const entry: PendingCommand = {\n seq,\n resolve: (result) => {\n clearTimeout(timer);\n resolve(result);\n },\n reject: (error) => {\n clearTimeout(timer);\n reject(error);\n },\n };\n // A timed-out command fails the socket: the retry then rides reconnect +\n // attach-seq arbitration.\n const timer = setTimeout(() => {\n w.pending = w.pending.filter((p) => p !== entry);\n reject(new Error(\"timeout\"));\n ws.close();\n }, timing.heartbeatTimeoutMs);\n w.pending.push(entry);\n try {\n ws.send(JSON.stringify({ ...encodeWireCommand(command), seq }));\n } catch {\n clearTimeout(timer);\n w.pending = w.pending.filter((p) => p !== entry);\n reject(new Error(\"network\"));\n }\n });\n};\n\nconst useWsLink = (\n options: StatewireClient.WSOptions,\n ctx: StatewireRemoteProps,\n): RemoteLink => {\n const [w] = useState(\n (): WsChannel => ({ socket: null, pending: [], authStale: false }),\n );\n const c = useStatewireConn({\n url: options.url,\n onChange: options.onChange,\n ctx,\n attemptOnce: (c, abort) => attemptSocket(c, w, options, abort),\n });\n const [link] = useState(\n (): RemoteLink => ({\n get connection() {\n return c.connection;\n },\n send: (args) => sendCommand(w, args),\n }),\n );\n return link;\n};\n\nconst useStatewireWS = (\n options: StatewireClient.WSOptions,\n): Statewire<any, any, any> => useStatewireRemote(options, useWsLink);\n\nexport const StatewireWS = resource(useStatewireWS) as <\n TState = any,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n>(\n options: StatewireClient.WSOptions,\n) => ResourceElement<Statewire<TState | undefined, TCommands, TEvent>>;\n"],"mappings":"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"}
1
+ {"version":3,"file":"ws.js","names":["useState","flushTapSync","resource","ResourceElement","timing","handleEnvelope","useStatewireConn","AttemptOutcome","Conn","StatewireEnvelope","Statewire","StatewireClient","useStatewireRemote","CLIENT_QUERY_PARAM","RemoteLink","StatewireFrame","StatewireSubmitResult","StatewireRemoteProps","WsChannel","socket","WebSocket","authStale","OPEN","WS_SUBPROTOCOL","UNAUTHORIZED_CLOSE","socketUrl","options","WSOptions","clientId","url","query","params","URLSearchParams","set","base","replace","toString","firstFrame","stale","lanes","Promise","headers","resolved","Headers","staleAuth","JSON","stringify","Object","fromEntries","runSocket","c","w","ws","frame","abort","AbortController","resolve","reject","timer","ReturnType","setTimeout","timedOut","outcome","closed","arm","clearTimeout","close","heartbeatTimeoutMs","signal","addEventListener","onerror","onopen","send","Boolean","aborted","fail","error","Error","onmessage","event","env","parse","String","data","keys","length","done","onclose","code","episode","cause","attemptSocket","ctx","takeAttachLanes","throwIfAborted","protocols","webSocket","WS","globalThis","extra","undefined","sendFrame","readyState","type","useWsLink","onChange","attemptOnce","link","connection","useStatewireWS","StatewireWS","Commands","TState","TCommands"],"sources":["../../src/transports/ws.ts"],"sourcesContent":["import { useState } from \"react\";\nimport { flushTapSync, resource } from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { timing } from \"../client/timing\";\nimport { handleEnvelope, useStatewireConn } from \"../client/connection\";\nimport type {\n AttemptOutcome,\n Conn,\n StatewireEnvelope,\n} from \"../client/connection\";\nimport type { Statewire } from \"../types\";\nimport type { StatewireClient } from \"../statewire\";\nimport { useStatewireRemote } from \"./remote\";\nimport { CLIENT_QUERY_PARAM } from \"../host/adapter\";\nimport type {\n RemoteLink,\n StatewireFrame,\n StatewireSubmitResult,\n StatewireRemoteProps,\n} from \"./sse\";\n\ntype WsChannel = {\n socket: WebSocket | null;\n authStale: boolean;\n};\n\nconst OPEN = 1;\n\nexport const WS_SUBPROTOCOL = \"statewire.v1\";\nconst UNAUTHORIZED_CLOSE = 4401;\n\nconst socketUrl = async (\n options: StatewireClient.WSOptions,\n clientId: string,\n) => {\n const { url, query } = options;\n const params = new URLSearchParams(\n typeof query === \"function\" ? await query() : query,\n );\n params.set(CLIENT_QUERY_PARAM, clientId);\n const base = url.replace(/\\/$/, \"\").replace(/^http/, \"ws\");\n return `${base}/ws?${params.toString()}`;\n};\n\nconst firstFrame = async (\n options: StatewireClient.WSOptions,\n stale: boolean,\n lanes: StatewireFrame | null,\n): Promise<string> => {\n const { headers } = options;\n const resolved = new Headers(\n typeof headers === \"function\"\n ? await headers({ staleAuth: stale })\n : headers,\n );\n return JSON.stringify({\n headers: Object.fromEntries(resolved),\n ...(lanes ?? {}),\n });\n};\n\nconst runSocket = (\n c: Conn,\n w: WsChannel,\n ws: WebSocket,\n frame: string,\n staleAuth: boolean,\n abort: AbortController,\n): Promise<AttemptOutcome> =>\n new Promise((resolve, reject) => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n let timedOut = false;\n let outcome: AttemptOutcome | null = null;\n let closed = false;\n const arm = () => {\n clearTimeout(timer);\n timer = setTimeout(() => {\n timedOut = true;\n ws.close();\n }, timing.heartbeatTimeoutMs);\n };\n arm();\n abort.signal.addEventListener(\"abort\", () => ws.close());\n ws.onerror = () => {};\n ws.onopen = () => ws.send(frame);\n const stale = () => Boolean(outcome) || closed || abort.signal.aborted;\n const fail = (error: unknown) => {\n clearTimeout(timer);\n reject(error instanceof Error ? error : new Error(\"protocol\"));\n ws.close();\n };\n ws.onmessage = (event) => {\n if (stale()) return;\n arm();\n let env: StatewireEnvelope;\n try {\n env = JSON.parse(String(event.data)) as StatewireEnvelope;\n } catch {\n fail(new Error(\"protocol\"));\n return;\n }\n // {} heartbeats live below the packet layer: invisible to handlers.\n if (\n typeof env === \"object\" &&\n env !== null &&\n Object.keys(env).length === 0\n ) {\n return;\n }\n try {\n const done = flushTapSync(() => handleEnvelope(c, env));\n if (done) {\n outcome = done;\n ws.close();\n }\n } catch (error) {\n fail(error);\n }\n };\n ws.onclose = (event?: { code?: number }) => {\n closed = true;\n clearTimeout(timer);\n if (outcome) resolve(outcome);\n else if (event?.code === UNAUTHORIZED_CLOSE) {\n if (staleAuth) c.episode = { cause: \"unauthorized\" };\n w.authStale = true;\n reject(new Error(\"unauthorized\"));\n } else reject(new Error(timedOut ? \"timeout\" : \"network\"));\n };\n });\n\nconst attemptSocket = async (\n c: Conn,\n w: WsChannel,\n options: StatewireClient.WSOptions,\n abort: AbortController,\n): Promise<AttemptOutcome> => {\n let ws: WebSocket | null = null;\n try {\n const url = await socketUrl(options, c.ctx.clientId);\n const stale = w.authStale;\n w.authStale = false;\n const lanes = flushTapSync(() => c.ctx.takeAttachLanes());\n const frame = await firstFrame(options, stale, lanes);\n abort.signal.throwIfAborted();\n const { protocols, webSocket } = options;\n const WS = webSocket ?? globalThis.WebSocket;\n if (!WS) throw new Error(\"no WebSocket implementation\");\n const extra =\n protocols === undefined\n ? []\n : typeof protocols === \"string\"\n ? [protocols]\n : protocols;\n ws = new WS(url, [WS_SUBPROTOCOL, ...extra]);\n w.socket = ws;\n return await runSocket(c, w, ws, frame, stale, abort);\n } finally {\n if (w.socket === ws) w.socket = null;\n }\n};\n\nconst sendFrame = (\n w: WsChannel,\n frame: StatewireFrame,\n): Promise<StatewireSubmitResult> => {\n const ws = w.socket;\n if (!ws || ws.readyState !== OPEN) {\n return Promise.reject(new Error(\"network\"));\n }\n try {\n ws.send(JSON.stringify(frame));\n } catch {\n return Promise.reject(new Error(\"network\"));\n }\n return Promise.resolve({ type: \"ok\" });\n};\n\nconst useWsLink = (\n options: StatewireClient.WSOptions,\n ctx: StatewireRemoteProps,\n): RemoteLink => {\n const [w] = useState((): WsChannel => ({ socket: null, authStale: false }));\n const c = useStatewireConn({\n url: options.url,\n onChange: options.onChange,\n ctx,\n attemptOnce: (c, abort) => attemptSocket(c, w, options, abort),\n });\n const [link] = useState(\n (): RemoteLink => ({\n get connection() {\n return c.connection;\n },\n send: (frame) => sendFrame(w, frame),\n }),\n );\n return link;\n};\n\nconst useStatewireWS = (\n options: StatewireClient.WSOptions,\n): Statewire<any, any> => useStatewireRemote(options, useWsLink);\n\nexport const StatewireWS = resource(useStatewireWS) as <\n TState = any,\n TCommands extends Statewire.Commands = Statewire.Commands,\n>(\n options: StatewireClient.WSOptions,\n) => ResourceElement<Statewire<TState | undefined, TCommands>>;\n"],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -12,7 +12,7 @@ declare namespace Statewire {
12
12
  method: K;
13
13
  params: Parameters<TCommands[K]>;
14
14
  seq: number;
15
- status: "queued" | "posted" | "acked" | "settled";
15
+ status: "queued" | "posted" | "applied" | "settled";
16
16
  result?: unknown;
17
17
  failure?: StatewireClient.SendFailure;
18
18
  }; }[keyof TCommands];
@@ -35,23 +35,23 @@ declare namespace Statewire {
35
35
  } | {
36
36
  op: "remove";
37
37
  path: StatewirePath;
38
- } | {
39
- op: "event";
40
- value: object;
41
38
  };
42
39
  type Changes = {
43
40
  isChangedAt(path: readonly (string | number)[]): boolean;
44
41
  getChangedKeys(path: readonly (string | number)[]): string[];
45
42
  };
46
43
  }
47
- interface Statewire<TState = any, TCommands extends Statewire.Commands = Statewire.Commands, TEvent = any> {
44
+ interface Statewire<TState = any, TCommands extends Statewire.Commands = Statewire.Commands> {
48
45
  state: TState;
49
46
  commands: Statewire.CommandsProxy<TCommands>;
50
- emit?: (event: TEvent) => void;
51
47
  connection?: StatewireClient.Connection;
52
48
  source?: Statewire.StateSource;
53
- /** Replace this client's ambient context on the server; absent on transports without a client context channel. */
54
- setContext?: (value: Record<string, unknown>) => Promise<unknown>;
49
+ /** Highest command seq released from the resend queue by the server's dur flag. */
50
+ durableSeq?: number;
51
+ /** Negation of the server's broadcast idle level; absent means always busy. */
52
+ isBusy?: boolean;
53
+ /** Replace this client's ambient context on the server; it rides the ctx lane (no verdict). */
54
+ setContext?: (value: Record<string, unknown>) => void;
55
55
  }
56
56
  declare class StatewireSendError extends Error {
57
57
  readonly failure: StatewireClient.SendFailure;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;kBAGiB;OACH,WAAW,mBAAmB;OAE9B,QAAQ,kBAAkB,WAAW,eAC9C,WAAW;IAAc,QAAQ;IAAG,QAAQ,WAAW,UAAU;aAC5D;OAEI,cAAc,kBAAkB,eACzC,WAAW,gBACP,MAAM,WAAW,UAAU,QAC3B,QAAQ,QAAQ,WAAW,UAAU;OAGhC,cAAc,kBAAkB,WAAW,eACpD,WAAW;IACV,QAAQ;IACR,QAAQ,WAAW,UAAU;IAC7B;IACA;IACA;IACA,UAAU,gBAAgB;aAEtB;;OAGI;IACV;;IAEA,mBAAmB;;IAEnB,cAAc;;OAGJ;IACN;IAAe,MAAM;IAAe;;IACpC;IAAW,MAAM;IAAe;;IAChC;IAAc,MAAM;;IACpB;IAAa;;OAEP;IACV,YAAY;IACZ,eAAe;;;UAIF,UACf,cACA,kBAAkB,UAAU,WAAW,UAAU,UACjD;EAEA,OAAO;EACP,UAAU,UAAU,cAAc;EAClC,QAAQ,OAAO;EACf,aAAa,gBAAgB;EAC7B,SAAS,UAAU;;EAEnB,cAAc,OAAO,4BAA4B;;cAsBtC,2BAA2B;WAC7B,SAAS,gBAAgB;EAEtB,YAAA,SAAS,gBAAgB"}
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;kBAGiB;OACH,WAAW,mBAAmB;OAE9B,QAAQ,kBAAkB,WAAW,eAC9C,WAAW;IAAc,QAAQ;IAAG,QAAQ,WAAW,UAAU;aAC5D;OAEI,cAAc,kBAAkB,eACzC,WAAW,gBACP,MAAM,WAAW,UAAU,QAC3B,QAAQ,QAAQ,WAAW,UAAU;OAGhC,cAAc,kBAAkB,WAAW,eACpD,WAAW;IACV,QAAQ;IACR,QAAQ,WAAW,UAAU;IAC7B;IACA;IACA;IACA,UAAU,gBAAgB;aAEtB;;OAGI;IACV;;IAEA,mBAAmB;;IAEnB,cAAc;;OAGJ;IACN;IAAe,MAAM;IAAe;;IACpC;IAAW,MAAM;IAAe;;IAChC;IAAc,MAAM;;OAEd;IACV,YAAY;IACZ,eAAe;;;UAIF,UACf,cACA,kBAAkB,UAAU,WAAW,UAAU;EAEjD,OAAO;EACP,UAAU,UAAU,cAAc;EAClC,aAAa,gBAAgB;EAC7B,SAAS,UAAU;;EAEnB;;EAEA;;EAEA,cAAc,OAAO;;cAsBV,2BAA2B;WAC7B,SAAS,gBAAgB;EAEtB,YAAA,SAAS,gBAAgB"}
package/dist/types.js CHANGED
@@ -1,13 +1,12 @@
1
1
  //#region src/types.ts
2
2
  const describe = (failure) => {
3
3
  switch (failure.fate) {
4
- case "rejected": return `statewire: command rejected: ${failure.message}`;
5
- case "unknown-command": return "statewire: unknown command";
6
- case "invalid-params": return `statewire: invalid params: ${failure.message}`;
4
+ case "rejected": return `statewire: command rejected${failure.code ? ` (${failure.code})` : ""}${failure.message ? `: ${failure.message}` : ""}`;
7
5
  case "aborted": return `statewire: command aborted (${failure.cause})`;
8
6
  case "lost": return `statewire: command lost (${failure.cause})`;
7
+ case "failed": return failure.message ? `statewire: command failed: ${failure.message}` : "statewire: command failed";
9
8
  case "crashed": return "statewire: command crashed";
10
- case "result-unavailable": return `statewire: command result unavailable (${failure.cause})`;
9
+ case "expired": return `statewire: command result expired (${failure.cause})`;
11
10
  }
12
11
  };
13
12
  var StatewireSendError = class extends Error {
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":["StatewireClient","StatewirePath","Statewire","Commands","Record","args","Command","TCommands","method","K","params","Parameters","CommandsProxy","Promise","Awaited","ReturnType","CommandUpdate","seq","status","result","failure","SendFailure","StateSource","snapshot","takeChanged","setOnChange","listener","Op","op","path","value","Changes","isChangedAt","getChangedKeys","state","TState","commands","emit","event","TEvent","connection","Connection","source","setContext","describe","fate","message","cause","StatewireSendError","Error","constructor","name"],"sources":["../src/types.ts"],"sourcesContent":["import type { StatewireClient } from \"./statewire\";\nimport type { StatewirePath } from \"./wire/ops\";\n\nexport namespace Statewire {\n export type Commands = Record<string, (...args: never[]) => unknown>;\n\n export type Command<TCommands extends Commands = Commands> = {\n [K in keyof TCommands]: { method: K; params: Parameters<TCommands[K]> };\n }[keyof TCommands];\n\n export type CommandsProxy<TCommands extends Commands> = {\n [K in keyof TCommands]: (\n ...args: Parameters<TCommands[K]>\n ) => Promise<Awaited<ReturnType<TCommands[K]>>>;\n };\n\n export type CommandUpdate<TCommands extends Commands = Commands> = {\n [K in keyof TCommands]: {\n method: K;\n params: Parameters<TCommands[K]>;\n seq: number;\n status: \"queued\" | \"posted\" | \"acked\" | \"settled\";\n result?: unknown;\n failure?: StatewireClient.SendFailure;\n };\n }[keyof TCommands];\n\n /** Push-based canonical state feed a statewire hands to the client core. */\n export type StateSource = {\n snapshot: () => unknown;\n /** Drain the paths changed since the last take (`[]` entry = whole state). */\n takeChanged: () => StatewirePath[];\n /** Synchronous, per change; the consumer owns coalescing. */\n setOnChange: (listener: (() => void) | null) => void;\n };\n\n export type Op =\n | { op: \"replace\"; path: StatewirePath; value: unknown }\n | { op: \"add\"; path: StatewirePath; value: unknown }\n | { op: \"remove\"; path: StatewirePath }\n | { op: \"event\"; value: object };\n\n export type Changes = {\n isChangedAt(path: readonly (string | number)[]): boolean;\n getChangedKeys(path: readonly (string | number)[]): string[];\n };\n}\n\nexport interface Statewire<\n TState = any,\n TCommands extends Statewire.Commands = Statewire.Commands,\n TEvent = any,\n> {\n state: TState;\n commands: Statewire.CommandsProxy<TCommands>;\n emit?: (event: TEvent) => void;\n connection?: StatewireClient.Connection;\n source?: Statewire.StateSource;\n /** Replace this client's ambient context on the server; absent on transports without a client context channel. */\n setContext?: (value: Record<string, unknown>) => Promise<unknown>;\n}\n\nconst describe = (failure: StatewireClient.SendFailure): string => {\n switch (failure.fate) {\n case \"rejected\":\n return `statewire: command rejected: ${failure.message}`;\n case \"unknown-command\":\n return \"statewire: unknown command\";\n case \"invalid-params\":\n return `statewire: invalid params: ${failure.message}`;\n case \"aborted\":\n return `statewire: command aborted (${failure.cause})`;\n case \"lost\":\n return `statewire: command lost (${failure.cause})`;\n case \"crashed\":\n return \"statewire: command crashed\";\n case \"result-unavailable\":\n return `statewire: command result unavailable (${failure.cause})`;\n }\n};\n\nexport class StatewireSendError extends Error {\n readonly failure: StatewireClient.SendFailure;\n\n constructor(failure: StatewireClient.SendFailure) {\n super(describe(failure));\n this.name = \"StatewireSendError\";\n this.failure = failure;\n }\n}\n"],"mappings":";AA8DA,MAAM4C,YAAYxB,YAAiD;CACjE,QAAQA,QAAQyB,MAAhB;EACE,KAAK,YACH,OAAO,gCAAgCzB,QAAQ0B;EACjD,KAAK,mBACH,OAAO;EACT,KAAK,kBACH,OAAO,8BAA8B1B,QAAQ0B;EAC/C,KAAK,WACH,OAAO,+BAA+B1B,QAAQ2B,MAAK;EACrD,KAAK,QACH,OAAO,4BAA4B3B,QAAQ2B,MAAK;EAClD,KAAK,WACH,OAAO;EACT,KAAK,sBACH,OAAO,0CAA0C3B,QAAQ2B,MAAK;CAClE;AACF;AAEA,IAAaC,qBAAb,cAAwCC,MAAM;CAC5C;CAEAC,YAAY9B,SAAsC;EAChD,MAAMwB,SAASxB,OAAO,CAAC;EACvB,KAAK+B,OAAO;EACZ,KAAK/B,UAAUA;CACjB;AACF"}
1
+ {"version":3,"file":"types.js","names":["StatewireClient","StatewirePath","Statewire","Commands","Record","args","Command","TCommands","method","K","params","Parameters","CommandsProxy","Promise","Awaited","ReturnType","CommandUpdate","seq","status","result","failure","SendFailure","StateSource","snapshot","takeChanged","setOnChange","listener","Op","op","path","value","Changes","isChangedAt","getChangedKeys","state","TState","commands","connection","Connection","source","durableSeq","isBusy","setContext","describe","fate","code","message","cause","StatewireSendError","Error","constructor","name"],"sources":["../src/types.ts"],"sourcesContent":["import type { StatewireClient } from \"./statewire\";\nimport type { StatewirePath } from \"./wire/ops\";\n\nexport namespace Statewire {\n export type Commands = Record<string, (...args: never[]) => unknown>;\n\n export type Command<TCommands extends Commands = Commands> = {\n [K in keyof TCommands]: { method: K; params: Parameters<TCommands[K]> };\n }[keyof TCommands];\n\n export type CommandsProxy<TCommands extends Commands> = {\n [K in keyof TCommands]: (\n ...args: Parameters<TCommands[K]>\n ) => Promise<Awaited<ReturnType<TCommands[K]>>>;\n };\n\n export type CommandUpdate<TCommands extends Commands = Commands> = {\n [K in keyof TCommands]: {\n method: K;\n params: Parameters<TCommands[K]>;\n seq: number;\n status: \"queued\" | \"posted\" | \"applied\" | \"settled\";\n result?: unknown;\n failure?: StatewireClient.SendFailure;\n };\n }[keyof TCommands];\n\n /** Push-based canonical state feed a statewire hands to the client core. */\n export type StateSource = {\n snapshot: () => unknown;\n /** Drain the paths changed since the last take (`[]` entry = whole state). */\n takeChanged: () => StatewirePath[];\n /** Synchronous, per change; the consumer owns coalescing. */\n setOnChange: (listener: (() => void) | null) => void;\n };\n\n export type Op =\n | { op: \"replace\"; path: StatewirePath; value: unknown }\n | { op: \"add\"; path: StatewirePath; value: unknown }\n | { op: \"remove\"; path: StatewirePath };\n\n export type Changes = {\n isChangedAt(path: readonly (string | number)[]): boolean;\n getChangedKeys(path: readonly (string | number)[]): string[];\n };\n}\n\nexport interface Statewire<\n TState = any,\n TCommands extends Statewire.Commands = Statewire.Commands,\n> {\n state: TState;\n commands: Statewire.CommandsProxy<TCommands>;\n connection?: StatewireClient.Connection;\n source?: Statewire.StateSource;\n /** Highest command seq released from the resend queue by the server's dur flag. */\n durableSeq?: number;\n /** Negation of the server's broadcast idle level; absent means always busy. */\n isBusy?: boolean;\n /** Replace this client's ambient context on the server; it rides the ctx lane (no verdict). */\n setContext?: (value: Record<string, unknown>) => void;\n}\n\nconst describe = (failure: StatewireClient.SendFailure): string => {\n switch (failure.fate) {\n case \"rejected\":\n return `statewire: command rejected${failure.code ? ` (${failure.code})` : \"\"}${failure.message ? `: ${failure.message}` : \"\"}`;\n case \"aborted\":\n return `statewire: command aborted (${failure.cause})`;\n case \"lost\":\n return `statewire: command lost (${failure.cause})`;\n case \"failed\":\n return failure.message\n ? `statewire: command failed: ${failure.message}`\n : \"statewire: command failed\";\n case \"crashed\":\n return \"statewire: command crashed\";\n case \"expired\":\n return `statewire: command result expired (${failure.cause})`;\n }\n};\n\nexport class StatewireSendError extends Error {\n readonly failure: StatewireClient.SendFailure;\n\n constructor(failure: StatewireClient.SendFailure) {\n super(describe(failure));\n this.name = \"StatewireSendError\";\n this.failure = failure;\n }\n}\n"],"mappings":";AA+DA,MAAM2C,YAAYvB,YAAiD;CACjE,QAAQA,QAAQwB,MAAhB;EACE,KAAK,YACH,OAAO,8BAA8BxB,QAAQyB,OAAO,KAAKzB,QAAQyB,KAAI,KAAM,KAAKzB,QAAQ0B,UAAU,KAAK1B,QAAQ0B,YAAY;EAC7H,KAAK,WACH,OAAO,+BAA+B1B,QAAQ2B,MAAK;EACrD,KAAK,QACH,OAAO,4BAA4B3B,QAAQ2B,MAAK;EAClD,KAAK,UACH,OAAO3B,QAAQ0B,UACX,8BAA8B1B,QAAQ0B,YACtC;EACN,KAAK,WACH,OAAO;EACT,KAAK,WACH,OAAO,sCAAsC1B,QAAQ2B,MAAK;CAC9D;AACF;AAEA,IAAaC,qBAAb,cAAwCC,MAAM;CAC5C;CAEAC,YAAY9B,SAAsC;EAChD,MAAMuB,SAASvB,OAAO,CAAC;EACvB,KAAK+B,OAAO;EACZ,KAAK/B,UAAUA;CACjB;AACF"}
@@ -2,16 +2,12 @@ import { Statewire } from "../types.js";
2
2
  //#region src/wire/ops.d.ts
3
3
  type StatewirePath = readonly (string | number)[];
4
4
  declare const sanitizeValue: (value: unknown, parent?: string) => unknown;
5
- type MutableOp = Extract<Statewire.Op, {
6
- op: "replace" | "add" | "remove";
7
- }>;
8
- declare const applyOp: (root: unknown, op: MutableOp) => {
5
+ declare const applyOp: (root: unknown, op: Statewire.Op) => {
9
6
  state: unknown;
10
7
  changedPaths: StatewirePath[];
11
8
  };
12
9
  declare const applyOps: (state: unknown, ops: readonly unknown[]) => {
13
10
  state: unknown;
14
- events: unknown[];
15
11
  changedPaths: StatewirePath[];
16
12
  };
17
13
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"ops.d.ts","names":[],"sources":["../../src/wire/ops.ts"],"mappings":";;KAIY;cAwDC,gBAAiB,gBAAgB;KAGzC,YAAY,QAAQ,UAAU;EAAM;;cAkG5B,UACX,eACA,IAAI;EACD;EAAgB,cAAc;;cAUtB,WACX,gBACA;EAEA;EACA;EACA,cAAc"}
1
+ {"version":3,"file":"ops.d.ts","names":[],"sources":["../../src/wire/ops.ts"],"mappings":";;KAIY;cAwDC,gBAAiB,gBAAgB;cAmGjC,UACX,eACA,IAAI,UAAU;EACX;EAAgB,cAAc;;cAUtB,WACX,gBACA;EAEA;EACA,cAAc"}
package/dist/wire/ops.js CHANGED
@@ -92,17 +92,14 @@ const applyOp = (root, op) => {
92
92
  };
93
93
  };
94
94
  const applyOps = (state, ops) => {
95
- const events = [];
96
95
  const mutable = [];
97
- for (const op of ops) if (op.op === "event") events.push(sanitizeValue(op.value));
98
- else if (op.op === "replace" || op.op === "add" || op.op === "remove") mutable.push(op);
96
+ for (const op of ops) if (op.op === "replace" || op.op === "add" || op.op === "remove") mutable.push(op);
99
97
  else warnUnknownOp(op.op);
100
98
  const recorded = recordMutations({ state }, (draft) => {
101
99
  for (const op of mutable) applyOpToDraft(draft, op);
102
100
  });
103
101
  return {
104
102
  state: recorded.state.state,
105
- events,
106
103
  changedPaths: unboxPaths(recorded.changedPaths)
107
104
  };
108
105
  };
@@ -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","MutableOp","Extract","Op","op","invalid","insertedText","current","offset","slice","applyAdd","box","state","walked","Number","isInteger","target","leaf","splice","next","applyInList","list","applyInObject","obj","hasOwn","applyOpToDraft","warnedOps","Set","warnUnknownOp","type","has","add","JSON","stringify","unboxPaths","paths","applyOp","root","changedPaths","recorded","draft","applyOps","ops","events","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\ntype MutableOp = Extract<Statewire.Op, { op: \"replace\" | \"add\" | \"remove\" }>;\n\nconst invalid = (op: MutableOp) =>\n new Error(`Statewire cannot apply ${op.op} at path: ${op.path.join(\".\")}`);\n\nconst insertedText = (\n current: string,\n op: Extract<MutableOp, { 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<MutableOp, { 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: MutableOp) => {\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: MutableOp,\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: MutableOp): 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 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: MutableOp,\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 events: unknown[];\n changedPaths: StatewirePath[];\n} => {\n const events: unknown[] = [];\n const mutable: MutableOp[] = [];\n for (const op of ops as Statewire.Op[]) {\n if (op.op === \"event\") {\n events.push(sanitizeValue(op.value));\n } else 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 events,\n changedPaths: unboxPaths(recorded.changedPaths),\n };\n};\n"],"mappings":"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"}
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","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 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 (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.8.1",
3
+ "version": "0.9.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": {