experimental-a2 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +128 -0
- package/dist/ai-server.browser.d.ts +1 -0
- package/dist/ai-server.browser.js +4 -0
- package/dist/ai-server.d.ts +65 -0
- package/dist/ai-server.js +494 -0
- package/dist/ai.d.ts +282 -0
- package/dist/ai.js +922 -0
- package/dist/cache-indexeddb.d.ts +1 -0
- package/dist/cache-indexeddb.js +0 -0
- package/dist/client.d.ts +90 -0
- package/dist/client.js +410 -0
- package/dist/contract-B0kAXoaL.js +60 -0
- package/dist/contract-DL8btVd9.d.ts +161 -0
- package/dist/devtools-server.browser.d.ts +1 -0
- package/dist/devtools-server.browser.js +4 -0
- package/dist/devtools-server.d.ts +22 -0
- package/dist/devtools-server.js +1087 -0
- package/dist/errors-BJRMd-h6.js +23 -0
- package/dist/errors-xL_JTXsY.d.ts +20 -0
- package/dist/http.d.ts +44 -0
- package/dist/http.js +119 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3 -0
- package/dist/inspection-E7qbD0Xj.js +10 -0
- package/dist/internal-Dm8Ejnud.js +36 -0
- package/dist/log-Dg1I8NRr.d.ts +245 -0
- package/dist/log-memory.d.ts +11 -0
- package/dist/log-memory.js +345 -0
- package/dist/log-polling-RO7kclzR.js +83 -0
- package/dist/log-postgres.d.ts +40 -0
- package/dist/log-postgres.js +628 -0
- package/dist/log-redis.d.ts +31 -0
- package/dist/log-redis.js +711 -0
- package/dist/log-sqlite.d.ts +17 -0
- package/dist/log-sqlite.js +450 -0
- package/dist/log-yJbXUf72.js +5 -0
- package/dist/otel.d.ts +12 -0
- package/dist/otel.js +41 -0
- package/dist/react.d.ts +54 -0
- package/dist/react.js +85 -0
- package/dist/recovery-vercel.d.ts +60 -0
- package/dist/recovery-vercel.js +120 -0
- package/dist/retryable-lazy-DZWmHpii.js +19 -0
- package/dist/server-DYsnKTTy.js +780 -0
- package/dist/server.browser.d.ts +1 -0
- package/dist/server.browser.js +11 -0
- package/dist/server.d.ts +136 -0
- package/dist/server.js +2 -0
- package/dist/telemetry-C78al20p.d.ts +32 -0
- package/dist/validate-XKT4FSNn.js +28 -0
- package/dist/wire-2QpU1EtJ.js +62 -0
- package/docs/01-quickstart.mdx +214 -0
- package/docs/concepts/01-contracts.mdx +138 -0
- package/docs/concepts/02-handlers.mdx +146 -0
- package/docs/concepts/03-durability.mdx +230 -0
- package/docs/concepts/04-state.mdx +133 -0
- package/docs/guides/01-timers.mdx +85 -0
- package/docs/guides/02-cancellation.mdx +107 -0
- package/docs/guides/03-react.mdx +234 -0
- package/docs/guides/04-local-first.mdx +88 -0
- package/docs/guides/05-production.mdx +179 -0
- package/docs/guides/06-ai-agents.mdx +659 -0
- package/docs/guides/07-devtools.mdx +101 -0
- package/docs/guides/08-application-data.mdx +114 -0
- package/docs/index.mdx +282 -0
- package/docs/reference/01-api.mdx +637 -0
- package/docs/reference/02-errors.mdx +77 -0
- package/package.json +111 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { t as A2Error } from "./errors-BJRMd-h6.js";
|
|
2
|
+
import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./log-yJbXUf72.js";
|
|
3
|
+
//#region src/log-memory.ts
|
|
4
|
+
/**
|
|
5
|
+
* a2/log-memory — in-memory log backend (the test default).
|
|
6
|
+
*
|
|
7
|
+
* Implements the A2Log interface with zero dependencies. See
|
|
8
|
+
* specs/a2-implementation.md §3 for the contract; the conformance suite
|
|
9
|
+
* in test/conformance is the executable version of it.
|
|
10
|
+
*/
|
|
11
|
+
const toStored = (row) => ({
|
|
12
|
+
id: row.id,
|
|
13
|
+
type: row.type,
|
|
14
|
+
payload: structuredClone(row.payload),
|
|
15
|
+
index: row.index,
|
|
16
|
+
sessionId: row.sessionId,
|
|
17
|
+
createdAt: new Date(row.createdAt),
|
|
18
|
+
cause: row.cause ? { ...row.cause } : null,
|
|
19
|
+
processedAt: row.processedAt ? new Date(row.processedAt) : null,
|
|
20
|
+
processedByAttempt: row.processedByAttempt,
|
|
21
|
+
firstClaimedAt: row.firstClaimedAt ? new Date(row.firstClaimedAt) : null,
|
|
22
|
+
lastClaimedAt: row.lastClaimedAt ? new Date(row.lastClaimedAt) : null,
|
|
23
|
+
attemptCount: row.attemptCount,
|
|
24
|
+
failureCount: row.failureCount,
|
|
25
|
+
lastFailedAt: row.lastFailedAt ? new Date(row.lastFailedAt) : null,
|
|
26
|
+
lastFailedAttempt: row.lastFailedAttempt,
|
|
27
|
+
lastError: row.lastError,
|
|
28
|
+
failedAt: row.failedAt ? new Date(row.failedAt) : null
|
|
29
|
+
});
|
|
30
|
+
const toEvent = (row) => ({
|
|
31
|
+
id: row.id,
|
|
32
|
+
type: row.type,
|
|
33
|
+
payload: structuredClone(row.payload),
|
|
34
|
+
index: row.index,
|
|
35
|
+
sessionId: row.sessionId,
|
|
36
|
+
createdAt: new Date(row.createdAt)
|
|
37
|
+
});
|
|
38
|
+
function memory(options = {}) {
|
|
39
|
+
const clock = options.clock ?? SYSTEM_CLOCK;
|
|
40
|
+
const generateId = options.ids ?? RANDOM_IDS;
|
|
41
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
42
|
+
/** Global unique index on event ids, like the SQL schema's. */
|
|
43
|
+
const byEventId = /* @__PURE__ */ new Map();
|
|
44
|
+
const leases = /* @__PURE__ */ new Map();
|
|
45
|
+
/** Array offset of the first row not known to be processed. */
|
|
46
|
+
const pendingHeads = /* @__PURE__ */ new Map();
|
|
47
|
+
/** Keyed by `${sessionId}\u0000${reducerName}` — a pure cache. */
|
|
48
|
+
const snapshots = /* @__PURE__ */ new Map();
|
|
49
|
+
const streamSubscribers = /* @__PURE__ */ new Map();
|
|
50
|
+
const notifyStreams = (sessionId, rows) => {
|
|
51
|
+
const subs = streamSubscribers.get(sessionId);
|
|
52
|
+
if (!subs) return;
|
|
53
|
+
for (const listener of subs) for (const row of rows) listener(row);
|
|
54
|
+
};
|
|
55
|
+
const rowsOf = (sessionId) => {
|
|
56
|
+
let rows = sessions.get(sessionId);
|
|
57
|
+
if (!rows) {
|
|
58
|
+
rows = [];
|
|
59
|
+
sessions.set(sessionId, rows);
|
|
60
|
+
}
|
|
61
|
+
return rows;
|
|
62
|
+
};
|
|
63
|
+
const find = (sessionId, index) => {
|
|
64
|
+
const row = sessions.get(sessionId)?.find((candidate) => candidate.index === index);
|
|
65
|
+
if (!row) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
|
|
66
|
+
return row;
|
|
67
|
+
};
|
|
68
|
+
const pendingHead = (sessionId) => {
|
|
69
|
+
const rows = sessions.get(sessionId) ?? [];
|
|
70
|
+
let offset = pendingHeads.get(sessionId) ?? 0;
|
|
71
|
+
while (offset < rows.length && rows[offset]?.processedAt !== null) offset += 1;
|
|
72
|
+
pendingHeads.set(sessionId, offset);
|
|
73
|
+
return rows[offset];
|
|
74
|
+
};
|
|
75
|
+
const claim = (row, now) => {
|
|
76
|
+
row.attemptCount += 1;
|
|
77
|
+
row.firstClaimedAt ??= new Date(now);
|
|
78
|
+
row.lastClaimedAt = new Date(now);
|
|
79
|
+
return toStored(row);
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
async append(sessionId, events) {
|
|
83
|
+
if (events.length === 0) return [];
|
|
84
|
+
const supplied = events.filter((e) => e.id !== void 0);
|
|
85
|
+
if (new Set(supplied.map((e) => e.id)).size !== supplied.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "batch contains the same event id more than once");
|
|
86
|
+
const existing = supplied.map((e) => byEventId.get(e.id)).filter((row) => row !== void 0);
|
|
87
|
+
if (existing.length > 0) {
|
|
88
|
+
const foreign = existing.find((row) => row.sessionId !== sessionId);
|
|
89
|
+
if (foreign) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${foreign.id}' already exists in another session`);
|
|
90
|
+
if (existing.length === events.length) return existing.toSorted((a, b) => a.index - b.index).map(toStored);
|
|
91
|
+
throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
|
|
92
|
+
}
|
|
93
|
+
const rows = rowsOf(sessionId);
|
|
94
|
+
const now = clock.now();
|
|
95
|
+
const base = rows.length === 0 ? 0 : rows[rows.length - 1].index;
|
|
96
|
+
const inserted = events.map((e, i) => {
|
|
97
|
+
return {
|
|
98
|
+
id: e.id ?? generateId(),
|
|
99
|
+
type: e.type,
|
|
100
|
+
payload: structuredClone(e.payload),
|
|
101
|
+
index: base + 1 + i,
|
|
102
|
+
sessionId,
|
|
103
|
+
createdAt: new Date(now),
|
|
104
|
+
cause: e.cause ? { ...e.cause } : null,
|
|
105
|
+
processedAt: null,
|
|
106
|
+
processedByAttempt: null,
|
|
107
|
+
firstClaimedAt: null,
|
|
108
|
+
lastClaimedAt: null,
|
|
109
|
+
attemptCount: 0,
|
|
110
|
+
failureCount: 0,
|
|
111
|
+
lastFailedAt: null,
|
|
112
|
+
lastFailedAttempt: null,
|
|
113
|
+
lastError: null,
|
|
114
|
+
failedAt: null
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
for (const row of inserted) if (byEventId.has(row.id)) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${row.id}' already exists`);
|
|
118
|
+
for (const row of inserted) {
|
|
119
|
+
rows.push(row);
|
|
120
|
+
byEventId.set(row.id, row);
|
|
121
|
+
}
|
|
122
|
+
notifyStreams(sessionId, inserted);
|
|
123
|
+
return inserted.map(toStored);
|
|
124
|
+
},
|
|
125
|
+
async read(sessionId, opts) {
|
|
126
|
+
return (sessions.get(sessionId) ?? []).filter((row) => {
|
|
127
|
+
if (opts?.afterIndex !== void 0 && row.index <= opts.afterIndex) return false;
|
|
128
|
+
if (opts?.unprocessedOnly && row.processedAt !== null) return false;
|
|
129
|
+
return true;
|
|
130
|
+
}).map(toStored);
|
|
131
|
+
},
|
|
132
|
+
async claimNext({ sessionId, holder, ttlMs, expiresAtMs, maxIndex }) {
|
|
133
|
+
const next = pendingHead(sessionId);
|
|
134
|
+
if (!next || next.failedAt || maxIndex !== void 0 && next.index > maxIndex) return { outcome: "settled" };
|
|
135
|
+
const claimedAt = clock.now();
|
|
136
|
+
const now = claimedAt.getTime();
|
|
137
|
+
const current = leases.get(sessionId);
|
|
138
|
+
if (current && current.expiresAt > now && current.holder !== holder) return { outcome: "busy" };
|
|
139
|
+
leases.set(sessionId, {
|
|
140
|
+
holder,
|
|
141
|
+
expiresAt: expiresAtMs ?? now + ttlMs
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
outcome: "claimed",
|
|
145
|
+
event: claim(next, claimedAt)
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
async completeAndClaimNext({ sessionId, holder, completedIndex, attempt, maxIndex }) {
|
|
149
|
+
const claimedAt = clock.now();
|
|
150
|
+
const completed = find(sessionId, completedIndex);
|
|
151
|
+
if (completed.processedAt || completed.attemptCount !== attempt) return { outcome: "superseded" };
|
|
152
|
+
completed.processedAt = new Date(claimedAt);
|
|
153
|
+
completed.processedByAttempt = attempt;
|
|
154
|
+
const next = pendingHead(sessionId);
|
|
155
|
+
if (!next || next.failedAt || maxIndex !== void 0 && next.index > maxIndex) return { outcome: "settled" };
|
|
156
|
+
const now = claimedAt.getTime();
|
|
157
|
+
const current = leases.get(sessionId);
|
|
158
|
+
if (!current || current.holder !== holder || current.expiresAt <= now) return { outcome: "busy" };
|
|
159
|
+
return {
|
|
160
|
+
outcome: "claimed",
|
|
161
|
+
event: claim(next, claimedAt)
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
async markProcessed(sessionId, index) {
|
|
165
|
+
const row = find(sessionId, index);
|
|
166
|
+
row.processedAt ??= clock.now();
|
|
167
|
+
},
|
|
168
|
+
async failAttempt({ sessionId, index, attempt, error, maxFailures }) {
|
|
169
|
+
const row = find(sessionId, index);
|
|
170
|
+
if (row.processedAt || row.attemptCount !== attempt) return {
|
|
171
|
+
outcome: "superseded",
|
|
172
|
+
failureCount: row.failureCount
|
|
173
|
+
};
|
|
174
|
+
if (row.failedAt) return {
|
|
175
|
+
outcome: "dead_lettered",
|
|
176
|
+
failureCount: row.failureCount
|
|
177
|
+
};
|
|
178
|
+
row.failureCount += 1;
|
|
179
|
+
row.lastError = error;
|
|
180
|
+
const failedAt = clock.now();
|
|
181
|
+
row.lastFailedAt = new Date(failedAt);
|
|
182
|
+
row.lastFailedAttempt = attempt;
|
|
183
|
+
if (row.failureCount >= maxFailures) {
|
|
184
|
+
row.failedAt = new Date(failedAt);
|
|
185
|
+
return {
|
|
186
|
+
outcome: "dead_lettered",
|
|
187
|
+
failureCount: row.failureCount
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
outcome: "failed",
|
|
192
|
+
failureCount: row.failureCount
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
async markFailed(sessionId, index) {
|
|
196
|
+
find(sessionId, index).failedAt = clock.now();
|
|
197
|
+
},
|
|
198
|
+
async readState(sessionId, reducerName) {
|
|
199
|
+
const snap = snapshots.get(`${sessionId}\u0000${reducerName}`);
|
|
200
|
+
const snapshot = snap ? {
|
|
201
|
+
index: snap.index,
|
|
202
|
+
state: structuredClone(snap.state)
|
|
203
|
+
} : null;
|
|
204
|
+
const afterIndex = snapshot?.index ?? 0;
|
|
205
|
+
return {
|
|
206
|
+
snapshot,
|
|
207
|
+
events: (sessions.get(sessionId) ?? []).filter((row) => row.index > afterIndex).map(toEvent)
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
async putSnapshot(sessionId, reducerName, index, state) {
|
|
211
|
+
const key = `${sessionId}\u0000${reducerName}`;
|
|
212
|
+
const existing = snapshots.get(key);
|
|
213
|
+
if (existing && existing.index >= index) return;
|
|
214
|
+
snapshots.set(key, {
|
|
215
|
+
index,
|
|
216
|
+
state: structuredClone(state),
|
|
217
|
+
updatedAt: clock.now()
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
inspect: {
|
|
221
|
+
async listSessions(inspectionOptions) {
|
|
222
|
+
const ids = [...sessions.keys()].filter((sessionId) => sessionId.startsWith(inspectionOptions.prefix) && (inspectionOptions.cursor === void 0 || sessionId > inspectionOptions.cursor)).toSorted();
|
|
223
|
+
const visible = ids.slice(0, inspectionOptions.limit);
|
|
224
|
+
return {
|
|
225
|
+
sessions: visible.map((sessionId) => {
|
|
226
|
+
const rows = sessions.get(sessionId);
|
|
227
|
+
const first = rows[0];
|
|
228
|
+
let updatedAt = first.createdAt;
|
|
229
|
+
let pendingCount = 0;
|
|
230
|
+
let failedCount = 0;
|
|
231
|
+
let attemptCount = 0;
|
|
232
|
+
let failureCount = 0;
|
|
233
|
+
for (const row of rows) {
|
|
234
|
+
attemptCount += row.attemptCount;
|
|
235
|
+
failureCount += row.failureCount;
|
|
236
|
+
if (row.processedAt === null && row.failedAt === null) pendingCount += 1;
|
|
237
|
+
if (row.failedAt !== null) failedCount += 1;
|
|
238
|
+
for (const timestamp of [
|
|
239
|
+
row.createdAt,
|
|
240
|
+
row.firstClaimedAt,
|
|
241
|
+
row.lastClaimedAt,
|
|
242
|
+
row.lastFailedAt,
|
|
243
|
+
row.processedAt,
|
|
244
|
+
row.failedAt
|
|
245
|
+
]) if (timestamp && timestamp > updatedAt) updatedAt = timestamp;
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
sessionId,
|
|
249
|
+
eventCount: rows.length,
|
|
250
|
+
pendingCount,
|
|
251
|
+
failedCount,
|
|
252
|
+
attemptCount,
|
|
253
|
+
failureCount,
|
|
254
|
+
firstEventAt: new Date(first.createdAt),
|
|
255
|
+
updatedAt: new Date(updatedAt)
|
|
256
|
+
};
|
|
257
|
+
}),
|
|
258
|
+
cursor: ids.length > visible.length ? visible.at(-1) ?? null : null
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
async listSnapshots(sessionId) {
|
|
262
|
+
const prefix = `${sessionId}\u0000`;
|
|
263
|
+
return [...snapshots.entries()].filter(([key]) => key.startsWith(prefix)).map(([key, snapshot]) => ({
|
|
264
|
+
reducerName: key.slice(prefix.length),
|
|
265
|
+
index: snapshot.index,
|
|
266
|
+
updatedAt: new Date(snapshot.updatedAt)
|
|
267
|
+
})).toSorted((a, b) => a.reducerName.localeCompare(b.reducerName));
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
stream(sessionId, opts) {
|
|
271
|
+
const startAt = opts?.startAt ?? 0;
|
|
272
|
+
return { [Symbol.asyncIterator]() {
|
|
273
|
+
let last = startAt;
|
|
274
|
+
const buffer = [];
|
|
275
|
+
let wake = null;
|
|
276
|
+
let closed = false;
|
|
277
|
+
const onRow = (row) => {
|
|
278
|
+
buffer.push(row);
|
|
279
|
+
wake?.();
|
|
280
|
+
};
|
|
281
|
+
let subs = streamSubscribers.get(sessionId);
|
|
282
|
+
if (!subs) {
|
|
283
|
+
subs = /* @__PURE__ */ new Set();
|
|
284
|
+
streamSubscribers.set(sessionId, subs);
|
|
285
|
+
}
|
|
286
|
+
subs.add(onRow);
|
|
287
|
+
const existing = sessions.get(sessionId) ?? [];
|
|
288
|
+
buffer.unshift(...existing.filter((row) => row.index > startAt));
|
|
289
|
+
const unsubscribe = () => {
|
|
290
|
+
subs.delete(onRow);
|
|
291
|
+
if (subs.size === 0) streamSubscribers.delete(sessionId);
|
|
292
|
+
};
|
|
293
|
+
return {
|
|
294
|
+
async next() {
|
|
295
|
+
for (;;) {
|
|
296
|
+
while (buffer.length > 0) {
|
|
297
|
+
const row = buffer.shift();
|
|
298
|
+
if (row.index <= last) continue;
|
|
299
|
+
last = row.index;
|
|
300
|
+
return {
|
|
301
|
+
value: toStored(row),
|
|
302
|
+
done: false
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
if (closed) return {
|
|
306
|
+
value: void 0,
|
|
307
|
+
done: true
|
|
308
|
+
};
|
|
309
|
+
await new Promise((resolve) => {
|
|
310
|
+
wake = resolve;
|
|
311
|
+
});
|
|
312
|
+
wake = null;
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
async return() {
|
|
316
|
+
closed = true;
|
|
317
|
+
unsubscribe();
|
|
318
|
+
wake?.();
|
|
319
|
+
return {
|
|
320
|
+
value: void 0,
|
|
321
|
+
done: true
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
} };
|
|
326
|
+
},
|
|
327
|
+
lease: {
|
|
328
|
+
async acquire({ sessionId, holder, ttlMs, expiresAtMs }) {
|
|
329
|
+
const now = clock.now().getTime();
|
|
330
|
+
const current = leases.get(sessionId);
|
|
331
|
+
if (current && current.expiresAt > now && current.holder !== holder) return false;
|
|
332
|
+
leases.set(sessionId, {
|
|
333
|
+
holder,
|
|
334
|
+
expiresAt: expiresAtMs ?? now + ttlMs
|
|
335
|
+
});
|
|
336
|
+
return true;
|
|
337
|
+
},
|
|
338
|
+
async release({ sessionId, holder }) {
|
|
339
|
+
if (leases.get(sessionId)?.holder === holder) leases.delete(sessionId);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
//#endregion
|
|
345
|
+
export { memory };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { n as POLL_TIMINGS } from "./internal-Dm8Ejnud.js";
|
|
2
|
+
//#region src/log-polling.ts
|
|
3
|
+
/**
|
|
4
|
+
* The shared polling stream — how poll-based backends (sqlite,
|
|
5
|
+
* postgres) implement `A2Log.stream`.
|
|
6
|
+
*
|
|
7
|
+
* The cadence is activity-adaptive: while events are flowing, polls
|
|
8
|
+
* run at a tight floor (25ms) so streams feel pushed — a token
|
|
9
|
+
* generation reads smoothly instead of clumping at the idle interval;
|
|
10
|
+
* every empty poll doubles the gap until the idle ceiling. The cadence
|
|
11
|
+
* is owned (POLL_TIMINGS in internal.ts), not a public option: the
|
|
12
|
+
* floor already keeps active streams smooth, so tuning it is a cost
|
|
13
|
+
* decision a2 makes, not one it delegates. Correctness never depends
|
|
14
|
+
* on the cadence — each poll is a catch-up read (`index > last`, in
|
|
15
|
+
* order), so nothing can be missed, only arrive a beat later.
|
|
16
|
+
*/
|
|
17
|
+
const defaultSleep = (ms) => {
|
|
18
|
+
let timer;
|
|
19
|
+
let wake;
|
|
20
|
+
return {
|
|
21
|
+
promise: new Promise((resolve) => {
|
|
22
|
+
wake = resolve;
|
|
23
|
+
timer = setTimeout(resolve, ms);
|
|
24
|
+
timer.unref?.();
|
|
25
|
+
}),
|
|
26
|
+
cancel: () => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
wake?.();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
const ceiling = () => POLL_TIMINGS.idleCeilingMs;
|
|
33
|
+
const floor = () => Math.min(POLL_TIMINGS.activeFloorMs, POLL_TIMINGS.idleCeilingMs);
|
|
34
|
+
/**
|
|
35
|
+
* Build the `stream()` iterable from a backend's catch-up read. The
|
|
36
|
+
* iterable ends when the consumer calls `return()` (a disconnecting
|
|
37
|
+
* SSE client), waking any pending sleep immediately.
|
|
38
|
+
*/
|
|
39
|
+
function pollingStream(readAfter, options) {
|
|
40
|
+
const startAt = options.startAt ?? 0;
|
|
41
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
42
|
+
return { [Symbol.asyncIterator]() {
|
|
43
|
+
let last = startAt;
|
|
44
|
+
let buffer = [];
|
|
45
|
+
let closed = false;
|
|
46
|
+
let pending = null;
|
|
47
|
+
let interval = floor();
|
|
48
|
+
return {
|
|
49
|
+
async next() {
|
|
50
|
+
for (;;) {
|
|
51
|
+
if (closed) return {
|
|
52
|
+
value: void 0,
|
|
53
|
+
done: true
|
|
54
|
+
};
|
|
55
|
+
if (buffer.length === 0) buffer = await readAfter(last);
|
|
56
|
+
const row = buffer.shift();
|
|
57
|
+
if (row) {
|
|
58
|
+
interval = floor();
|
|
59
|
+
last = row.index;
|
|
60
|
+
return {
|
|
61
|
+
value: row,
|
|
62
|
+
done: false
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
pending = sleep(interval);
|
|
66
|
+
await pending.promise;
|
|
67
|
+
pending = null;
|
|
68
|
+
interval = Math.min(interval * 2, ceiling());
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
async return() {
|
|
72
|
+
closed = true;
|
|
73
|
+
pending?.cancel();
|
|
74
|
+
return {
|
|
75
|
+
value: void 0,
|
|
76
|
+
done: true
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
} };
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
export { pollingStream as t };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { c as IdSource, i as Clock, t as A2Log } from "./log-Dg1I8NRr.js";
|
|
2
|
+
//#region src/log-postgres.d.ts
|
|
3
|
+
/** The result shape this backend reads: just rows. */
|
|
4
|
+
type PostgresQueryResult = {
|
|
5
|
+
rows: Record<string, unknown>[];
|
|
6
|
+
};
|
|
7
|
+
/** One checked-out connection — what transactions run on. */
|
|
8
|
+
type PostgresConnection = {
|
|
9
|
+
query(text: string, values?: unknown[]): Promise<PostgresQueryResult>;
|
|
10
|
+
release(): void;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The minimal client this backend needs. `pg.Pool` matches it
|
|
14
|
+
* structurally; so does PGlite (which has no `connect` — transactions
|
|
15
|
+
* then serialize on an internal mutex, fine for a single-connection
|
|
16
|
+
* client).
|
|
17
|
+
*/
|
|
18
|
+
type PostgresClient = {
|
|
19
|
+
query(text: string, values?: unknown[]): Promise<PostgresQueryResult>;
|
|
20
|
+
connect?(): Promise<PostgresConnection>;
|
|
21
|
+
end?(): Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
type PostgresLogOptions = {
|
|
24
|
+
/** Creates a `pg` Pool lazily (requires the optional peer dep `pg`). */
|
|
25
|
+
connectionString?: string | undefined;
|
|
26
|
+
/** Bring your own client — a `pg` Pool, PGlite, a pooler wrapper. */
|
|
27
|
+
client?: PostgresClient;
|
|
28
|
+
/** Injectable clock — every stored timestamp comes from here. */
|
|
29
|
+
clock?: Clock;
|
|
30
|
+
/** Injectable id source for generated event ids. */
|
|
31
|
+
ids?: IdSource;
|
|
32
|
+
};
|
|
33
|
+
type PostgresLog = A2Log & {
|
|
34
|
+
/** End the underlying pool/client (when this backend created it, or
|
|
35
|
+
* the injected client has `end`). */
|
|
36
|
+
close(): Promise<void>;
|
|
37
|
+
};
|
|
38
|
+
declare function postgres(options?: PostgresLogOptions): PostgresLog;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { PostgresClient, PostgresConnection, PostgresLog, PostgresLogOptions, PostgresQueryResult, postgres };
|