statewire 0.19.4 → 0.20.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 (64) hide show
  1. package/README.md +15 -1
  2. package/dist/client/session.d.ts +8 -1
  3. package/dist/client/session.d.ts.map +1 -1
  4. package/dist/client/session.js +52 -12
  5. package/dist/client/session.js.map +1 -1
  6. package/dist/client/sessionStore.d.ts +2 -0
  7. package/dist/client/sessionStore.d.ts.map +1 -1
  8. package/dist/client/sessionStore.js.map +1 -1
  9. package/dist/host/host.d.ts +2 -1
  10. package/dist/host/host.d.ts.map +1 -1
  11. package/dist/host/host.js +6 -1
  12. package/dist/host/host.js.map +1 -1
  13. package/dist/host/http.js +1 -1
  14. package/dist/host/socket.d.ts +6 -1
  15. package/dist/host/socket.d.ts.map +1 -1
  16. package/dist/host/socket.js +79 -9
  17. package/dist/host/socket.js.map +1 -1
  18. package/dist/host-internal.d.ts +1 -1
  19. package/dist/host.d.ts +6 -0
  20. package/dist/host.d.ts.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/statewire.d.ts +3 -0
  23. package/dist/statewire.d.ts.map +1 -1
  24. package/dist/statewire.js +22 -5
  25. package/dist/statewire.js.map +1 -1
  26. package/dist/transports/remote.d.ts.map +1 -1
  27. package/dist/transports/remote.js +3 -2
  28. package/dist/transports/remote.js.map +1 -1
  29. package/dist/transports/sse.d.ts +4 -0
  30. package/dist/transports/sse.d.ts.map +1 -1
  31. package/dist/transports/sse.js +5 -0
  32. package/dist/transports/sse.js.map +1 -1
  33. package/dist/transports/testing.d.ts +2 -0
  34. package/dist/transports/testing.d.ts.map +1 -1
  35. package/dist/transports/testing.js +11 -4
  36. package/dist/transports/testing.js.map +1 -1
  37. package/dist/types.d.ts +4 -0
  38. package/dist/types.d.ts.map +1 -1
  39. package/dist/types.js.map +1 -1
  40. package/dist/wire/context.d.ts +14 -0
  41. package/dist/wire/context.d.ts.map +1 -0
  42. package/dist/wire/context.js +31 -0
  43. package/dist/wire/context.js.map +1 -0
  44. package/dist/wire/limits.d.ts +2 -1
  45. package/dist/wire/limits.d.ts.map +1 -1
  46. package/dist/wire/limits.js +2 -1
  47. package/dist/wire/limits.js.map +1 -1
  48. package/dist/wire/protocol.d.ts +1 -1
  49. package/dist/wire/protocol.js +4 -4
  50. package/dist/wire/protocol.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/client/session.ts +74 -4
  53. package/src/client/sessionStore.ts +5 -1
  54. package/src/host/host.ts +9 -0
  55. package/src/host/socket.ts +121 -7
  56. package/src/host.ts +5 -0
  57. package/src/statewire.ts +29 -3
  58. package/src/transports/remote.ts +3 -0
  59. package/src/transports/sse.ts +7 -0
  60. package/src/transports/testing.ts +14 -3
  61. package/src/types.ts +12 -0
  62. package/src/wire/context.ts +40 -0
  63. package/src/wire/limits.ts +2 -0
  64. package/src/wire/protocol.ts +1 -1
