experimental-a2 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/dist/ai-B4YhEnfw.d.ts +333 -0
  3. package/dist/ai-server.d.ts +48 -9
  4. package/dist/ai-server.js +121 -49
  5. package/dist/ai.d.ts +2 -303
  6. package/dist/ai.js +231 -86
  7. package/dist/cli-B3VuxoDe.js +597 -0
  8. package/dist/cli-bin.d.ts +1 -0
  9. package/dist/cli-bin.js +5 -0
  10. package/dist/cli.d.ts +19 -0
  11. package/dist/cli.js +2 -0
  12. package/dist/client-BrfDXQ8A.d.ts +155 -0
  13. package/dist/client-Bt4tAKi9.js +798 -0
  14. package/dist/client.d.ts +2 -90
  15. package/dist/client.js +1 -409
  16. package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +10 -2
  17. package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +62 -8
  18. package/dist/devtools-J_jZ2vQf.d.ts +151 -0
  19. package/dist/devtools-kJJaORn-.js +338 -0
  20. package/dist/devtools-server.browser.js +1 -1
  21. package/dist/devtools-server.d.ts +2 -2
  22. package/dist/devtools-server.js +224 -43
  23. package/dist/devtools.d.ts +2 -0
  24. package/dist/devtools.js +2 -0
  25. package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +4 -4
  26. package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +1 -1
  27. package/dist/http.d.ts +71 -13
  28. package/dist/http.js +302 -41
  29. package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +2 -2
  30. package/dist/index.d.ts +5 -5
  31. package/dist/index.js +2 -2
  32. package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +2 -1
  33. package/dist/internal-aEotMzu_.js +209 -0
  34. package/dist/otel.d.ts +1 -1
  35. package/dist/platform-B4TnJtWu.js +32 -0
  36. package/dist/react.d.ts +37 -14
  37. package/dist/react.js +26 -15
  38. package/dist/scheduler-qstash.d.ts +78 -0
  39. package/dist/scheduler-qstash.js +499 -0
  40. package/dist/scheduler-task-BpzhPnRS.js +54 -0
  41. package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +17 -24
  42. package/dist/scheduler-vercel.js +226 -0
  43. package/dist/server-CcNnFnoW.js +1405 -0
  44. package/dist/server-YtPq7hjw.d.ts +260 -0
  45. package/dist/server.d.ts +4 -155
  46. package/dist/server.js +2 -2
  47. package/dist/{log-ldf5g8Cx.d.ts → store-C3sNAaBT.d.ts} +111 -35
  48. package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +1 -1
  49. package/dist/store-codec-DTG0Ftek.js +8 -0
  50. package/dist/store-memory.d.ts +11 -0
  51. package/dist/{log-memory.js → store-memory.js} +127 -24
  52. package/dist/{log-polling-DZ1MiKLg.js → store-polling-DgrrAE3d.js} +5 -5
  53. package/dist/{log-postgres.d.ts → store-postgres.d.ts} +6 -6
  54. package/dist/{log-postgres.js → store-postgres.js} +158 -24
  55. package/dist/{log-redis-core-CyJ5L8yR.js → store-redis-core-DWqx3F47.js} +269 -66
  56. package/dist/{log-redis-http.d.ts → store-redis-http.d.ts} +7 -7
  57. package/dist/{log-redis-http.js → store-redis-http.js} +20 -12
  58. package/dist/{log-redis.d.ts → store-redis.d.ts} +6 -6
  59. package/dist/{log-redis.js → store-redis.js} +99 -39
  60. package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +6 -6
  61. package/dist/{log-sqlite.js → store-sqlite.js} +116 -22
  62. package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +1 -1
  63. package/dist/testing.browser.d.ts +1 -0
  64. package/dist/testing.browser.js +4 -0
  65. package/dist/testing.d.ts +31 -0
  66. package/dist/testing.js +101 -0
  67. package/dist/wire-DCUZBUlT.js +222 -0
  68. package/docs/01-quickstart.mdx +3 -3
  69. package/docs/concepts/01-contracts.mdx +18 -13
  70. package/docs/concepts/02-handlers.mdx +7 -7
  71. package/docs/concepts/03-durability.mdx +21 -21
  72. package/docs/concepts/04-state.mdx +18 -21
  73. package/docs/guides/01-timers.mdx +154 -49
  74. package/docs/guides/02-cancellation.mdx +30 -4
  75. package/docs/guides/03-react.mdx +20 -21
  76. package/docs/guides/04-local-first.mdx +1 -1
  77. package/docs/guides/05-production.mdx +310 -61
  78. package/docs/guides/06-ai-agents.mdx +249 -49
  79. package/docs/guides/07-devtools.mdx +137 -12
  80. package/docs/guides/08-application-data.mdx +1 -1
  81. package/docs/guides/09-presence.mdx +284 -0
  82. package/docs/guides/10-transports.mdx +131 -0
  83. package/docs/index.mdx +18 -14
  84. package/docs/reference/01-api.mdx +751 -103
  85. package/docs/reference/02-errors.mdx +10 -5
  86. package/package.json +23 -7
  87. package/dist/internal-gCd5qMry.js +0 -44
  88. package/dist/log-memory.d.ts +0 -11
  89. package/dist/recovery-vercel.js +0 -119
  90. package/dist/server-BcLa4RFL.js +0 -877
  91. package/dist/wire-BVsgR8o9.js +0 -62
