experimental-a2 0.21.1 → 0.23.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.
- package/CHANGELOG.md +32 -0
- package/dist/{actor-HnGmrOZZ.d.ts → actor-CqibLgjQ.d.ts} +13 -5
- package/dist/actor-CqibLgjQ.d.ts.map +1 -0
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +1 -1
- package/dist/actor-react.js +1 -1
- package/dist/actor-shared-DFET5q16.js.map +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +3 -3
- package/dist/{ai-CvEnqVfo.d.ts → ai-DAu77CDr.d.ts} +2 -2
- package/dist/{ai-CvEnqVfo.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
- package/dist/{ai-CzE672Q3.js → ai-EEfKyc-z.js} +2 -2
- package/dist/{ai-CzE672Q3.js.map → ai-EEfKyc-z.js.map} +1 -1
- package/dist/ai-client.d.ts +1 -1
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.d.ts +4 -2
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +234 -62
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-Bfk-Lycn.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
- package/dist/{ai-harness-Bfk-Lycn.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
- package/dist/ai-harness-dQeJaR1t.js.map +1 -1
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +1 -1
- package/dist/ai-react.d.ts +1 -1
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +3 -3
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +10 -4
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/{client-9IeTsiAs.js → client-BQS4i_V9.js} +27 -4
- package/dist/client-BQS4i_V9.js.map +1 -0
- package/dist/client-CUMbS_38.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/{internal-Bc4PvfHk.js → internal-CvsrprnU.js} +32 -15
- package/dist/{internal-Bc4PvfHk.js.map → internal-CvsrprnU.js.map} +1 -1
- package/dist/react.js +1 -1
- package/dist/{runtime-consumer-BWmht60z.js → runtime-consumer-CIeL33wj.js} +2 -2
- package/dist/{runtime-consumer-BWmht60z.js.map → runtime-consumer-CIeL33wj.js.map} +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +2 -2
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +2 -2
- package/dist/{server-DvIB8kj5.js → server-DtFrWxi3.js} +30 -8
- package/dist/server-DtFrWxi3.js.map +1 -0
- package/dist/{server-BshFZ0eL.d.ts → server-qgez0Xny.d.ts} +3 -3
- package/dist/{server-BshFZ0eL.d.ts.map → server-qgez0Xny.d.ts.map} +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/{session-clear-Bk_29eEw.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
- package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
- package/dist/{store-Z2qjw50n.d.ts → store-D0MhBvg6.d.ts} +15 -6
- package/dist/store-D0MhBvg6.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +15 -17
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-DYNyxCev.js → store-polling-BZt2Rf3D.js} +2 -2
- package/dist/{store-polling-DYNyxCev.js.map → store-polling-BZt2Rf3D.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +31 -40
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
- package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -3
- package/dist/{store-redis-notify-IiVdlYHc.js → store-redis-notify-CBDVJxA_.js} +545 -399
- package/dist/store-redis-notify-CBDVJxA_.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +3 -4
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +16 -18
- package/dist/store-sqlite.js.map +1 -1
- package/dist/store-valkey.d.ts +33 -0
- package/dist/store-valkey.d.ts.map +1 -0
- package/dist/store-valkey.js +218 -0
- package/dist/store-valkey.js.map +1 -0
- package/dist/stream-buffer-DrTyiIuo.js +57 -0
- package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
- package/dist/{wire-CZGqNZRx.js → wire-CV-Z7wz-.js} +3 -3
- package/dist/{wire-CZGqNZRx.js.map → wire-CV-Z7wz-.js.map} +1 -1
- package/docs/actors/01-introduction.mdx +5 -2
- package/docs/agents/03-conversations.mdx +9 -8
- package/docs/agents/04-harnesses.mdx +7 -4
- package/docs/client/03-presence.mdx +1 -0
- package/docs/reference/01-core.mdx +4 -2
- package/docs/reference/02-server.mdx +5 -1
- package/docs/reference/04-ai.mdx +13 -4
- package/docs/reference/06-errors.mdx +1 -1
- package/docs/server/01-consumers.mdx +4 -1
- package/docs/server/06-guarantees.mdx +7 -4
- package/docs/stores/01-overview.mdx +1 -0
- package/docs/stores/02-redis.mdx +18 -10
- package/docs/stores/03-redis-http.mdx +6 -4
- package/docs/stores/04-postgres.mdx +2 -2
- package/docs/stores/07-valkey.mdx +97 -0
- package/docs/upgrades/01-overview.mdx +1 -0
- package/docs/upgrades/78-0-22.mdx +115 -0
- package/examples/playground/app/a0/sandbox.ts +58 -13
- package/examples/playground/app/a2slack/db/a2.ts +7 -4
- package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +8 -1
- package/examples/playground/app/chat/server.ts +2 -2
- package/examples/playground/lib/store.ts +1 -1
- package/examples/playground/package.json +1 -1
- package/package.json +8 -2
- package/sql/postgres/drizzle-schema.ts +7 -4
- package/sql/postgres/schema.sql +24 -3
- package/src/actor-shared.ts +10 -2
- package/src/ai-agent.ts +13 -0
- package/src/ai-fx.ts +365 -78
- package/src/ai-harness.ts +7 -0
- package/src/client.ts +19 -2
- package/src/deterministic-id.ts +1 -1
- package/src/internal.ts +42 -11
- package/src/libfx.d.ts +2 -0
- package/src/presence-recovery.ts +18 -11
- package/src/presence-tombstones.ts +52 -0
- package/src/push-envelope.ts +2 -2
- package/src/server.ts +49 -15
- package/src/store-memory.ts +24 -32
- package/src/store-postgres.ts +43 -53
- package/src/store-presence-polling.ts +59 -34
- package/src/store-redis-core.ts +595 -439
- package/src/store-redis-notify.ts +82 -28
- package/src/store-redis.ts +2 -3
- package/src/store-sqlite.ts +34 -40
- package/src/store-valkey.ts +376 -0
- package/src/store.ts +14 -5
- package/dist/actor-HnGmrOZZ.d.ts.map +0 -1
- package/dist/client-9IeTsiAs.js.map +0 -1
- package/dist/server-DvIB8kj5.js.map +0 -1
- package/dist/session-clear-Bk_29eEw.d.ts.map +0 -1
- package/dist/store-Z2qjw50n.d.ts.map +0 -1
- package/dist/store-presence-polling-CzQ0wdtM.js +0 -94
- package/dist/store-presence-polling-CzQ0wdtM.js.map +0 -1
- package/dist/store-redis-notify-IiVdlYHc.js.map +0 -1
- package/dist/stream-buffer-Cx7AAPGq.js +0 -23
- package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { i as A2Error } from "./session-clear-wHd5GCpZ.js";
|
|
2
|
+
import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
|
|
3
|
+
import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
|
|
4
|
+
import { t as createRedisNotifyStore } from "./store-redis-notify-CBDVJxA_.js";
|
|
5
|
+
//#region src/store-valkey.ts
|
|
6
|
+
/** Overrides any `commandOptions.typeMapping`: replies decode as strings. */
|
|
7
|
+
const STRING_REPLIES = { typeMapping: {} };
|
|
8
|
+
/**
|
|
9
|
+
* node-redis 6 needs `destroy()` to release a client whose reconnect
|
|
10
|
+
* strategy gave up; node-redis 5 throws there instead.
|
|
11
|
+
*/
|
|
12
|
+
const dispose = (client) => {
|
|
13
|
+
try {
|
|
14
|
+
client?.destroy();
|
|
15
|
+
} catch {}
|
|
16
|
+
};
|
|
17
|
+
const closedError = () => new A2Error("STORE_UNAVAILABLE", "the store was closed");
|
|
18
|
+
/** node-redis 6's default `commandOptions.timeout`; node-redis 5 has none. */
|
|
19
|
+
const DEFAULT_TIMEOUT_MS = 5e3;
|
|
20
|
+
/**
|
|
21
|
+
* One A2-owned client at a time. `get` hands a new client out once it
|
|
22
|
+
* first reports ready, waiting at most `timeoutMs` per call while it keeps
|
|
23
|
+
* connecting, so `disableOfflineQueue` cannot reject a cold start's first
|
|
24
|
+
* calls. Later reconnects follow node-redis's offline queue,
|
|
25
|
+
* `disableOfflineQueue`, and command timeouts. A client whose connect
|
|
26
|
+
* failed, or whose reconnect strategy gave up, is closed for good, so the
|
|
27
|
+
* next `get` replaces it.
|
|
28
|
+
*/
|
|
29
|
+
const ownedClient = (options) => {
|
|
30
|
+
let current = null;
|
|
31
|
+
let closed = false;
|
|
32
|
+
const firstReady = async (entry) => {
|
|
33
|
+
const client = await entry.created;
|
|
34
|
+
if (entry.ready) return client;
|
|
35
|
+
let timer;
|
|
36
|
+
try {
|
|
37
|
+
await Promise.race([entry.connected, new Promise((_, reject) => {
|
|
38
|
+
timer = setTimeout(() => {
|
|
39
|
+
reject(new A2Error("STORE_UNAVAILABLE", `connect timed out after ${options.timeoutMs}ms`));
|
|
40
|
+
}, options.timeoutMs);
|
|
41
|
+
})]);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err instanceof A2Error) throw err;
|
|
44
|
+
throw new A2Error("STORE_UNAVAILABLE", "connect failed", { cause: err });
|
|
45
|
+
} finally {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
}
|
|
48
|
+
return client;
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
get() {
|
|
52
|
+
if (closed) return Promise.reject(closedError());
|
|
53
|
+
if (current && (current.client === null || current.client.isOpen)) return firstReady(current);
|
|
54
|
+
dispose(current?.client);
|
|
55
|
+
const entry = {
|
|
56
|
+
client: null,
|
|
57
|
+
connected: null,
|
|
58
|
+
ready: false,
|
|
59
|
+
created: options.driver().then((driver) => {
|
|
60
|
+
if (closed) throw closedError();
|
|
61
|
+
const client = driver.create();
|
|
62
|
+
entry.client = client;
|
|
63
|
+
client.on("error", () => {});
|
|
64
|
+
entry.connected = client.connect().then(() => {
|
|
65
|
+
entry.ready = true;
|
|
66
|
+
}, (err) => {
|
|
67
|
+
if (current === entry) {
|
|
68
|
+
dispose(client);
|
|
69
|
+
current = null;
|
|
70
|
+
}
|
|
71
|
+
throw err;
|
|
72
|
+
});
|
|
73
|
+
entry.connected.catch(() => {});
|
|
74
|
+
options.opened(client);
|
|
75
|
+
return client;
|
|
76
|
+
})
|
|
77
|
+
};
|
|
78
|
+
current = entry;
|
|
79
|
+
entry.created.catch(() => {
|
|
80
|
+
if (current === entry) current = null;
|
|
81
|
+
});
|
|
82
|
+
return firstReady(entry);
|
|
83
|
+
},
|
|
84
|
+
close() {
|
|
85
|
+
closed = true;
|
|
86
|
+
dispose(current?.client);
|
|
87
|
+
current = null;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
function valkey(options = {}) {
|
|
92
|
+
const clock = options.clock ?? SYSTEM_CLOCK;
|
|
93
|
+
const ids = options.ids ?? RANDOM_IDS;
|
|
94
|
+
const prefix = options.keyPrefix ?? "a2";
|
|
95
|
+
if (!options.url && !options.clientOptions) throw new TypeError("valkey() needs a url or clientOptions");
|
|
96
|
+
const clientOptions = {
|
|
97
|
+
...options.clientOptions,
|
|
98
|
+
...options.url ? { url: options.url } : {},
|
|
99
|
+
RESP: 2
|
|
100
|
+
};
|
|
101
|
+
const driver = retryableLazy(async () => {
|
|
102
|
+
const mod = await import("redis").catch(() => {
|
|
103
|
+
throw new A2Error("STORE_NOT_CONFIGURED", "experimental-a2/store-valkey needs the 'redis' package (optional peer dependency) — install it");
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
create: () => mod.createClient(clientOptions),
|
|
107
|
+
bytes: { typeMapping: { [mod.RESP_TYPES.BLOB_STRING]: Buffer } }
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
const timeoutMs = clientOptions.commandOptions?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
111
|
+
const commands = ownedClient({
|
|
112
|
+
driver: driver.get,
|
|
113
|
+
opened: () => {},
|
|
114
|
+
timeoutMs
|
|
115
|
+
});
|
|
116
|
+
/**
|
|
117
|
+
* Channels the subscriber holds, restored on a replacement client. Each
|
|
118
|
+
* maps to the call that subscribed it, so a failed call only forgets its
|
|
119
|
+
* own subscription.
|
|
120
|
+
*/
|
|
121
|
+
const channels = /* @__PURE__ */ new Map();
|
|
122
|
+
let deliver;
|
|
123
|
+
let reconnected;
|
|
124
|
+
const onMessage = (message, channel) => {
|
|
125
|
+
deliver?.(channel, message);
|
|
126
|
+
};
|
|
127
|
+
const resubscribed = () => {
|
|
128
|
+
for (const channel of channels.keys()) reconnected?.(channel);
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* node-redis queues SUBSCRIBE outside `sendCommand`, so neither
|
|
132
|
+
* `disableOfflineQueue` nor `commandOptions.timeout` applies to it: a
|
|
133
|
+
* subscribe sent while the subscriber reconnects would wait for the
|
|
134
|
+
* server indefinitely. Apply both here. A timed-out SUBSCRIBE stays
|
|
135
|
+
* queued, so an UNSUBSCRIBE queued behind it cancels it.
|
|
136
|
+
*/
|
|
137
|
+
const subscribeBounded = async (client, channel) => {
|
|
138
|
+
if (!client.isReady && clientOptions.disableOfflineQueue) throw new A2Error("STORE_UNAVAILABLE", "the subscriber is offline");
|
|
139
|
+
let timer;
|
|
140
|
+
const timedOut = new Promise((_, reject) => {
|
|
141
|
+
timer = setTimeout(() => {
|
|
142
|
+
client.unsubscribe(channel).catch(() => {});
|
|
143
|
+
reject(new A2Error("STORE_UNAVAILABLE", `subscribe timed out after ${timeoutMs}ms`));
|
|
144
|
+
}, timeoutMs);
|
|
145
|
+
});
|
|
146
|
+
try {
|
|
147
|
+
return await Promise.race([client.subscribe(channel, onMessage), timedOut]);
|
|
148
|
+
} finally {
|
|
149
|
+
clearTimeout(timer);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
const subscriber = ownedClient({
|
|
153
|
+
driver: driver.get,
|
|
154
|
+
opened: (client) => {
|
|
155
|
+
let readies = 0;
|
|
156
|
+
client.on("ready", () => {
|
|
157
|
+
readies += 1;
|
|
158
|
+
if (readies > 1) resubscribed();
|
|
159
|
+
});
|
|
160
|
+
if (channels.size === 0) return;
|
|
161
|
+
Promise.all([...channels.keys()].map((channel) => client.subscribe(channel, onMessage))).then(resubscribed, () => {});
|
|
162
|
+
},
|
|
163
|
+
timeoutMs
|
|
164
|
+
});
|
|
165
|
+
const subscriberConnection = {
|
|
166
|
+
async call(command, ...args) {
|
|
167
|
+
const channel = String(args[0]);
|
|
168
|
+
const name = command.toLowerCase();
|
|
169
|
+
if (name === "subscribe") {
|
|
170
|
+
const call = Symbol(channel);
|
|
171
|
+
try {
|
|
172
|
+
const client = await subscriber.get();
|
|
173
|
+
channels.set(channel, call);
|
|
174
|
+
return await subscribeBounded(client, channel);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (channels.get(channel) === call) channels.delete(channel);
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (name === "unsubscribe") {
|
|
181
|
+
channels.delete(channel);
|
|
182
|
+
return (await subscriber.get()).unsubscribe(channel);
|
|
183
|
+
}
|
|
184
|
+
return (await subscriber.get()).sendCommand([command, ...args.map(String)], STRING_REPLIES);
|
|
185
|
+
},
|
|
186
|
+
duplicate: () => subscriberConnection,
|
|
187
|
+
on(_event, listener) {
|
|
188
|
+
deliver = listener;
|
|
189
|
+
},
|
|
190
|
+
disconnect: subscriber.close
|
|
191
|
+
};
|
|
192
|
+
const commandConnection = {
|
|
193
|
+
call: async (command, ...args) => (await commands.get()).sendCommand([command, ...args.map(String)], STRING_REPLIES),
|
|
194
|
+
duplicate: () => subscriberConnection,
|
|
195
|
+
on: () => void 0,
|
|
196
|
+
disconnect: commands.close
|
|
197
|
+
};
|
|
198
|
+
return createRedisNotifyStore({
|
|
199
|
+
client: () => Promise.resolve(commandConnection),
|
|
200
|
+
callBytes: async (command, ...args) => {
|
|
201
|
+
const { bytes } = await driver.get();
|
|
202
|
+
return (await commands.get()).sendCommand([command, ...args.map(String)], bytes);
|
|
203
|
+
},
|
|
204
|
+
clock,
|
|
205
|
+
ids,
|
|
206
|
+
keyPrefix: prefix,
|
|
207
|
+
onReconnect: (listener) => {
|
|
208
|
+
reconnected = listener;
|
|
209
|
+
},
|
|
210
|
+
async closeClient() {
|
|
211
|
+
commands.close();
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
216
|
+
export { valkey };
|
|
217
|
+
|
|
218
|
+
//# sourceMappingURL=store-valkey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-valkey.js","names":[],"sources":["../src/store-valkey.ts"],"sourcesContent":["/**\n * experimental-a2/store-valkey — the Redis store on node-redis connections,\n * for Valkey and Redis servers that need more than a URL: TLS settings,\n * rotating credentials (such as ElastiCache IAM tokens), reconnect\n * strategies, and timeouts. Storage and the live feed are the ones\n * experimental-a2/store-redis uses (store-redis-notify.ts); this module owns\n * the two connections.\n *\n * A2 creates both clients from the same options: one for commands, one\n * dedicated subscriber. node-redis restores subscriptions before it reports\n * a reconnect ready, so every feed and presence subscription on a channel\n * catches up right after a reconnect instead of waiting for the safety read.\n * A client whose reconnect strategy gives up is replaced on next use.\n *\n * Cluster mode is out: each script's keys share a session's hash tag, but\n * the store runs on one non-cluster connection for commands and one for\n * pub/sub. `redis` (node-redis) is an optional peer dependency.\n */\n\nimport type { RedisClientOptions } from 'redis'\nimport { A2Error } from './errors.ts'\nimport { retryableLazy } from './retryable-lazy.ts'\nimport {\n createRedisNotifyStore,\n type RedisConnection,\n} from './store-redis-notify.ts'\nimport {\n RANDOM_IDS,\n SYSTEM_CLOCK,\n type A2Store,\n type Clock,\n type IdSource,\n} from './store.ts'\n\n/**\n * node-redis `createClient` options. A2 reads replies in their RESP2\n * shapes, so it always connects with `RESP: 2`, which rules out `RESP`\n * and the options that need RESP3.\n */\nexport type ValkeyClientOptions = Omit<\n RedisClientOptions,\n 'RESP' | 'clientSideCache' | 'emitInvalidate' | 'maintNotifications'\n>\n\nexport type ValkeyStoreOptions = {\n /** `redis://` or `rediss://` URL. Overrides `clientOptions.url`. */\n url?: string | undefined\n /**\n * Options for both clients A2 creates: `socket` (TLS, connect timeout,\n * `reconnectStrategy`), `credentialsProvider`, `commandOptions`,\n * `disableOfflineQueue`, and so on.\n */\n clientOptions?: ValkeyClientOptions\n /** Key prefix — isolates multiple apps on one server. Default `'a2'`. */\n keyPrefix?: string\n /** Injectable clock — every stored timestamp comes from here. */\n clock?: Clock\n /** Injectable id source for generated event ids. */\n ids?: IdSource\n}\n\nexport type ValkeyStore = A2Store & {\n /** Destroy the command client and the subscriber. */\n close(): Promise<void>\n}\n\n/** The node-redis client surface this module uses. */\ntype Client = {\n readonly isOpen: boolean\n readonly isReady: boolean\n connect(): Promise<unknown>\n destroy(): void\n on(event: 'error' | 'ready', listener: () => void): unknown\n sendCommand(\n args: string[],\n options: { typeMapping: Record<number, unknown> },\n ): Promise<unknown>\n subscribe(\n channel: string,\n listener: (message: string, channel: string) => void,\n ): Promise<unknown>\n unsubscribe(channel: string): Promise<unknown>\n}\n\ntype Driver = {\n create(): Client\n bytes: { typeMapping: Record<number, unknown> }\n}\n\n/** Overrides any `commandOptions.typeMapping`: replies decode as strings. */\nconst STRING_REPLIES = { typeMapping: {} }\n\n/**\n * node-redis 6 needs `destroy()` to release a client whose reconnect\n * strategy gave up; node-redis 5 throws there instead.\n */\nconst dispose = (client: Client | null | undefined): void => {\n try {\n client?.destroy()\n } catch {}\n}\n\nconst closedError = (): A2Error =>\n new A2Error('STORE_UNAVAILABLE', 'the store was closed')\n\n/** node-redis 6's default `commandOptions.timeout`; node-redis 5 has none. */\nconst DEFAULT_TIMEOUT_MS = 5_000\n\ntype OwnedEntry = {\n client: Client | null\n created: Promise<Client>\n /** Settles when the client first reports ready, or its connect fails. */\n connected: Promise<unknown> | null\n ready: boolean\n}\n\n/**\n * One A2-owned client at a time. `get` hands a new client out once it\n * first reports ready, waiting at most `timeoutMs` per call while it keeps\n * connecting, so `disableOfflineQueue` cannot reject a cold start's first\n * calls. Later reconnects follow node-redis's offline queue,\n * `disableOfflineQueue`, and command timeouts. A client whose connect\n * failed, or whose reconnect strategy gave up, is closed for good, so the\n * next `get` replaces it.\n */\nconst ownedClient = (options: {\n driver: () => Promise<Driver>\n opened: (client: Client) => void\n timeoutMs: number\n}) => {\n let current: OwnedEntry | null = null\n let closed = false\n\n const firstReady = async (entry: OwnedEntry): Promise<Client> => {\n const client = await entry.created\n if (entry.ready) return client\n let timer: ReturnType<typeof setTimeout> | undefined\n try {\n await Promise.race([\n entry.connected,\n new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n reject(\n new A2Error(\n 'STORE_UNAVAILABLE',\n `connect timed out after ${options.timeoutMs}ms`,\n ),\n )\n }, options.timeoutMs)\n }),\n ])\n } catch (err) {\n if (err instanceof A2Error) throw err\n throw new A2Error('STORE_UNAVAILABLE', 'connect failed', { cause: err })\n } finally {\n clearTimeout(timer)\n }\n return client\n }\n\n return {\n get(): Promise<Client> {\n if (closed) return Promise.reject(closedError())\n if (current && (current.client === null || current.client.isOpen))\n return firstReady(current)\n dispose(current?.client)\n const entry: OwnedEntry = {\n client: null,\n connected: null,\n ready: false,\n created: options.driver().then((driver) => {\n if (closed) throw closedError()\n const client = driver.create()\n entry.client = client\n // Without a listener, node-redis's 'error' event crashes the\n // process; failures still surface as command rejections.\n client.on('error', () => {})\n entry.connected = client.connect().then(\n () => {\n entry.ready = true\n return undefined\n },\n (err: unknown) => {\n if (current === entry) {\n dispose(client)\n current = null\n }\n throw err\n },\n )\n void entry.connected.catch(() => {})\n options.opened(client)\n return client\n }),\n }\n current = entry\n void entry.created.catch(() => {\n if (current === entry) current = null\n })\n return firstReady(entry)\n },\n close(): void {\n closed = true\n dispose(current?.client)\n current = null\n },\n }\n}\n\nexport function valkey(options: ValkeyStoreOptions = {}): ValkeyStore {\n const clock = options.clock ?? SYSTEM_CLOCK\n const ids = options.ids ?? RANDOM_IDS\n const prefix = options.keyPrefix ?? 'a2'\n\n if (!options.url && !options.clientOptions) {\n throw new TypeError('valkey() needs a url or clientOptions')\n }\n const clientOptions = {\n ...options.clientOptions,\n ...(options.url ? { url: options.url } : {}),\n RESP: 2 as const,\n }\n\n // Lazy init — constructing the backend does no I/O.\n const driver = retryableLazy(async (): Promise<Driver> => {\n const mod = await import('redis').catch(() => {\n throw new A2Error(\n 'STORE_NOT_CONFIGURED',\n \"experimental-a2/store-valkey needs the 'redis' package (optional peer dependency) — install it\",\n )\n })\n return {\n create: () => mod.createClient(clientOptions) as unknown as Client,\n bytes: { typeMapping: { [mod.RESP_TYPES.BLOB_STRING]: Buffer } },\n }\n })\n\n const timeoutMs = clientOptions.commandOptions?.timeout ?? DEFAULT_TIMEOUT_MS\n const commands = ownedClient({\n driver: driver.get,\n opened: () => {},\n timeoutMs,\n })\n\n /**\n * Channels the subscriber holds, restored on a replacement client. Each\n * maps to the call that subscribed it, so a failed call only forgets its\n * own subscription.\n */\n const channels = new Map<string, symbol>()\n let deliver: ((channel: string, message: string) => void) | undefined\n let reconnected: ((channel: string) => void) | undefined\n const onMessage = (message: string, channel: string): void => {\n deliver?.(channel, message)\n }\n const resubscribed = (): void => {\n for (const channel of channels.keys()) reconnected?.(channel)\n }\n /**\n * node-redis queues SUBSCRIBE outside `sendCommand`, so neither\n * `disableOfflineQueue` nor `commandOptions.timeout` applies to it: a\n * subscribe sent while the subscriber reconnects would wait for the\n * server indefinitely. Apply both here. A timed-out SUBSCRIBE stays\n * queued, so an UNSUBSCRIBE queued behind it cancels it.\n */\n const subscribeBounded = async (\n client: Client,\n channel: string,\n ): Promise<unknown> => {\n if (!client.isReady && clientOptions.disableOfflineQueue) {\n throw new A2Error('STORE_UNAVAILABLE', 'the subscriber is offline')\n }\n let timer: ReturnType<typeof setTimeout> | undefined\n const timedOut = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n void client.unsubscribe(channel).catch(() => {})\n reject(\n new A2Error(\n 'STORE_UNAVAILABLE',\n `subscribe timed out after ${timeoutMs}ms`,\n ),\n )\n }, timeoutMs)\n })\n try {\n return await Promise.race([\n client.subscribe(channel, onMessage),\n timedOut,\n ])\n } finally {\n clearTimeout(timer)\n }\n }\n const subscriber = ownedClient({\n driver: driver.get,\n opened: (client) => {\n // node-redis restores subscriptions before a reconnect's 'ready'.\n let readies = 0\n client.on('ready', () => {\n readies += 1\n if (readies > 1) resubscribed()\n })\n if (channels.size === 0) return\n void Promise.all(\n [...channels.keys()].map((channel) =>\n client.subscribe(channel, onMessage),\n ),\n ).then(resubscribed, () => {})\n },\n timeoutMs,\n })\n\n const subscriberConnection: RedisConnection = {\n async call(command, ...args) {\n const channel = String(args[0])\n const name = command.toLowerCase()\n if (name === 'subscribe') {\n const call = Symbol(channel)\n try {\n // Restore only held channels while a replacement connects; a new\n // channel belongs to the bounded subscribe after first readiness.\n const client = await subscriber.get()\n channels.set(channel, call)\n return await subscribeBounded(client, channel)\n } catch (err) {\n if (channels.get(channel) === call) channels.delete(channel)\n throw err\n }\n }\n if (name === 'unsubscribe') {\n channels.delete(channel)\n return (await subscriber.get()).unsubscribe(channel)\n }\n return (await subscriber.get()).sendCommand(\n [command, ...args.map(String)],\n STRING_REPLIES,\n )\n },\n duplicate: () => subscriberConnection,\n on(_event, listener) {\n deliver = listener\n },\n disconnect: subscriber.close,\n }\n\n const commandConnection: RedisConnection = {\n call: async (command, ...args) =>\n (await commands.get()).sendCommand(\n [command, ...args.map(String)],\n STRING_REPLIES,\n ),\n duplicate: () => subscriberConnection,\n on: () => undefined,\n disconnect: commands.close,\n }\n\n return createRedisNotifyStore({\n client: () => Promise.resolve(commandConnection),\n callBytes: async (command, ...args) => {\n const { bytes } = await driver.get()\n return (await commands.get()).sendCommand(\n [command, ...args.map(String)],\n bytes,\n )\n },\n clock,\n ids,\n keyPrefix: prefix,\n onReconnect: (listener) => {\n reconnected = listener\n },\n async closeClient() {\n commands.close()\n },\n })\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { o as PRESENCE_TIMINGS } from "./internal-CvsrprnU.js";
|
|
2
|
+
//#region src/presence-tombstones.ts
|
|
3
|
+
const key = ({ participant, field }) => JSON.stringify([participant, field]);
|
|
4
|
+
function presenceTombstones() {
|
|
5
|
+
const stamps = /* @__PURE__ */ new Map();
|
|
6
|
+
const prune = () => {
|
|
7
|
+
const cutoff = Date.now() - PRESENCE_TIMINGS.tombstoneMs;
|
|
8
|
+
for (const [id, stamp] of stamps) {
|
|
9
|
+
if (stamp.keptAtMs > cutoff) return;
|
|
10
|
+
stamps.delete(id);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
outranks(write) {
|
|
15
|
+
if (stamps.size === 0) return false;
|
|
16
|
+
prune();
|
|
17
|
+
const stamp = stamps.get(key(write));
|
|
18
|
+
return stamp !== void 0 && stamp.atMs > write.at.getTime();
|
|
19
|
+
},
|
|
20
|
+
keep(deletion) {
|
|
21
|
+
prune();
|
|
22
|
+
const id = key(deletion);
|
|
23
|
+
stamps.delete(id);
|
|
24
|
+
stamps.set(id, {
|
|
25
|
+
atMs: deletion.at.getTime(),
|
|
26
|
+
keptAtMs: Date.now()
|
|
27
|
+
});
|
|
28
|
+
},
|
|
29
|
+
drop(field) {
|
|
30
|
+
if (stamps.size > 0) stamps.delete(key(field));
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/stream-buffer.ts
|
|
36
|
+
const isSerializedEvent = (event) => "payloadJson" in event;
|
|
37
|
+
const parseSerializedEvent = (event) => ({
|
|
38
|
+
id: event.id,
|
|
39
|
+
type: event.type,
|
|
40
|
+
payload: JSON.parse(event.payloadJson),
|
|
41
|
+
index: event.index,
|
|
42
|
+
sessionId: event.sessionId,
|
|
43
|
+
createdAt: new Date(event.createdAtMs)
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* The next event a stream iterator already holds, removed without
|
|
47
|
+
* awaiting its `next()`, which then continues after it; `undefined` when
|
|
48
|
+
* it holds none. Call only while no `next()` is pending. The SSE encoder
|
|
49
|
+
* writes these with the event `next()` just returned, so a buffered
|
|
50
|
+
* backlog leaves in few writes.
|
|
51
|
+
*/
|
|
52
|
+
const bufferedEvents = /* @__PURE__ */ new WeakMap();
|
|
53
|
+
const takeBuffered = (iterator) => bufferedEvents.get(iterator)?.();
|
|
54
|
+
//#endregion
|
|
55
|
+
export { presenceTombstones as a, takeBuffered as i, isSerializedEvent as n, parseSerializedEvent as r, bufferedEvents as t };
|
|
56
|
+
|
|
57
|
+
//# sourceMappingURL=stream-buffer-DrTyiIuo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream-buffer-DrTyiIuo.js","names":[],"sources":["../src/presence-tombstones.ts","../src/stream-buffer.ts"],"sourcesContent":["import { PRESENCE_TIMINGS } from './internal.ts'\n\ntype PresenceField = { participant: string; field: string }\ntype PresenceWrite = PresenceField & { at: Date }\n\n/**\n * The deletion stamps a presence applier keeps per (participant, field),\n * so a null keeps beating older sets that arrive after it. Each lives\n * `PRESENCE_TIMINGS.tombstoneMs` of local receipt time and is pruned\n * lazily on use, without timers.\n */\nexport type PresenceTombstones = {\n /** Whether the field's deletion is strictly newer than the write. */\n outranks(write: PresenceWrite): boolean\n /** Keep the deletion; the caller has already ordered it past any prior one. */\n keep(deletion: PresenceWrite): void\n /** Forget the field's deletion once a write at or past it applies. */\n drop(field: PresenceField): void\n}\n\nconst key = ({ participant, field }: PresenceField): string =>\n JSON.stringify([participant, field])\n\nexport function presenceTombstones(): PresenceTombstones {\n // Insertion order is receipt order, so pruning stops at the first\n // tombstone still inside the window.\n const stamps = new Map<string, { atMs: number; keptAtMs: number }>()\n const prune = (): void => {\n const cutoff = Date.now() - PRESENCE_TIMINGS.tombstoneMs\n for (const [id, stamp] of stamps) {\n if (stamp.keptAtMs > cutoff) return\n stamps.delete(id)\n }\n }\n return {\n outranks(write) {\n if (stamps.size === 0) return false\n prune()\n const stamp = stamps.get(key(write))\n return stamp !== undefined && stamp.atMs > write.at.getTime()\n },\n keep(deletion) {\n prune()\n const id = key(deletion)\n stamps.delete(id)\n stamps.set(id, { atMs: deletion.at.getTime(), keptAtMs: Date.now() })\n },\n drop(field) {\n if (stamps.size > 0) stamps.delete(key(field))\n },\n }\n}\n","import type { Event } from './store.ts'\n\n/**\n * An event as a store returned it, its payload still the JSON text A2's\n * store wrote with `JSON.stringify`. For payloads built from ordinary objects,\n * parsing and re-serializing that text yields the same text, so the SSE lane\n * can write it into a frame unchanged.\n */\nexport type SerializedEvent = {\n id: string\n type: string\n payloadJson: string\n index: number\n sessionId: string\n createdAtMs: number\n}\n\nexport type HeldEvent = Event | SerializedEvent\n\nexport const isSerializedEvent = (event: object): event is SerializedEvent =>\n 'payloadJson' in event\n\nexport const parseSerializedEvent = (event: SerializedEvent): Event => ({\n id: event.id,\n type: event.type,\n payload: JSON.parse(event.payloadJson),\n index: event.index,\n sessionId: event.sessionId,\n createdAt: new Date(event.createdAtMs),\n})\n\n/**\n * The next event a stream iterator already holds, removed without\n * awaiting its `next()`, which then continues after it; `undefined` when\n * it holds none. Call only while no `next()` is pending. The SSE encoder\n * writes these with the event `next()` just returned, so a buffered\n * backlog leaves in few writes.\n */\nexport const bufferedEvents: WeakMap<object, () => HeldEvent | undefined> =\n new WeakMap()\n\nexport const takeBuffered = (iterator: object): HeldEvent | undefined =>\n bufferedEvents.get(iterator)?.()\n"],"mappings":";;AAoBA,MAAM,OAAO,EAAE,aAAa,YAC1B,KAAK,UAAU,CAAC,aAAa,KAAK,CAAC;AAErC,SAAgB,qBAAyC;CAGvD,MAAM,yBAAS,IAAI,IAAgD;CACnE,MAAM,cAAoB;EACxB,MAAM,SAAS,KAAK,IAAI,IAAI,iBAAiB;EAC7C,KAAK,MAAM,CAAC,IAAI,UAAU,QAAQ;GAChC,IAAI,MAAM,WAAW,QAAQ;GAC7B,OAAO,OAAO,EAAE;EAClB;CACF;CACA,OAAO;EACL,SAAS,OAAO;GACd,IAAI,OAAO,SAAS,GAAG,OAAO;GAC9B,MAAM;GACN,MAAM,QAAQ,OAAO,IAAI,IAAI,KAAK,CAAC;GACnC,OAAO,UAAU,KAAA,KAAa,MAAM,OAAO,MAAM,GAAG,QAAQ;EAC9D;EACA,KAAK,UAAU;GACb,MAAM;GACN,MAAM,KAAK,IAAI,QAAQ;GACvB,OAAO,OAAO,EAAE;GAChB,OAAO,IAAI,IAAI;IAAE,MAAM,SAAS,GAAG,QAAQ;IAAG,UAAU,KAAK,IAAI;GAAE,CAAC;EACtE;EACA,KAAK,OAAO;GACV,IAAI,OAAO,OAAO,GAAG,OAAO,OAAO,IAAI,KAAK,CAAC;EAC/C;CACF;AACF;;;AChCA,MAAa,qBAAqB,UAChC,iBAAiB;AAEnB,MAAa,wBAAwB,WAAmC;CACtE,IAAI,MAAM;CACV,MAAM,MAAM;CACZ,SAAS,KAAK,MAAM,MAAM,WAAW;CACrC,OAAO,MAAM;CACb,WAAW,MAAM;CACjB,WAAW,IAAI,KAAK,MAAM,WAAW;AACvC;;;;;;;;AASA,MAAa,iCACX,IAAI,QAAQ;AAEd,MAAa,gBAAgB,aAC3B,eAAe,IAAI,QAAQ,CAAC,GAAG"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as A2Error } from "./session-clear-wHd5GCpZ.js";
|
|
2
|
-
import {
|
|
3
|
-
import { i as takeBuffered, n as isSerializedEvent } from "./stream-buffer-
|
|
2
|
+
import { y as nullProtoRecord } from "./internal-CvsrprnU.js";
|
|
3
|
+
import { i as takeBuffered, n as isSerializedEvent } from "./stream-buffer-DrTyiIuo.js";
|
|
4
4
|
//#region src/wire.ts
|
|
5
5
|
/**
|
|
6
6
|
* The wire format shared by server.fetch and experimental-a2/client
|
|
@@ -407,4 +407,4 @@ const sessionTag = (frame) => {
|
|
|
407
407
|
//#endregion
|
|
408
408
|
export { stateSnapshotToWire as A, serializedEventToWireJson as C, socketFrameFor as D, socketEventsFor as E, socketSubscribedFor as O, presenceSnapshotToWire as S, socketErrorAckFor as T, isWirePresenceSnapshot as _, errorFromWire as a, presencePatchToWire as b, eventFromCompactWire as c, heldEventBatch as d, isCompactWireEvent as f, isWirePresencePatch as g, isWireEvent as h, compactEventJson as i, wireVersion as j, socketUnsubscribedFor as k, eventFromWire as l, isStreamEvent as m, SOCKET_PING_FRAME as n, errorStatus as o, isStateSnapshotFrame as p, asA2Error as r, errorToWire as s, EVENT_WRITE_CHARS as t, eventToWire as u, parseSocketFrame as v, socketAckFor as w, presenceSnapshotFromWire as x, presencePatchFromWire as y };
|
|
409
409
|
|
|
410
|
-
//# sourceMappingURL=wire-
|
|
410
|
+
//# sourceMappingURL=wire-CV-Z7wz-.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wire-CZGqNZRx.js","names":[],"sources":["../src/wire.ts"],"sourcesContent":["/**\n * The wire format shared by server.fetch and experimental-a2/client\n * (browser side): events as JSON with ISO timestamps, and the A2Error\n * envelope. Internal module — both entry points re-export what's\n * public.\n */\n\nimport { A2Error, type A2ErrorCode, type A2ServerErrorCode } from './errors.ts'\nimport { nullProtoRecord } from './internal.ts'\nimport type { Event } from './store.ts'\nimport {\n isSerializedEvent,\n takeBuffered,\n type HeldEvent,\n type SerializedEvent,\n} from './stream-buffer.ts'\nimport type {\n PresenceMap,\n PresencePatch,\n PresenceSnapshot,\n} from './contract.ts'\n\nexport type WireEvent = {\n id: string\n type: string\n payload: unknown\n index: number\n sessionId: string\n /** ISO 8601 — revived to a Date on the client so reducers see the\n * same shape on both sides of the wire. */\n createdAt: string\n}\n\nexport function eventToWire(event: Event): WireEvent {\n return {\n id: event.id,\n type: event.type,\n payload: event.payload,\n index: event.index,\n sessionId: event.sessionId,\n createdAt: event.createdAt.toISOString(),\n }\n}\n\n// Consecutive frames usually repeat a stream's session, an event type, and\n// the timestamp of events appended together; each keeps its last encoding.\nconst lastEncoding = <T>(encode: (value: T) => string) => {\n let last: T | undefined\n let text = ''\n return (value: T): string => {\n if (value !== last) {\n text = encode(value)\n last = value\n }\n return text\n }\n}\nconst isoJson = lastEncoding((ms: number) =>\n JSON.stringify(new Date(ms).toISOString()),\n)\nconst sessionJson = lastEncoding((id: string) => JSON.stringify(id))\nconst typeJson = lastEncoding((type: string) => JSON.stringify(type))\n\n/**\n * `JSON.stringify(eventToWire(event))` for the parsed event, built around the\n * stored payload text instead of parsing and re-serializing it.\n */\nexport function serializedEventToWireJson(event: SerializedEvent): string {\n return `{\"id\":${JSON.stringify(event.id)},\"type\":${typeJson(event.type)},\"payload\":${event.payloadJson},\"index\":${event.index},\"sessionId\":${sessionJson(event.sessionId)},\"createdAt\":${isoJson(event.createdAtMs)}}`\n}\n\n/**\n * Wire version 2: what a client opts into with `wire=2` on the stream URL.\n * Live events travel in batches, without the `sessionId` the connection\n * already names, with `createdAt` as epoch milliseconds. Any other value,\n * or none, selects version 1.\n */\nexport type WireVersion = 1 | 2\n\nexport function wireVersion(value: string | null): WireVersion {\n return value === '2' ? 2 : 1\n}\n\n/**\n * Events already held by a stream join one write while it is shorter than\n * this many characters, so a write can exceed it by the event that crosses it.\n */\nexport const EVENT_WRITE_CHARS: number = 64 * 1024\n\n/** Stream items that are events, as opposed to snapshot or presence frames. */\nexport function isStreamEvent(item: object): item is HeldEvent {\n return !('kind' in item) && !('snapshot' in item) && !('participant' in item)\n}\n\n/**\n * `first` and the events `iterator` already holds, encoded, until the batch\n * reaches `EVENT_WRITE_CHARS`; `last` is the batch's final event.\n */\nexport function heldEventBatch(options: {\n first: HeldEvent\n iterator: object\n encode: (event: HeldEvent) => string\n}): { events: string[]; last: HeldEvent } {\n const { iterator, encode } = options\n let last = options.first\n const events = [encode(last)]\n let chars = events[0]!.length\n while (chars < EVENT_WRITE_CHARS) {\n const held = takeBuffered(iterator)\n if (!held) break\n const json = encode(held)\n events.push(json)\n chars += json.length + 1\n last = held\n }\n return { events, last }\n}\n\n/** A version 2 live event: `WireEvent` minus `sessionId`, ms timestamp. */\nexport type CompactWireEvent = {\n id: string\n type: string\n payload: unknown\n index: number\n createdAt: number\n}\n\n/** One version 2 event as JSON, splicing a held row's stored payload. */\nexport function compactEventJson(event: HeldEvent): string {\n if (!isSerializedEvent(event))\n return JSON.stringify({\n id: event.id,\n type: event.type,\n payload: event.payload,\n index: event.index,\n createdAt: event.createdAt.getTime(),\n })\n return `{\"id\":${JSON.stringify(event.id)},\"type\":${typeJson(event.type)},\"payload\":${event.payloadJson},\"index\":${event.index},\"createdAt\":${event.createdAtMs}}`\n}\n\nexport function isCompactWireEvent(value: unknown): value is CompactWireEvent {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n return (\n typeof v['id'] === 'string' &&\n typeof v['type'] === 'string' &&\n typeof v['index'] === 'number' &&\n typeof v['createdAt'] === 'number' &&\n Number.isFinite(v['createdAt'])\n )\n}\n\nexport function eventFromCompactWire(\n wire: CompactWireEvent,\n sessionId: string,\n): Event {\n return {\n id: wire.id,\n type: wire.type,\n payload: wire.payload,\n index: wire.index,\n sessionId,\n createdAt: new Date(wire.createdAt),\n }\n}\n\nexport function eventFromWire(wire: WireEvent): Event {\n return {\n id: wire.id,\n type: wire.type,\n payload: wire.payload,\n index: wire.index,\n sessionId: wire.sessionId,\n createdAt: new Date(wire.createdAt),\n }\n}\n\nexport function isWireEvent(value: unknown): value is WireEvent {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n return (\n typeof v['id'] === 'string' &&\n typeof v['type'] === 'string' &&\n typeof v['index'] === 'number' &&\n typeof v['sessionId'] === 'string' &&\n typeof v['createdAt'] === 'string'\n )\n}\n\n// ── the presence wire pair ───────────────────────────────────────────\n\n/** The `event: presence` SSE frame body: one patch as JSON. */\nexport type WirePresencePatch = {\n participant: string\n values: Record<string, unknown>\n seen: number\n /** ISO 8601 — revived to a Date on the client. */\n at: string\n}\n\n/**\n * The `event: presence-snapshot` SSE frame body: the in-memory\n * `PresenceSnapshot` shape verbatim, with each per-field `at`\n * serialized to ISO 8601.\n */\nexport type WirePresenceSnapshot = {\n snapshot: {\n [participant: string]: {\n [field: string]: { value: unknown; seen: number; at: string }\n }\n }\n}\n\nexport function presencePatchToWire(patch: PresencePatch): WirePresencePatch {\n return {\n participant: patch.participant,\n values: patch.values,\n seen: patch.seen,\n at: patch.at.toISOString(),\n }\n}\n\nexport function presencePatchFromWire(wire: WirePresencePatch): PresencePatch {\n return {\n participant: wire.participant,\n values: wire.values,\n seen: wire.seen,\n at: new Date(wire.at),\n }\n}\n\n// Participant and field keys come off the wire, so every object keyed\n// by them is built null-prototype — see `nullProtoRecord`.\nexport function presenceSnapshotToWire(\n snapshot: PresenceSnapshot,\n): WirePresenceSnapshot {\n const wire: WirePresenceSnapshot['snapshot'] = nullProtoRecord()\n for (const [participant, fields] of Object.entries(snapshot.snapshot)) {\n const wireFields: WirePresenceSnapshot['snapshot'][string] =\n nullProtoRecord()\n for (const [field, entry] of Object.entries(fields)) {\n if (entry === undefined) continue\n wireFields[field] = {\n value: entry.value,\n seen: entry.seen,\n at: entry.at.toISOString(),\n }\n }\n wire[participant] = wireFields\n }\n return { snapshot: wire }\n}\n\nexport function presenceSnapshotFromWire(\n wire: WirePresenceSnapshot,\n): PresenceSnapshot {\n const map: PresenceMap = nullProtoRecord()\n for (const [participant, fields] of Object.entries(wire.snapshot)) {\n const revived: PresenceMap[string] = nullProtoRecord()\n for (const [field, entry] of Object.entries(fields)) {\n revived[field] = {\n value: entry.value,\n seen: entry.seen,\n at: new Date(entry.at),\n }\n }\n map[participant] = revived\n }\n return { snapshot: map }\n}\n\nexport function isWirePresencePatch(\n value: unknown,\n): value is WirePresencePatch {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n return (\n typeof v['participant'] === 'string' &&\n v['values'] !== null &&\n typeof v['values'] === 'object' &&\n !Array.isArray(v['values']) &&\n typeof v['seen'] === 'number' &&\n typeof v['at'] === 'string'\n )\n}\n\nexport function isWirePresenceSnapshot(\n value: unknown,\n): value is WirePresenceSnapshot {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n return (\n v['snapshot'] !== null &&\n typeof v['snapshot'] === 'object' &&\n !Array.isArray(v['snapshot'])\n )\n}\n\n// ── the A2Error envelope ─────────────────────────────────────────────\n\nexport type WireError = {\n error: { code: A2ServerErrorCode; message: string; details?: unknown }\n}\n\nconst ERROR_STATUS: Record<A2ServerErrorCode, number> = {\n INVALID_PAYLOAD: 400,\n FORBIDDEN: 403,\n UNKNOWN_EVENT_TYPE: 400,\n PARTIAL_DUPLICATE_BATCH: 400,\n SUPERSEDED_ATTEMPT: 409,\n SESSION_CLEARED: 409,\n CLAIM_EXPIRED: 409,\n STORE_UNAVAILABLE: 503,\n STORE_NOT_CONFIGURED: 500,\n UNKNOWN_PRESENCE_FIELD: 400,\n PRESENCE_NOT_SUPPORTED: 500,\n CONSUMER_DEAD_LETTERED: 500,\n}\n\nexport function errorStatus(code: A2ErrorCode): number {\n return ERROR_STATUS[isServerErrorCode(code) ? code : 'STORE_UNAVAILABLE']\n}\n\nexport function errorToWire(error: A2Error): WireError {\n const failure = asA2Error(error)\n const body: WireError = {\n error: {\n code: failure.code as A2ServerErrorCode,\n message: failure.message,\n },\n }\n if (failure.details !== undefined) body.error.details = failure.details\n return body\n}\n\n/** Wrap an arbitrary thrown value for the wire: known server codes pass through,\n * anything else becomes STORE_UNAVAILABLE — from the client's\n * perspective an unknown server failure is retryable-once, not a\n * protocol contract. */\nexport function asA2Error(error: unknown): A2Error {\n return error instanceof A2Error && isServerErrorCode(error.code)\n ? error\n : new A2Error('STORE_UNAVAILABLE', 'internal error', { cause: error })\n}\n\n/** Rebuild an A2Error from a wire body; null if the body isn't one. */\nexport function errorFromWire(body: unknown): A2Error | null {\n if (body === null || typeof body !== 'object') return null\n const err = (body as { error?: unknown }).error\n if (err === null || typeof err !== 'object') return null\n const { code, message, details } = err as Record<string, unknown>\n if (typeof code !== 'string' || !isServerErrorCode(code)) return null\n return new A2Error(code, String(message ?? code), {\n details,\n })\n}\n\nconst isServerErrorCode = (code: string): code is A2ServerErrorCode =>\n Object.hasOwn(ERROR_STATUS, code)\n\n// ── the ws frame layer ───────────────────────────────────────────────\n// The SSE lanes reframed for a socket (specs/a2-api.md §13): every\n// message is one JSON text frame, and a frame is its wire payload plus\n// a `kind` discriminant (plus `req` where a reply must correlate).\n// Unknown kinds are skipped by both sides — the same\n// forward-compatibility rule as named SSE frames.\n//\n// The multiplexed superset rides the same frames: `subscribe` /\n// `unsubscribe` up-frames open and close per-session lanes on one\n// socket, `sessionId` tags route everything else. Single-session\n// frames (no tags) keep parsing unchanged — a route opts into\n// multiplexing by choosing the multi-session server handler, never by\n// breaking the old protocol.\n\n/**\n * Server → client, parsed: a live-stream item, a push ack, a\n * subscription lifecycle notice, or the heartbeat. `parseSocketFrame`\n * yields these; frames of unknown kind (or ones failing their shape\n * guard) come back `null`. `sessionId` is present on frames from a\n * multiplexed socket and absent on a single-session one.\n */\nexport type StateSnapshotFrame = {\n kind: 'snapshot'\n sessionId: string\n name: string\n index: number\n state: unknown\n stateUndefined?: true\n /** The session's latest clear marker, when it has been cleared. */\n clearIndex?: number\n}\n\nexport function stateSnapshotToWire(\n frame: StateSnapshotFrame,\n): StateSnapshotFrame {\n return frame.state === undefined\n ? { ...frame, state: null, stateUndefined: true }\n : frame\n}\n\nexport function isStateSnapshotFrame(\n value: unknown,\n): value is StateSnapshotFrame {\n if (value === null || typeof value !== 'object') return false\n const frame = value as Record<string, unknown>\n return (\n frame['kind'] === 'snapshot' &&\n typeof frame['sessionId'] === 'string' &&\n typeof frame['name'] === 'string' &&\n frame['name'].length > 0 &&\n typeof frame['index'] === 'number' &&\n Number.isSafeInteger(frame['index']) &&\n frame['index'] >= 0 &&\n Object.hasOwn(frame, 'state') &&\n (!Object.hasOwn(frame, 'stateUndefined') ||\n (frame['stateUndefined'] === true && frame['state'] === null)) &&\n (!Object.hasOwn(frame, 'clearIndex') ||\n (typeof frame['clearIndex'] === 'number' &&\n Number.isSafeInteger(frame['clearIndex']) &&\n frame['clearIndex'] > 0 &&\n frame['clearIndex'] <= frame['index']))\n )\n}\n\nexport type SubscriptionStart =\n | { index: number; bootstrap?: never; reducer?: string }\n | { bootstrap: true; reducer: string; index?: never }\n\nexport type SocketDownFrame =\n | StateSnapshotFrame\n | { kind: 'event'; event: WireEvent }\n | { kind: 'events'; events: CompactWireEvent[]; sessionId?: string }\n | { kind: 'presence'; patch: WirePresencePatch; sessionId?: string }\n | {\n kind: 'presence-snapshot'\n snapshot: WirePresenceSnapshot\n sessionId?: string\n }\n | { kind: 'ack'; req: number; events: WireEvent[]; sessionId?: string }\n | { kind: 'ack'; req: number; error: A2Error; sessionId?: string }\n | { kind: 'subscribed'; sessionId: string; attempt?: number }\n | {\n kind: 'unsubscribed'\n sessionId: string\n attempt?: number\n reason?: string\n error?: A2Error\n }\n | { kind: 'ping' }\n\n/**\n * Client → server: the push envelope's two planes. `sessionId` is\n * implied by the socket on a single-session connection and required by\n * the multiplexed handler. Plain JSON on the wire; the server parses\n * and validates them through the same seams as `parsePushBody`.\n */\nexport type SocketPushFrame = {\n kind: 'push'\n /** Client-local ack correlator — opaque to the server. */\n req: number\n events: Array<{ type: string; payload: unknown; id?: string }>\n sessionId?: string\n}\n\nexport type SocketPresenceFrame = {\n kind: 'presence'\n participant: string\n values: Record<string, unknown>\n seen?: number\n at?: number\n sessionId?: string\n}\n\n/** One session lane in a subscribe frame. The server echoes `attempt`\n * on that lane's `subscribed`/`unsubscribed` answers, so a client that\n * re-subscribed before an answer arrived can tell the answers apart. */\nexport type SocketSubscribeEntry = {\n id: string\n attempt?: number\n} & SubscriptionStart\n\n/** Open per-session lanes on a multiplexed socket. `index` is each\n * session's exclusive resume frontier — `stream({ startAfter })`. */\nexport type SocketSubscribeFrame = {\n kind: 'subscribe'\n sessions: SocketSubscribeEntry[]\n}\n\nexport type SocketUnsubscribeFrame = {\n kind: 'unsubscribe'\n sessions: string[]\n}\n\nexport type SocketUpFrame =\n | SocketPushFrame\n | SocketPresenceFrame\n | SocketSubscribeFrame\n | SocketUnsubscribeFrame\n\nexport const SOCKET_PING_FRAME: string = JSON.stringify({ kind: 'ping' })\n\n/** One stream item as a socket frame — `sseResponse`'s framing over\n * the same codecs, with `kind` instead of an SSE event name. On a\n * multiplexed socket presence frames carry the `sessionId` tag; events\n * already carry theirs in the wire event. */\nexport function socketFrameFor(\n item: Event | PresencePatch | PresenceSnapshot | StateSnapshotFrame,\n sessionId?: string,\n): string {\n const tag = sessionId === undefined ? {} : { sessionId }\n if ('kind' in item) return JSON.stringify(stateSnapshotToWire(item))\n if ('snapshot' in item) {\n return JSON.stringify({\n kind: 'presence-snapshot',\n ...presenceSnapshotToWire(item),\n ...tag,\n })\n }\n if ('participant' in item) {\n return JSON.stringify({\n kind: 'presence',\n ...presencePatchToWire(item),\n ...tag,\n })\n }\n return JSON.stringify({ kind: 'event', ...eventToWire(item) })\n}\n\n/** A version 2 batch frame from compact event JSON; a multiplexed\n * socket names the batch's session once. */\nexport function socketEventsFor(events: string[], sessionId?: string): string {\n const tag =\n sessionId === undefined ? '' : `\"sessionId\":${JSON.stringify(sessionId)},`\n return `{\"kind\":\"events\",${tag}\"events\":[${events.join(',')}]}`\n}\n\nexport function socketAckFor(\n req: number,\n events: Event[],\n sessionId?: string,\n): string {\n return JSON.stringify({\n kind: 'ack',\n req,\n events: events.map(eventToWire),\n ...(sessionId === undefined ? {} : { sessionId }),\n })\n}\n\nexport function socketErrorAckFor(\n req: number,\n error: A2Error,\n sessionId?: string,\n): string {\n return JSON.stringify({\n kind: 'ack',\n req,\n ...errorToWire(error),\n ...(sessionId === undefined ? {} : { sessionId }),\n })\n}\n\nexport function socketSubscribedFor(options: {\n sessionId: string\n attempt?: number | undefined\n}): string {\n const { sessionId, attempt } = options\n return JSON.stringify({\n kind: 'subscribed',\n sessionId,\n ...(attempt === undefined ? {} : { attempt }),\n })\n}\n\n/** The subscription-over notice: with a `reason` the server rejected\n * or lost the session's stream, without one it ended cleanly. */\nexport function socketUnsubscribedFor(options: {\n sessionId: string\n attempt?: number | undefined\n reason?: string\n error?: A2Error\n}): string {\n const { sessionId, attempt, reason, error } = options\n return JSON.stringify({\n kind: 'unsubscribed',\n sessionId,\n ...(attempt === undefined ? {} : { attempt }),\n ...(reason === undefined ? {} : { reason }),\n ...(error === undefined ? {} : errorToWire(error)),\n })\n}\n\nconst isAttempt = (value: unknown): value is number =>\n typeof value === 'number' && Number.isSafeInteger(value) && value >= 0\n\nexport function parseSocketFrame(data: string): SocketDownFrame | null {\n let parsed: unknown\n try {\n parsed = JSON.parse(data)\n } catch {\n return null\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return null\n }\n const frame = parsed as Record<string, unknown>\n switch (frame['kind']) {\n case 'snapshot':\n if (!isStateSnapshotFrame(parsed))\n throw new A2Error('INVALID_PAYLOAD', 'invalid bootstrap snapshot')\n return parsed\n case 'ping':\n return { kind: 'ping' }\n case 'event': {\n if (!isWireEvent(parsed)) return null\n const { id, type, payload, index, sessionId, createdAt } = parsed\n return {\n kind: 'event',\n event: { id, type, payload, index, sessionId, createdAt },\n }\n }\n case 'events': {\n // Dropping a batch would leave a gap the frontier then skips past.\n const events = frame['events']\n if (!Array.isArray(events) || !events.every(isCompactWireEvent))\n throw new A2Error('INVALID_PAYLOAD', 'invalid event batch')\n return { kind: 'events', events, ...sessionTag(frame) }\n }\n case 'presence': {\n if (!isWirePresencePatch(parsed)) return null\n const { participant, values, seen, at } = parsed\n return {\n kind: 'presence',\n patch: { participant, values, seen, at },\n ...sessionTag(frame),\n }\n }\n case 'presence-snapshot': {\n if (!isWirePresenceSnapshot(parsed)) return null\n return {\n kind: 'presence-snapshot',\n snapshot: { snapshot: parsed.snapshot },\n ...sessionTag(frame),\n }\n }\n case 'subscribed': {\n const { sessionId, attempt } = frame\n if (typeof sessionId !== 'string') return null\n if (attempt !== undefined && !isAttempt(attempt)) return null\n return {\n kind: 'subscribed',\n sessionId,\n ...(attempt === undefined ? {} : { attempt }),\n }\n }\n case 'unsubscribed': {\n const { sessionId, attempt } = frame\n if (typeof sessionId !== 'string') return null\n if (attempt !== undefined && !isAttempt(attempt)) return null\n const reason = frame['reason']\n const error =\n errorFromWire(parsed) ??\n ('error' in frame\n ? new A2Error(\n 'STORE_UNAVAILABLE',\n 'unintelligible subscription error',\n )\n : null)\n return {\n kind: 'unsubscribed',\n sessionId,\n ...(attempt === undefined ? {} : { attempt }),\n ...(typeof reason === 'string' ? { reason } : {}),\n ...(error === null ? {} : { error }),\n }\n }\n case 'ack': {\n const req = frame['req']\n if (typeof req !== 'number') return null\n const tag = sessionTag(frame)\n const events = frame['events']\n if (Array.isArray(events) && events.every(isWireEvent)) {\n return { kind: 'ack', req, events, ...tag }\n }\n const error = errorFromWire(parsed)\n if (error) return { kind: 'ack', req, error, ...tag }\n // A correlatable ack must never be dropped: its waiter would hang\n // forever behind a healthy socket (pings keep the watchdog fed).\n // An outcome this client cannot interpret — an error code from a\n // newer server, an event shape that fails a guard — degrades to\n // a lost ack: retryable, and the client-generated ids make the\n // retry idempotent even if the append actually committed.\n return {\n kind: 'ack',\n req,\n error: new A2Error('STORE_UNAVAILABLE', 'unintelligible ack'),\n ...tag,\n }\n }\n default:\n return null\n }\n}\n\nconst sessionTag = (frame: Record<string, unknown>): { sessionId?: string } => {\n const sessionId = frame['sessionId']\n return typeof sessionId === 'string' ? { sessionId } : {}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"wire-CV-Z7wz-.js","names":[],"sources":["../src/wire.ts"],"sourcesContent":["/**\n * The wire format shared by server.fetch and experimental-a2/client\n * (browser side): events as JSON with ISO timestamps, and the A2Error\n * envelope. Internal module — both entry points re-export what's\n * public.\n */\n\nimport { A2Error, type A2ErrorCode, type A2ServerErrorCode } from './errors.ts'\nimport { nullProtoRecord } from './internal.ts'\nimport type { Event } from './store.ts'\nimport {\n isSerializedEvent,\n takeBuffered,\n type HeldEvent,\n type SerializedEvent,\n} from './stream-buffer.ts'\nimport type {\n PresenceMap,\n PresencePatch,\n PresenceSnapshot,\n} from './contract.ts'\n\nexport type WireEvent = {\n id: string\n type: string\n payload: unknown\n index: number\n sessionId: string\n /** ISO 8601 — revived to a Date on the client so reducers see the\n * same shape on both sides of the wire. */\n createdAt: string\n}\n\nexport function eventToWire(event: Event): WireEvent {\n return {\n id: event.id,\n type: event.type,\n payload: event.payload,\n index: event.index,\n sessionId: event.sessionId,\n createdAt: event.createdAt.toISOString(),\n }\n}\n\n// Consecutive frames usually repeat a stream's session, an event type, and\n// the timestamp of events appended together; each keeps its last encoding.\nconst lastEncoding = <T>(encode: (value: T) => string) => {\n let last: T | undefined\n let text = ''\n return (value: T): string => {\n if (value !== last) {\n text = encode(value)\n last = value\n }\n return text\n }\n}\nconst isoJson = lastEncoding((ms: number) =>\n JSON.stringify(new Date(ms).toISOString()),\n)\nconst sessionJson = lastEncoding((id: string) => JSON.stringify(id))\nconst typeJson = lastEncoding((type: string) => JSON.stringify(type))\n\n/**\n * `JSON.stringify(eventToWire(event))` for the parsed event, built around the\n * stored payload text instead of parsing and re-serializing it.\n */\nexport function serializedEventToWireJson(event: SerializedEvent): string {\n return `{\"id\":${JSON.stringify(event.id)},\"type\":${typeJson(event.type)},\"payload\":${event.payloadJson},\"index\":${event.index},\"sessionId\":${sessionJson(event.sessionId)},\"createdAt\":${isoJson(event.createdAtMs)}}`\n}\n\n/**\n * Wire version 2: what a client opts into with `wire=2` on the stream URL.\n * Live events travel in batches, without the `sessionId` the connection\n * already names, with `createdAt` as epoch milliseconds. Any other value,\n * or none, selects version 1.\n */\nexport type WireVersion = 1 | 2\n\nexport function wireVersion(value: string | null): WireVersion {\n return value === '2' ? 2 : 1\n}\n\n/**\n * Events already held by a stream join one write while it is shorter than\n * this many characters, so a write can exceed it by the event that crosses it.\n */\nexport const EVENT_WRITE_CHARS: number = 64 * 1024\n\n/** Stream items that are events, as opposed to snapshot or presence frames. */\nexport function isStreamEvent(item: object): item is HeldEvent {\n return !('kind' in item) && !('snapshot' in item) && !('participant' in item)\n}\n\n/**\n * `first` and the events `iterator` already holds, encoded, until the batch\n * reaches `EVENT_WRITE_CHARS`; `last` is the batch's final event.\n */\nexport function heldEventBatch(options: {\n first: HeldEvent\n iterator: object\n encode: (event: HeldEvent) => string\n}): { events: string[]; last: HeldEvent } {\n const { iterator, encode } = options\n let last = options.first\n const events = [encode(last)]\n let chars = events[0]!.length\n while (chars < EVENT_WRITE_CHARS) {\n const held = takeBuffered(iterator)\n if (!held) break\n const json = encode(held)\n events.push(json)\n chars += json.length + 1\n last = held\n }\n return { events, last }\n}\n\n/** A version 2 live event: `WireEvent` minus `sessionId`, ms timestamp. */\nexport type CompactWireEvent = {\n id: string\n type: string\n payload: unknown\n index: number\n createdAt: number\n}\n\n/** One version 2 event as JSON, splicing a held row's stored payload. */\nexport function compactEventJson(event: HeldEvent): string {\n if (!isSerializedEvent(event))\n return JSON.stringify({\n id: event.id,\n type: event.type,\n payload: event.payload,\n index: event.index,\n createdAt: event.createdAt.getTime(),\n })\n return `{\"id\":${JSON.stringify(event.id)},\"type\":${typeJson(event.type)},\"payload\":${event.payloadJson},\"index\":${event.index},\"createdAt\":${event.createdAtMs}}`\n}\n\nexport function isCompactWireEvent(value: unknown): value is CompactWireEvent {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n return (\n typeof v['id'] === 'string' &&\n typeof v['type'] === 'string' &&\n typeof v['index'] === 'number' &&\n typeof v['createdAt'] === 'number' &&\n Number.isFinite(v['createdAt'])\n )\n}\n\nexport function eventFromCompactWire(\n wire: CompactWireEvent,\n sessionId: string,\n): Event {\n return {\n id: wire.id,\n type: wire.type,\n payload: wire.payload,\n index: wire.index,\n sessionId,\n createdAt: new Date(wire.createdAt),\n }\n}\n\nexport function eventFromWire(wire: WireEvent): Event {\n return {\n id: wire.id,\n type: wire.type,\n payload: wire.payload,\n index: wire.index,\n sessionId: wire.sessionId,\n createdAt: new Date(wire.createdAt),\n }\n}\n\nexport function isWireEvent(value: unknown): value is WireEvent {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n return (\n typeof v['id'] === 'string' &&\n typeof v['type'] === 'string' &&\n typeof v['index'] === 'number' &&\n typeof v['sessionId'] === 'string' &&\n typeof v['createdAt'] === 'string'\n )\n}\n\n// ── the presence wire pair ───────────────────────────────────────────\n\n/** The `event: presence` SSE frame body: one patch as JSON. */\nexport type WirePresencePatch = {\n participant: string\n values: Record<string, unknown>\n seen: number\n /** ISO 8601 — revived to a Date on the client. */\n at: string\n}\n\n/**\n * The `event: presence-snapshot` SSE frame body: the in-memory\n * `PresenceSnapshot` shape verbatim, with each per-field `at`\n * serialized to ISO 8601.\n */\nexport type WirePresenceSnapshot = {\n snapshot: {\n [participant: string]: {\n [field: string]: { value: unknown; seen: number; at: string }\n }\n }\n}\n\nexport function presencePatchToWire(patch: PresencePatch): WirePresencePatch {\n return {\n participant: patch.participant,\n values: patch.values,\n seen: patch.seen,\n at: patch.at.toISOString(),\n }\n}\n\nexport function presencePatchFromWire(wire: WirePresencePatch): PresencePatch {\n return {\n participant: wire.participant,\n values: wire.values,\n seen: wire.seen,\n at: new Date(wire.at),\n }\n}\n\n// Participant and field keys come off the wire, so every object keyed\n// by them is built null-prototype — see `nullProtoRecord`.\nexport function presenceSnapshotToWire(\n snapshot: PresenceSnapshot,\n): WirePresenceSnapshot {\n const wire: WirePresenceSnapshot['snapshot'] = nullProtoRecord()\n for (const [participant, fields] of Object.entries(snapshot.snapshot)) {\n const wireFields: WirePresenceSnapshot['snapshot'][string] =\n nullProtoRecord()\n for (const [field, entry] of Object.entries(fields)) {\n if (entry === undefined) continue\n wireFields[field] = {\n value: entry.value,\n seen: entry.seen,\n at: entry.at.toISOString(),\n }\n }\n wire[participant] = wireFields\n }\n return { snapshot: wire }\n}\n\nexport function presenceSnapshotFromWire(\n wire: WirePresenceSnapshot,\n): PresenceSnapshot {\n const map: PresenceMap = nullProtoRecord()\n for (const [participant, fields] of Object.entries(wire.snapshot)) {\n const revived: PresenceMap[string] = nullProtoRecord()\n for (const [field, entry] of Object.entries(fields)) {\n revived[field] = {\n value: entry.value,\n seen: entry.seen,\n at: new Date(entry.at),\n }\n }\n map[participant] = revived\n }\n return { snapshot: map }\n}\n\nexport function isWirePresencePatch(\n value: unknown,\n): value is WirePresencePatch {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n return (\n typeof v['participant'] === 'string' &&\n v['values'] !== null &&\n typeof v['values'] === 'object' &&\n !Array.isArray(v['values']) &&\n typeof v['seen'] === 'number' &&\n typeof v['at'] === 'string'\n )\n}\n\nexport function isWirePresenceSnapshot(\n value: unknown,\n): value is WirePresenceSnapshot {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n return (\n v['snapshot'] !== null &&\n typeof v['snapshot'] === 'object' &&\n !Array.isArray(v['snapshot'])\n )\n}\n\n// ── the A2Error envelope ─────────────────────────────────────────────\n\nexport type WireError = {\n error: { code: A2ServerErrorCode; message: string; details?: unknown }\n}\n\nconst ERROR_STATUS: Record<A2ServerErrorCode, number> = {\n INVALID_PAYLOAD: 400,\n FORBIDDEN: 403,\n UNKNOWN_EVENT_TYPE: 400,\n PARTIAL_DUPLICATE_BATCH: 400,\n SUPERSEDED_ATTEMPT: 409,\n SESSION_CLEARED: 409,\n CLAIM_EXPIRED: 409,\n STORE_UNAVAILABLE: 503,\n STORE_NOT_CONFIGURED: 500,\n UNKNOWN_PRESENCE_FIELD: 400,\n PRESENCE_NOT_SUPPORTED: 500,\n CONSUMER_DEAD_LETTERED: 500,\n}\n\nexport function errorStatus(code: A2ErrorCode): number {\n return ERROR_STATUS[isServerErrorCode(code) ? code : 'STORE_UNAVAILABLE']\n}\n\nexport function errorToWire(error: A2Error): WireError {\n const failure = asA2Error(error)\n const body: WireError = {\n error: {\n code: failure.code as A2ServerErrorCode,\n message: failure.message,\n },\n }\n if (failure.details !== undefined) body.error.details = failure.details\n return body\n}\n\n/** Wrap an arbitrary thrown value for the wire: known server codes pass through,\n * anything else becomes STORE_UNAVAILABLE — from the client's\n * perspective an unknown server failure is retryable-once, not a\n * protocol contract. */\nexport function asA2Error(error: unknown): A2Error {\n return error instanceof A2Error && isServerErrorCode(error.code)\n ? error\n : new A2Error('STORE_UNAVAILABLE', 'internal error', { cause: error })\n}\n\n/** Rebuild an A2Error from a wire body; null if the body isn't one. */\nexport function errorFromWire(body: unknown): A2Error | null {\n if (body === null || typeof body !== 'object') return null\n const err = (body as { error?: unknown }).error\n if (err === null || typeof err !== 'object') return null\n const { code, message, details } = err as Record<string, unknown>\n if (typeof code !== 'string' || !isServerErrorCode(code)) return null\n return new A2Error(code, String(message ?? code), {\n details,\n })\n}\n\nconst isServerErrorCode = (code: string): code is A2ServerErrorCode =>\n Object.hasOwn(ERROR_STATUS, code)\n\n// ── the ws frame layer ───────────────────────────────────────────────\n// The SSE lanes reframed for a socket (specs/a2-api.md §13): every\n// message is one JSON text frame, and a frame is its wire payload plus\n// a `kind` discriminant (plus `req` where a reply must correlate).\n// Unknown kinds are skipped by both sides — the same\n// forward-compatibility rule as named SSE frames.\n//\n// The multiplexed superset rides the same frames: `subscribe` /\n// `unsubscribe` up-frames open and close per-session lanes on one\n// socket, `sessionId` tags route everything else. Single-session\n// frames (no tags) keep parsing unchanged — a route opts into\n// multiplexing by choosing the multi-session server handler, never by\n// breaking the old protocol.\n\n/**\n * Server → client, parsed: a live-stream item, a push ack, a\n * subscription lifecycle notice, or the heartbeat. `parseSocketFrame`\n * yields these; frames of unknown kind (or ones failing their shape\n * guard) come back `null`. `sessionId` is present on frames from a\n * multiplexed socket and absent on a single-session one.\n */\nexport type StateSnapshotFrame = {\n kind: 'snapshot'\n sessionId: string\n name: string\n index: number\n state: unknown\n stateUndefined?: true\n /** The session's latest clear marker, when it has been cleared. */\n clearIndex?: number\n}\n\nexport function stateSnapshotToWire(\n frame: StateSnapshotFrame,\n): StateSnapshotFrame {\n return frame.state === undefined\n ? { ...frame, state: null, stateUndefined: true }\n : frame\n}\n\nexport function isStateSnapshotFrame(\n value: unknown,\n): value is StateSnapshotFrame {\n if (value === null || typeof value !== 'object') return false\n const frame = value as Record<string, unknown>\n return (\n frame['kind'] === 'snapshot' &&\n typeof frame['sessionId'] === 'string' &&\n typeof frame['name'] === 'string' &&\n frame['name'].length > 0 &&\n typeof frame['index'] === 'number' &&\n Number.isSafeInteger(frame['index']) &&\n frame['index'] >= 0 &&\n Object.hasOwn(frame, 'state') &&\n (!Object.hasOwn(frame, 'stateUndefined') ||\n (frame['stateUndefined'] === true && frame['state'] === null)) &&\n (!Object.hasOwn(frame, 'clearIndex') ||\n (typeof frame['clearIndex'] === 'number' &&\n Number.isSafeInteger(frame['clearIndex']) &&\n frame['clearIndex'] > 0 &&\n frame['clearIndex'] <= frame['index']))\n )\n}\n\nexport type SubscriptionStart =\n | { index: number; bootstrap?: never; reducer?: string }\n | { bootstrap: true; reducer: string; index?: never }\n\nexport type SocketDownFrame =\n | StateSnapshotFrame\n | { kind: 'event'; event: WireEvent }\n | { kind: 'events'; events: CompactWireEvent[]; sessionId?: string }\n | { kind: 'presence'; patch: WirePresencePatch; sessionId?: string }\n | {\n kind: 'presence-snapshot'\n snapshot: WirePresenceSnapshot\n sessionId?: string\n }\n | { kind: 'ack'; req: number; events: WireEvent[]; sessionId?: string }\n | { kind: 'ack'; req: number; error: A2Error; sessionId?: string }\n | { kind: 'subscribed'; sessionId: string; attempt?: number }\n | {\n kind: 'unsubscribed'\n sessionId: string\n attempt?: number\n reason?: string\n error?: A2Error\n }\n | { kind: 'ping' }\n\n/**\n * Client → server: the push envelope's two planes. `sessionId` is\n * implied by the socket on a single-session connection and required by\n * the multiplexed handler. Plain JSON on the wire; the server parses\n * and validates them through the same seams as `parsePushBody`.\n */\nexport type SocketPushFrame = {\n kind: 'push'\n /** Client-local ack correlator — opaque to the server. */\n req: number\n events: Array<{ type: string; payload: unknown; id?: string }>\n sessionId?: string\n}\n\nexport type SocketPresenceFrame = {\n kind: 'presence'\n participant: string\n values: Record<string, unknown>\n seen?: number\n at?: number\n sessionId?: string\n}\n\n/** One session lane in a subscribe frame. The server echoes `attempt`\n * on that lane's `subscribed`/`unsubscribed` answers, so a client that\n * re-subscribed before an answer arrived can tell the answers apart. */\nexport type SocketSubscribeEntry = {\n id: string\n attempt?: number\n} & SubscriptionStart\n\n/** Open per-session lanes on a multiplexed socket. `index` is each\n * session's exclusive resume frontier — `stream({ startAfter })`. */\nexport type SocketSubscribeFrame = {\n kind: 'subscribe'\n sessions: SocketSubscribeEntry[]\n}\n\nexport type SocketUnsubscribeFrame = {\n kind: 'unsubscribe'\n sessions: string[]\n}\n\nexport type SocketUpFrame =\n | SocketPushFrame\n | SocketPresenceFrame\n | SocketSubscribeFrame\n | SocketUnsubscribeFrame\n\nexport const SOCKET_PING_FRAME: string = JSON.stringify({ kind: 'ping' })\n\n/** One stream item as a socket frame — `sseResponse`'s framing over\n * the same codecs, with `kind` instead of an SSE event name. On a\n * multiplexed socket presence frames carry the `sessionId` tag; events\n * already carry theirs in the wire event. */\nexport function socketFrameFor(\n item: Event | PresencePatch | PresenceSnapshot | StateSnapshotFrame,\n sessionId?: string,\n): string {\n const tag = sessionId === undefined ? {} : { sessionId }\n if ('kind' in item) return JSON.stringify(stateSnapshotToWire(item))\n if ('snapshot' in item) {\n return JSON.stringify({\n kind: 'presence-snapshot',\n ...presenceSnapshotToWire(item),\n ...tag,\n })\n }\n if ('participant' in item) {\n return JSON.stringify({\n kind: 'presence',\n ...presencePatchToWire(item),\n ...tag,\n })\n }\n return JSON.stringify({ kind: 'event', ...eventToWire(item) })\n}\n\n/** A version 2 batch frame from compact event JSON; a multiplexed\n * socket names the batch's session once. */\nexport function socketEventsFor(events: string[], sessionId?: string): string {\n const tag =\n sessionId === undefined ? '' : `\"sessionId\":${JSON.stringify(sessionId)},`\n return `{\"kind\":\"events\",${tag}\"events\":[${events.join(',')}]}`\n}\n\nexport function socketAckFor(\n req: number,\n events: Event[],\n sessionId?: string,\n): string {\n return JSON.stringify({\n kind: 'ack',\n req,\n events: events.map(eventToWire),\n ...(sessionId === undefined ? {} : { sessionId }),\n })\n}\n\nexport function socketErrorAckFor(\n req: number,\n error: A2Error,\n sessionId?: string,\n): string {\n return JSON.stringify({\n kind: 'ack',\n req,\n ...errorToWire(error),\n ...(sessionId === undefined ? {} : { sessionId }),\n })\n}\n\nexport function socketSubscribedFor(options: {\n sessionId: string\n attempt?: number | undefined\n}): string {\n const { sessionId, attempt } = options\n return JSON.stringify({\n kind: 'subscribed',\n sessionId,\n ...(attempt === undefined ? {} : { attempt }),\n })\n}\n\n/** The subscription-over notice: with a `reason` the server rejected\n * or lost the session's stream, without one it ended cleanly. */\nexport function socketUnsubscribedFor(options: {\n sessionId: string\n attempt?: number | undefined\n reason?: string\n error?: A2Error\n}): string {\n const { sessionId, attempt, reason, error } = options\n return JSON.stringify({\n kind: 'unsubscribed',\n sessionId,\n ...(attempt === undefined ? {} : { attempt }),\n ...(reason === undefined ? {} : { reason }),\n ...(error === undefined ? {} : errorToWire(error)),\n })\n}\n\nconst isAttempt = (value: unknown): value is number =>\n typeof value === 'number' && Number.isSafeInteger(value) && value >= 0\n\nexport function parseSocketFrame(data: string): SocketDownFrame | null {\n let parsed: unknown\n try {\n parsed = JSON.parse(data)\n } catch {\n return null\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return null\n }\n const frame = parsed as Record<string, unknown>\n switch (frame['kind']) {\n case 'snapshot':\n if (!isStateSnapshotFrame(parsed))\n throw new A2Error('INVALID_PAYLOAD', 'invalid bootstrap snapshot')\n return parsed\n case 'ping':\n return { kind: 'ping' }\n case 'event': {\n if (!isWireEvent(parsed)) return null\n const { id, type, payload, index, sessionId, createdAt } = parsed\n return {\n kind: 'event',\n event: { id, type, payload, index, sessionId, createdAt },\n }\n }\n case 'events': {\n // Dropping a batch would leave a gap the frontier then skips past.\n const events = frame['events']\n if (!Array.isArray(events) || !events.every(isCompactWireEvent))\n throw new A2Error('INVALID_PAYLOAD', 'invalid event batch')\n return { kind: 'events', events, ...sessionTag(frame) }\n }\n case 'presence': {\n if (!isWirePresencePatch(parsed)) return null\n const { participant, values, seen, at } = parsed\n return {\n kind: 'presence',\n patch: { participant, values, seen, at },\n ...sessionTag(frame),\n }\n }\n case 'presence-snapshot': {\n if (!isWirePresenceSnapshot(parsed)) return null\n return {\n kind: 'presence-snapshot',\n snapshot: { snapshot: parsed.snapshot },\n ...sessionTag(frame),\n }\n }\n case 'subscribed': {\n const { sessionId, attempt } = frame\n if (typeof sessionId !== 'string') return null\n if (attempt !== undefined && !isAttempt(attempt)) return null\n return {\n kind: 'subscribed',\n sessionId,\n ...(attempt === undefined ? {} : { attempt }),\n }\n }\n case 'unsubscribed': {\n const { sessionId, attempt } = frame\n if (typeof sessionId !== 'string') return null\n if (attempt !== undefined && !isAttempt(attempt)) return null\n const reason = frame['reason']\n const error =\n errorFromWire(parsed) ??\n ('error' in frame\n ? new A2Error(\n 'STORE_UNAVAILABLE',\n 'unintelligible subscription error',\n )\n : null)\n return {\n kind: 'unsubscribed',\n sessionId,\n ...(attempt === undefined ? {} : { attempt }),\n ...(typeof reason === 'string' ? { reason } : {}),\n ...(error === null ? {} : { error }),\n }\n }\n case 'ack': {\n const req = frame['req']\n if (typeof req !== 'number') return null\n const tag = sessionTag(frame)\n const events = frame['events']\n if (Array.isArray(events) && events.every(isWireEvent)) {\n return { kind: 'ack', req, events, ...tag }\n }\n const error = errorFromWire(parsed)\n if (error) return { kind: 'ack', req, error, ...tag }\n // A correlatable ack must never be dropped: its waiter would hang\n // forever behind a healthy socket (pings keep the watchdog fed).\n // An outcome this client cannot interpret — an error code from a\n // newer server, an event shape that fails a guard — degrades to\n // a lost ack: retryable, and the client-generated ids make the\n // retry idempotent even if the append actually committed.\n return {\n kind: 'ack',\n req,\n error: new A2Error('STORE_UNAVAILABLE', 'unintelligible ack'),\n ...tag,\n }\n }\n default:\n return null\n }\n}\n\nconst sessionTag = (frame: Record<string, unknown>): { sessionId?: string } => {\n const sessionId = frame['sessionId']\n return typeof sessionId === 'string' ? { sessionId } : {}\n}\n"],"mappings":"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"}
|
|
@@ -103,8 +103,11 @@ no retry. The message settles, and the caller receives
|
|
|
103
103
|
backoff while the instance's lane waits; see
|
|
104
104
|
[Errors](/reference/errors).
|
|
105
105
|
- **A crash** re-runs the handler from the pre-message state. `ctx.id`
|
|
106
|
-
is the message id, stable across re-runs
|
|
107
|
-
key for external I/O.
|
|
106
|
+
is the message id, stable across re-runs and unique within the
|
|
107
|
+
instance: use it as the idempotency key for external I/O. An explicit
|
|
108
|
+
message `id` that repeats in another instance repeats there too, so
|
|
109
|
+
derive explicit ids per instance, for example with
|
|
110
|
+
[`idempotentId()`](/reference/core#idempotentidparts).
|
|
108
111
|
- **A clear** by `handle.session.clear()` before the answer rejects the
|
|
109
112
|
call with an `A2Error` whose code is `SESSION_CLEARED`. The handler
|
|
110
113
|
may already have run. Over HTTP it's a `410`.
|
|
@@ -292,9 +292,10 @@ no `reducer`; the types enforce this through `AI_STATE_REDUCER`.
|
|
|
292
292
|
|
|
293
293
|
When the agent belongs inside something larger, such as a shared chat room,
|
|
294
294
|
keep two sessions. A consumer of the room appends each message the agent should
|
|
295
|
-
see to the agent's session with `inputs.message()`.
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
see to the agent's session with `inputs.message()`. Derive the message ID from
|
|
296
|
+
the room event's ID with `idempotentId()`, so a retried run converges on the
|
|
297
|
+
same message. Event IDs are unique per session, so when several rooms feed one
|
|
298
|
+
agent session, include the room's session ID too. A tool
|
|
298
299
|
the agent calls to speak appends to the room with an `idempotentId()` of its
|
|
299
300
|
`toolCallId`. Neither fold reads the other's state.
|
|
300
301
|
|
|
@@ -509,11 +510,11 @@ full handoff rules.
|
|
|
509
510
|
New-input commands from `inputs.message()`, `inputs.seed()`, and
|
|
510
511
|
`inputs.steer()` use the message ID as their event ID, and hand-written commands
|
|
511
512
|
must too. Caller IDs cannot start with the reserved `a2.ai:` prefix and must be
|
|
512
|
-
unique
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
513
|
+
unique within the session; the same ID in another session is another message.
|
|
514
|
+
An identical submission replays without new work, even after removal or
|
|
515
|
+
compaction. Reusing an ID in the session with different content or action
|
|
516
|
+
rejects with `PARTIAL_DUPLICATE_BATCH`: use `queue.edit()` to change a pending
|
|
517
|
+
input and a fresh ID for a new one.
|
|
517
518
|
|
|
518
519
|
### Optimistic conversation display
|
|
519
520
|
|
|
@@ -179,8 +179,11 @@ first model call sees it.
|
|
|
179
179
|
A2 records each model call and each tool result as the harness works. When a
|
|
180
180
|
process dies in the middle of a response, the next attempt resumes at the
|
|
181
181
|
interrupted step: finished model calls are not made again and finished tools
|
|
182
|
-
do not run again. A2
|
|
183
|
-
last response resumes the harness from its state at the end of that
|
|
184
|
-
FX's checkpoint or Pi's session, kept in memory, and any other
|
|
185
|
-
the session from the log.
|
|
182
|
+
do not run again. A2 keeps nothing durable outside the log: a process that ran
|
|
183
|
+
the last response resumes the harness from its state at the end of that
|
|
184
|
+
response, FX's checkpoint or Pi's session, kept in memory, and any other
|
|
185
|
+
process replays the session from the log. Under `fx()`, the process that ran a
|
|
186
|
+
step also keeps FX's kernel waiting on that step's tool calls until the session
|
|
187
|
+
has nothing left to run there, so the response's next step, or the first step
|
|
188
|
+
of a response that steers it, continues it without a replay. A tool call that a crash cut short reruns unless its
|
|
186
189
|
`toolRecovery` is `'report'`; see [Delivery semantics](/agents/conversations#delivery-semantics).
|
|
@@ -207,6 +207,7 @@ store has it, at two tiers:
|
|
|
207
207
|
| Backend | Delivery |
|
|
208
208
|
| --- | --- |
|
|
209
209
|
| `store-redis` | push: TTL'd hash plus pub/sub patches |
|
|
210
|
+
| `store-valkey` | push: TTL'd hash plus pub/sub patches |
|
|
210
211
|
| `store-redis-http` | push over HTTP subscriptions; adaptive polling when unsupported |
|
|
211
212
|
| `store-memory` | in-process, immediate |
|
|
212
213
|
| `store-postgres` | native notifications with `listenConnectionString`; adaptive polling by default |
|
|
@@ -20,6 +20,7 @@ description: "Every entry point, and the isomorphic `experimental-a2` core: cont
|
|
|
20
20
|
| [`experimental-a2/store-postgres`](/stores/postgres) | `postgres`: Postgres store backend | `pg` (or inject a client) |
|
|
21
21
|
| [`experimental-a2/store-redis`](/stores/redis) | `redis`: Redis Streams store backend, push-native streaming | `ioredis` (or inject a client) |
|
|
22
22
|
| [`experimental-a2/store-redis-http`](/stores/redis-http) | `redisHttp`: the same Redis store over provider REST APIs (Upstash) | none |
|
|
23
|
+
| [`experimental-a2/store-valkey`](/stores/valkey) | `valkey`: the same Redis store on node-redis connections (Valkey, ElastiCache) | `redis` |
|
|
23
24
|
| [`experimental-a2/store-sqlite`](/stores/sqlite) | SQLite store backend | none |
|
|
24
25
|
| [`experimental-a2/store-memory`](/stores/memory) | in-memory store backend | none |
|
|
25
26
|
| [`experimental-a2/scheduler-vercel`](/schedulers/vercel-queues) | `vercelQueues`: recovery and timers | `@vercel/queue` |
|
|
@@ -110,8 +111,9 @@ export const id = await idempotentId('orders', 'shop-7', 'request-42')
|
|
|
110
111
|
- String boundaries, order, and case matter; strings are not normalized. Empty
|
|
111
112
|
strings and an empty tuple are valid.
|
|
112
113
|
- Every argument has the same role; a prefix such as `'orders'` is a namespace
|
|
113
|
-
by convention. Include enough context to distinguish operations
|
|
114
|
-
|
|
114
|
+
by convention. Include enough context to distinguish operations within a
|
|
115
|
+
session: event IDs are unique per session, so the same ID in another session
|
|
116
|
+
names a different event.
|
|
115
117
|
- A2 preserves explicit IDs and does not change its automatic ID generation.
|
|
116
118
|
Changing an existing recipe changes retry identity, so retries of already
|
|
117
119
|
committed requests must keep their original IDs.
|
|
@@ -517,7 +517,8 @@ The only way to move a session forward. See [Guarantees](/server/guarantees).
|
|
|
517
517
|
- A multi-event append is atomic: all-or-nothing, consecutive positions, one
|
|
518
518
|
transaction.
|
|
519
519
|
- An explicit `id` makes it idempotent across retries; re-sending an identical
|
|
520
|
-
batch returns the original rows.
|
|
520
|
+
batch returns the original rows. Ids are unique per session: the same `id`
|
|
521
|
+
in another session is another event.
|
|
521
522
|
- Events pushed through `server.fetch` arrive through this same append.
|
|
522
523
|
- Only events some consumer takes are dispatched; the append itself settles
|
|
523
524
|
the rest. An append no consumer takes still starts session healing when
|
|
@@ -883,6 +884,9 @@ broadcasts. No log row, no dispatch, no scheduler arm; `history()` and
|
|
|
883
884
|
epoch-ms stamp; the browser client stamps its sends automatically. Without
|
|
884
885
|
`at`, receipt time stands in: a stampless caller is a single writer, so
|
|
885
886
|
receipt order is its send order.
|
|
887
|
+
- `null` clears a field. The store keeps the clear's `at` until the field's
|
|
888
|
+
TTL runs out, so an older value that reaches it later, such as one sent
|
|
889
|
+
through another server instance, still loses.
|
|
886
890
|
- A bad payload throws `INVALID_PAYLOAD`, an unknown field throws
|
|
887
891
|
`UNKNOWN_PRESENCE_FIELD`, and a non-finite or negative `at` throws a
|
|
888
892
|
`TypeError`.
|