experimental-a2 0.3.0 → 0.5.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 (205) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/dist/ai-CFNeCrRl.d.ts +334 -0
  3. package/dist/ai-CFNeCrRl.d.ts.map +1 -0
  4. package/dist/ai-server.browser.js +2 -0
  5. package/dist/ai-server.browser.js.map +1 -0
  6. package/dist/ai-server.d.ts +49 -9
  7. package/dist/ai-server.d.ts.map +1 -0
  8. package/dist/ai-server.js +123 -49
  9. package/dist/ai-server.js.map +1 -0
  10. package/dist/ai.d.ts +2 -303
  11. package/dist/ai.js +233 -86
  12. package/dist/ai.js.map +1 -0
  13. package/dist/cli-B3VuxoDe.js +599 -0
  14. package/dist/cli-B3VuxoDe.js.map +1 -0
  15. package/dist/cli-bin.d.ts +1 -0
  16. package/dist/cli-bin.js +7 -0
  17. package/dist/cli-bin.js.map +1 -0
  18. package/dist/cli.d.ts +20 -0
  19. package/dist/cli.d.ts.map +1 -0
  20. package/dist/cli.js +2 -0
  21. package/dist/client-BKlyLiOU.js +1008 -0
  22. package/dist/client-BKlyLiOU.js.map +1 -0
  23. package/dist/client-D7mvIXrF.d.ts +191 -0
  24. package/dist/client-D7mvIXrF.d.ts.map +1 -0
  25. package/dist/client.d.ts +2 -90
  26. package/dist/client.js +1 -409
  27. package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +12 -2
  28. package/dist/contract-48bUMgcL.js.map +1 -0
  29. package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +63 -8
  30. package/dist/contract-jIfaR085.d.ts.map +1 -0
  31. package/dist/devtools-J_jZ2vQf.d.ts +152 -0
  32. package/dist/devtools-J_jZ2vQf.d.ts.map +1 -0
  33. package/dist/devtools-kJJaORn-.js +340 -0
  34. package/dist/devtools-kJJaORn-.js.map +1 -0
  35. package/dist/devtools-server.browser.js +3 -1
  36. package/dist/devtools-server.browser.js.map +1 -0
  37. package/dist/devtools-server.d.ts +4 -3
  38. package/dist/devtools-server.d.ts.map +1 -0
  39. package/dist/devtools-server.js +226 -43
  40. package/dist/devtools-server.js.map +1 -0
  41. package/dist/devtools.d.ts +2 -0
  42. package/dist/devtools.js +2 -0
  43. package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +6 -4
  44. package/dist/errors-BQuJpe82.js.map +1 -0
  45. package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +3 -2
  46. package/dist/errors-W6nwJ-fm.d.ts.map +1 -0
  47. package/dist/http.d.ts +135 -28
  48. package/dist/http.d.ts.map +1 -0
  49. package/dist/http.js +647 -61
  50. package/dist/http.js.map +1 -0
  51. package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +4 -2
  52. package/dist/idempotent-replay-DuqEkYA7.js.map +1 -0
  53. package/dist/index.d.ts +5 -5
  54. package/dist/index.js +2 -2
  55. package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +4 -1
  56. package/dist/inspection-DaxB5jM2.js.map +1 -0
  57. package/dist/internal-DstsI6Re.js +211 -0
  58. package/dist/internal-DstsI6Re.js.map +1 -0
  59. package/dist/otel.d.ts +3 -2
  60. package/dist/otel.d.ts.map +1 -0
  61. package/dist/otel.js +2 -0
  62. package/dist/otel.js.map +1 -0
  63. package/dist/platform-B4TnJtWu.js +34 -0
  64. package/dist/platform-B4TnJtWu.js.map +1 -0
  65. package/dist/react.d.ts +47 -15
  66. package/dist/react.d.ts.map +1 -0
  67. package/dist/react.js +30 -15
  68. package/dist/react.js.map +1 -0
  69. package/dist/retryable-lazy-DZWmHpii.js +2 -0
  70. package/dist/retryable-lazy-DZWmHpii.js.map +1 -0
  71. package/dist/scheduler-qstash.d.ts +79 -0
  72. package/dist/scheduler-qstash.d.ts.map +1 -0
  73. package/dist/scheduler-qstash.js +501 -0
  74. package/dist/scheduler-qstash.js.map +1 -0
  75. package/dist/scheduler-task-BpzhPnRS.js +56 -0
  76. package/dist/scheduler-task-BpzhPnRS.js.map +1 -0
  77. package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +18 -24
  78. package/dist/scheduler-vercel.d.ts.map +1 -0
  79. package/dist/scheduler-vercel.js +228 -0
  80. package/dist/scheduler-vercel.js.map +1 -0
  81. package/dist/server-C72KOw51.js +1457 -0
  82. package/dist/server-C72KOw51.js.map +1 -0
  83. package/dist/server-DUF9pjsx.d.ts +261 -0
  84. package/dist/server-DUF9pjsx.d.ts.map +1 -0
  85. package/dist/server.browser.js +2 -0
  86. package/dist/server.browser.js.map +1 -0
  87. package/dist/server.d.ts +4 -155
  88. package/dist/server.js +2 -2
  89. package/dist/{log-ldf5g8Cx.d.ts → store-DysUkTH3.d.ts} +120 -35
  90. package/dist/store-DysUkTH3.d.ts.map +1 -0
  91. package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +3 -1
  92. package/dist/store-N8PXxDAS.js.map +1 -0
  93. package/dist/store-codec-DTG0Ftek.js +10 -0
  94. package/dist/store-codec-DTG0Ftek.js.map +1 -0
  95. package/dist/store-memory.d.ts +12 -0
  96. package/dist/store-memory.d.ts.map +1 -0
  97. package/dist/{log-memory.js → store-memory.js} +145 -34
  98. package/dist/store-memory.js.map +1 -0
  99. package/dist/{log-polling-DZ1MiKLg.js → store-polling-dSeLxzfb.js} +7 -5
  100. package/dist/store-polling-dSeLxzfb.js.map +1 -0
  101. package/dist/{log-postgres.d.ts → store-postgres.d.ts} +7 -6
  102. package/dist/store-postgres.d.ts.map +1 -0
  103. package/dist/{log-postgres.js → store-postgres.js} +214 -24
  104. package/dist/store-postgres.js.map +1 -0
  105. package/dist/{log-redis-core-CyJ5L8yR.js → store-redis-core-BFLwz0Wj.js} +271 -66
  106. package/dist/store-redis-core-BFLwz0Wj.js.map +1 -0
  107. package/dist/{log-redis-http.d.ts → store-redis-http.d.ts} +8 -7
  108. package/dist/store-redis-http.d.ts.map +1 -0
  109. package/dist/{log-redis-http.js → store-redis-http.js} +22 -12
  110. package/dist/store-redis-http.js.map +1 -0
  111. package/dist/{log-redis.d.ts → store-redis.d.ts} +7 -6
  112. package/dist/store-redis.d.ts.map +1 -0
  113. package/dist/{log-redis.js → store-redis.js} +101 -39
  114. package/dist/store-redis.js.map +1 -0
  115. package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +7 -6
  116. package/dist/store-sqlite.d.ts.map +1 -0
  117. package/dist/{log-sqlite.js → store-sqlite.js} +118 -22
  118. package/dist/store-sqlite.js.map +1 -0
  119. package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +3 -2
  120. package/dist/telemetry-BjYHTfh2.d.ts.map +1 -0
  121. package/dist/testing.browser.d.ts +1 -0
  122. package/dist/testing.browser.js +6 -0
  123. package/dist/testing.browser.js.map +1 -0
  124. package/dist/testing.d.ts +32 -0
  125. package/dist/testing.d.ts.map +1 -0
  126. package/dist/testing.js +103 -0
  127. package/dist/testing.js.map +1 -0
  128. package/dist/validate-XKT4FSNn.js +2 -0
  129. package/dist/validate-XKT4FSNn.js.map +1 -0
  130. package/dist/wire-BFQmSJ-9.js +284 -0
  131. package/dist/wire-BFQmSJ-9.js.map +1 -0
  132. package/docs/01-quickstart.mdx +3 -3
  133. package/docs/concepts/01-contracts.mdx +18 -13
  134. package/docs/concepts/02-handlers.mdx +7 -7
  135. package/docs/concepts/03-durability.mdx +21 -21
  136. package/docs/concepts/04-state.mdx +18 -21
  137. package/docs/guides/01-timers.mdx +154 -49
  138. package/docs/guides/02-cancellation.mdx +30 -4
  139. package/docs/guides/03-react.mdx +76 -57
  140. package/docs/guides/04-local-first.mdx +1 -1
  141. package/docs/guides/05-production.mdx +310 -61
  142. package/docs/guides/06-ai-agents.mdx +252 -74
  143. package/docs/guides/07-devtools.mdx +137 -12
  144. package/docs/guides/08-application-data.mdx +1 -1
  145. package/docs/guides/09-presence.mdx +263 -0
  146. package/docs/guides/10-transports.mdx +140 -0
  147. package/docs/index.mdx +18 -14
  148. package/docs/reference/01-api.mdx +839 -110
  149. package/docs/reference/02-errors.mdx +13 -6
  150. package/package.json +24 -7
  151. package/src/ai-coordinator.ts +358 -0
  152. package/src/ai-projector.ts +524 -0
  153. package/src/ai-sdk-step.ts +261 -0
  154. package/src/ai-server.browser.ts +5 -0
  155. package/src/ai-server.ts +1719 -0
  156. package/src/ai.ts +2155 -0
  157. package/src/cache-indexeddb.ts +10 -0
  158. package/src/cli-bin.ts +5 -0
  159. package/src/cli.ts +1046 -0
  160. package/src/client.ts +1826 -0
  161. package/src/contract.ts +206 -0
  162. package/src/deterministic-id.ts +72 -0
  163. package/src/devtools-app.ts +989 -0
  164. package/src/devtools-server.browser.ts +5 -0
  165. package/src/devtools-server.ts +604 -0
  166. package/src/devtools.ts +716 -0
  167. package/src/errors.ts +50 -0
  168. package/src/http.ts +394 -0
  169. package/src/idempotent-replay.ts +53 -0
  170. package/src/index.ts +37 -0
  171. package/src/inspection.ts +39 -0
  172. package/src/internal.ts +426 -0
  173. package/src/otel.ts +59 -0
  174. package/src/platform.ts +60 -0
  175. package/src/push-envelope.ts +137 -0
  176. package/src/react.ts +284 -0
  177. package/src/reducer.ts +108 -0
  178. package/src/retryable-lazy.ts +27 -0
  179. package/src/scheduler-qstash.ts +915 -0
  180. package/src/scheduler-task.ts +106 -0
  181. package/src/scheduler-vercel.ts +437 -0
  182. package/src/server.browser.ts +12 -0
  183. package/src/server.ts +2700 -0
  184. package/src/session-socket.ts +548 -0
  185. package/src/sse.ts +141 -0
  186. package/src/standard-schema.ts +77 -0
  187. package/src/store-codec.ts +10 -0
  188. package/src/store-memory.ts +788 -0
  189. package/src/store-polling.ts +102 -0
  190. package/src/store-postgres.ts +1212 -0
  191. package/src/store-redis-core.ts +1494 -0
  192. package/src/store-redis-http.ts +116 -0
  193. package/src/store-redis.ts +458 -0
  194. package/src/store-sqlite.ts +1108 -0
  195. package/src/store.ts +385 -0
  196. package/src/telemetry.ts +47 -0
  197. package/src/testing.browser.ts +5 -0
  198. package/src/testing.ts +185 -0
  199. package/src/validate.ts +39 -0
  200. package/src/wire.ts +454 -0
  201. package/dist/internal-gCd5qMry.js +0 -44
  202. package/dist/log-memory.d.ts +0 -11
  203. package/dist/recovery-vercel.js +0 -119
  204. package/dist/server-BcLa4RFL.js +0 -877
  205. package/dist/wire-BVsgR8o9.js +0 -62
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-BKlyLiOU.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,14 +49,23 @@ 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
  }
