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.
- package/CHANGELOG.md +51 -0
- package/dist/{ai-B4YhEnfw.d.ts → ai-CFNeCrRl.d.ts} +3 -2
- package/dist/ai-CFNeCrRl.d.ts.map +1 -0
- package/dist/ai-server.browser.js +2 -0
- package/dist/ai-server.browser.js.map +1 -0
- package/dist/ai-server.d.ts +4 -3
- package/dist/ai-server.d.ts.map +1 -0
- package/dist/ai-server.js +4 -2
- package/dist/ai-server.js.map +1 -0
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +3 -1
- package/dist/ai.js.map +1 -0
- package/dist/cli-B3VuxoDe.js +2 -0
- package/dist/cli-B3VuxoDe.js.map +1 -0
- package/dist/cli-bin.js +2 -0
- package/dist/cli-bin.js.map +1 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -0
- package/dist/{client-Bt4tAKi9.js → client-BKlyLiOU.js} +295 -85
- package/dist/client-BKlyLiOU.js.map +1 -0
- package/dist/{client-BrfDXQ8A.d.ts → client-D7mvIXrF.d.ts} +40 -4
- package/dist/client-D7mvIXrF.d.ts.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/contract-48bUMgcL.js +2 -0
- package/dist/contract-48bUMgcL.js.map +1 -0
- package/dist/contract-jIfaR085.d.ts +2 -1
- package/dist/contract-jIfaR085.d.ts.map +1 -0
- package/dist/devtools-J_jZ2vQf.d.ts +2 -1
- package/dist/devtools-J_jZ2vQf.d.ts.map +1 -0
- package/dist/devtools-kJJaORn-.js +2 -0
- package/dist/devtools-kJJaORn-.js.map +1 -0
- package/dist/devtools-server.browser.js +2 -0
- package/dist/devtools-server.browser.js.map +1 -0
- package/dist/devtools-server.d.ts +2 -1
- package/dist/devtools-server.d.ts.map +1 -0
- package/dist/devtools-server.js +2 -0
- package/dist/devtools-server.js.map +1 -0
- package/dist/errors-BQuJpe82.js +2 -0
- package/dist/errors-BQuJpe82.js.map +1 -0
- package/dist/errors-W6nwJ-fm.d.ts +2 -1
- package/dist/errors-W6nwJ-fm.d.ts.map +1 -0
- package/dist/http.d.ts +121 -72
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +503 -178
- package/dist/http.js.map +1 -0
- package/dist/idempotent-replay-DuqEkYA7.js +2 -0
- package/dist/idempotent-replay-DuqEkYA7.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/inspection-DaxB5jM2.js +2 -0
- package/dist/inspection-DaxB5jM2.js.map +1 -0
- package/dist/{internal-aEotMzu_.js → internal-DstsI6Re.js} +3 -1
- package/dist/internal-DstsI6Re.js.map +1 -0
- package/dist/otel.d.ts +2 -1
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +2 -0
- package/dist/otel.js.map +1 -0
- package/dist/platform-B4TnJtWu.js +2 -0
- package/dist/platform-B4TnJtWu.js.map +1 -0
- package/dist/react.d.ts +12 -3
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +5 -1
- package/dist/react.js.map +1 -0
- package/dist/retryable-lazy-DZWmHpii.js +2 -0
- package/dist/retryable-lazy-DZWmHpii.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +4 -3
- package/dist/scheduler-qstash.d.ts.map +1 -0
- package/dist/scheduler-qstash.js +4 -2
- package/dist/scheduler-qstash.js.map +1 -0
- package/dist/scheduler-task-BpzhPnRS.js +2 -0
- package/dist/scheduler-task-BpzhPnRS.js.map +1 -0
- package/dist/scheduler-vercel.d.ts +4 -3
- package/dist/scheduler-vercel.d.ts.map +1 -0
- package/dist/scheduler-vercel.js +4 -2
- package/dist/scheduler-vercel.js.map +1 -0
- package/dist/{server-CcNnFnoW.js → server-C72KOw51.js} +100 -48
- package/dist/server-C72KOw51.js.map +1 -0
- package/dist/{server-YtPq7hjw.d.ts → server-DUF9pjsx.d.ts} +5 -4
- package/dist/server-DUF9pjsx.d.ts.map +1 -0
- package/dist/server.browser.js +2 -0
- package/dist/server.browser.js.map +1 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/{store-C3sNAaBT.d.ts → store-DysUkTH3.d.ts} +10 -1
- package/dist/store-DysUkTH3.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js +2 -0
- package/dist/store-N8PXxDAS.js.map +1 -0
- package/dist/store-codec-DTG0Ftek.js +2 -0
- package/dist/store-codec-DTG0Ftek.js.map +1 -0
- package/dist/store-memory.d.ts +3 -2
- package/dist/store-memory.d.ts.map +1 -0
- package/dist/store-memory.js +19 -11
- package/dist/store-memory.js.map +1 -0
- package/dist/{store-polling-DgrrAE3d.js → store-polling-dSeLxzfb.js} +3 -1
- package/dist/store-polling-dSeLxzfb.js.map +1 -0
- package/dist/store-postgres.d.ts +3 -2
- package/dist/store-postgres.d.ts.map +1 -0
- package/dist/store-postgres.js +57 -1
- package/dist/store-postgres.js.map +1 -0
- package/dist/{store-redis-core-DWqx3F47.js → store-redis-core-BFLwz0Wj.js} +3 -1
- package/dist/store-redis-core-BFLwz0Wj.js.map +1 -0
- package/dist/store-redis-http.d.ts +3 -2
- package/dist/store-redis-http.d.ts.map +1 -0
- package/dist/store-redis-http.js +4 -2
- package/dist/store-redis-http.js.map +1 -0
- package/dist/store-redis.d.ts +3 -2
- package/dist/store-redis.d.ts.map +1 -0
- package/dist/store-redis.js +5 -3
- package/dist/store-redis.js.map +1 -0
- package/dist/store-sqlite.d.ts +3 -2
- package/dist/store-sqlite.d.ts.map +1 -0
- package/dist/store-sqlite.js +3 -1
- package/dist/store-sqlite.js.map +1 -0
- package/dist/telemetry-BjYHTfh2.d.ts +2 -1
- package/dist/telemetry-BjYHTfh2.d.ts.map +1 -0
- package/dist/testing.browser.js +2 -0
- package/dist/testing.browser.js.map +1 -0
- package/dist/testing.d.ts +2 -1
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -0
- package/dist/validate-XKT4FSNn.js +2 -0
- package/dist/validate-XKT4FSNn.js.map +1 -0
- package/dist/{wire-DCUZBUlT.js → wire-BFQmSJ-9.js} +77 -15
- package/dist/wire-BFQmSJ-9.js.map +1 -0
- package/docs/guides/03-react.mdx +59 -39
- package/docs/guides/06-ai-agents.mdx +5 -27
- package/docs/guides/09-presence.mdx +19 -40
- package/docs/guides/10-transports.mdx +49 -40
- package/docs/reference/01-api.mdx +107 -26
- package/docs/reference/02-errors.mdx +4 -2
- package/package.json +2 -1
- package/src/ai-coordinator.ts +358 -0
- package/src/ai-projector.ts +524 -0
- package/src/ai-sdk-step.ts +261 -0
- package/src/ai-server.browser.ts +5 -0
- package/src/ai-server.ts +1719 -0
- package/src/ai.ts +2155 -0
- package/src/cache-indexeddb.ts +10 -0
- package/src/cli-bin.ts +5 -0
- package/src/cli.ts +1046 -0
- package/src/client.ts +1826 -0
- package/src/contract.ts +206 -0
- package/src/deterministic-id.ts +72 -0
- package/src/devtools-app.ts +989 -0
- package/src/devtools-server.browser.ts +5 -0
- package/src/devtools-server.ts +604 -0
- package/src/devtools.ts +716 -0
- package/src/errors.ts +50 -0
- package/src/http.ts +394 -0
- package/src/idempotent-replay.ts +53 -0
- package/src/index.ts +37 -0
- package/src/inspection.ts +39 -0
- package/src/internal.ts +426 -0
- package/src/otel.ts +59 -0
- package/src/platform.ts +60 -0
- package/src/push-envelope.ts +137 -0
- package/src/react.ts +284 -0
- package/src/reducer.ts +108 -0
- package/src/retryable-lazy.ts +27 -0
- package/src/scheduler-qstash.ts +915 -0
- package/src/scheduler-task.ts +106 -0
- package/src/scheduler-vercel.ts +437 -0
- package/src/server.browser.ts +12 -0
- package/src/server.ts +2700 -0
- package/src/session-socket.ts +548 -0
- package/src/sse.ts +141 -0
- package/src/standard-schema.ts +77 -0
- package/src/store-codec.ts +10 -0
- package/src/store-memory.ts +788 -0
- package/src/store-polling.ts +102 -0
- package/src/store-postgres.ts +1212 -0
- package/src/store-redis-core.ts +1494 -0
- package/src/store-redis-http.ts +116 -0
- package/src/store-redis.ts +458 -0
- package/src/store-sqlite.ts +1108 -0
- package/src/store.ts +385 -0
- package/src/telemetry.ts +47 -0
- package/src/testing.browser.ts +5 -0
- package/src/testing.ts +185 -0
- package/src/validate.ts +39 -0
- 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-
|
|
4
|
-
import {
|
|
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)}>e=${gte}<e=${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
|
|
105
|
-
|
|
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 = (
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
socket.close(1e3);
|
|
206
|
-
} catch {}
|
|
349
|
+
settleChannel(channel, null);
|
|
350
|
+
detach(channel);
|
|
207
351
|
}
|
|
208
352
|
},
|
|
209
353
|
async push(body) {
|
|
210
|
-
const
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
371
|
+
target.send(JSON.stringify(frame));
|
|
226
372
|
} catch (cause) {
|
|
227
|
-
|
|
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
|
|
234
|
-
if (
|
|
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
|
|
256
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|