package/dist/client.d.ts CHANGED
@@ -1,90 +1,2 @@
1
- import { a as EventDefs, c as Reducer, r as ContractEvent, t as AppendInput } from "./contract-C_3dIIEU.js";
2
- //#region src/client.d.ts
3
- type ConnectionStatus = "idle" | "connecting" | "live" | "closed";
4
- /**
5
- * The connection, as a discriminated union — impossible states are
6
- * unrepresentable: an error only exists while disconnected,
7
- * `reconnects` only once a connection has been attempted.
8
- * "Reconnecting…" is `status === 'connecting' && reconnects > 0`.
9
- */
10
- type Connection = {
11
- status: "idle";
12
- } | {
13
- status: "connecting";
14
- /** Drops of an established stream so far. `0` = first connect. */
15
- reconnects: number;
16
- /** Why the last connection ended; `null` on the first connect. */
17
- error: Error | null;
18
- } | {
19
- status: "live";
20
- reconnects: number;
21
- } | {
22
- status: "closed";
23
- };
24
- /**
25
- * What `push` returns: resolves at the server ack (exactly like a
26
- * plain promise — `await push(...)` gives the acked events), and
27
- * carries `confirmed` for the later moment when the live stream has
28
- * delivered the whole batch back and the optimistic overlay entry
29
- * retired — the view now shows server truth. `confirmed` is lazy:
30
- * never accessed, never created. A rejected push rejects both.
31
- */
32
- type PushResult<D extends EventDefs> = Promise<ContractEvent<D>[]> & {
33
- readonly confirmed: Promise<ContractEvent<D>[]>;
34
- };
35
- /** One immutable view of the session — stable identity between changes. */
36
- type SessionSnapshot<D extends EventDefs, S> = {
37
- /** The live view: server events folded, optimistic pushes applied. */
38
- state: S;
39
- /** The observed feed `state` is folded from — server truth plus
40
- * pending optimistic events (provisional indexes past the frontier). */
41
- events: ContractEvent<D>[];
42
- /** The stream frontier: the last server-confirmed index. This is the
43
- * `lastSeenIndex` cancellation wants. */
44
- index: number;
45
- connection: Connection;
46
- };
47
- type SessionClient<D extends EventDefs, S> = {
48
- readonly sessionId: string;
49
- subscribe(listener: () => void): () => void;
50
- getSnapshot(): SessionSnapshot<D, S>;
51
- /**
52
- * Optimistic append: validates locally against the reducer's event
53
- * schemas (instant `INVALID_PAYLOAD`, no flicker), applies to the
54
- * local fold, POSTs, swaps in the ack, rolls back on rejection.
55
- * Auto-retries only `LOG_UNAVAILABLE`. Resolves with the appended
56
- * events as the server recorded them.
57
- */
58
- push(...events: AppendInput<D>[]): PushResult<D>;
59
- /** Open the live stream (idempotent while open). Reconnects with
60
- * backoff and resumes from the frontier until `close()`. */
61
- connect(): void;
62
- /**
63
- * Stop the live stream. Not terminal: `connect()` starts it again
64
- * from the current frontier — which is what makes the React
65
- * StrictMode mount dance (setup → cleanup → setup) work.
66
- */
67
- close(): void;
68
- };
69
- type SessionOptions<D extends EventDefs, S> = {
70
- initialState?: S;
71
- initialIndex?: number;
72
- /** Server-rendered history through `initialIndex`. Seeds the event feed. */
73
- initialEvents?: ContractEvent<D>[];
74
- };
75
- type A2Client<D extends EventDefs, S> = {
76
- session(sessionId: string, options?: SessionOptions<D, S>): SessionClient<D, S>;
77
- };
78
- type CreateClientOptions<D extends EventDefs, S> = {
79
- reducer: Reducer<D, S>;
80
- /** Base path (or absolute URL) of the route exposing GET/POST. */
81
- api: string;
82
- /** Injectable fetch — defaults to the global. */
83
- fetch?: typeof globalThis.fetch;
84
- /** How long an idle session keeps its in-memory identity, in
85
- * milliseconds. Defaults to five minutes; `Infinity` disables GC. */
86
- gcTime?: number;
87
- };
88
- declare function createClient<D extends EventDefs, S>(options: CreateClientOptions<D, S>): A2Client<D, S>;
89
- //#endregion
90
- export { A2Client, Connection, ConnectionStatus, CreateClientOptions, PushResult, SessionClient, SessionOptions, SessionSnapshot, createClient };
1
+ import { a as Connection, c as PushResult, d as SessionOptions, f as SessionSnapshot, i as ClientWebSocketConstructor, l as SessionClient, n as ClientApi, o as ConnectionStatus, p as createClient, r as ClientWebSocket, s as CreateClientOptions, t as A2Client, u as SessionClientPresence } from "./client-BrfDXQ8A.js";
2
+ export { A2Client, ClientApi, ClientWebSocket, ClientWebSocketConstructor, Connection, ConnectionStatus, CreateClientOptions, PushResult, SessionClient, SessionClientPresence, SessionOptions, SessionSnapshot, createClient };
package/dist/client.js CHANGED
@@ -1,410 +1,2 @@
1
- import { n as validateSync } from "./validate-XKT4FSNn.js";
2
- import { t as A2Error } from "./errors-BJRMd-h6.js";
3
- import { i as STREAM_TIMINGS } from "./internal-gCd5qMry.js";
4
- import { i as eventFromWire, o as isWireEvent, t as errorFromWire } from "./wire-BVsgR8o9.js";
5
- //#region src/client.ts
6
- /**
7
- * experimental-a2/client — the framework-agnostic session client.
8
- *
9
- * Everything the browser needs to read a session live and push
10
- * optimistically, with no framework attached: the SSE subscription with
11
- * frontier resume and reconnection, the optimistic push queue with
12
- * ack/rollback, and the local fold through the same reducer the server
13
- * uses. `experimental-a2/react`'s `createReact` is a thin binding over it, and the
14
- * store contract (`subscribe`/`getSnapshot`) is exactly what
15
- * `useSyncExternalStore` wants.
16
- *
17
- * The client is a replica, never an access path — the routes it talks
18
- * to authorize every read and write.
19
- */
20
- const PUSH_ATTEMPTS = 3;
21
- const RECONNECT_BASE_MS = 500;
22
- const RECONNECT_MAX_MS = 5e3;
23
- const DEFAULT_GC_TIME_MS = 3e5;
24
- function createClient(options) {
25
- const { reducer, api } = options;
26
- const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
27
- const gcTime = options.gcTime ?? DEFAULT_GC_TIME_MS;
28
- if (Number.isNaN(gcTime) || gcTime < 0) throw new RangeError("gcTime must be a non-negative number or Infinity");
29
- const sessions = /* @__PURE__ */ new Map();
30
- const scheduleGc = (sessionId, delay = gcTime) => {
31
- const entry = sessions.get(sessionId);
32
- if (!entry) return;
33
- clearTimeout(entry.timer);
34
- if (gcTime === Infinity) return;
35
- entry.timer = setTimeout(() => {
36
- if (sessions.get(sessionId) !== entry) return;
37
- if (entry.runtime.canEvict()) {
38
- sessions.delete(sessionId);
39
- return;
40
- }
41
- scheduleGc(sessionId, Math.max(gcTime, 1e3));
42
- }, delay);
43
- entry.timer.unref?.();
44
- };
45
- const touch = (sessionId) => {
46
- scheduleGc(sessionId);
47
- };
48
- const makeSession = (sessionId, sessionOptions) => {
49
- let frontier = sessionOptions?.initialIndex ?? 0;
50
- let foldedState = sessionOptions?.initialState !== void 0 ? sessionOptions.initialState : reducer.initialState;
51
- const serverEvents = (sessionOptions?.initialEvents ?? []).filter((event) => event.index <= frontier).toSorted((a, b) => a.index - b.index);
52
- let pending = [];
53
- const listeners = /* @__PURE__ */ new Set();
54
- let snapshot = null;
55
- let status = "idle";
56
- let reconnects = 0;
57
- let lastError = null;
58
- let inFlightPushes = 0;
59
- let deferredHydration;
60
- let hydrationNotification;
61
- const emit = () => {
62
- for (const listener of [...listeners]) listener();
63
- };
64
- const notify = () => {
65
- snapshot = null;
66
- emit();
67
- };
68
- const notifyHydrated = () => {
69
- snapshot = null;
70
- clearTimeout(hydrationNotification);
71
- hydrationNotification = setTimeout(emit, 0);
72
- hydrationNotification.unref?.();
73
- };
74
- const mergeServerEvents = (events, throughIndex) => {
75
- if (!events || events.length === 0) return false;
76
- const byIndex = new Map(serverEvents.map((event) => [event.index, event]));
77
- let changed = false;
78
- for (const event of events) {
79
- if (event.index > throughIndex) continue;
80
- if (byIndex.get(event.index)?.id === event.id) continue;
81
- byIndex.set(event.index, event);
82
- changed = true;
83
- }
84
- if (!changed) return false;
85
- serverEvents.splice(0, serverEvents.length, ...[...byIndex.values()].toSorted((a, b) => a.index - b.index));
86
- return true;
87
- };
88
- const overlayEvents = () => {
89
- const acked = pending.filter((p) => p.acked).map((p) => p.acked).toSorted((a, b) => a.index - b.index);
90
- const maxKnown = acked.at(-1)?.index ?? frontier;
91
- const unacked = pending.filter((p) => !p.acked).map((p, i) => ({
92
- id: p.id,
93
- type: p.type,
94
- payload: p.payload,
95
- index: maxKnown + 1 + i,
96
- sessionId,
97
- createdAt: p.createdAt
98
- }));
99
- return [...acked, ...unacked];
100
- };
101
- const connection = () => {
102
- switch (status) {
103
- case "idle": return { status: "idle" };
104
- case "closed": return { status: "closed" };
105
- case "live": return {
106
- status: "live",
107
- reconnects
108
- };
109
- case "connecting": return {
110
- status: "connecting",
111
- reconnects,
112
- error: lastError
113
- };
114
- }
115
- };
116
- const buildSnapshot = () => {
117
- const overlay = overlayEvents();
118
- let state = foldedState;
119
- for (const event of overlay) state = reducer.fold(state, event);
120
- return {
121
- state,
122
- events: [...serverEvents, ...overlay],
123
- index: frontier,
124
- connection: connection()
125
- };
126
- };
127
- /** Pushes awaiting stream confirmation — resolved by `ingest` the
128
- * moment the frontier passes their batch. */
129
- let confirmWatchers = [];
130
- /** The single ingest point: every server-confirmed event, in log
131
- * order, from the stream. */
132
- const ingest = (event) => {
133
- if (event.index <= frontier) return;
134
- frontier = event.index;
135
- serverEvents.push(event);
136
- foldedState = reducer.fold(foldedState, event);
137
- pending = pending.filter((p) => p.id !== event.id);
138
- if (confirmWatchers.some((w) => w.index <= frontier)) {
139
- const due = confirmWatchers.filter((w) => w.index <= frontier);
140
- confirmWatchers = confirmWatchers.filter((w) => w.index > frontier);
141
- for (const watcher of due) watcher.resolve();
142
- }
143
- notify();
144
- touch(sessionId);
145
- };
146
- const validated = (events) => events.map((event) => {
147
- const schema = Object.hasOwn(reducer.events, event.type) ? reducer.events[event.type] : void 0;
148
- if (!schema) throw new A2Error("UNKNOWN_EVENT_TYPE", `no event type '${String(event.type)}' in the reducer's vocabulary`);
149
- const result = validateSync(schema, event.payload, `event '${String(event.type)}'`);
150
- if (result.issues) throw new A2Error("INVALID_PAYLOAD", `invalid payload for event '${String(event.type)}'`, { details: result.issues });
151
- return {
152
- id: event.id ?? crypto.randomUUID(),
153
- type: event.type,
154
- payload: result.value
155
- };
156
- });
157
- const post = async (body) => {
158
- let lastPushError = new A2Error("LOG_UNAVAILABLE", "push failed");
159
- for (let attempt = 1; attempt <= PUSH_ATTEMPTS; attempt += 1) {
160
- try {
161
- const res = await fetchImpl(api, {
162
- method: "POST",
163
- headers: { "content-type": "application/json" },
164
- body: JSON.stringify(body)
165
- });
166
- if (res.ok) {
167
- const rows = await res.json();
168
- if (!Array.isArray(rows) || !rows.every(isWireEvent)) throw new A2Error("LOG_UNAVAILABLE", "push ack was not a list of events");
169
- return rows.map((row) => eventFromWire(row));
170
- }
171
- lastPushError = errorFromWire(await res.json().catch(() => null)) ?? new A2Error("LOG_UNAVAILABLE", `push failed with ${res.status}`);
172
- } catch (err) {
173
- lastPushError = err instanceof A2Error ? err : new A2Error("LOG_UNAVAILABLE", "push request failed", { cause: err });
174
- }
175
- if (lastPushError.code !== "LOG_UNAVAILABLE") throw lastPushError;
176
- if (attempt < PUSH_ATTEMPTS) await new Promise((resolve) => setTimeout(resolve, 250 * attempt));
177
- }
178
- throw lastPushError;
179
- };
180
- /**
181
- * Decorate the ack promise into a PushResult. `confirmed` is a
182
- * lazy getter — materialized on first access, so callers that
183
- * ignore it can't leak an unhandled rejection.
184
- */
185
- const withConfirmed = (ack) => {
186
- let confirmed;
187
- return Object.defineProperty(ack, "confirmed", { get() {
188
- confirmed ??= ack.then((acked) => new Promise((resolve) => {
189
- const last = acked.at(-1)?.index ?? 0;
190
- if (frontier >= last) {
191
- resolve(acked);
192
- return;
193
- }
194
- confirmWatchers.push({
195
- index: last,
196
- resolve: () => resolve(acked)
197
- });
198
- }));
199
- return confirmed;
200
- } });
201
- };
202
- const push = (...events) => {
203
- touch(sessionId);
204
- return withConfirmed((async () => {
205
- if (events.length === 0) throw new TypeError("push requires at least one event");
206
- const entries = validated(events).map((e) => ({
207
- id: e.id,
208
- type: e.type,
209
- payload: e.payload,
210
- createdAt: /* @__PURE__ */ new Date()
211
- }));
212
- pending.push(...entries);
213
- inFlightPushes += 1;
214
- notify();
215
- try {
216
- const acked = await post({
217
- sessionId,
218
- events: entries.map(({ id, type, payload }) => ({
219
- id,
220
- type,
221
- payload
222
- }))
223
- });
224
- for (const event of acked) {
225
- const entry = pending.find((p) => p.id === event.id);
226
- if (entry) entry.acked = event;
227
- }
228
- notify();
229
- return acked;
230
- } catch (err) {
231
- const ids = new Set(entries.map((e) => e.id));
232
- pending = pending.filter((p) => !ids.has(p.id));
233
- notify();
234
- throw err;
235
- } finally {
236
- inFlightPushes -= 1;
237
- if (inFlightPushes === 0 && deferredHydration) {
238
- const next = deferredHydration;
239
- deferredHydration = void 0;
240
- runtime.hydrate(next);
241
- }
242
- touch(sessionId);
243
- }
244
- })());
245
- };
246
- let generation = 0;
247
- let active = false;
248
- let abort = null;
249
- const streamUrl = () => {
250
- const sep = api.includes("?") ? "&" : "?";
251
- return `${api}${sep}sessionId=${encodeURIComponent(sessionId)}&index=${frontier}`;
252
- };
253
- const consume = async (body, onActivity) => {
254
- const decoder = new TextDecoder();
255
- const reader = body.getReader();
256
- let buffer = "";
257
- try {
258
- for (;;) {
259
- const { done, value } = await reader.read();
260
- if (done) return;
261
- onActivity();
262
- buffer += decoder.decode(value, { stream: true });
263
- for (;;) {
264
- const boundary = buffer.indexOf("\n\n");
265
- if (boundary === -1) break;
266
- const frame = buffer.slice(0, boundary);
267
- buffer = buffer.slice(boundary + 2);
268
- const data = frame.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
269
- if (!data) continue;
270
- const parsed = JSON.parse(data);
271
- if (isWireEvent(parsed)) ingest(eventFromWire(parsed));
272
- }
273
- }
274
- } finally {
275
- reader.cancel().catch(() => {});
276
- }
277
- };
278
- const runStream = async (run) => {
279
- let backoff = RECONNECT_BASE_MS;
280
- while (generation === run) {
281
- const controller = new AbortController();
282
- abort = controller;
283
- let stall;
284
- let stalled = false;
285
- const armStall = () => {
286
- clearTimeout(stall);
287
- stall = setTimeout(() => {
288
- stalled = true;
289
- controller.abort();
290
- }, STREAM_TIMINGS.stallTimeoutMs);
291
- stall.unref?.();
292
- };
293
- let wasLive = false;
294
- try {
295
- const res = await fetchImpl(streamUrl(), {
296
- headers: { accept: "text/event-stream" },
297
- signal: controller.signal
298
- });
299
- if (!res.ok || !res.body) throw new Error(`stream failed with ${res.status}`);
300
- if (generation !== run) break;
301
- backoff = RECONNECT_BASE_MS;
302
- wasLive = true;
303
- status = "live";
304
- lastError = null;
305
- notify();
306
- armStall();
307
- await consume(res.body, armStall);
308
- if (generation === run) lastError = null;
309
- } catch (err) {
310
- if (generation === run) lastError = stalled ? /* @__PURE__ */ new Error(`stream stalled: no data for ${STREAM_TIMINGS.stallTimeoutMs}ms`) : err instanceof Error ? err : new Error(String(err));
311
- } finally {
312
- clearTimeout(stall);
313
- }
314
- if (generation !== run) break;
315
- if (wasLive) reconnects += 1;
316
- status = "connecting";
317
- notify();
318
- await new Promise((resolve) => {
319
- let settled = false;
320
- const finish = () => {
321
- if (settled) return;
322
- settled = true;
323
- clearTimeout(timer);
324
- resolve();
325
- };
326
- const timer = setTimeout(finish, backoff);
327
- timer.unref?.();
328
- abort?.signal.addEventListener("abort", finish);
329
- });
330
- backoff = Math.min(backoff * 2, RECONNECT_MAX_MS);
331
- }
332
- };
333
- const runtime = {
334
- sessionId,
335
- subscribe(listener) {
336
- touch(sessionId);
337
- listeners.add(listener);
338
- return () => {
339
- listeners.delete(listener);
340
- touch(sessionId);
341
- };
342
- },
343
- getSnapshot() {
344
- snapshot ??= buildSnapshot();
345
- return snapshot;
346
- },
347
- push,
348
- connect() {
349
- touch(sessionId);
350
- if (active) return;
351
- active = true;
352
- status = "connecting";
353
- notify();
354
- runStream(generation);
355
- },
356
- close() {
357
- touch(sessionId);
358
- if (!active) return;
359
- active = false;
360
- generation += 1;
361
- abort?.abort();
362
- abort = null;
363
- status = "closed";
364
- notify();
365
- },
366
- hydrate(next) {
367
- touch(sessionId);
368
- if (!next) return;
369
- const nextIndex = next.initialIndex ?? 0;
370
- if (nextIndex > frontier && inFlightPushes > 0) {
371
- if (!deferredHydration || nextIndex > (deferredHydration.initialIndex ?? 0)) deferredHydration = next;
372
- return;
373
- }
374
- const historyChanged = mergeServerEvents(next.initialEvents, Math.min(nextIndex, frontier));
375
- if (next.initialState === void 0 || nextIndex <= frontier) {
376
- if (historyChanged) notifyHydrated();
377
- return;
378
- }
379
- frontier = nextIndex;
380
- foldedState = next.initialState;
381
- mergeServerEvents(next.initialEvents, frontier);
382
- pending = pending.filter((entry) => !entry.acked || entry.acked.index > frontier);
383
- if (confirmWatchers.some((watcher) => watcher.index <= frontier)) {
384
- const due = confirmWatchers.filter((watcher) => watcher.index <= frontier);
385
- confirmWatchers = confirmWatchers.filter((watcher) => watcher.index > frontier);
386
- for (const watcher of due) watcher.resolve();
387
- }
388
- notifyHydrated();
389
- },
390
- canEvict() {
391
- return listeners.size === 0 && !active && inFlightPushes === 0 && confirmWatchers.length === 0;
392
- }
393
- };
394
- return runtime;
395
- };
396
- return { session(sessionId, sessionOptions) {
397
- const existing = sessions.get(sessionId);
398
- if (existing) {
399
- existing.runtime.hydrate(sessionOptions);
400
- touch(sessionId);
401
- return existing.runtime;
402
- }
403
- const runtime = makeSession(sessionId, sessionOptions);
404
- sessions.set(sessionId, { runtime });
405
- touch(sessionId);
406
- return runtime;
407
- } };
408
- }
409
- //#endregion
1
+ import { t as createClient } from "./client-Bt4tAKi9.js";
410
2
  export { createClient };