60
68
  //#endregion
61
69
  export { contract as t };
70
+
71
+ //# sourceMappingURL=contract-48bUMgcL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract-48bUMgcL.js","names":[],"sources":["../src/reducer.ts","../src/contract.ts"],"sourcesContent":["import type { ContractEvent, EventDefs, PresenceDefs } from './contract.ts'\nimport type { StandardSchemaV1 } from './standard-schema.ts'\nimport { validateSync } from './validate.ts'\n\n/**\n * A pure fold over a session's events — `(state, event) => state` —\n * derived from a contract via `contract.reducer(options).fold(fn)`.\n *\n * The name is the reducer's identity. Cached folds (server snapshots,\n * the browser cache) are keyed by it, which makes it the invalidation\n * knob too: changed the fold's logic? Change the name. Snapshots under\n * the old name are ignored and the next read refolds from raw events.\n * See docs/concepts/04-state.mdx.\n */\nexport type Reducer<\n D extends EventDefs = EventDefs,\n S = unknown,\n P extends PresenceDefs = Record<never, never>,\n> = {\n /** Identity and cache key. Rename when the fold's logic changes. */\n readonly name: string\n /** The contract's vocabulary — types every event in the fold. */\n readonly events: Readonly<D>\n /**\n * The contract's presence vocabulary — carried because the reducer\n * is the client's typed handle on the contract. The fold never sees\n * presence.\n */\n readonly presence: Readonly<P>\n readonly fold: (state: S, event: ContractEvent<D>) => S\n readonly initialState: S\n /**\n * When present: `initialState` was validated against it at\n * definition, and any cached fold a2 rehydrates (server snapshots,\n * the browser cache) is checked against it — failing snapshots are\n * discarded and refolded, catching shape drift a stale name can't.\n */\n readonly stateSchema: StandardSchemaV1<unknown, S> | undefined\n}\n\n/** The second step of `contract.reducer` — see the method's doc. */\nexport type ReducerBuilder<\n D extends EventDefs,\n S,\n P extends PresenceDefs = Record<never, never>,\n> = {\n fold(fold: (state: S, event: ContractEvent<D>) => S): Reducer<D, S, P>\n}\n\n/** Internal — reducers are created through `contract.reducer(...).fold(...)`. */\nexport function makeReducerBuilder<\n D extends EventDefs,\n P extends PresenceDefs,\n Seed,\n S,\n>(\n events: Readonly<D>,\n presence: Readonly<P>,\n options: {\n name: string\n initialState: Seed\n stateSchema?: StandardSchemaV1<Seed, S>\n },\n): ReducerBuilder<D, S, P> {\n const { name } = options\n if (typeof name !== 'string' || name.length === 0) {\n throw new TypeError(\n 'reducer name must be a non-empty string — it identifies the reducer and keys cached snapshots',\n )\n }\n\n // Without a schema, S = Seed (the type parameter default), so the\n // seed is the state. With one, the seed is the schema's input and the\n // validated output (defaults applied) becomes the fold's actual seed.\n let initialState = options.initialState as unknown as S\n if (options.stateSchema) {\n const result = validateSync(\n options.stateSchema,\n options.initialState,\n 'the stateSchema',\n )\n if (result.issues) {\n throw new TypeError(\n `reducer initialState does not match its stateSchema: ${result.issues\n .map((issue) => issue.message)\n .join('; ')}`,\n )\n }\n initialState = result.value as S\n }\n\n return {\n fold(fold) {\n if (typeof fold !== 'function') {\n throw new TypeError('reducer fold must be a function')\n }\n return {\n name,\n events,\n presence,\n fold,\n initialState,\n stateSchema: options.stateSchema as\n StandardSchemaV1<unknown, S> | undefined,\n }\n },\n }\n}\n","/**\n * The contract — a2's primary noun. A named event vocabulary, shared by\n * everything: the server implements it (experimental-a2/server's `createServer`),\n * reducers derive from it (`contract.reducer`), the browser types its\n * pushes off it. Isomorphic by construction: this module imports\n * nothing but schema plumbing.\n *\n * The contract says what can be *said*; the server says how it's\n * *reacted to*; a reducer says how it's *seen*.\n */\n\nimport { makeReducerBuilder, type ReducerBuilder } from './reducer.ts'\nimport type { StandardSchemaV1 } from './standard-schema.ts'\nimport { assertSyncSchema } from './validate.ts'\n\n/** The event vocabulary shape: event name → payload schema. */\nexport type EventDefs = Record<string, StandardSchemaV1>\n\n/** The presence vocabulary shape: field name → value schema. */\nexport type PresenceDefs = Record<string, StandardSchemaV1>\n\n/** One event of a contract, narrowed to type `K`. */\nexport type ContractEvent<\n D extends EventDefs,\n K extends keyof D & string = keyof D & string,\n> = {\n [T in keyof D & string]: {\n id: string\n type: T\n payload: StandardSchemaV1.InferOutput<D[T]>\n index: number\n sessionId: string\n createdAt: Date\n }\n}[K]\n\n/** Input to `append`/`push`, typed against the contract's schemas. */\nexport type AppendInput<D extends EventDefs> = {\n [T in keyof D & string]: {\n type: T\n payload: StandardSchemaV1.InferInput<D[T]>\n id?: string\n }\n}[keyof D & string]\n\nexport type EventBatchFactory<D extends EventDefs> = <\n const E extends readonly AppendInput<D>[],\n>(\n ...events: E\n) => E\n\n/**\n * One presence update: a participant's changed fields (`null` marks a\n * cleared one), stamped with the sender's stream frontier (`seen`).\n * This is what `stream({ presence: true })` interleaves with events.\n */\nexport type PresencePatch<P extends PresenceDefs = PresenceDefs> = {\n participant: string\n values: {\n [F in keyof P & string]?: StandardSchemaV1.InferOutput<P[F]> | null\n }\n seen: number\n at: Date\n}\n\n/**\n * The replicated ephemeral map: participant → field → latest value,\n * last-writer-wins per field. Never stored in the log; how long a\n * value stays painted is view logic, decided at render time against\n * `at`.\n */\nexport type PresenceMap<P extends PresenceDefs = PresenceDefs> = {\n [participant: string]: {\n [F in keyof P & string]?: {\n value: StandardSchemaV1.InferOutput<P[F]>\n seen: number\n at: Date\n }\n }\n}\n\n/**\n * The one item a presence stream yields before live patches: the\n * current pruned map, per-field stamps exact. Discriminate\n * structurally (`'snapshot' in item`).\n */\nexport type PresenceSnapshot<P extends PresenceDefs = PresenceDefs> = {\n snapshot: PresenceMap<P>\n}\n\n/**\n * Intersect into a surface type to add `Members` only when the\n * contract declares presence fields — on presence-less contracts the\n * members do not exist, a type error rather than a runtime throw.\n */\nexport type WithPresence<\n P extends PresenceDefs,\n Members,\n> = keyof P extends never ? unknown : Members\n\nexport type ReducerOptions<Seed, S> = {\n /**\n * Identity and snapshot cache key. Rename it when the fold's logic\n * changes — snapshots under the old name are ignored and refolded.\n */\n name: string\n /**\n * The seed the fold starts from. With a `stateSchema`, it's typed as\n * the schema's *input*: validated at definition, and the validated\n * output (defaults applied) is what the fold actually starts from.\n */\n initialState: Seed\n /**\n * Optional: declares the state's shape (the fold's state type becomes\n * the schema's output) and guards cached folds — server snapshots and\n * the browser cache reject mismatching shapes and refold.\n */\n stateSchema?: StandardSchemaV1<Seed, S>\n}\n\nexport type Contract<\n D extends EventDefs = EventDefs,\n P extends PresenceDefs = Record<never, never>,\n> = {\n /** Identity — prefixes storage keys and queue messages. */\n readonly name: string\n /** The vocabulary. */\n readonly events: Readonly<D>\n /** The presence vocabulary — empty unless declared. */\n readonly presence: Readonly<P>\n /** Preserve literal types for a handler-returned event batch. */\n readonly batch: EventBatchFactory<D>\n /**\n * Derive a view from this contract. The reducer's `name` is its\n * identity and snapshot cache key: rename it when the fold's logic\n * changes. Two steps by design: this call fixes the state and event\n * types, so `.fold()` receives fully concrete types — literal unions\n * in your state survive, with zero annotations. (A single call can't\n * do this: TypeScript never contextually types the returns of a\n * context-sensitive argument against generics inferred in the same\n * call.)\n */\n reducer<Seed, S = Seed>(\n options: ReducerOptions<Seed, S>,\n ): ReducerBuilder<D, S, P>\n}\n\n/**\n * Define a contract: a name plus the events it understands. Validators\n * must be synchronous — async ones are rejected here, at definition\n * time. The result is a plain, importable, isomorphic value.\n */\nexport function contract<\n D extends EventDefs,\n P extends PresenceDefs = Record<never, never>,\n>(options: {\n /** Identity — prefixes storage keys and queue messages. */\n name: string\n /** The vocabulary: event name → payload schema (Standard Schema). */\n events: D\n /**\n * Optional second vocabulary: presence field → value schema.\n * Per-participant ephemeral state, replicated last-writer-wins and\n * never stored in the log — reducers cannot see it.\n */\n presence?: P\n}): Contract<D, P> {\n const { name } = options\n if (typeof name !== 'string' || name.length === 0) {\n throw new TypeError('contract name must be a non-empty string')\n }\n if (name.includes('\\u001f')) {\n throw new TypeError('contract name contains a reserved control character')\n }\n const defs = options.events\n for (const [type, schema] of Object.entries(defs)) {\n if (typeof schema?.['~standard']?.validate !== 'function') {\n throw new TypeError(\n `contract '${name}': the value for '${type}' is not a Standard Schema (expected an object with '~standard')`,\n )\n }\n assertSyncSchema(schema, `event '${type}'`)\n }\n const events = Object.freeze({ ...defs })\n\n const presenceDefs = options.presence ?? ({} as P)\n for (const [field, schema] of Object.entries(presenceDefs)) {\n if (typeof schema?.['~standard']?.validate !== 'function') {\n throw new TypeError(\n `contract '${name}': the value for presence field '${field}' is not a Standard Schema (expected an object with '~standard')`,\n )\n }\n assertSyncSchema(schema, `presence field '${field}'`)\n }\n const presence = Object.freeze({ ...presenceDefs })\n\n return {\n name,\n events,\n presence,\n batch: (...items) => items,\n reducer(reducerOptions) {\n return makeReducerBuilder(events, presence, reducerOptions)\n },\n }\n}\n"],"mappings":";;;AAkDA,SAAgB,mBAMd,QACA,UACA,SAKyB;CACzB,MAAM,EAAE,SAAS;CACjB,IAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAC9C,MAAM,IAAI,UACR,+FACF;CAMF,IAAI,eAAe,QAAQ;CAC3B,IAAI,QAAQ,aAAa;EACvB,MAAM,SAAS,aACb,QAAQ,aACR,QAAQ,cACR,iBACF;EACA,IAAI,OAAO,QACT,MAAM,IAAI,UACR,wDAAwD,OAAO,OAC5D,KAAK,UAAU,MAAM,OAAO,CAAC,CAC7B,KAAK,IAAI,GACd;EAEF,eAAe,OAAO;CACxB;CAEA,OAAO,EACL,KAAK,MAAM;EACT,IAAI,OAAO,SAAS,YAClB,MAAM,IAAI,UAAU,iCAAiC;EAEvD,OAAO;GACL;GACA;GACA;GACA;GACA;GACA,aAAa,QAAQ;EAEvB;CACF,EACF;AACF;;;;;;;;;;;;;;;;;;AC6CA,SAAgB,SAGd,SAWiB;CACjB,MAAM,EAAE,SAAS;CACjB,IAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAC9C,MAAM,IAAI,UAAU,0CAA0C;CAEhE,IAAI,KAAK,SAAS,GAAQ,GACxB,MAAM,IAAI,UAAU,qDAAqD;CAE3E,MAAM,OAAO,QAAQ;CACrB,KAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,IAAI,GAAG;EACjD,IAAI,OAAO,SAAS,YAAY,EAAE,aAAa,YAC7C,MAAM,IAAI,UACR,aAAa,KAAK,oBAAoB,KAAK,iEAC7C;EAEF,iBAAiB,QAAQ,UAAU,KAAK,EAAE;CAC5C;CACA,MAAM,SAAS,OAAO,OAAO,EAAE,GAAG,KAAK,CAAC;CAExC,MAAM,eAAe,QAAQ,YAAa,CAAC;CAC3C,KAAK,MAAM,CAAC,OAAO,WAAW,OAAO,QAAQ,YAAY,GAAG;EAC1D,IAAI,OAAO,SAAS,YAAY,EAAE,aAAa,YAC7C,MAAM,IAAI,UACR,aAAa,KAAK,mCAAmC,MAAM,iEAC7D;EAEF,iBAAiB,QAAQ,mBAAmB,MAAM,EAAE;CACtD;CACA,MAAM,WAAW,OAAO,OAAO,EAAE,GAAG,aAAa,CAAC;CAElD,OAAO;EACL;EACA;EACA;EACA,QAAQ,GAAG,UAAU;EACrB,QAAQ,gBAAgB;GACtB,OAAO,mBAAmB,QAAQ,UAAU,cAAc;EAC5D;CACF;AACF"}
@@ -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,25 @@ 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 };
219
+ //# sourceMappingURL=contract-jIfaR085.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract-jIfaR085.d.ts","names":[],"sources":["../src/standard-schema.ts","../src/reducer.ts","../src/contract.ts"],"mappings":";;;;;;;;UAQiB,iBAAiB,iBAAiB,SAAS;;WAEjD,aAAa,iBAAiB,MAAM,OAAO;;kBAG7B;;mBAEN,MAAM,iBAAiB,SAAS;;aAEtC;;aAEA;;aAEA,WACP,mBACG,OAAO,UAAU,QAAQ,OAAO;;aAE5B,QAAQ,MAAM,OAAO;;;cAIpB,OAAO,UAAU,cAAc,UAAU;;mBAGpC,cAAc;;aAEpB,OAAO;;aAEP;;;mBAIM;;aAEN,QAAQ,cAAc;;;mBAIhB;;aAEN;;aAEA,OAAO,cAAc,cAAc;;;mBAI7B;;aAEN,KAAK;;;mBAIC,MAAM,iBAAiB,SAAS;;aAEtC,OAAO;;aAEP,QAAQ;;;cAIP,WAAW,eAAe,oBAAoB,YACxD;;cAIU,YAAY,eAAe,oBAAoB,YACzD;;;;;;;;;;;;;;KC5DQ,QACV,UAAU,YAAY,WACtB,aACA,UAAU,eAAe;;WAGhB;;WAEA,QAAQ,SAAS;;;;;;WAMjB,UAAU,SAAS;WACnB,OAAO,OAAO,GAAG,OAAO,cAAc,OAAO;WAC7C,cAAc;;;;;;;WAOd,aAAa,0BAA0B;;;KAItC,eACV,UAAU,WACV,GACA,UAAU,eAAe;EAEzB,KAAK,OAAO,OAAO,GAAG,OAAO,cAAc,OAAO,IAAI,QAAQ,GAAG,GAAG;;;;;KC9B1D,YAAY,eAAe;;KAG3B,eAAe,eAAe;;KAG9B,cACV,UAAU,WACV,gBAAgB,mBAAmB,iBAElC,WAAW;EACV;EACA,MAAM;EACN,SAAS,iBAAiB,YAAY,EAAE;EACxC;EACA;EACA,WAAW;KAEb;;KAGU,YAAY,UAAU,gBAC/B,WAAW;EACV,MAAM;EACN,SAAS,iBAAiB,WAAW,EAAE;EACvC;WAEI;KAEI,kBAAkB,UAAU,oBAChC,mBAAmB,YAAY,SAElC,QAAQ,MACR;;;;;;KAOO,cAAc,UAAU,eAAe;EACjD;EACA,WACG,WAAW,cAAc,iBAAiB,YAAY,EAAE;EAE3D;EACA,IAAI;;;;;;;;KASM,YAAY,UAAU,eAAe;GAE5C,yBAAA,WAAW;IACV,OAAO,iBAAiB,YAAY,EAAE;IACtC;IACA,IAAI;;;;;;;;KAUE,iBAAiB,UAAU,eAAe;EACpD,UAAU,YAAY;;;;;;;KAQZ,aACV,UAAU,cACV,iBACQ,4BAA4B;KAE1B,eAAe,MAAM;;;;;EAK/B;;;;;;EAMA,cAAc;;;;;;EAMd,cAAc,iBAAiB,MAAM;;KAG3B,SACV,UAAU,YAAY,WACtB,UAAU,eAAe;;WAGhB;;WAEA,QAAQ,SAAS;;WAEjB,UAAU,SAAS;;WAEnB,OAAO,kBAAkB;;;;;;;;;;;EAWlC,QAAQ,MAAM,IAAI,MAChB,SAAS,eAAe,MAAM,KAC7B,eAAe,GAAG,GAAG;;;;;;;iBAQV,SACd,UAAU,WACV,UAAU,eAAe,sBACzB;;EAEA;;EAEA,QAAQ;;;;;;EAMR,WAAW;IACT,SAAS,GAAG"}