experimental-a2 0.4.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 (182) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/{ai-B4YhEnfw.d.ts → ai-CFNeCrRl.d.ts} +3 -2
  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 +4 -3
  7. package/dist/ai-server.d.ts.map +1 -0
  8. package/dist/ai-server.js +4 -2
  9. package/dist/ai-server.js.map +1 -0
  10. package/dist/ai.d.ts +1 -1
  11. package/dist/ai.js +3 -1
  12. package/dist/ai.js.map +1 -0
  13. package/dist/cli-B3VuxoDe.js +2 -0
  14. package/dist/cli-B3VuxoDe.js.map +1 -0
  15. package/dist/cli-bin.js +2 -0
  16. package/dist/cli-bin.js.map +1 -0
  17. package/dist/cli.d.ts +2 -1
  18. package/dist/cli.d.ts.map +1 -0
  19. package/dist/{client-Bt4tAKi9.js → client-BKlyLiOU.js} +295 -85
  20. package/dist/client-BKlyLiOU.js.map +1 -0
  21. package/dist/{client-BrfDXQ8A.d.ts → client-D7mvIXrF.d.ts} +40 -4
  22. package/dist/client-D7mvIXrF.d.ts.map +1 -0
  23. package/dist/client.d.ts +2 -2
  24. package/dist/client.js +1 -1
  25. package/dist/contract-48bUMgcL.js +2 -0
  26. package/dist/contract-48bUMgcL.js.map +1 -0
  27. package/dist/contract-jIfaR085.d.ts +2 -1
  28. package/dist/contract-jIfaR085.d.ts.map +1 -0
  29. package/dist/devtools-J_jZ2vQf.d.ts +2 -1
  30. package/dist/devtools-J_jZ2vQf.d.ts.map +1 -0
  31. package/dist/devtools-kJJaORn-.js +2 -0
  32. package/dist/devtools-kJJaORn-.js.map +1 -0
  33. package/dist/devtools-server.browser.js +2 -0
  34. package/dist/devtools-server.browser.js.map +1 -0
  35. package/dist/devtools-server.d.ts +2 -1
  36. package/dist/devtools-server.d.ts.map +1 -0
  37. package/dist/devtools-server.js +2 -0
  38. package/dist/devtools-server.js.map +1 -0
  39. package/dist/errors-BQuJpe82.js +2 -0
  40. package/dist/errors-BQuJpe82.js.map +1 -0
  41. package/dist/errors-W6nwJ-fm.d.ts +2 -1
  42. package/dist/errors-W6nwJ-fm.d.ts.map +1 -0
  43. package/dist/http.d.ts +121 -72
  44. package/dist/http.d.ts.map +1 -0
  45. package/dist/http.js +503 -178
  46. package/dist/http.js.map +1 -0
  47. package/dist/idempotent-replay-DuqEkYA7.js +2 -0
  48. package/dist/idempotent-replay-DuqEkYA7.js.map +1 -0
  49. package/dist/index.d.ts +1 -1
  50. package/dist/inspection-DaxB5jM2.js +2 -0
  51. package/dist/inspection-DaxB5jM2.js.map +1 -0
  52. package/dist/{internal-aEotMzu_.js → internal-DstsI6Re.js} +3 -1
  53. package/dist/internal-DstsI6Re.js.map +1 -0
  54. package/dist/otel.d.ts +2 -1
  55. package/dist/otel.d.ts.map +1 -0
  56. package/dist/otel.js +2 -0
  57. package/dist/otel.js.map +1 -0
  58. package/dist/platform-B4TnJtWu.js +2 -0
  59. package/dist/platform-B4TnJtWu.js.map +1 -0
  60. package/dist/react.d.ts +12 -3
  61. package/dist/react.d.ts.map +1 -0
  62. package/dist/react.js +5 -1
  63. package/dist/react.js.map +1 -0
  64. package/dist/retryable-lazy-DZWmHpii.js +2 -0
  65. package/dist/retryable-lazy-DZWmHpii.js.map +1 -0
  66. package/dist/scheduler-qstash.d.ts +4 -3
  67. package/dist/scheduler-qstash.d.ts.map +1 -0
  68. package/dist/scheduler-qstash.js +4 -2
  69. package/dist/scheduler-qstash.js.map +1 -0
  70. package/dist/scheduler-task-BpzhPnRS.js +2 -0
  71. package/dist/scheduler-task-BpzhPnRS.js.map +1 -0
  72. package/dist/scheduler-vercel.d.ts +4 -3
  73. package/dist/scheduler-vercel.d.ts.map +1 -0
  74. package/dist/scheduler-vercel.js +4 -2
  75. package/dist/scheduler-vercel.js.map +1 -0
  76. package/dist/{server-CcNnFnoW.js → server-C72KOw51.js} +100 -48
  77. package/dist/server-C72KOw51.js.map +1 -0
  78. package/dist/{server-YtPq7hjw.d.ts → server-DUF9pjsx.d.ts} +5 -4
  79. package/dist/server-DUF9pjsx.d.ts.map +1 -0
  80. package/dist/server.browser.js +2 -0
  81. package/dist/server.browser.js.map +1 -0
  82. package/dist/server.d.ts +2 -2
  83. package/dist/server.js +1 -1
  84. package/dist/{store-C3sNAaBT.d.ts → store-DysUkTH3.d.ts} +10 -1
  85. package/dist/store-DysUkTH3.d.ts.map +1 -0
  86. package/dist/store-N8PXxDAS.js +2 -0
  87. package/dist/store-N8PXxDAS.js.map +1 -0
  88. package/dist/store-codec-DTG0Ftek.js +2 -0
  89. package/dist/store-codec-DTG0Ftek.js.map +1 -0
  90. package/dist/store-memory.d.ts +3 -2
  91. package/dist/store-memory.d.ts.map +1 -0
  92. package/dist/store-memory.js +19 -11
  93. package/dist/store-memory.js.map +1 -0
  94. package/dist/{store-polling-DgrrAE3d.js → store-polling-dSeLxzfb.js} +3 -1
  95. package/dist/store-polling-dSeLxzfb.js.map +1 -0
  96. package/dist/store-postgres.d.ts +3 -2
  97. package/dist/store-postgres.d.ts.map +1 -0
  98. package/dist/store-postgres.js +57 -1
  99. package/dist/store-postgres.js.map +1 -0
  100. package/dist/{store-redis-core-DWqx3F47.js → store-redis-core-BFLwz0Wj.js} +3 -1
  101. package/dist/store-redis-core-BFLwz0Wj.js.map +1 -0
  102. package/dist/store-redis-http.d.ts +3 -2
  103. package/dist/store-redis-http.d.ts.map +1 -0
  104. package/dist/store-redis-http.js +4 -2
  105. package/dist/store-redis-http.js.map +1 -0
  106. package/dist/store-redis.d.ts +3 -2
  107. package/dist/store-redis.d.ts.map +1 -0
  108. package/dist/store-redis.js +5 -3
  109. package/dist/store-redis.js.map +1 -0
  110. package/dist/store-sqlite.d.ts +3 -2
  111. package/dist/store-sqlite.d.ts.map +1 -0
  112. package/dist/store-sqlite.js +3 -1
  113. package/dist/store-sqlite.js.map +1 -0
  114. package/dist/telemetry-BjYHTfh2.d.ts +2 -1
  115. package/dist/telemetry-BjYHTfh2.d.ts.map +1 -0
  116. package/dist/testing.browser.js +2 -0
  117. package/dist/testing.browser.js.map +1 -0
  118. package/dist/testing.d.ts +2 -1
  119. package/dist/testing.d.ts.map +1 -0
  120. package/dist/testing.js +2 -0
  121. package/dist/testing.js.map +1 -0
  122. package/dist/validate-XKT4FSNn.js +2 -0
  123. package/dist/validate-XKT4FSNn.js.map +1 -0
  124. package/dist/{wire-DCUZBUlT.js → wire-BFQmSJ-9.js} +77 -15
  125. package/dist/wire-BFQmSJ-9.js.map +1 -0
  126. package/docs/guides/03-react.mdx +59 -39
  127. package/docs/guides/06-ai-agents.mdx +5 -27
  128. package/docs/guides/09-presence.mdx +19 -40
  129. package/docs/guides/10-transports.mdx +49 -40
  130. package/docs/reference/01-api.mdx +107 -26
  131. package/docs/reference/02-errors.mdx +4 -2
  132. package/package.json +2 -1
  133. package/src/ai-coordinator.ts +358 -0
  134. package/src/ai-projector.ts +524 -0
  135. package/src/ai-sdk-step.ts +261 -0
  136. package/src/ai-server.browser.ts +5 -0
  137. package/src/ai-server.ts +1719 -0
  138. package/src/ai.ts +2155 -0
  139. package/src/cache-indexeddb.ts +10 -0
  140. package/src/cli-bin.ts +5 -0
  141. package/src/cli.ts +1046 -0
  142. package/src/client.ts +1826 -0
  143. package/src/contract.ts +206 -0
  144. package/src/deterministic-id.ts +72 -0
  145. package/src/devtools-app.ts +989 -0
  146. package/src/devtools-server.browser.ts +5 -0
  147. package/src/devtools-server.ts +604 -0
  148. package/src/devtools.ts +716 -0
  149. package/src/errors.ts +50 -0
  150. package/src/http.ts +394 -0
  151. package/src/idempotent-replay.ts +53 -0
  152. package/src/index.ts +37 -0
  153. package/src/inspection.ts +39 -0
  154. package/src/internal.ts +426 -0
  155. package/src/otel.ts +59 -0
  156. package/src/platform.ts +60 -0
  157. package/src/push-envelope.ts +137 -0
  158. package/src/react.ts +284 -0
  159. package/src/reducer.ts +108 -0
  160. package/src/retryable-lazy.ts +27 -0
  161. package/src/scheduler-qstash.ts +915 -0
  162. package/src/scheduler-task.ts +106 -0
  163. package/src/scheduler-vercel.ts +437 -0
  164. package/src/server.browser.ts +12 -0
  165. package/src/server.ts +2700 -0
  166. package/src/session-socket.ts +548 -0
  167. package/src/sse.ts +141 -0
  168. package/src/standard-schema.ts +77 -0
  169. package/src/store-codec.ts +10 -0
  170. package/src/store-memory.ts +788 -0
  171. package/src/store-polling.ts +102 -0
  172. package/src/store-postgres.ts +1212 -0
  173. package/src/store-redis-core.ts +1494 -0
  174. package/src/store-redis-http.ts +116 -0
  175. package/src/store-redis.ts +458 -0
  176. package/src/store-sqlite.ts +1108 -0
  177. package/src/store.ts +385 -0
  178. package/src/telemetry.ts +47 -0
  179. package/src/testing.browser.ts +5 -0
  180. package/src/testing.ts +185 -0
  181. package/src/validate.ts +39 -0
  182. package/src/wire.ts +454 -0