@@ -1,7 +1,7 @@
1
1
  import { n as validateSync, t as assertSyncSchema } from "./validate-XKT4FSNn.js";
2
2
  //#region src/reducer.ts
3
3
  /** Internal — reducers are created through `contract.reducer(...).fold(...)`. */
4
- function makeReducerBuilder(events, options) {
4
+ function makeReducerBuilder(events, presence, options) {
5
5
  const { name } = options;
6
6
  if (typeof name !== "string" || name.length === 0) throw new TypeError("reducer name must be a non-empty string — it identifies the reducer and keys cached snapshots");
7
7
  let initialState = options.initialState;
@@ -15,6 +15,7 @@ function makeReducerBuilder(events, options) {
15
15
  return {
16
16
  name,
17
17
  events,
18
+ presence,
18
19
  fold,
19
20
  initialState,
20
21
  stateSchema: options.stateSchema
@@ -48,12 +49,19 @@ function contract(options) {
48
49
  assertSyncSchema(schema, `event '${type}'`);
49
50
  }
50
51
  const events = Object.freeze({ ...defs });
52
+ const presenceDefs = options.presence ?? {};
53
+ for (const [field, schema] of Object.entries(presenceDefs)) {
54
+ if (typeof schema?.["~standard"]?.validate !== "function") throw new TypeError(`contract '${name}': the value for presence field '${field}' is not a Standard Schema (expected an object with '~standard')`);
55
+ assertSyncSchema(schema, `presence field '${field}'`);
56
+ }
57
+ const presence = Object.freeze({ ...presenceDefs });
51
58
  return {
52
59
  name,
53
60
  events,
61
+ presence,
54
62
  batch: (...items) => items,
55
63
  reducer(reducerOptions) {
56
- return makeReducerBuilder(events, reducerOptions);
64
+ return makeReducerBuilder(events, presence, reducerOptions);
57
65
  }
58
66
  };
59
67
  }
@@ -72,11 +72,17 @@ declare namespace StandardSchemaV1 {
72
72
  * the old name are ignored and the next read refolds from raw events.
73
73
  * See docs/concepts/04-state.mdx.
74
74
  */
75
- type Reducer<D extends EventDefs = EventDefs, S = unknown> = {
75
+ type Reducer<D extends EventDefs = EventDefs, S = unknown, P extends PresenceDefs = Record<never, never>> = {
76
76
  /** Identity and cache key. Rename when the fold's logic changes. */
77
77
  readonly name: string;
78
78
  /** The contract's vocabulary — types every event in the fold. */
79
79
  readonly events: Readonly<D>;
80
+ /**
81
+ * The contract's presence vocabulary — carried because the reducer
82
+ * is the client's typed handle on the contract. The fold never sees
83
+ * presence.
84
+ */
85
+ readonly presence: Readonly<P>;
80
86
  readonly fold: (state: S, event: ContractEvent<D>) => S;
81
87
  readonly initialState: S;
82
88
  /**
@@ -88,13 +94,15 @@ type Reducer<D extends EventDefs = EventDefs, S = unknown> = {
88
94
  readonly stateSchema: StandardSchemaV1<unknown, S> | undefined;
89
95
  };
90
96
  /** The second step of `contract.reducer` — see the method's doc. */
91
- type ReducerBuilder<D extends EventDefs, S> = {
92
- fold(fold: (state: S, event: ContractEvent<D>) => S): Reducer<D, S>;
97
+ type ReducerBuilder<D extends EventDefs, S, P extends PresenceDefs = Record<never, never>> = {
98
+ fold(fold: (state: S, event: ContractEvent<D>) => S): Reducer<D, S, P>;
93
99
  };
94
100
  //#endregion
95
101
  //#region src/contract.d.ts
96
102
  /** The event vocabulary shape: event name → payload schema. */
97
103
  type EventDefs = Record<string, StandardSchemaV1>;
104
+ /** The presence vocabulary shape: field name → value schema. */
105
+ type PresenceDefs = Record<string, StandardSchemaV1>;
98
106
  /** One event of a contract, narrowed to type `K`. */
99
107
  type ContractEvent<D extends EventDefs, K extends keyof D & string = keyof D & string> = { [T in keyof D & string]: {
100
108
  id: string;
@@ -111,6 +119,44 @@ type AppendInput<D extends EventDefs> = { [T in keyof D & string]: {
111
119
  id?: string;
112
120
  }; }[keyof D & string];
113
121
  type EventBatchFactory<D extends EventDefs> = <const E extends readonly AppendInput<D>[]>(...events: E) => E;
122
+ /**
123
+ * One presence update: a participant's changed fields (`null` marks a
124
+ * cleared one), stamped with the sender's stream frontier (`seen`).
125
+ * This is what `stream({ presence: true })` interleaves with events.
126
+ */
127
+ type PresencePatch<P extends PresenceDefs = PresenceDefs> = {
128
+ participant: string;
129
+ values: { [F in keyof P & string]?: StandardSchemaV1.InferOutput<P[F]> | null; };
130
+ seen: number;
131
+ at: Date;
132
+ };
133
+ /**
134
+ * The replicated ephemeral map: participant → field → latest value,
135
+ * last-writer-wins per field. Never stored in the log; how long a
136
+ * value stays painted is view logic, decided at render time against
137
+ * `at`.
138
+ */
139
+ type PresenceMap<P extends PresenceDefs = PresenceDefs> = {
140
+ [participant: string]: { [F in keyof P & string]?: {
141
+ value: StandardSchemaV1.InferOutput<P[F]>;
142
+ seen: number;
143
+ at: Date;
144
+ }; };
145
+ };
146
+ /**
147
+ * The one item a presence stream yields before live patches: the
148
+ * current pruned map, per-field stamps exact. Discriminate
149
+ * structurally (`'snapshot' in item`).
150
+ */
151
+ type PresenceSnapshot<P extends PresenceDefs = PresenceDefs> = {
152
+ snapshot: PresenceMap<P>;
153
+ };
154
+ /**
155
+ * Intersect into a surface type to add `Members` only when the
156
+ * contract declares presence fields — on presence-less contracts the
157
+ * members do not exist, a type error rather than a runtime throw.
158
+ */
159
+ type WithPresence<P extends PresenceDefs, Members> = keyof P extends never ? unknown : Members;
114
160
  type ReducerOptions<Seed, S> = {
115
161
  /**
116
162
  * Identity and snapshot cache key. Rename it when the fold's logic
@@ -130,11 +176,13 @@ type ReducerOptions<Seed, S> = {
130
176
  */
131
177
  stateSchema?: StandardSchemaV1<Seed, S>;
132
178
  };
133
- type Contract<D extends EventDefs = EventDefs> = {
179
+ type Contract<D extends EventDefs = EventDefs, P extends PresenceDefs = Record<never, never>> = {
134
180
  /** Identity — prefixes storage keys and queue messages. */
135
181
  readonly name: string;
136
182
  /** The vocabulary. */
137
183
  readonly events: Readonly<D>;
184
+ /** The presence vocabulary — empty unless declared. */
185
+ readonly presence: Readonly<P>;
138
186
  /** Preserve literal types for a handler-returned event batch. */
139
187
  readonly batch: EventBatchFactory<D>;
140
188
  /**
@@ -147,18 +195,24 @@ type Contract<D extends EventDefs = EventDefs> = {
147
195
  * context-sensitive argument against generics inferred in the same
148
196
  * call.)
149
197
  */
150
- reducer<Seed, S = Seed>(options: ReducerOptions<Seed, S>): ReducerBuilder<D, S>;
198
+ reducer<Seed, S = Seed>(options: ReducerOptions<Seed, S>): ReducerBuilder<D, S, P>;
151
199
  };
152
200
  /**
153
201
  * Define a contract: a name plus the events it understands. Validators
154
202
  * must be synchronous — async ones are rejected here, at definition
155
203
  * time. The result is a plain, importable, isomorphic value.
156
204
  */
157
- declare function contract<D extends EventDefs>(options: {
205
+ declare function contract<D extends EventDefs, P extends PresenceDefs = Record<never, never>>(options: {
158
206
  /** Identity — prefixes storage keys and queue messages. */
159
207
  name: string;
160
208
  /** The vocabulary: event name → payload schema (Standard Schema). */
161
209
  events: D;
162
- }): Contract<D>;
210
+ /**
211
+ * Optional second vocabulary: presence field → value schema.
212
+ * Per-participant ephemeral state, replicated last-writer-wins and
213
+ * never stored in the log — reducers cannot see it.
214
+ */
215
+ presence?: P;
216
+ }): Contract<D, P>;
163
217
  //#endregion
164
- export { EventDefs as a, Reducer as c, EventBatchFactory as i, ReducerBuilder as l, Contract as n, ReducerOptions as o, ContractEvent as r, contract as s, AppendInput as t, StandardSchemaV1 as u };
218
+ export { EventDefs as a, PresencePatch as c, WithPresence as d, contract as f, StandardSchemaV1 as h, EventBatchFactory as i, PresenceSnapshot as l, ReducerBuilder as m, Contract as n, PresenceDefs as o, Reducer as p, ContractEvent as r, PresenceMap as s, AppendInput as t, ReducerOptions as u };