statewire 0.24.1 → 0.24.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.
package/src/host/host.ts CHANGED
@@ -182,6 +182,7 @@ export type CommandsSubmission = {
182
182
  clientId?: string;
183
183
  caller?: StatewireHost.Caller;
184
184
  context?: Statewire.Context;
185
+ version?: string;
185
186
  attach?: StatewireHost.Attach;
186
187
  onApplied?: () => void;
187
188
  onSettle?: (outcome: CommandsOutcome) => void;
@@ -206,6 +207,7 @@ export const bindCommandsHost = (commands: object, host: CommandsHost) => {
206
207
 
207
208
  type QueuedCall = {
208
209
  context?: Statewire.Context;
210
+ version?: string | undefined;
209
211
  method: string;
210
212
  params: readonly unknown[];
211
213
  ts: number;
@@ -223,6 +225,7 @@ type ActiveInvocation = CommandsInvocation & {
223
225
 
224
226
  type CommandsCell = {
225
227
  context: (() => Statewire.Context) | undefined;
228
+ version: string | undefined;
226
229
  handlers: Record<string, (...args: never[]) => unknown> | null;
227
230
  queue: QueuedCall[];
228
231
  active: Map<ActiveInvocation, QueuedCall>;
@@ -364,9 +367,16 @@ const dispatch = (cell: CommandsCell, call: QueuedCall) => {
364
367
  cell.listener?.onStart(inv);
365
368
  cell.listener?.onPosted?.(inv);
366
369
  const attach = call.origin?.attach;
370
+ const version =
371
+ call.origin !== undefined ? call.origin.version : call.version;
367
372
  const ctx: StatewireHost.Ctx = {
368
373
  caller: call.origin?.caller ?? LOCAL_CALLER,
369
374
  context: call.context ?? call.origin?.context ?? Object.freeze({}),
375
+ get version() {
376
+ if (version === undefined)
377
+ throw new Error("statewire: in-process dispatch has no version");
378
+ return version;
379
+ },
370
380
  get attach() {
371
381
  if (attach === undefined)
372
382
  throw new Error("statewire: in-process dispatch has no attach");
@@ -445,6 +455,7 @@ export const useStatewireCommands = <THandlers extends Statewire.Commands>(
445
455
  const [built] = useState(() => {
446
456
  const cell: CommandsCell = {
447
457
  context: undefined,
458
+ version: undefined,
448
459
  handlers: null,
449
460
  queue: [],
450
461
  active: new Map(),
@@ -465,6 +476,7 @@ export const useStatewireCommands = <THandlers extends Statewire.Commands>(
465
476
  }
466
477
  cell.queue.push({
467
478
  context: captureContext(cell.context?.() ?? {}),
479
+ version: cell.version,
468
480
  method,
469
481
  params,
470
482
  ts: Date.now(),
@@ -507,6 +519,7 @@ export const useStatewireCommands = <THandlers extends Statewire.Commands>(
507
519
  });
508
520
 
509
521
  built.cell.context = options?.context;
522
+ built.cell.version = options?.version;
510
523
  useEffect(() => {
511
524
  const { cell } = built;
512
525
  cell.unmounted = false;
@@ -3,7 +3,6 @@ import { MAX_CONTEXT_BYTES } from "../wire/limits";
3
3
  import { encodeDocument } from "../document";
4
4
  import {
5
5
  DEFAULT_PROTOCOL,
6
- WIRE_VERSION,
7
6
  DEFAULT_PROTOCOL_VERSION,
8
7
  GAP_FILL_VERSION,
9
8
  MIN_SERVED_WIRE_VERSION,
@@ -183,8 +182,9 @@ export type ParsedFrame = {
183
182
  res?: { protocol: string; seq: number }[];
184
183
  };
185
184
 
185
+ // This host serves through the gap-fill wire: it names no attach to retire.
186
186
  const SERVED_WIRE: StatewireProtocol.Range = {
187
- version: WIRE_VERSION,
187
+ version: GAP_FILL_VERSION,
188
188
  minVersion: MIN_SERVED_WIRE_VERSION,
189
189
  };
190
190
 
@@ -1147,6 +1147,9 @@ const admitCmd = (
1147
1147
  caller: callerOf(h, rec, conn),
1148
1148
  attach: conn.attach,
1149
1149
  context: captureContext(conn.contexts.get(statement.protocol) ?? {}),
1150
+ version: conn.selection.protocols.find(
1151
+ (p) => p.name === statement.protocol,
1152
+ )!.version,
1150
1153
  onApplied: () => {
1151
1154
  if (adm.settled || adm.applied) return;
1152
1155
  adm.applied = true;
package/src/host.ts CHANGED
@@ -45,19 +45,27 @@ export namespace StatewireHost {
45
45
  setOnDocuments: (listener: (() => void) | null) => void;
46
46
  };
47
47
 
48
+ /** What `this.context` and `this.version` read for a call made on the proxy (in-process dispatch). */
49
+ export type CommandsOptions = {
50
+ context?: () => Statewire.Context;
51
+ version?: string;
52
+ };
53
+
48
54
  /**
49
55
  * Per-invocation handle bound as `this` of a command handler: `caller` is
50
56
  * the submitting client, `attach` the admitting attach's document view
51
- * (in-process dispatch has none — reading it throws), `applied()` states the
52
- * broadcast state now contains the command's effects — the client drops its
53
- * optimistic overlay — before the handler settles (idempotent, a no-op after
54
- * settle). The command settles when the handler's returned promise does.
57
+ * (in-process dispatch has none — reading it throws), `version` the
58
+ * command protocol's version that attach negotiated (in-process dispatch
59
+ * reads `CommandsOptions.version`; without one, reading it throws),
60
+ * `applied()` states the broadcast state now contains the command's effects
61
+ * — the client drops its optimistic overlay — before the handler settles
62
+ * (idempotent, a no-op after settle). The command settles when the
63
+ * handler's returned promise does.
55
64
  */
56
- export type CommandsOptions = { context?: () => Statewire.Context };
57
-
58
65
  export type Ctx = {
59
66
  readonly caller: Caller;
60
67
  readonly context: Statewire.Context;
68
+ readonly version: string;
61
69
  readonly attach: Attach;
62
70
  readonly signal: AbortSignal;
63
71
  readonly applied: () => void;
@@ -4,6 +4,7 @@ import type { ResourceElement } from "@assistant-ui/tap";
4
4
  import { createSseParser } from "./sse-parser";
5
5
  import { timing } from "../client/timing";
6
6
  import {
7
+ replacing,
7
8
  settleFin,
8
9
  classify,
9
10
  handleEnvelope,
@@ -149,6 +150,7 @@ const attemptStream = async (
149
150
  if (!headers.has(name)) headers.set(name, value);
150
151
  }
151
152
  c.negotiation = parseNegotiation(headers);
153
+ replacing(c, headers);
152
154
  c.authStale = false;
153
155
  const lanes = flushTapSync(() => c.ctx.takeAttachLanes());
154
156
  const body = lanes === null ? undefined : JSON.stringify(lanes);
@@ -18,9 +18,17 @@ export namespace StatewireProtocol {
18
18
  };
19
19
  }
20
20
 
21
- export const WIRE_VERSION = "2026-11-01";
21
+ export const WIRE_VERSION = "2026-11-02";
22
22
  /** Attach carries a results cursor and gets its verdicts gap-filled; a re-sent seq at or below the watermark is a protocol error. */
23
23
  export const GAP_FILL_VERSION = "2026-11-01";
24
+ /** Every attach of a connection names its attacher; a host retires the attacher's earlier attaches before the hello. */
25
+ export const FENCE_VERSION = "2026-11-02";
26
+ /** The wire a client offers by default: the gap-fill contract and newer. */
27
+ export const CLIENT_WIRE: StatewireProtocol.Range = {
28
+ version: WIRE_VERSION,
29
+ minVersion: GAP_FILL_VERSION,
30
+ };
31
+ export const ATTACHER_HEADER = "Statewire-Attacher";
24
32
  /** The oldest wire contract a host serves by default (re-sent seqs re-emit their verdicts). */
25
33
  export const MIN_SERVED_WIRE_VERSION = "2026-09-30";
26
34
  export const DEFAULT_PROTOCOL_VERSION = "2026-09-30";
@@ -81,7 +89,7 @@ export const negotiationHeaders = (
81
89
  if (offers !== "*" && !offers.length)
82
90
  invalid("protocol list must not be empty");
83
91
  return {
84
- [VERSION_HEADER]: suffix(options.wireVersion ?? { version: WIRE_VERSION }),
92
+ [VERSION_HEADER]: suffix(options.wireVersion ?? CLIENT_WIRE),
85
93
  [PROTOCOL_HEADER]:
86
94
  offers === "*"
87
95
  ? "*"
@@ -201,7 +209,7 @@ export const validateSelection = (
201
209
  return invalid("missing negotiation selection");
202
210
  const syn = value as Record<string, unknown>;
203
211
  const version = validateVersion(syn.version);
204
- if (!inRange(version, options.wireVersion ?? { version: WIRE_VERSION }))
212
+ if (!inRange(version, options.wireVersion ?? CLIENT_WIRE))
205
213
  invalid("selected wire version is outside offer");
206
214
  if (!Array.isArray(syn.protocols))
207
215
  return invalid("syn.protocols must be an array");