experimental-a2 0.0.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 (68) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/ai-server.browser.d.ts +1 -0
  3. package/dist/ai-server.browser.js +4 -0
  4. package/dist/ai-server.d.ts +65 -0
  5. package/dist/ai-server.js +494 -0
  6. package/dist/ai.d.ts +282 -0
  7. package/dist/ai.js +922 -0
  8. package/dist/cache-indexeddb.d.ts +1 -0
  9. package/dist/cache-indexeddb.js +0 -0
  10. package/dist/client.d.ts +90 -0
  11. package/dist/client.js +410 -0
  12. package/dist/contract-B0kAXoaL.js +60 -0
  13. package/dist/contract-DL8btVd9.d.ts +161 -0
  14. package/dist/devtools-server.browser.d.ts +1 -0
  15. package/dist/devtools-server.browser.js +4 -0
  16. package/dist/devtools-server.d.ts +22 -0
  17. package/dist/devtools-server.js +1087 -0
  18. package/dist/errors-BJRMd-h6.js +23 -0
  19. package/dist/errors-xL_JTXsY.d.ts +20 -0
  20. package/dist/http.d.ts +44 -0
  21. package/dist/http.js +119 -0
  22. package/dist/index.d.ts +5 -0
  23. package/dist/index.js +3 -0
  24. package/dist/inspection-E7qbD0Xj.js +10 -0
  25. package/dist/internal-Dm8Ejnud.js +36 -0
  26. package/dist/log-Dg1I8NRr.d.ts +245 -0
  27. package/dist/log-memory.d.ts +11 -0
  28. package/dist/log-memory.js +345 -0
  29. package/dist/log-polling-RO7kclzR.js +83 -0
  30. package/dist/log-postgres.d.ts +40 -0
  31. package/dist/log-postgres.js +628 -0
  32. package/dist/log-redis.d.ts +31 -0
  33. package/dist/log-redis.js +711 -0
  34. package/dist/log-sqlite.d.ts +17 -0
  35. package/dist/log-sqlite.js +450 -0
  36. package/dist/log-yJbXUf72.js +5 -0
  37. package/dist/otel.d.ts +12 -0
  38. package/dist/otel.js +41 -0
  39. package/dist/react.d.ts +54 -0
  40. package/dist/react.js +85 -0
  41. package/dist/recovery-vercel.d.ts +60 -0
  42. package/dist/recovery-vercel.js +120 -0
  43. package/dist/retryable-lazy-DZWmHpii.js +19 -0
  44. package/dist/server-DYsnKTTy.js +780 -0
  45. package/dist/server.browser.d.ts +1 -0
  46. package/dist/server.browser.js +11 -0
  47. package/dist/server.d.ts +136 -0
  48. package/dist/server.js +2 -0
  49. package/dist/telemetry-C78al20p.d.ts +32 -0
  50. package/dist/validate-XKT4FSNn.js +28 -0
  51. package/dist/wire-2QpU1EtJ.js +62 -0
  52. package/docs/01-quickstart.mdx +214 -0
  53. package/docs/concepts/01-contracts.mdx +138 -0
  54. package/docs/concepts/02-handlers.mdx +146 -0
  55. package/docs/concepts/03-durability.mdx +230 -0
  56. package/docs/concepts/04-state.mdx +133 -0
  57. package/docs/guides/01-timers.mdx +85 -0
  58. package/docs/guides/02-cancellation.mdx +107 -0
  59. package/docs/guides/03-react.mdx +234 -0
  60. package/docs/guides/04-local-first.mdx +88 -0
  61. package/docs/guides/05-production.mdx +179 -0
  62. package/docs/guides/06-ai-agents.mdx +659 -0
  63. package/docs/guides/07-devtools.mdx +101 -0
  64. package/docs/guides/08-application-data.mdx +114 -0
  65. package/docs/index.mdx +282 -0
  66. package/docs/reference/01-api.mdx +637 -0
  67. package/docs/reference/02-errors.mdx +77 -0
  68. package/package.json +111 -0
@@ -0,0 +1 @@
1
+ export {}
File without changes
@@ -0,0 +1,90 @@
1
+ import { i as EventDefs, r as ContractEvent, s as Reducer, t as AppendInput } from "./contract-DL8btVd9.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 };
package/dist/client.js ADDED
@@ -0,0 +1,410 @@
1
+ import { n as validateSync } from "./validate-XKT4FSNn.js";
2
+ import { t as A2Error } from "./errors-BJRMd-h6.js";
3
+ import { r as STREAM_TIMINGS } from "./internal-Dm8Ejnud.js";
4
+ import { i as eventFromWire, o as isWireEvent, t as errorFromWire } from "./wire-2QpU1EtJ.js";
5
+ //#region src/client.ts
6
+ /**
7
+ * 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. `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
410
+ export { createClient };
@@ -0,0 +1,60 @@
1
+ import { n as validateSync, t as assertSyncSchema } from "./validate-XKT4FSNn.js";
2
+ //#region src/reducer.ts
3
+ /** Internal — reducers are created through `contract.reducer(...).fold(...)`. */
4
+ function makeReducerBuilder(events, options) {
5
+ const { name } = options;
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
+ let initialState = options.initialState;
8
+ if (options.stateSchema) {
9
+ const result = validateSync(options.stateSchema, options.initialState, "the stateSchema");
10
+ if (result.issues) throw new TypeError(`reducer initialState does not match its stateSchema: ${result.issues.map((issue) => issue.message).join("; ")}`);
11
+ initialState = result.value;
12
+ }
13
+ return { fold(fold) {
14
+ if (typeof fold !== "function") throw new TypeError("reducer fold must be a function");
15
+ return {
16
+ name,
17
+ events,
18
+ fold,
19
+ initialState,
20
+ stateSchema: options.stateSchema
21
+ };
22
+ } };
23
+ }
24
+ //#endregion
25
+ //#region src/contract.ts
26
+ /**
27
+ * The contract — a2's primary noun. A named event vocabulary, shared by
28
+ * everything: the server implements it (a2/server's `createServer`),
29
+ * reducers derive from it (`contract.reducer`), the browser types its
30
+ * pushes off it. Isomorphic by construction: this module imports
31
+ * nothing but schema plumbing.
32
+ *
33
+ * The contract says what can be *said*; the server says how it's
34
+ * *reacted to*; a reducer says how it's *seen*.
35
+ */
36
+ /**
37
+ * Define a contract: a name plus the events it understands. Validators
38
+ * must be synchronous — async ones are rejected here, at definition
39
+ * time. The result is a plain, importable, isomorphic value.
40
+ */
41
+ function contract(options) {
42
+ const { name } = options;
43
+ if (typeof name !== "string" || name.length === 0) throw new TypeError("contract name must be a non-empty string");
44
+ if (name.includes("")) throw new TypeError("contract name contains a reserved control character");
45
+ const defs = options.events;
46
+ for (const [type, schema] of Object.entries(defs)) {
47
+ if (typeof schema?.["~standard"]?.validate !== "function") throw new TypeError(`contract '${name}': the value for '${type}' is not a Standard Schema (expected an object with '~standard')`);
48
+ assertSyncSchema(schema, `event '${type}'`);
49
+ }
50
+ const events = Object.freeze({ ...defs });
51
+ return {
52
+ name,
53
+ events,
54
+ reducer(reducerOptions) {
55
+ return makeReducerBuilder(events, reducerOptions);
56
+ }
57
+ };
58
+ }
59
+ //#endregion
60
+ export { contract as t };