@@ -1,7 +1,7 @@
1
1
  import { n as validateSync } from "./validate-XKT4FSNn.js";
2
2
  import { t as A2Error } from "./errors-BQuJpe82.js";
3
- import { a as PRESENCE_TIMINGS, c as STREAM_TIMINGS, g as nullProtoRecord } from "./internal-aEotMzu_.js";
4
- import { a as eventFromWire, c as isWirePresencePatch, d as presencePatchFromWire, l as isWirePresenceSnapshot, n as errorFromWire, p as presenceSnapshotFromWire, s as isWireEvent, u as parseSocketFrame } from "./wire-DCUZBUlT.js";
3
+ import { a as PRESENCE_TIMINGS, c as STREAM_TIMINGS, g as nullProtoRecord } from "./internal-DstsI6Re.js";
4
+ import { c as isWireEvent, d as parseSocketFrame, f as presencePatchFromWire, l as isWirePresencePatch, m as presenceSnapshotFromWire, o as eventFromWire, r as errorFromWire, u as isWirePresenceSnapshot } from "./wire-BFQmSJ-9.js";
5
5
  //#region src/client.ts
6
6
  /**
7
7
  * experimental-a2/client — the framework-agnostic session client.
@@ -19,6 +19,7 @@ import { a as eventFromWire, c as isWirePresencePatch, d as presencePatchFromWir
19
19
  */
