statewire 0.24.1 → 0.24.3
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/dist/client/connection.d.ts +4 -0
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +9 -2
- package/dist/client/connection.js.map +1 -1
- package/dist/client/session.d.ts +2 -0
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +52 -11
- package/dist/client/session.js.map +1 -1
- package/dist/host/host.d.ts +1 -0
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +8 -0
- package/dist/host/host.js.map +1 -1
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +3 -2
- package/dist/host/socket.js.map +1 -1
- package/dist/host.d.ts +13 -7
- package/dist/host.d.ts.map +1 -1
- package/dist/storage.js +1 -1
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js +2 -1
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +6 -2
- package/dist/transports/testing.js.map +1 -1
- package/dist/wire/protocol.d.ts +6 -1
- package/dist/wire/protocol.d.ts.map +1 -1
- package/dist/wire/protocol.js +12 -4
- package/dist/wire/protocol.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +11 -0
- package/src/client/session.ts +85 -11
- package/src/host/host.ts +13 -0
- package/src/host/socket.ts +5 -2
- package/src/host.ts +14 -6
- package/src/transports/sse.ts +2 -0
- package/src/transports/testing.ts +15 -2
- package/src/wire/protocol.ts +11 -3
package/src/client/session.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { encodeContext } from "../wire/context";
|
|
|
2
2
|
import { wrapStorageError } from "./storageError";
|
|
3
3
|
import {
|
|
4
4
|
DEFAULT_PROTOCOL,
|
|
5
|
+
CLIENT_WIRE,
|
|
5
6
|
GAP_FILL_VERSION,
|
|
6
|
-
WIRE_VERSION,
|
|
7
7
|
protocolOffers,
|
|
8
8
|
inRange,
|
|
9
9
|
} from "../wire/protocol";
|
|
@@ -163,6 +163,8 @@ type Session = {
|
|
|
163
163
|
/** Ephemeral lane clocks: restart at 1 on every attach. */
|
|
164
164
|
ephemeralSeqs: Record<string, number>;
|
|
165
165
|
resumeAt: number;
|
|
166
|
+
/** A refusal named sent durable stamps: a reattach's hello decides them, after `resumeAt`. */
|
|
167
|
+
reattach: boolean;
|
|
166
168
|
idle: boolean;
|
|
167
169
|
onSessionLost: StatewireClient.OnSessionLost | undefined;
|
|
168
170
|
onDeprecatedPath: StatewireClient.OnDeprecatedPath | undefined;
|
|
@@ -1326,14 +1328,16 @@ const sessionLostInner = (s: Session, protocol: string, seqWm: number) => {
|
|
|
1326
1328
|
},
|
|
1327
1329
|
);
|
|
1328
1330
|
} else {
|
|
1329
|
-
// Default: the
|
|
1331
|
+
// Default: the queue goes, each command a failure notice. Held entries
|
|
1332
|
+
// never went out: they stay held where they are.
|
|
1330
1333
|
const stamps = mergeBacklog(s, protocol);
|
|
1331
|
-
const
|
|
1332
|
-
const
|
|
1334
|
+
const held = outboxOf(lane).filter((c) => c.held);
|
|
1335
|
+
const queued = outboxOf(lane).filter((c) => !c.held);
|
|
1336
|
+
const keep = new Set(held.map((c) => c.id));
|
|
1333
1337
|
setRecord(
|
|
1334
1338
|
s,
|
|
1335
1339
|
withLane(s.record, protocol, {
|
|
1336
|
-
...withNotices(
|
|
1340
|
+
...withNotices(withOutbox(lane, held), [
|
|
1337
1341
|
...stamps.map((c) => lostNotice(c)),
|
|
1338
1342
|
...queued.map((c) => lostNotice(c)),
|
|
1339
1343
|
]),
|
|
@@ -1350,7 +1354,9 @@ const sessionLostInner = (s: Session, protocol: string, seqWm: number) => {
|
|
|
1350
1354
|
),
|
|
1351
1355
|
}),
|
|
1352
1356
|
);
|
|
1353
|
-
for (const p of s.pending.filter(
|
|
1357
|
+
for (const p of s.pending.filter(
|
|
1358
|
+
(q) => q.command.protocol === protocol && !keep.has(q.id),
|
|
1359
|
+
)) {
|
|
1354
1360
|
s.lifecycle?.onCovered?.(p.inv);
|
|
1355
1361
|
settleEntry(s, p, { ok: false, failure }, "noticed");
|
|
1356
1362
|
}
|
|
@@ -1451,6 +1457,21 @@ const selectionOf = (
|
|
|
1451
1457
|
};
|
|
1452
1458
|
};
|
|
1453
1459
|
|
|
1460
|
+
// The host admits an attach's durable statements right after its hello.
|
|
1461
|
+
const carriedThrough = (
|
|
1462
|
+
lane: { name: string; seq?: number },
|
|
1463
|
+
attached: ReadonlySet<Pending>,
|
|
1464
|
+
) => {
|
|
1465
|
+
const carried = new Set(
|
|
1466
|
+
[...attached]
|
|
1467
|
+
.filter((p) => p.command.protocol === lane.name)
|
|
1468
|
+
.map((p) => p.seq),
|
|
1469
|
+
);
|
|
1470
|
+
let seq = lane.seq ?? 0;
|
|
1471
|
+
while (carried.has(seq + 1)) seq += 1;
|
|
1472
|
+
return seq;
|
|
1473
|
+
};
|
|
1474
|
+
|
|
1454
1475
|
const handleHello = (
|
|
1455
1476
|
s: Session,
|
|
1456
1477
|
{ ops, syn }: { ops: unknown[]; syn: StatewireSyn },
|
|
@@ -1472,6 +1493,7 @@ const handleHello = (
|
|
|
1472
1493
|
s.attachEpoch += 1;
|
|
1473
1494
|
s.frameFailures = 0;
|
|
1474
1495
|
s.resumeAt = 0;
|
|
1496
|
+
s.reattach = false;
|
|
1475
1497
|
const lanes = syn.protocols ?? [];
|
|
1476
1498
|
s.ephemeral =
|
|
1477
1499
|
lanes.length > 0 && lanes.every((lane) => lane.record === "ephemeral");
|
|
@@ -1494,7 +1516,7 @@ const handleHello = (
|
|
|
1494
1516
|
);
|
|
1495
1517
|
continue;
|
|
1496
1518
|
}
|
|
1497
|
-
demote(s, lane.name, lane
|
|
1519
|
+
demote(s, lane.name, carriedThrough(lane, attached), demoted, waiting);
|
|
1498
1520
|
s.durableSeq[lane.name] = lane.seq ?? 0;
|
|
1499
1521
|
}
|
|
1500
1522
|
for (const entry of s.pending) {
|
|
@@ -1543,10 +1565,26 @@ const handleOps = (
|
|
|
1543
1565
|
// A retry-less {syn} refusal is the one session-loss verdict: a gap against
|
|
1544
1566
|
// a durable lane's exact watermark settles that lane's history lost; a
|
|
1545
1567
|
// pushback (retry present) or a gapless refusal only reconciles the resend queues.
|
|
1568
|
+
// Under the gap-fill wire a refusal can predate frames sent since: durable
|
|
1569
|
+
// lanes never resend from it, a reattach's hello decides them.
|
|
1546
1570
|
const handleRefusal = (s: Session, syn: StatewireSyn) => {
|
|
1547
|
-
const
|
|
1571
|
+
const stated = (syn.protocols ?? []).filter(
|
|
1548
1572
|
(lane): lane is { name: string; seq: number } => lane.seq !== undefined,
|
|
1549
1573
|
);
|
|
1574
|
+
const lanes = gapFill(s)
|
|
1575
|
+
? stated.filter(({ name }) => isEphemeral(s, name))
|
|
1576
|
+
: stated;
|
|
1577
|
+
if (
|
|
1578
|
+
gapFill(s) &&
|
|
1579
|
+
s.pending.some((p) => {
|
|
1580
|
+
if (!isStamped(p) || !p.attempted) return false;
|
|
1581
|
+
const protocol = p.command.protocol;
|
|
1582
|
+
if (isEphemeral(s, protocol)) return false;
|
|
1583
|
+
const seq = stated.find((lane) => lane.name === protocol)?.seq ?? 0;
|
|
1584
|
+
return p.seq > Math.max(seq, durableSeqOf(s, protocol));
|
|
1585
|
+
})
|
|
1586
|
+
)
|
|
1587
|
+
s.reattach = true;
|
|
1550
1588
|
for (const { name, seq } of lanes) {
|
|
1551
1589
|
const watermark = isEphemeral(s, name)
|
|
1552
1590
|
? seq
|
|
@@ -1728,6 +1766,29 @@ const pump = async (s: Session) => {
|
|
|
1728
1766
|
while (s.running && !isFinished(s)) {
|
|
1729
1767
|
if (!s.hydrated) await s.ready;
|
|
1730
1768
|
const transport = s.transport;
|
|
1769
|
+
if (s.reattach && s.attach === "attached" && transport) {
|
|
1770
|
+
const delay = s.resumeAt - Date.now();
|
|
1771
|
+
if (delay > 0) {
|
|
1772
|
+
await sleepUntilWake(s, delay);
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
1775
|
+
// Ephemeral answers ride only this attach: drain them first.
|
|
1776
|
+
if (
|
|
1777
|
+
s.pending.some(
|
|
1778
|
+
(p) =>
|
|
1779
|
+
isStamped(p) && p.attempted && isEphemeral(s, p.command.protocol),
|
|
1780
|
+
)
|
|
1781
|
+
) {
|
|
1782
|
+
await waitForWake(s);
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
s.reattach = false;
|
|
1786
|
+
const epoch = s.attachEpoch;
|
|
1787
|
+
transport.connection.reconnect();
|
|
1788
|
+
while (s.running && !isFinished(s) && epoch === s.attachEpoch)
|
|
1789
|
+
await waitForWake(s);
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1731
1792
|
const flushable = flushCandidates(s).length > 0;
|
|
1732
1793
|
const queued =
|
|
1733
1794
|
s.pending.some((p) => isOutboxed(p) && !p.inv.held) ||
|
|
@@ -2444,7 +2505,7 @@ export const updateOutbox = (
|
|
|
2444
2505
|
const producedOf = (
|
|
2445
2506
|
s: Session,
|
|
2446
2507
|
): NonNullable<StatewireClient.SessionCommand["produced"]> => ({
|
|
2447
|
-
wire: s.options.wireVersion ??
|
|
2508
|
+
wire: s.options.wireVersion ?? CLIENT_WIRE,
|
|
2448
2509
|
protocols:
|
|
2449
2510
|
protocolOffers(s.options) === "*" && s.selection
|
|
2450
2511
|
? s.selection.protocols
|
|
@@ -2512,6 +2573,7 @@ export const insertOutbox = (
|
|
|
2512
2573
|
return;
|
|
2513
2574
|
}
|
|
2514
2575
|
let base = s.record;
|
|
2576
|
+
let entered = stored;
|
|
2515
2577
|
if (entry.from !== undefined) {
|
|
2516
2578
|
// Re-inserting a settled update consumes it in the same write: a
|
|
2517
2579
|
// redelivery finds it gone.
|
|
@@ -2519,16 +2581,27 @@ export const insertOutbox = (
|
|
|
2519
2581
|
l.inbox?.some((n) => n.id === entry.from),
|
|
2520
2582
|
);
|
|
2521
2583
|
if (source === undefined || locate(s, id) !== undefined) return;
|
|
2584
|
+
const notice = source[1].inbox!.find((n) => n.id === entry.from)!;
|
|
2585
|
+
entered = { ...stored, ts: notice.ts };
|
|
2522
2586
|
base = withLane(base, source[0], withoutNotice(source[1], entry.from));
|
|
2523
2587
|
}
|
|
2524
2588
|
const lane = laneOf(base, protocol);
|
|
2589
|
+
// A re-queued command keeps its place: ahead of what was queued after it.
|
|
2590
|
+
const later = outboxOf(lane).find((c) => c.ts >= entered.ts);
|
|
2591
|
+
const position =
|
|
2592
|
+
entry.from !== undefined &&
|
|
2593
|
+
entry.after === undefined &&
|
|
2594
|
+
entry.before === undefined &&
|
|
2595
|
+
later !== undefined
|
|
2596
|
+
? { before: later.id }
|
|
2597
|
+
: entry;
|
|
2525
2598
|
try {
|
|
2526
2599
|
setRecord(
|
|
2527
2600
|
s,
|
|
2528
2601
|
withLane(
|
|
2529
2602
|
base,
|
|
2530
2603
|
protocol,
|
|
2531
|
-
withOutbox(lane, placed(outboxOf(lane),
|
|
2604
|
+
withOutbox(lane, placed(outboxOf(lane), entered, position)),
|
|
2532
2605
|
),
|
|
2533
2606
|
);
|
|
2534
2607
|
} catch (error) {
|
|
@@ -2800,7 +2873,7 @@ const loadSessionRecord = (
|
|
|
2800
2873
|
let stored = store.load();
|
|
2801
2874
|
if (stored !== null) validateLanes(stored);
|
|
2802
2875
|
const current = {
|
|
2803
|
-
wire: options.wireVersion ??
|
|
2876
|
+
wire: options.wireVersion ?? CLIENT_WIRE,
|
|
2804
2877
|
protocols: protocolOffers(options),
|
|
2805
2878
|
};
|
|
2806
2879
|
const compatible = (record: StatewireClient.SessionRecord) =>
|
|
@@ -2933,6 +3006,7 @@ export const useStatewireSession = (
|
|
|
2933
3006
|
durableSeq: {},
|
|
2934
3007
|
ephemeralSeqs: {},
|
|
2935
3008
|
resumeAt: 0,
|
|
3009
|
+
reattach: false,
|
|
2936
3010
|
idle: false,
|
|
2937
3011
|
onSessionLost: undefined,
|
|
2938
3012
|
onDeprecatedPath: undefined,
|
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;
|
package/src/host/socket.ts
CHANGED
|
@@ -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:
|
|
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), `
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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;
|
package/src/transports/sse.ts
CHANGED
|
@@ -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);
|
|
@@ -7,6 +7,7 @@ import { openSSEStream, sseFrame } from "./fake-sse";
|
|
|
7
7
|
import type { FakeStreamHandle } from "./fake-sse";
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_PROTOCOL_VERSION,
|
|
10
|
+
GAP_FILL_VERSION,
|
|
10
11
|
WIRE_VERSION,
|
|
11
12
|
parseNegotiation,
|
|
12
13
|
} from "../wire/protocol";
|
|
@@ -93,7 +94,19 @@ export const createMockTransport = <TState>(
|
|
|
93
94
|
if (!clients.has(id)) clients.set(id, {});
|
|
94
95
|
const rec = clients.get(id)!;
|
|
95
96
|
const ephemeral = ephemeralLanes();
|
|
96
|
-
const
|
|
97
|
+
const negotiation = parseNegotiation(headers);
|
|
98
|
+
const offers = negotiation.applicationProtocols;
|
|
99
|
+
// The newest wire this mock speaks that the client offered. It speaks
|
|
100
|
+
// GAP_FILL_VERSION and newer: it skips a re-sent seq instead of
|
|
101
|
+
// re-emitting its verdict, as older wires require.
|
|
102
|
+
const offered = negotiation.wireVersion;
|
|
103
|
+
const wireVersion =
|
|
104
|
+
offered.version < WIRE_VERSION ? offered.version : WIRE_VERSION;
|
|
105
|
+
if (
|
|
106
|
+
wireVersion < GAP_FILL_VERSION ||
|
|
107
|
+
(offered.minVersion !== undefined && wireVersion < offered.minVersion)
|
|
108
|
+
)
|
|
109
|
+
throw new Error("mock wire version was not offered");
|
|
97
110
|
const applicationVersion =
|
|
98
111
|
offers === "*"
|
|
99
112
|
? DEFAULT_PROTOCOL_VERSION
|
|
@@ -111,7 +124,7 @@ export const createMockTransport = <TState>(
|
|
|
111
124
|
sseFrame({
|
|
112
125
|
ops: [{ op: "mount", protocol, main: true, value: state }],
|
|
113
126
|
syn: {
|
|
114
|
-
version:
|
|
127
|
+
version: wireVersion,
|
|
115
128
|
protocols: [
|
|
116
129
|
{
|
|
117
130
|
name: protocol,
|
package/src/wire/protocol.ts
CHANGED
|
@@ -18,9 +18,17 @@ export namespace StatewireProtocol {
|
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
export const WIRE_VERSION = "2026-11-
|
|
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 ??
|
|
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 ??
|
|
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");
|