package/README.md CHANGED
@@ -35,7 +35,7 @@ export const App = () => {
35
35
 
36
36
  Connections negotiate date ranges through `Statewire-Version` and `Statewire-Protocol`. Protocols are additive and required by default. The server selects versions in `syn`; WebSockets use `statewire.v1`.
37
37
 
38
- Each protocol owns its commands, context documents, and state documents. The main document is independent of its optional read key. Command sequences and replay remain shared across protocols. Python hosts support lazy, chained compatibility adapters.
38
+ Each protocol owns its commands, lease context, and state documents. The main document is independent of its optional read key. Command sequences and replay remain shared across protocols. Python hosts support lazy, chained compatibility adapters.
39
39
 
40
40
  See the [protocol registration and migration guide](https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/statewire/docs/spec/statewire-protocol/protocols/page.mdx).
41
41
 
@@ -45,3 +45,17 @@ See the [protocol registration and migration guide](https://github.com/assistant
45
45
  - Guide: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/statewire/docs/guides/statewire/consume/page.mdx
46
46
  - Reference: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/statewire/docs/reference/statewire/page.mdx
47
47
  - Spec: https://github.com/assistant-ui/harness-sdk/blob/main/apps/docs/app/statewire/docs/spec/statewire-protocol/page.mdx
48
+
49
+ ## Lease context
50
+
51
+ Native context belongs to a protocol and a connection lease, independently of command history:
52
+
53
+ ```ts
54
+ const client = new StatewireClient({
55
+ transport,
56
+ context: { "acme-chat": { locale: "en" } },
57
+ });
58
+ client.setContext("acme-chat", { locale: "de" });
59
+ ```
60
+
61
+ The client restores its latest object on every attachment, including a fresh hello on a preserved socket. Hosts validate through `validateContext` and react through `contextChanged`. Command handlers read `this.context`, captured when the command was admitted; later replacements cannot change queued commands. Context uses document encoding (including dates), full replacements, and a 64 KiB limit per entry. Updates consume no command sequence numbers.
@@ -5,6 +5,7 @@ import { RemoteLink, StatewireRemoteProps } from "../transports/sse.js";
5
5
  import { StatewireClient } from "../statewire.js";
6
6
  import { Statewire } from "../types.js";
7
7
  import { StatewirePath } from "../wire/ops.js";
8
+ import { encodeContext } from "../wire/context.js";
8
9
  //#region src/client/session.d.ts
9
10
  type WireCommand = {
10
11
  protocol: string;
@@ -28,6 +29,7 @@ type Pending = {
28
29
  };
29
30
  type AttachState = "detached" | "attached" | "finished";
30
31
  type StatewireSessionOptions = StatewireProtocol.Options & {
32
+ context?: Readonly<Record<string, Statewire.Context>>;
31
33
  sessionStore?: StatewireClient.SessionStorage | undefined;
32
34
  onSessionLost?: StatewireClient.OnSessionLost | undefined;
33
35
  onDone?: (() => void) | undefined;
@@ -42,6 +44,10 @@ type Shared = {
42
44
  };
43
45
  type Session = {
44
46
  options: StatewireSessionOptions;
47
+ configuredContext: StatewireSessionOptions["context"];
48
+ contexts: Map<string, ReturnType<typeof encodeContext>>;
49
+ dirtyContexts: Set<string>;
50
+ attachContexts: Map<string, ReturnType<typeof encodeContext>>;
45
51
  selection: StatewireProtocol.Selection | undefined;
46
52
  record: StatewireClient.SessionRecord;
47
53
  ctx: StatewireRemoteProps;
@@ -93,7 +99,8 @@ declare const discardFailed: (s: Session, entry: StatewireClient.FailedCommand)
93
99
  declare const retryFailed: (s: Session, entry: StatewireClient.FailedCommand) => Promise<unknown>;
94
100
  declare const documentSource: (s: Session) => StatewireDocuments.Source;
95
101
  declare const invokeProtocol: (s: Session, protocol: string, method: string, params: readonly unknown[]) => Promise<unknown>;
102
+ declare const setContext: (s: Session, protocol: string, value: Statewire.Context) => void;
96
103
  declare const useStatewireSession: (options: StatewireSessionOptions) => Session;
97
104
  //#endregion
98
- export { StatewireSessionOptions, discardFailed, documentSource, failedCommands, invokeProtocol, retryFailed, setTransport, useStatewireSession };
105
+ export { StatewireSessionOptions, discardFailed, documentSource, failedCommands, invokeProtocol, retryFailed, setContext, setTransport, useStatewireSession };
99
106
  //# sourceMappingURL=session.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","names":[],"sources":["../../src/client/session.ts"],"mappings":";;;;;;;;KAyCK;EACH;EACA,WAAW,YAAY,gBAAgB;EACvC;EACA;EACA;;KAGG;WACM;WACA,SAAS;WACT,KAAK;;EAEd;;EAEA;;EAEA;EACA,UAAU;EACV,SAAS;;KAeN;KAEO,0BAA0B,kBAAkB;EACtD,eAAe,gBAAgB;EAC/B,gBAAgB,gBAAgB;EAChC;;KAGG;EACH,KAAK;EACL;IAAU;IAAkB,SAAS,gBAAgB;;EACrD;;KAGG;EACH,SAAS;EACT,WAAW,kBAAkB;EAC7B,QAAQ,gBAAgB;EACxB,KAAK;EACL;EACA;EACA,OAAO;EACP;EACA,iBAAiB,gBAAgB;EACjC,OAAO,gBAAgB;EACvB,eAAe,mBAAmB;EAClC,QAAQ;;EAER;;EAEA;EACA;EACA,YAAY;;EAEZ,eAAe;EACf;EACA;EACA,eAAe,gBAAgB;EAC/B;EACA;EACA,eAAe,YAAY;EAC3B;EACA,SAAS;EACT,qBAAqB,gBAAgB;EACrC,gCAAgC,gBAAgB;EAChD;EACA,mBAAmB;EACnB,WAAW,mBAAmB;EAC9B;EACA,SAAS;EACT,QAAQ,UAAU;EAClB,QAAQ;EACR;EACA;EACA;EACA,WAAW;EACX;EACA,WAAW;EACX;EACA,UAAU,mBAAmB,sBAAsB;;cA2MxC,iBACX,GAAG,qBACO,gBAAgB;cA+uBtB,eAAgB,GAAG,SAAS,WAAW;cAiNhC,gBACX,GAAG,SACH,OAAO,gBAAgB;cAYZ,cACX,GAAG,SACH,OAAO,gBAAgB,kBACtB;cA4DU,iBAAkB,GAAG,YAAU,mBAAmB;cA+BlD,iBACX,GAAG,SACH,kBACA,gBACA,+BAA0B;cAoEf,sBACX,SAAS,4BACR"}
1
+ {"version":3,"file":"session.d.ts","names":[],"sources":["../../src/client/session.ts"],"mappings":";;;;;;;;;KA0CK;EACH;EACA,WAAW,YAAY,gBAAgB;EACvC;EACA;EACA;;KAGG;WACM;WACA,SAAS;WACT,KAAK;;EAEd;;EAEA;;EAEA;EACA,UAAU;EACV,SAAS;;KAeN;KAEO,0BAA0B,kBAAkB;EACtD,UAAU,SAAS,eAAe,UAAU;EAC5C,eAAe,gBAAgB;EAC/B,gBAAgB,gBAAgB;EAChC;;KAGG;EACH,KAAK;EACL;IAAU;IAAkB,SAAS,gBAAgB;;EACrD;;KAGG;EACH,SAAS;EACT,mBAAmB;EACnB,UAAU,YAAY,kBAAkB;EACxC,eAAe;EACf,gBAAgB,YAAY,kBAAkB;EAC9C,WAAW,kBAAkB;EAC7B,QAAQ,gBAAgB;EACxB,KAAK;EACL;EACA;EACA,OAAO;EACP;EACA,iBAAiB,gBAAgB;EACjC,OAAO,gBAAgB;EACvB,eAAe,mBAAmB;EAClC,QAAQ;;EAER;;EAEA;EACA;EACA,YAAY;;EAEZ,eAAe;EACf;EACA;EACA,eAAe,gBAAgB;EAC/B;EACA;EACA,eAAe,YAAY;EAC3B;EACA,SAAS;EACT,qBAAqB,gBAAgB;EACrC,gCAAgC,gBAAgB;EAChD;EACA,mBAAmB;EACnB,WAAW,mBAAmB;EAC9B;EACA,SAAS;EACT,QAAQ,UAAU;EAClB,QAAQ;EACR;EACA;EACA;EACA,WAAW;EACX;EACA,WAAW;EACX;EACA,UAAU,mBAAmB,sBAAsB;;cA2MxC,iBACX,GAAG,qBACO,gBAAgB;cA+wBtB,eAAgB,GAAG,SAAS,WAAW;cAiNhC,gBACX,GAAG,SACH,OAAO,gBAAgB;cAYZ,cACX,GAAG,SACH,OAAO,gBAAgB,kBACtB;cA4DU,iBAAkB,GAAG,YAAU,mBAAmB;cA+BlD,iBACX,GAAG,SACH,kBACA,gBACA,+BAA0B;cAoEf,aACX,GAAG,SACH,kBACA,OAAO,UAAU;cAcN,sBACX,SAAS,4BACR"}
@@ -1,10 +1,11 @@
1
1
  import { reviveDocumentValue } from "../document.js";
2
+ import "../wire/limits.js";
2
3
  import { inRange, protocolOffers } from "../wire/protocol.js";
4
+ import { encodeContext } from "../wire/context.js";
3
5
  import { createMethodProxy } from "../util/methodProxy.js";
4
6
  import { StatewireSendError } from "../types.js";
5
7
  import { bindCommandsHost } from "../host/host.js";
6
8
  import { encodeParams, reviveResponse } from "../wire/meta.js";
7
- import "../wire/limits.js";
8
9
  import { applyDocumentOps, findDocument, mainDocument, resolveDocumentKey } from "../wire/documents.js";
9
10
  import { DocumentStorageContext, SessionStorageContext, createMemorySessionStore } from "./sessionStore.js";
10
11
  import { backoff, retryHint } from "./timing.js";
@@ -475,6 +476,13 @@ const selectionOf = (syn) => {
475
476
  };
476
477
  };
477
478
  const handleHello = (s, { ops, syn }) => {
479
+ if (s.attach === "attached") {
480
+ s.ephemeralSeqs = {};
481
+ detachEphemeralLanes(s);
482
+ }
483
+ for (const [protocol, value] of s.contexts) if (s.attachContexts.get(protocol) === value) s.dirtyContexts.delete(protocol);
484
+ else s.dirtyContexts.add(protocol);
485
+ s.attachContexts.clear();
478
486
  s.selection = selectionOf(syn);
479
487
  s.documents = {
480
488
  mode: null,
@@ -574,15 +582,19 @@ const buildLanes = (s, cmdGate = "all") => {
574
582
  last[protocol] = entry.seq;
575
583
  entries.push(entry);
576
584
  }
577
- if (entries.length === 0) return null;
585
+ const ctx = [...s.dirtyContexts].map((protocol) => s.contexts.get(protocol));
586
+ if (entries.length === 0 && ctx.length === 0) return null;
578
587
  return {
579
- frame: { cmd: entries.map((p) => {
580
- const { produced: _produced, ...command } = p.command;
581
- return {
582
- ...command,
583
- seq: p.seq
584
- };
585
- }) },
588
+ frame: {
589
+ ...ctx.length > 0 && { ctx },
590
+ ...entries.length > 0 && { cmd: entries.map((p) => {
591
+ const { produced: _produced, ...command } = p.command;
592
+ return {
593
+ ...command,
594
+ seq: p.seq
595
+ };
596
+ }) }
597
+ },
586
598
  entries
587
599
  };
588
600
  };
@@ -590,6 +602,8 @@ const takeAttachLanes = (s) => {
590
602
  s.attachEntries = /* @__PURE__ */ new Set();
591
603
  const wanted = s.lanesOnAttach;
592
604
  s.lanesOnAttach = false;
605
+ for (const protocol of s.contexts.keys()) s.dirtyContexts.add(protocol);
606
+ s.attachContexts = new Map(s.contexts);
593
607
  const lanes = buildLanes(s, wanted && s.sessionKnown ? "persisted" : "none");
594
608
  if (!lanes) return null;
595
609
  s.attachEntries = new Set(lanes.entries);
@@ -620,6 +634,7 @@ const pump = async (s) => {
620
634
  if (!s.hydrated) await s.ready;
621
635
  const transport = s.transport;
622
636
  if (buildLanes(s) === null) return;
637
+ if (transport?.parked && s.pending.length === 0) return;
623
638
  if (s.attach !== "attached" || !transport) {
624
639
  if (transport?.connection.status === "standby" && transport.connection.reason === "idle" || transport?.parked && s.pending.length > 0) transport.connection.reconnect();
625
640
  await waitForWake(s);
@@ -644,12 +659,21 @@ const pump = async (s) => {
644
659
  result = await (s.shared && !s.deactivated ? s.store.lock(send) : send());
645
660
  } catch {
646
661
  if (!s.running || isFinished(s)) return;
662
+ if (lanes.frame.ctx?.length) {
663
+ s.lanesOnAttach = true;
664
+ transport.connection.reconnect();
665
+ await waitForWake(s);
666
+ continue;
667
+ }
647
668
  s.frameFailures += 1;
648
669
  await sleepUntilWake(s, backoff(s.frameFailures));
649
670
  continue;
650
671
  }
651
672
  if (!s.running) return;
652
673
  s.frameFailures = 0;
674
+ if (epoch === s.attachEpoch && (result.type === "ok" || result.type === "refusal")) {
675
+ for (const entry of lanes.frame.ctx ?? []) if (s.contexts.get(entry.protocol) === entry) s.dirtyContexts.delete(entry.protocol);
676
+ }
653
677
  if (result.type === "ok") {
654
678
  if (epoch === s.attachEpoch) {
655
679
  for (const entry of lanes.entries) if (s.pending.includes(entry)) {
@@ -836,7 +860,7 @@ const sendCommand = (s, method, params, protocol = s.options.protocol ?? "defaul
836
860
  enqueue(s, {
837
861
  protocol,
838
862
  produced: {
839
- wire: s.options.wireVersion ?? { version: "2026-09-13" },
863
+ wire: s.options.wireVersion ?? { version: "2026-09-30" },
840
864
  protocols: protocolOffers(s.options) === "*" && s.selection ? s.selection.protocols : protocolOffers(s.options)
841
865
  },
842
866
  method,
@@ -968,7 +992,7 @@ const loadSessionRecord = (store, options) => {
968
992
  let stored = store.load();
969
993
  if (stored !== null) validateLanes(stored);
970
994
  const current = {
971
- wire: options.wireVersion ?? { version: "2026-09-13" },
995
+ wire: options.wireVersion ?? { version: "2026-09-30" },
972
996
  protocols: protocolOffers(options)
973
997
  };
974
998
  const compatible = (record) => Object.entries(record.lanes).every(([protocol, lane]) => lane.commands.every((command) => storedCommandCompatible(protocol, command, current)));
@@ -988,6 +1012,13 @@ const loadSessionRecord = (store, options) => {
988
1012
  if (stored === null) store.save(record);
989
1013
  return record;
990
1014
  };
1015
+ const setContext = (s, protocol, value) => {
1016
+ if (s.selection ? !s.selection.protocols.some((p) => p.name === protocol) : !offered(s, protocol)) throw new Error(`statewire: protocol "${protocol}" is not negotiated`);
1017
+ s.contexts.set(protocol, encodeContext(protocol, value));
1018
+ s.dirtyContexts.add(protocol);
1019
+ wake(s);
1020
+ pump(s);
1021
+ };
991
1022
  const useStatewireSession = (options) => {
992
1023
  const [defaultStore] = useState(createMemorySessionStore);
993
1024
  const mounted = useContext(SessionStorageContext);
@@ -1002,6 +1033,10 @@ const useStatewireSession = (options) => {
1002
1033
  };
1003
1034
  const session = {
1004
1035
  options,
1036
+ configuredContext: options.context,
1037
+ contexts: new Map(Object.entries(options.context ?? {}).map(([protocol, value]) => [protocol, encodeContext(protocol, value)])),
1038
+ dirtyContexts: new Set(Object.keys(options.context ?? {})),
1039
+ attachContexts: /* @__PURE__ */ new Map(),
1005
1040
  selection: void 0,
1006
1041
  record,
1007
1042
  ctx: {
@@ -1095,6 +1130,11 @@ const useStatewireSession = (options) => {
1095
1130
  return session;
1096
1131
  });
1097
1132
  if (store.lock !== void 0 !== (s.shared !== false)) throw new Error("statewire: session storage changed sharedness across renders — lock/subscribe must be present from the first render or never");
1133
+ useEffect(() => {
1134
+ if (s.configuredContext === options.context) return;
1135
+ s.configuredContext = options.context;
1136
+ for (const [protocol_0, value_0] of Object.entries(options.context ?? {})) setContext(s, protocol_0, value_0);
1137
+ }, [s, options.context]);
1098
1138
  s.options = options;
1099
1139
  s.store = store;
1100
1140
  s.documentStore = documentStore;
@@ -1118,6 +1158,6 @@ const useStatewireSession = (options) => {
1118
1158
  return s;
1119
1159
  };
1120
1160
  //#endregion
1121
- export { discardFailed, documentSource, failedCommands, invokeProtocol, retryFailed, setTransport, useStatewireSession };
1161
+ export { discardFailed, documentSource, failedCommands, invokeProtocol, retryFailed, setContext, setTransport, useStatewireSession };
1122
1162
 
1123
1163
  //# sourceMappingURL=session.js.map