20
20
  const PUSH_ATTEMPTS = 3;
21
21
  const DEFAULT_GC_TIME_MS = 3e5;
22
+ const DEFAULT_HISTORY_LIMIT = 50;
22
23
  const httpTransport = (routes, fetchImpl, presence) => ({
23
24
  async *connect({ sessionId, startAfter, signal }) {
24
25
  const sep = routes.stream.includes("?") ? "&" : "?";
@@ -86,6 +87,14 @@ const httpTransport = (routes, fetchImpl, presence) => ({
86
87
  }
87
88
  throw errorFromWire(await res.json().catch(() => null)) ?? new A2Error("STORE_UNAVAILABLE", `push failed with ${res.status}`);
88
89
  },
90
+ async history({ sessionId, gte, lte }) {
91
+ const sep = routes.stream.includes("?") ? "&" : "?";
92
+ const res = await fetchImpl(`${routes.stream}${sep}sessionId=${encodeURIComponent(sessionId)}&gte=${gte}&lte=${lte}`, { headers: { accept: "application/json" } });
93
+ if (!res.ok) throw errorFromWire(await res.json().catch(() => null)) ?? new A2Error("STORE_UNAVAILABLE", `history failed with ${res.status}`);
94
+ const rows = await res.json();
95
+ if (!Array.isArray(rows) || !rows.every(isWireEvent)) throw new A2Error("STORE_UNAVAILABLE", "history response was not a list of events");
96
+ return rows;
97
+ },
89
98
  sendPresence(envelope) {
90
99
  (async () => {
91
100
  try {
@@ -100,10 +109,25 @@ const httpTransport = (routes, fetchImpl, presence) => ({
100
109
  });
101
110
  /** `WebSocket.OPEN` — fixed by the spec, identical in every runtime. */
102
111
  const WS_OPEN = 1;
112
+ const wakeChannel = (channel) => {
113
+ channel.wake?.();
114
+ channel.wake = null;
115
+ };
116
+ const subscribeFrame = (sessions) => {
117
+ return JSON.stringify({
118
+ kind: "subscribe",
119
+ sessions
120
+ });
121
+ };
103
122
  const wsTransport = (url, webSocketImpl, presence) => {
104
- const connections = /* @__PURE__ */ new Map();
105
- /** Push retries parked in `awaitReady`, flushed on socket open. */
123
+ const channels = /* @__PURE__ */ new Map();
124
+ const pending = /* @__PURE__ */ new Map();
125
+ /** Push retries parked in `awaitReady`, flushed per session on its
126
+ * subscribe confirmation. */
106
127
  const readyWaiters = /* @__PURE__ */ new Map();
128
+ let socket = null;
129
+ let socketOpen = false;
130
+ let watchdog;
107
131
  let nextReq = 1;
108
132
  const flushReadyWaiters = (sessionId) => {
109
133
  const waiters = readyWaiters.get(sessionId);
@@ -111,7 +135,7 @@ const wsTransport = (url, webSocketImpl, presence) => {
111
135
  readyWaiters.delete(sessionId);
112
136
  for (const waiter of waiters) waiter();
113
137
  };
114
- const socketUrl = (sessionId, startAfter) => {
138
+ const socketUrl = () => {
115
139
  let resolved;
116
140
  if (/^(https?|wss?):\/\//i.test(url)) resolved = new URL(url);
117
141
  else {
@@ -121,117 +145,239 @@ const wsTransport = (url, webSocketImpl, presence) => {
121
145
  }
122
146
  if (resolved.protocol === "http:") resolved.protocol = "ws:";
123
147
  else if (resolved.protocol === "https:") resolved.protocol = "wss:";
124
- resolved.searchParams.set("sessionId", sessionId);
125
- resolved.searchParams.set("index", String(startAfter));
126
148
  return resolved.href;
127
149
  };
150
+ const trySend = (data) => {
151
+ if (socket === null || !socketOpen) return;
152
+ try {
153
+ socket.send(data);
154
+ } catch {}
155
+ };
156
+ const deliver = (channel, frame) => {
157
+ channel.queue.push(frame);
158
+ wakeChannel(channel);
159
+ };
160
+ const settleChannel = (channel, failure) => {
161
+ if (channel.ended) return;
162
+ channel.ended = true;
163
+ channel.failure = failure;
164
+ wakeChannel(channel);
165
+ };
166
+ const releaseSocket = () => {
167
+ const current = socket;
168
+ socket = null;
169
+ socketOpen = false;
170
+ clearTimeout(watchdog);
171
+ watchdog = void 0;
172
+ if (pending.size > 0) {
173
+ const rejection = new A2Error("STORE_UNAVAILABLE", "socket closed with the ack outstanding");
174
+ for (const waiter of pending.values()) waiter.reject(rejection);
175
+ pending.clear();
176
+ }
177
+ return current;
178
+ };
179
+ const failSocket = (failure) => {
180
+ const current = releaseSocket();
181
+ for (const channel of channels.values()) settleChannel(channel, failure);
182
+ try {
183
+ current?.close(1e3);
184
+ } catch {}
185
+ };
186
+ const detach = (channel) => {
187
+ if (channels.get(channel.sessionId) !== channel) return;
188
+ channels.delete(channel.sessionId);
189
+ if (!channel.dropped) {
190
+ const frame = {
191
+ kind: "unsubscribe",
192
+ sessions: [channel.sessionId]
193
+ };
194
+ trySend(JSON.stringify(frame));
195
+ }
196
+ if (channels.size === 0) {
197
+ const current = releaseSocket();
198
+ try {
199
+ current?.close(1e3);
200
+ } catch {}
201
+ }
202
+ };
203
+ const armWatchdog = (created) => {
204
+ clearTimeout(watchdog);
205
+ watchdog = setTimeout(() => {
206
+ if (socket !== created) return;
207
+ failSocket(/* @__PURE__ */ new Error(`socket stalled: no data for ${STREAM_TIMINGS.stallTimeoutMs}ms`));
208
+ }, STREAM_TIMINGS.stallTimeoutMs);
209
+ watchdog.unref?.();
210
+ };
211
+ const route = (frame) => {
212
+ switch (frame.kind) {
213
+ case "ping":
214
+ for (const channel of channels.values()) if (channel.live && !channel.ended) deliver(channel, { kind: "ping" });
215
+ return;
216
+ case "subscribed": {
217
+ const channel = channels.get(frame.sessionId);
218
+ if (channel === void 0 || channel.ended || channel.live) return;
219
+ channel.live = true;
220
+ deliver(channel, { kind: "ping" });
221
+ flushReadyWaiters(frame.sessionId);
222
+ return;
223
+ }
224
+ case "unsubscribed": {
225
+ const channel = channels.get(frame.sessionId);
226
+ if (channel === void 0) return;
227
+ channel.dropped = true;
228
+ settleChannel(channel, frame.reason === void 0 ? null : new Error(frame.reason));
229
+ return;
230
+ }
231
+ case "ack": {
232
+ const waiter = pending.get(frame.req);
233
+ if (waiter === void 0) return;
234
+ pending.delete(frame.req);
235
+ if ("events" in frame) waiter.resolve(frame.events);
236
+ else waiter.reject(frame.error);
237
+ return;
238
+ }
239
+ case "event": {
240
+ const channel = channels.get(frame.event.sessionId);
241
+ if (channel !== void 0 && channel.live && !channel.ended) deliver(channel, {
242
+ kind: "event",
243
+ event: frame.event
244
+ });
245
+ return;
246
+ }
247
+ case "presence": {
248
+ if (!presence || frame.sessionId === void 0) return;
249
+ const channel = channels.get(frame.sessionId);
250
+ if (channel !== void 0 && channel.live && !channel.ended) deliver(channel, {
251
+ kind: "presence",
252
+ patch: frame.patch
253
+ });
254
+ return;
255
+ }
256
+ case "presence-snapshot": {
257
+ if (!presence || frame.sessionId === void 0) return;
258
+ const channel = channels.get(frame.sessionId);
259
+ if (channel !== void 0 && channel.live && !channel.ended) deliver(channel, {
260
+ kind: "presence-snapshot",
261
+ snapshot: frame.snapshot
262
+ });
263
+ return;
264
+ }
265
+ }
266
+ };
267
+ const ensureSocket = () => {
268
+ if (socket !== null) return;
269
+ if (webSocketImpl === void 0) throw new TypeError("no WebSocket implementation available — pass one via createClient({ webSocket }) where the global is missing");
270
+ const created = new webSocketImpl(socketUrl());
271
+ socket = created;
272
+ socketOpen = false;
273
+ armWatchdog(created);
274
+ created.addEventListener("open", () => {
275
+ if (socket !== created) return;
276
+ socketOpen = true;
277
+ armWatchdog(created);
278
+ const sessions = [...channels.values()].filter((channel) => !channel.ended).map((channel) => ({
279
+ id: channel.sessionId,
280
+ index: channel.startAfter
281
+ }));
282
+ if (sessions.length > 0) trySend(subscribeFrame(sessions));
283
+ });
284
+ created.addEventListener("message", (event) => {
285
+ if (socket !== created) return;
286
+ if (typeof event.data !== "string") return;
287
+ armWatchdog(created);
288
+ const frame = parseSocketFrame(event.data);
289
+ if (frame === null) return;
290
+ route(frame);
291
+ });
292
+ created.addEventListener("close", (event) => {
293
+ if (socket !== created) return;
294
+ failSocket(event.code === 1e3 ? null : /* @__PURE__ */ new Error(`socket closed (${event.code}${event.reason ? `: ${event.reason}` : ""})`));
295
+ });
296
+ created.addEventListener("error", () => {
297
+ if (socket !== created) return;
298
+ failSocket(/* @__PURE__ */ new Error("socket error"));
299
+ });
300
+ };
128
301
  return {
129
302
  async *connect({ sessionId, startAfter, signal }) {
130
- if (webSocketImpl === void 0) throw new TypeError("no WebSocket implementation available — pass one via createClient({ webSocket }) where the global is missing");
131
- const socket = new webSocketImpl(socketUrl(sessionId, startAfter));
132
- const connection = {
133
- socket,
134
- pending: /* @__PURE__ */ new Map()
135
- };
136
- connections.set(sessionId, connection);
137
- const queue = [];
138
- let ended = false;
139
- let failure = null;
140
- let wake = null;
141
- const notify = () => {
142
- wake?.();
143
- wake = null;
144
- };
145
- const finish = (error) => {
146
- if (ended) return;
147
- ended = true;
148
- failure = error;
149
- if (connections.get(sessionId) === connection) connections.delete(sessionId);
150
- const rejection = new A2Error("STORE_UNAVAILABLE", "socket closed with the ack outstanding");
151
- for (const waiter of connection.pending.values()) waiter.reject(rejection);
152
- connection.pending.clear();
153
- notify();
303
+ const channel = {
304
+ sessionId,
305
+ startAfter,
306
+ queue: [],
307
+ live: false,
308
+ dropped: false,
309
+ ended: false,
310
+ failure: null,
311
+ wake: null
154
312
  };
155
- socket.addEventListener("open", () => {
156
- queue.push({ kind: "ping" });
157
- notify();
158
- flushReadyWaiters(sessionId);
159
- });
160
- socket.addEventListener("message", (event) => {
161
- if (typeof event.data !== "string") return;
162
- const frame = parseSocketFrame(event.data);
163
- if (frame === null) return;
164
- if (frame.kind === "ack") {
165
- const waiter = connection.pending.get(frame.req);
166
- if (waiter === void 0) return;
167
- connection.pending.delete(frame.req);
168
- if ("events" in frame) waiter.resolve(frame.events);
169
- else waiter.reject(frame.error);
170
- return;
171
- }
172
- if (!presence && frame.kind !== "event" && frame.kind !== "ping") return;
173
- queue.push(frame);
174
- notify();
175
- });
176
- socket.addEventListener("close", (event) => {
177
- finish(event.code === 1e3 ? null : /* @__PURE__ */ new Error(`socket closed (${event.code}${event.reason ? `: ${event.reason}` : ""})`));
178
- });
179
- socket.addEventListener("error", () => {
180
- finish(/* @__PURE__ */ new Error("socket error"));
181
- });
313
+ channels.set(sessionId, channel);
314
+ try {
315
+ ensureSocket();
316
+ if (socketOpen) trySend(subscribeFrame([{
317
+ id: sessionId,
318
+ index: startAfter
319
+ }]));
320
+ } catch (error) {
321
+ if (channels.get(sessionId) === channel) channels.delete(sessionId);
322
+ throw error;
323
+ }
182
324
  const onAbort = () => {
183
- finish(signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("stream aborted"));
184
- try {
185
- socket.close(1e3);
186
- } catch {}
325
+ settleChannel(channel, signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("stream aborted"));
326
+ detach(channel);
187
327
  };
188
328
  if (signal.aborted) onAbort();
189
329
  else signal.addEventListener("abort", onAbort, { once: true });
330
+ const pendingSubscribe = setTimeout(() => {
331
+ if (channel.live) return;
332
+ settleChannel(channel, /* @__PURE__ */ new Error("subscribe not confirmed: is the ws route serving the multiplexed socket (handle with options.upgrade)?"));
333
+ }, STREAM_TIMINGS.stallTimeoutMs);
334
+ pendingSubscribe.unref?.();
190
335
  try {
191
336
  for (;;) {
192
- while (queue.length > 0) yield queue.shift();
193
- if (ended) {
194
- if (failure) throw failure;
337
+ while (channel.queue.length > 0) yield channel.queue.shift();
338
+ if (channel.ended) {
339
+ if (channel.failure) throw channel.failure;
195
340
  return;
196
341
  }
197
342
  await new Promise((resolve) => {
198
- wake = resolve;
343
+ channel.wake = resolve;
199
344
  });
200
345
  }
201
346
  } finally {
347
+ clearTimeout(pendingSubscribe);
202
348
  signal.removeEventListener("abort", onAbort);
203
- finish(null);
204
- try {
205
- socket.close(1e3);
206
- } catch {}
349
+ settleChannel(channel, null);
350
+ detach(channel);
207
351
  }
208
352
  },
209
353
  async push(body) {
210
- const connection = connections.get(body.sessionId);
211
- if (connection === void 0 || connection.socket.readyState !== WS_OPEN) throw new A2Error("STORE_UNAVAILABLE", "no open socket for the session");
354
+ const channel = channels.get(body.sessionId);
355
+ const target = socket;
356
+ if (channel === void 0 || !channel.live || channel.ended || target === null || !socketOpen || target.readyState !== WS_OPEN) throw new A2Error("STORE_UNAVAILABLE", "no open socket for the session");
212
357
  const req = nextReq;
213
358
  nextReq += 1;
214
359
  return await new Promise((resolve, reject) => {
215
- connection.pending.set(req, {
360
+ pending.set(req, {
216
361
  resolve,
217
362
  reject
218
363
  });
219
364
  const frame = {
220
365
  kind: "push",
221
366
  req,
367
+ sessionId: body.sessionId,
222
368
  events: body.events
223
369
  };
224
370
  try {
225
- connection.socket.send(JSON.stringify(frame));
371
+ target.send(JSON.stringify(frame));
226
372
  } catch (cause) {
227
- connection.pending.delete(req);
373
+ pending.delete(req);
228
374
  reject(new A2Error("STORE_UNAVAILABLE", "socket send failed", { cause }));
229
375
  }
230
376
  });
231
377
  },
232
378
  awaitReady(sessionId, maxWaitMs) {
233
- const connection = connections.get(sessionId);
234
- if (connection && connection.socket.readyState === WS_OPEN) return Promise.resolve();
379
+ const channel = channels.get(sessionId);
380
+ if (channel !== void 0 && channel.live && !channel.ended && socketOpen) return Promise.resolve();
235
381
  return new Promise((resolve) => {
236
382
  let waiters = readyWaiters.get(sessionId);
237
383
  if (!waiters) {
@@ -244,6 +390,7 @@ const wsTransport = (url, webSocketImpl, presence) => {
244
390
  settled = true;
245
391
  clearTimeout(timer);
246
392
  waiters.delete(done);
393
+ if (waiters.size === 0) readyWaiters.delete(sessionId);
247
394
  resolve();
248
395
  };
249
396
  const timer = setTimeout(done, maxWaitMs);
@@ -252,15 +399,14 @@ const wsTransport = (url, webSocketImpl, presence) => {
252
399
  });
253
400
  },
254
401
  sendPresence(envelope) {
255
- const connection = connections.get(envelope.sessionId);
256
- if (connection === void 0 || connection.socket.readyState !== WS_OPEN) return;
402
+ const channel = channels.get(envelope.sessionId);
403
+ if (channel === void 0 || !channel.live || channel.ended) return;
257
404
  const frame = {
258
405
  kind: "presence",
406
+ sessionId: envelope.sessionId,
259
407
  ...envelope.presence
260
408
  };
261
- try {
262
- connection.socket.send(JSON.stringify(frame));
263
- } catch {}
409
+ trySend(JSON.stringify(frame));
264
410
  }
265
411
  };
266
412
  };
@@ -384,6 +530,14 @@ function createClient(options) {
384
530
  };
385
531
  }
386
532
  };
533
+ const historyState = () => {
534
+ const oldestLoaded = serverEvents[0]?.index ?? null;
535
+ return {
536
+ loading: historyLoads > 0,
537
+ complete: oldestLoaded === 1 || frontier === 0,
538
+ oldestLoaded
539
+ };
540
+ };
387
541
  const buildSnapshot = () => {
388
542
  const overlay = overlayEvents();
389
543
  let state = foldedState;
@@ -392,6 +546,7 @@ function createClient(options) {
392
546
  state,
393
547
  events: [...serverEvents, ...overlay],
394
548
  index: frontier,
549
+ history: historyState(),
395
550
  connection: connection(),
396
551
  ...declaresPresence ? { presence: presenceState } : {}
397
552
  };
@@ -565,8 +720,7 @@ function createClient(options) {
565
720
  if (lastPushError.code !== "STORE_UNAVAILABLE") throw lastPushError;
566
721
  if (attempt < PUSH_ATTEMPTS) {
567
722
  const sleepMs = 250 * attempt;
568
- if (transport.awaitReady) await transport.awaitReady(sessionId, sleepMs + STREAM_TIMINGS.reconnectBaseMs * attempt);
569
- else await new Promise((resolve) => setTimeout(resolve, sleepMs));
723
+ await Promise.all([new Promise((resolve) => setTimeout(resolve, sleepMs)), transport.awaitReady?.(sessionId, sleepMs + STREAM_TIMINGS.reconnectBaseMs * attempt)]);
570
724
  }
571
725
  }
572
726
  throw lastPushError;
@@ -644,6 +798,59 @@ function createClient(options) {
644
798
  }
645
799
  })());
646
800
  };
801
+ let historyLoads = 0;
802
+ /** Calls chain per session: each resolves its bounds after the
803
+ * prior call's merge, so overlapping backscrolls page backward
804
+ * instead of re-fetching the same range. */
805
+ let historyTail = Promise.resolve();
806
+ const eventsBetween = (gte, lte) => serverEvents.filter((event) => event.index >= gte && event.index <= lte);
807
+ /** The narrowest sub-range of [gte, lte] missing from the feed, or
808
+ * null when the range is fully loaded (nothing to fetch). */
809
+ const missingBetween = (gte, lte) => {
810
+ const present = new Set(eventsBetween(gte, lte).map((e) => e.index));
811
+ let lo = gte;
812
+ while (lo <= lte && present.has(lo)) lo += 1;
813
+ if (lo > lte) return null;
814
+ let hi = lte;
815
+ while (hi > lo && present.has(hi)) hi -= 1;
816
+ return {
817
+ gte: lo,
818
+ lte: hi
819
+ };
820
+ };
821
+ const loadHistory = (loadOptions) => {
822
+ const readHistory = transport.history;
823
+ if (readHistory === void 0) throw new TypeError("loadHistory requires an http api — the ws transport has no history lane; serve the session over handle() and pass its route url as the string or { type: \"http\" } api");
824
+ const before = loadOptions?.before;
825
+ const limit = loadOptions?.limit;
826
+ if (before !== void 0 && (!Number.isSafeInteger(before) || before < 1)) throw new TypeError("loadHistory before must be a positive safe integer");
827
+ if (limit !== void 0 && (!Number.isSafeInteger(limit) || limit < 1)) throw new TypeError("loadHistory limit must be a positive safe integer");
828
+ touch(sessionId);
829
+ const run = historyTail.then(async () => {
830
+ const upper = before ?? serverEvents[0]?.index ?? frontier + 1;
831
+ const lte = Math.min(upper - 1, frontier);
832
+ const gte = Math.max(1, upper - (limit ?? DEFAULT_HISTORY_LIMIT));
833
+ if (gte > lte) return [];
834
+ const missing = missingBetween(gte, lte);
835
+ if (missing === null) return eventsBetween(gte, lte);
836
+ historyLoads += 1;
837
+ notify();
838
+ try {
839
+ const rows = await readHistory({
840
+ sessionId,
841
+ ...missing
842
+ });
843
+ mergeServerEvents(rows.map((row) => eventFromWire(row)), frontier);
844
+ return eventsBetween(gte, lte);
845
+ } finally {
846
+ historyLoads -= 1;
847
+ notify();
848
+ touch(sessionId);
849
+ }
850
+ });
851
+ historyTail = run.catch(() => {});
852
+ return run;
853
+ };
647
854
  let generation = 0;
648
855
  let active = false;
649
856
  let abort = null;
@@ -729,6 +936,7 @@ function createClient(options) {
729
936
  return snapshot;
730
937
  },
731
938
  push,
939
+ loadHistory,
732
940
  connect() {
733
941
  touch(sessionId);
734
942
  if (active) return;
@@ -776,7 +984,7 @@ function createClient(options) {
776
984
  notifyHydrated();
777
985
  },
778
986
  canEvict() {
779
- return listeners.size === 0 && !active && inFlightPushes === 0 && confirmWatchers.length === 0 && presenceTimer === void 0;
987
+ return listeners.size === 0 && !active && inFlightPushes === 0 && historyLoads === 0 && confirmWatchers.length === 0 && presenceTimer === void 0;
780
988
  }
781
989
  };
782
990
  return runtime;
@@ -796,3 +1004,5 @@ function createClient(options) {
796
1004
  }
797
1005
  //#endregion
798
1006
  export { createClient as t };
1007
+
1008
+ //# sourceMappingURL=client-BKlyLiOU.js.map