experimental-a2 0.0.0 → 0.2.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 +43 -0
- package/dist/ai-server.browser.js +2 -2
- package/dist/ai-server.d.ts +19 -7
- package/dist/ai-server.js +730 -96
- package/dist/ai.d.ts +32 -11
- package/dist/ai.js +253 -75
- package/dist/client.d.ts +1 -1
- package/dist/client.js +4 -4
- package/dist/{contract-B0kAXoaL.js → contract-CG_adnu_.js} +2 -1
- package/dist/{contract-DL8btVd9.d.ts → contract-C_3dIIEU.d.ts} +4 -1
- package/dist/devtools-server.browser.js +2 -2
- package/dist/devtools-server.js +1 -1
- package/dist/http.d.ts +1 -1
- package/dist/http.js +4 -3
- package/dist/idempotent-replay-BMyHrP0L.js +19 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +1 -1
- package/dist/{internal-Dm8Ejnud.js → internal-D6wNxTck.js} +3 -3
- package/dist/{log-Dg1I8NRr.d.ts → log-ldf5g8Cx.d.ts} +74 -56
- package/dist/log-memory.d.ts +1 -1
- package/dist/log-memory.js +173 -96
- package/dist/{log-polling-RO7kclzR.js → log-polling-6COoN60V.js} +1 -1
- package/dist/log-postgres.d.ts +1 -1
- package/dist/log-postgres.js +235 -192
- package/dist/log-redis.d.ts +1 -1
- package/dist/log-redis.js +453 -263
- package/dist/log-sqlite.d.ts +1 -1
- package/dist/log-sqlite.js +216 -127
- package/dist/otel.d.ts +1 -1
- package/dist/otel.js +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/recovery-vercel.d.ts +2 -2
- package/dist/recovery-vercel.js +9 -10
- package/dist/server-DJgD2YWP.js +877 -0
- package/dist/server.browser.js +4 -4
- package/dist/server.d.ts +46 -27
- package/dist/server.js +1 -1
- package/dist/{telemetry-C78al20p.d.ts → telemetry-Cso0qyHQ.d.ts} +1 -1
- package/dist/{wire-2QpU1EtJ.js → wire-BVsgR8o9.js} +1 -1
- package/docs/01-quickstart.mdx +7 -7
- package/docs/concepts/01-contracts.mdx +22 -22
- package/docs/concepts/02-handlers.mdx +223 -89
- package/docs/concepts/03-durability.mdx +199 -112
- package/docs/concepts/04-state.mdx +27 -1
- package/docs/guides/01-timers.mdx +4 -4
- package/docs/guides/02-cancellation.mdx +32 -4
- package/docs/guides/05-production.mdx +61 -27
- package/docs/guides/06-ai-agents.mdx +151 -70
- package/docs/guides/07-devtools.mdx +6 -3
- package/docs/guides/08-application-data.mdx +5 -6
- package/docs/index.mdx +30 -14
- package/docs/reference/01-api.mdx +305 -70
- package/package.json +31 -31
- package/dist/server-DYsnKTTy.js +0 -780
package/dist/log-sqlite.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as IdSource, i as Clock, t as A2Log } from "./log-
|
|
1
|
+
import { c as IdSource, i as Clock, t as A2Log } from "./log-ldf5g8Cx.js";
|
|
2
2
|
//#region src/log-sqlite.d.ts
|
|
3
3
|
type SqliteLogOptions = {
|
|
4
4
|
/** Database file path. Defaults to `.a2/dev.db`; `:memory:` works. */
|
package/dist/log-sqlite.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { t as A2Error } from "./errors-BJRMd-h6.js";
|
|
2
|
+
import { t as idempotentReplay } from "./idempotent-replay-BMyHrP0L.js";
|
|
2
3
|
import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./log-yJbXUf72.js";
|
|
3
|
-
import { t as pollingStream } from "./log-polling-
|
|
4
|
+
import { t as pollingStream } from "./log-polling-6COoN60V.js";
|
|
4
5
|
import { mkdirSync } from "node:fs";
|
|
5
6
|
import { dirname, resolve } from "node:path";
|
|
6
7
|
import { DatabaseSync } from "node:sqlite";
|
|
7
8
|
//#region src/log-sqlite.ts
|
|
8
9
|
/**
|
|
9
|
-
* a2/log-sqlite — sqlite log backend (the dev default, `.a2/dev.db`).
|
|
10
|
+
* experimental-a2/log-sqlite — sqlite log backend (the dev default, `.a2/dev.db`).
|
|
10
11
|
*
|
|
11
12
|
* Built on `node:sqlite` (Node ≥ 22.13) so it ships with zero
|
|
12
13
|
* dependencies. Implements the A2Log interface; the conformance suite
|
|
@@ -27,8 +28,11 @@ create table if not exists a2_events (
|
|
|
27
28
|
event_id text not null,
|
|
28
29
|
created_at integer not null,
|
|
29
30
|
cause text,
|
|
31
|
+
lane text,
|
|
32
|
+
lane_ready integer not null default 0,
|
|
30
33
|
processed_at integer,
|
|
31
34
|
processed_by_attempt integer,
|
|
35
|
+
returned_event_ids text,
|
|
32
36
|
first_claimed_at integer,
|
|
33
37
|
last_claimed_at integer,
|
|
34
38
|
attempt_count integer not null default 0,
|
|
@@ -37,18 +41,22 @@ create table if not exists a2_events (
|
|
|
37
41
|
last_failed_attempt integer,
|
|
38
42
|
last_error text,
|
|
39
43
|
failed_at integer,
|
|
44
|
+
claim_holder text,
|
|
45
|
+
claim_expires_at integer,
|
|
40
46
|
primary key (session_id, idx)
|
|
41
47
|
) strict;
|
|
42
48
|
|
|
43
49
|
create unique index if not exists a2_events_event_id on a2_events (event_id);
|
|
44
50
|
create index if not exists a2_events_unprocessed
|
|
45
51
|
on a2_events (session_id, idx) where processed_at is null;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
create index if not exists a2_events_lane_pending
|
|
53
|
+
on a2_events (session_id, lane, idx) where processed_at is null;
|
|
54
|
+
create index if not exists a2_events_dispatch_ready
|
|
55
|
+
on a2_events (session_id, idx)
|
|
56
|
+
where lane_ready = 1 and processed_at is null and failed_at is null;
|
|
57
|
+
create index if not exists a2_events_claim_expiry
|
|
58
|
+
on a2_events (session_id, claim_expires_at)
|
|
59
|
+
where lane_ready = 1 and processed_at is null and failed_at is null;
|
|
52
60
|
|
|
53
61
|
create table if not exists a2_snapshots (
|
|
54
62
|
session_id text not null,
|
|
@@ -88,9 +96,11 @@ const toCause = (json) => {
|
|
|
88
96
|
if (json === null) return null;
|
|
89
97
|
const value = JSON.parse(json);
|
|
90
98
|
if (!Number.isInteger(value.index) || Number(value.index) < 1 || !Number.isInteger(value.attempt) || Number(value.attempt) < 1) throw new TypeError("stored event has an invalid cause");
|
|
99
|
+
if (value.batchSize !== void 0 && (!Number.isInteger(value.batchSize) || Number(value.batchSize) < 1)) throw new TypeError("stored event has an invalid cause");
|
|
91
100
|
return {
|
|
92
101
|
index: Number(value.index),
|
|
93
|
-
attempt: Number(value.attempt)
|
|
102
|
+
attempt: Number(value.attempt),
|
|
103
|
+
...value.batchSize === void 0 ? {} : { batchSize: Number(value.batchSize) }
|
|
94
104
|
};
|
|
95
105
|
};
|
|
96
106
|
const toStored = (row) => ({
|
|
@@ -101,8 +111,10 @@ const toStored = (row) => ({
|
|
|
101
111
|
sessionId: row.session_id,
|
|
102
112
|
createdAt: new Date(Number(row.created_at)),
|
|
103
113
|
cause: toCause(row.cause),
|
|
114
|
+
lane: row.lane,
|
|
104
115
|
processedAt: toDate(row.processed_at),
|
|
105
116
|
processedByAttempt: row.processed_by_attempt === null ? null : Number(row.processed_by_attempt),
|
|
117
|
+
returnedEventIds: row.returned_event_ids === null ? null : JSON.parse(row.returned_event_ids),
|
|
106
118
|
firstClaimedAt: toDate(row.first_claimed_at),
|
|
107
119
|
lastClaimedAt: toDate(row.last_claimed_at),
|
|
108
120
|
attemptCount: Number(row.attempt_count),
|
|
@@ -110,7 +122,9 @@ const toStored = (row) => ({
|
|
|
110
122
|
lastFailedAt: toDate(row.last_failed_at),
|
|
111
123
|
lastFailedAttempt: row.last_failed_attempt === null ? null : Number(row.last_failed_attempt),
|
|
112
124
|
lastError: row.last_error,
|
|
113
|
-
failedAt: toDate(row.failed_at)
|
|
125
|
+
failedAt: toDate(row.failed_at),
|
|
126
|
+
claimHolder: row.claim_holder,
|
|
127
|
+
claimExpiresAt: toDate(row.claim_expires_at)
|
|
114
128
|
});
|
|
115
129
|
const toEvent = (row) => ({
|
|
116
130
|
id: row.event_id,
|
|
@@ -120,9 +134,6 @@ const toEvent = (row) => ({
|
|
|
120
134
|
sessionId: row.session_id,
|
|
121
135
|
createdAt: new Date(Number(row.created_at))
|
|
122
136
|
});
|
|
123
|
-
const requireChange = (changes, what) => {
|
|
124
|
-
if (Number(changes) === 0) throw new TypeError(what);
|
|
125
|
-
};
|
|
126
137
|
function sqlite(options = {}) {
|
|
127
138
|
const path = options.path ?? ".a2/dev.db";
|
|
128
139
|
const clock = options.clock ?? SYSTEM_CLOCK;
|
|
@@ -138,11 +149,41 @@ function sqlite(options = {}) {
|
|
|
138
149
|
db.exec(SCHEMA);
|
|
139
150
|
const insertEvent = db.prepare(`insert into a2_events
|
|
140
151
|
(session_id, idx, event_type, payload, event_id, created_at,
|
|
141
|
-
cause)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
152
|
+
cause, lane, lane_ready, processed_at)
|
|
153
|
+
select ?, ?, ?, ?, ?, ?, ?, ?,
|
|
154
|
+
case
|
|
155
|
+
when ? is null then 1
|
|
156
|
+
when not exists (
|
|
157
|
+
select 1 from a2_events
|
|
158
|
+
where session_id = ? and lane = ? and processed_at is null
|
|
159
|
+
) then 1
|
|
160
|
+
else 0
|
|
161
|
+
end,
|
|
162
|
+
?`);
|
|
163
|
+
const promoteLaneHead = db.prepare(`update a2_events set lane_ready = 1
|
|
164
|
+
where session_id = ?
|
|
165
|
+
and idx = (
|
|
166
|
+
select idx from a2_events
|
|
167
|
+
where session_id = ? and lane = ? and processed_at is null
|
|
168
|
+
order by idx limit 1
|
|
169
|
+
)`);
|
|
170
|
+
const appendStatus = db.prepare(`select coalesce(
|
|
171
|
+
(select max(idx) from a2_events where session_id = ?),
|
|
172
|
+
0
|
|
173
|
+
) as max,
|
|
174
|
+
exists(
|
|
175
|
+
select 1 from a2_events
|
|
176
|
+
where session_id = ? and processed_at is null
|
|
177
|
+
) as has_pending`);
|
|
178
|
+
const selectByIds = (count) => db.prepare(`select stored.*,
|
|
179
|
+
exists(
|
|
180
|
+
select 1 from a2_events pending
|
|
181
|
+
where pending.session_id = ?
|
|
182
|
+
and pending.processed_at is null
|
|
183
|
+
) as session_has_pending
|
|
184
|
+
from a2_events stored
|
|
185
|
+
where stored.event_id in (${Array.from({ length: count }, () => "?").join(", ")})
|
|
186
|
+
order by stored.idx`);
|
|
146
187
|
const tx = (fn) => {
|
|
147
188
|
db.exec("begin immediate");
|
|
148
189
|
try {
|
|
@@ -158,45 +199,62 @@ function sqlite(options = {}) {
|
|
|
158
199
|
};
|
|
159
200
|
return {
|
|
160
201
|
async append(sessionId, events) {
|
|
161
|
-
if (events.length === 0) return
|
|
202
|
+
if (events.length === 0) return wrap(() => {
|
|
203
|
+
const status = appendStatus.get(sessionId, sessionId);
|
|
204
|
+
return {
|
|
205
|
+
events: [],
|
|
206
|
+
hasPending: Number(status.has_pending) === 1
|
|
207
|
+
};
|
|
208
|
+
});
|
|
162
209
|
return wrap(() => tx(() => {
|
|
163
210
|
const suppliedIds = events.filter((e) => e.id !== void 0).map((e) => e.id);
|
|
164
211
|
if (new Set(suppliedIds).size !== suppliedIds.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "batch contains the same event id more than once");
|
|
165
212
|
if (suppliedIds.length > 0) {
|
|
166
|
-
const existing = selectByIds(suppliedIds.length).all(...suppliedIds);
|
|
213
|
+
const existing = selectByIds(suppliedIds.length).all(sessionId, ...suppliedIds);
|
|
167
214
|
if (existing.length > 0) {
|
|
168
215
|
const foreign = existing.find((row) => row.session_id !== sessionId);
|
|
169
216
|
if (foreign) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${foreign.event_id}' already exists in another session`);
|
|
170
|
-
if (existing.length === events.length) return
|
|
217
|
+
if (existing.length === events.length) return {
|
|
218
|
+
events: idempotentReplay(events, existing.map(toStored)),
|
|
219
|
+
hasPending: Number(existing[0].session_has_pending) === 1
|
|
220
|
+
};
|
|
171
221
|
throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
|
|
172
222
|
}
|
|
173
223
|
}
|
|
174
|
-
const
|
|
224
|
+
const status = appendStatus.get(sessionId, sessionId);
|
|
225
|
+
const base = Number(status.max);
|
|
175
226
|
const now = clock.now().getTime();
|
|
176
|
-
return
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
227
|
+
return {
|
|
228
|
+
events: events.map((e, i) => {
|
|
229
|
+
const id = e.id ?? generateId();
|
|
230
|
+
const index = base + 1 + i;
|
|
231
|
+
insertEvent.run(sessionId, index, e.type, JSON.stringify(e.payload) ?? "null", id, now, e.cause ? JSON.stringify(e.cause) : null, e.lane ?? null, e.lane ?? null, sessionId, e.lane ?? null, e.settled ? now : null);
|
|
232
|
+
return {
|
|
233
|
+
id,
|
|
234
|
+
type: e.type,
|
|
235
|
+
payload: structuredClone(e.payload),
|
|
236
|
+
index,
|
|
237
|
+
sessionId,
|
|
238
|
+
createdAt: new Date(now),
|
|
239
|
+
cause: e.cause ? { ...e.cause } : null,
|
|
240
|
+
lane: e.lane ?? null,
|
|
241
|
+
processedAt: e.settled ? new Date(now) : null,
|
|
242
|
+
processedByAttempt: null,
|
|
243
|
+
returnedEventIds: null,
|
|
244
|
+
firstClaimedAt: null,
|
|
245
|
+
lastClaimedAt: null,
|
|
246
|
+
attemptCount: 0,
|
|
247
|
+
failureCount: 0,
|
|
248
|
+
lastFailedAt: null,
|
|
249
|
+
lastFailedAttempt: null,
|
|
250
|
+
lastError: null,
|
|
251
|
+
failedAt: null,
|
|
252
|
+
claimHolder: null,
|
|
253
|
+
claimExpiresAt: null
|
|
254
|
+
};
|
|
255
|
+
}),
|
|
256
|
+
hasPending: Number(status.has_pending) === 1 || events.some((event) => event.settled !== true)
|
|
257
|
+
};
|
|
200
258
|
}));
|
|
201
259
|
},
|
|
202
260
|
async read(sessionId, opts) {
|
|
@@ -207,74 +265,116 @@ function sqlite(options = {}) {
|
|
|
207
265
|
conditions.push("idx > ?");
|
|
208
266
|
params.push(opts.afterIndex);
|
|
209
267
|
}
|
|
210
|
-
if (opts?.unprocessedOnly) conditions.push("processed_at is null");
|
|
211
268
|
return db.prepare(`select * from a2_events where ${conditions.join(" and ")} order by idx`).all(...params).map(toStored);
|
|
212
269
|
});
|
|
213
270
|
},
|
|
214
|
-
async
|
|
271
|
+
async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs, excludeIndexes = [] }) {
|
|
215
272
|
return wrap(() => tx(() => {
|
|
216
|
-
const next = db.prepare(`select * from a2_events
|
|
217
|
-
where session_id = ? and processed_at is null
|
|
218
|
-
order by idx limit 1`).get(sessionId);
|
|
219
|
-
if (!next || next.failed_at !== null || maxIndex !== void 0 && Number(next.idx) > maxIndex) return { outcome: "settled" };
|
|
220
273
|
const now = clock.now().getTime();
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
db.prepare(`
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
274
|
+
const expiresAt = expiresAtMs ?? now + ttlMs;
|
|
275
|
+
const exclusion = excludeIndexes.length === 0 ? "" : `and event.idx not in (${excludeIndexes.map(() => "?").join(", ")})`;
|
|
276
|
+
const eligible = db.prepare(`select event.idx
|
|
277
|
+
from a2_events event
|
|
278
|
+
where event.session_id = ?
|
|
279
|
+
and event.processed_at is null
|
|
280
|
+
and event.failed_at is null
|
|
281
|
+
and event.lane_ready = 1
|
|
282
|
+
and (event.claim_expires_at is null or event.claim_expires_at <= ?)
|
|
283
|
+
${exclusion}
|
|
284
|
+
order by event.idx`).all(sessionId, now, ...excludeIndexes);
|
|
285
|
+
if (eligible.length > 0) {
|
|
286
|
+
const update = db.prepare(`update a2_events set
|
|
287
|
+
attempt_count = attempt_count + 1,
|
|
288
|
+
first_claimed_at = coalesce(first_claimed_at, ?),
|
|
289
|
+
last_claimed_at = ?,
|
|
290
|
+
claim_holder = ?,
|
|
291
|
+
claim_expires_at = ?
|
|
292
|
+
where session_id = ? and idx = ?
|
|
293
|
+
returning *`);
|
|
294
|
+
return {
|
|
295
|
+
outcome: "claimed",
|
|
296
|
+
events: eligible.map((row) => {
|
|
297
|
+
const claimed = update.get(now, now, holder, expiresAt, sessionId, row.idx);
|
|
298
|
+
return toStored(claimed);
|
|
299
|
+
})
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
const active = db.prepare(`select min(claim_expires_at) as retry_at
|
|
303
|
+
from a2_events
|
|
304
|
+
where session_id = ?
|
|
305
|
+
and processed_at is null
|
|
306
|
+
and failed_at is null
|
|
307
|
+
and lane_ready = 1
|
|
308
|
+
and claim_expires_at > ?`).get(sessionId, now);
|
|
309
|
+
return active.retry_at === null ? { outcome: "settled" } : {
|
|
310
|
+
outcome: "busy",
|
|
311
|
+
retryAt: new Date(Number(active.retry_at))
|
|
237
312
|
};
|
|
238
313
|
}));
|
|
239
314
|
},
|
|
240
|
-
async
|
|
315
|
+
async renewClaims({ sessionId, holder, indexes, ttlMs, expiresAtMs }) {
|
|
316
|
+
if (indexes.length === 0) return [];
|
|
241
317
|
return wrap(() => tx(() => {
|
|
242
318
|
const now = clock.now().getTime();
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
319
|
+
const expiresAt = expiresAtMs ?? now + ttlMs;
|
|
320
|
+
const placeholders = indexes.map(() => "?").join(", ");
|
|
321
|
+
return db.prepare(`update a2_events set claim_expires_at = ?
|
|
322
|
+
where session_id = ?
|
|
323
|
+
and idx in (${placeholders})
|
|
324
|
+
and processed_at is null
|
|
325
|
+
and failed_at is null
|
|
326
|
+
and claim_holder = ?
|
|
327
|
+
and claim_expires_at > ?
|
|
328
|
+
returning idx`).all(expiresAt, sessionId, ...indexes, holder, now).map((row) => Number(row.idx)).toSorted((a, b) => a - b);
|
|
329
|
+
}));
|
|
330
|
+
},
|
|
331
|
+
async completeAttempt({ sessionId, index, attempt, events }) {
|
|
332
|
+
return wrap(() => tx(() => {
|
|
333
|
+
const parent = db.prepare("select * from a2_events where session_id = ? and idx = ?").get(sessionId, index);
|
|
334
|
+
if (!parent) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
|
|
335
|
+
const ids = events.map((event) => event.id);
|
|
336
|
+
if (new Set(ids).size !== ids.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains the same event id more than once");
|
|
337
|
+
if (parent.processed_at !== null) {
|
|
338
|
+
if (Number(parent.processed_by_attempt) !== attempt) return { outcome: "superseded" };
|
|
339
|
+
const returnedIds = parent.returned_event_ids === null ? null : JSON.parse(parent.returned_event_ids);
|
|
340
|
+
if (returnedIds === null || returnedIds.length !== ids.length || returnedIds.some((id, offset) => id !== ids[offset])) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "completed attempt does not match the returned event batch");
|
|
341
|
+
const existing = ids.length === 0 ? [] : selectByIds(ids.length).all(sessionId, ...ids);
|
|
342
|
+
const byId = new Map(existing.map((row) => [row.event_id, row]));
|
|
343
|
+
const ordered = ids.map((id) => byId.get(id));
|
|
344
|
+
if (ordered.some((row) => !row || row.session_id !== sessionId || row.cause === null || toCause(row.cause)?.index !== index || toCause(row.cause)?.attempt !== attempt)) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event retry does not match the committed batch");
|
|
345
|
+
return {
|
|
346
|
+
outcome: "completed",
|
|
347
|
+
events: idempotentReplay(events, ordered.map(toStored))
|
|
348
|
+
};
|
|
250
349
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (
|
|
350
|
+
if (Number(parent.attempt_count) !== attempt || parent.claim_holder === null || parent.failed_at !== null) return { outcome: "superseded" };
|
|
351
|
+
if (ids.length > 0 && selectByIds(ids.length).all(sessionId, ...ids).length > 0) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains an already-appended event id");
|
|
352
|
+
const status = appendStatus.get(sessionId, sessionId);
|
|
353
|
+
const now = clock.now().getTime();
|
|
354
|
+
db.prepare(`update a2_events set
|
|
355
|
+
processed_at = ?,
|
|
356
|
+
processed_by_attempt = ?,
|
|
357
|
+
returned_event_ids = ?,
|
|
358
|
+
lane_ready = 0,
|
|
359
|
+
claim_holder = null,
|
|
360
|
+
claim_expires_at = null
|
|
361
|
+
where session_id = ? and idx = ?`).run(now, attempt, JSON.stringify(ids), sessionId, index);
|
|
362
|
+
if (parent.lane !== null) promoteLaneHead.run(sessionId, sessionId, parent.lane);
|
|
264
363
|
return {
|
|
265
|
-
outcome: "
|
|
266
|
-
|
|
364
|
+
outcome: "completed",
|
|
365
|
+
events: events.map((event, offset) => {
|
|
366
|
+
const id = event.id;
|
|
367
|
+
const childIndex = Number(status.max) + offset + 1;
|
|
368
|
+
const cause = {
|
|
369
|
+
index,
|
|
370
|
+
attempt
|
|
371
|
+
};
|
|
372
|
+
insertEvent.run(sessionId, childIndex, event.type, JSON.stringify(event.payload) ?? "null", id, now, JSON.stringify(cause), event.lane ?? null, event.lane ?? null, sessionId, event.lane ?? null, event.settled ? now : null);
|
|
373
|
+
return toStored(db.prepare("select * from a2_events where session_id = ? and idx = ?").get(sessionId, childIndex));
|
|
374
|
+
})
|
|
267
375
|
};
|
|
268
376
|
}));
|
|
269
377
|
},
|
|
270
|
-
async markProcessed(sessionId, index) {
|
|
271
|
-
wrap(() => {
|
|
272
|
-
const { changes } = db.prepare(`update a2_events
|
|
273
|
-
set processed_at = coalesce(processed_at, ?)
|
|
274
|
-
where session_id = ? and idx = ?`).run(clock.now().getTime(), sessionId, index);
|
|
275
|
-
requireChange(changes, `no event at index ${index} in session '${sessionId}'`);
|
|
276
|
-
});
|
|
277
|
-
},
|
|
278
378
|
async failAttempt({ sessionId, index, attempt, error, maxFailures }) {
|
|
279
379
|
return wrap(() => tx(() => {
|
|
280
380
|
const current = db.prepare("select * from a2_events where session_id = ? and idx = ?").get(sessionId, index);
|
|
@@ -288,6 +388,14 @@ function sqlite(options = {}) {
|
|
|
288
388
|
outcome: "dead_lettered",
|
|
289
389
|
failureCount
|
|
290
390
|
};
|
|
391
|
+
if (current.claim_holder === null && current.last_failed_attempt !== null && Number(current.last_failed_attempt) === attempt) return {
|
|
392
|
+
outcome: "failed",
|
|
393
|
+
failureCount
|
|
394
|
+
};
|
|
395
|
+
if (current.claim_holder === null) return {
|
|
396
|
+
outcome: "superseded",
|
|
397
|
+
failureCount
|
|
398
|
+
};
|
|
291
399
|
const now = clock.now().getTime();
|
|
292
400
|
const nextFailureCount = failureCount + 1;
|
|
293
401
|
const deadLettered = nextFailureCount >= maxFailures;
|
|
@@ -296,7 +404,9 @@ function sqlite(options = {}) {
|
|
|
296
404
|
last_error = ?,
|
|
297
405
|
last_failed_at = ?,
|
|
298
406
|
last_failed_attempt = ?,
|
|
299
|
-
failed_at =
|
|
407
|
+
failed_at = ?,
|
|
408
|
+
claim_holder = null,
|
|
409
|
+
claim_expires_at = null
|
|
300
410
|
where session_id = ? and idx = ?`).run(nextFailureCount, error, now, attempt, deadLettered ? now : null, sessionId, index);
|
|
301
411
|
return {
|
|
302
412
|
outcome: deadLettered ? "dead_lettered" : "failed",
|
|
@@ -304,12 +414,6 @@ function sqlite(options = {}) {
|
|
|
304
414
|
};
|
|
305
415
|
}));
|
|
306
416
|
},
|
|
307
|
-
async markFailed(sessionId, index) {
|
|
308
|
-
wrap(() => {
|
|
309
|
-
const { changes } = db.prepare("update a2_events set failed_at = ? where session_id = ? and idx = ?").run(clock.now().getTime(), sessionId, index);
|
|
310
|
-
requireChange(changes, `no event at index ${index} in session '${sessionId}'`);
|
|
311
|
-
});
|
|
312
|
-
},
|
|
313
417
|
async readState(sessionId, reducerName) {
|
|
314
418
|
return wrap(() => {
|
|
315
419
|
const rows = db.prepare(`with snapshot as materialized (
|
|
@@ -322,12 +426,15 @@ function sqlite(options = {}) {
|
|
|
322
426
|
snapshot.state as snapshot_state,
|
|
323
427
|
null as session_id, null as idx, null as event_type,
|
|
324
428
|
null as payload, null as event_id, null as created_at,
|
|
325
|
-
null as cause, null as
|
|
326
|
-
null as
|
|
429
|
+
null as cause, null as lane, null as lane_ready,
|
|
430
|
+
null as processed_at,
|
|
431
|
+
null as processed_by_attempt, null as returned_event_ids,
|
|
432
|
+
null as first_claimed_at,
|
|
327
433
|
null as last_claimed_at, null as attempt_count,
|
|
328
434
|
null as failure_count, null as last_failed_at,
|
|
329
435
|
null as last_failed_attempt, null as last_error,
|
|
330
|
-
null as failed_at
|
|
436
|
+
null as failed_at, null as claim_holder,
|
|
437
|
+
null as claim_expires_at
|
|
331
438
|
from snapshot
|
|
332
439
|
union all
|
|
333
440
|
select 1 as row_order, 'event' as row_kind,
|
|
@@ -423,24 +530,6 @@ function sqlite(options = {}) {
|
|
|
423
530
|
});
|
|
424
531
|
return pollingStream(readAfter, opts?.startAt !== void 0 ? { startAt: opts.startAt } : {});
|
|
425
532
|
},
|
|
426
|
-
lease: {
|
|
427
|
-
async acquire({ sessionId, holder, ttlMs, expiresAtMs }) {
|
|
428
|
-
return wrap(() => tx(() => {
|
|
429
|
-
const now = clock.now().getTime();
|
|
430
|
-
const expiresAt = expiresAtMs ?? now + ttlMs;
|
|
431
|
-
const current = db.prepare("select * from a2_leases where session_id = ?").get(sessionId);
|
|
432
|
-
if (current && Number(current.expires_at) > now && current.holder !== holder) return false;
|
|
433
|
-
db.prepare(`insert into a2_leases (session_id, holder, expires_at) values (?, ?, ?)
|
|
434
|
-
on conflict (session_id) do update set holder = excluded.holder, expires_at = excluded.expires_at`).run(sessionId, holder, expiresAt);
|
|
435
|
-
return true;
|
|
436
|
-
}));
|
|
437
|
-
},
|
|
438
|
-
async release({ sessionId, holder }) {
|
|
439
|
-
wrap(() => {
|
|
440
|
-
db.prepare("delete from a2_leases where session_id = ? and holder = ?").run(sessionId, holder);
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
},
|
|
444
533
|
close() {
|
|
445
534
|
db.close();
|
|
446
535
|
}
|
package/dist/otel.d.ts
CHANGED
package/dist/otel.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SpanStatusCode, trace } from "@opentelemetry/api";
|
|
2
2
|
//#region src/otel.ts
|
|
3
3
|
/**
|
|
4
|
-
* a2/otel — OpenTelemetry adapter for the A2Telemetry interface.
|
|
4
|
+
* experimental-a2/otel — OpenTelemetry adapter for the A2Telemetry interface.
|
|
5
5
|
*
|
|
6
6
|
* Peer-depends on `@opentelemetry/api` (optional). Spans nest through
|
|
7
7
|
* the active context, so with a context manager registered (any real
|
package/dist/react.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as EventDefs, c as Reducer, r as ContractEvent, t as AppendInput } from "./contract-C_3dIIEU.js";
|
|
2
2
|
import { A2Client, Connection, PushResult } from "./client.js";
|
|
3
3
|
import { ReactElement, ReactNode } from "react";
|
|
4
4
|
//#region src/react.d.ts
|
package/dist/react.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createClient } from "./client.js";
|
|
|
3
3
|
import { createContext, createElement, useContext, useEffect, useMemo, useSyncExternalStore } from "react";
|
|
4
4
|
//#region src/react.ts
|
|
5
5
|
/**
|
|
6
|
-
* a2/react — React bindings over a2/client.
|
|
6
|
+
* experimental-a2/react — React bindings over experimental-a2/client.
|
|
7
7
|
*
|
|
8
8
|
* `createReact({ client })` is a factory (like createContext): call it
|
|
9
9
|
* once in a `'use client'` module with a shared A2 client and export the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as Clock } from "./log-
|
|
1
|
+
import { i as Clock } from "./log-ldf5g8Cx.js";
|
|
2
2
|
import { A2Recovery } from "./server.js";
|
|
3
3
|
//#region src/recovery-vercel.d.ts
|
|
4
4
|
/**
|
|
@@ -47,7 +47,7 @@ type VercelQueuesOptions = {
|
|
|
47
47
|
topic?: string;
|
|
48
48
|
/**
|
|
49
49
|
* Fallback delay when an arm does not supply an absolute due time.
|
|
50
|
-
* Core A2 arms
|
|
50
|
+
* Core A2 arms claim windows explicitly. Default 5.
|
|
51
51
|
*/
|
|
52
52
|
delaySeconds?: number;
|
|
53
53
|
/** Injectable transport (tests). Default: `@vercel/queue`. */
|
package/dist/recovery-vercel.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { i as serverInternals, t as DRAIN_TIMINGS } from "./internal-
|
|
1
|
+
import { i as serverInternals, t as DRAIN_TIMINGS } from "./internal-D6wNxTck.js";
|
|
2
2
|
import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
|
|
3
3
|
import { n as SYSTEM_CLOCK } from "./log-yJbXUf72.js";
|
|
4
4
|
//#region src/recovery-vercel.ts
|
|
5
5
|
/**
|
|
6
|
-
* a2/recovery-vercel — queue-backed recovery over Vercel Queues.
|
|
6
|
+
* experimental-a2/recovery-vercel — queue-backed recovery over Vercel Queues.
|
|
7
7
|
*
|
|
8
8
|
* `vercelQueues()` returns an `A2Recovery` (a2-implementation.md §9):
|
|
9
9
|
*
|
|
10
10
|
* - `arm` rounds the requested watchdog time up to a one-second slot.
|
|
11
11
|
* One message per `(contract, session, dueAt)` coalesces appends,
|
|
12
|
-
*
|
|
12
|
+
* claim renewals and racing callbacks. `DuplicateMessageError` is
|
|
13
13
|
* success because that due-time slot is already durable.
|
|
14
14
|
* - `handler(...servers)` is the delivery route: look up the server for
|
|
15
15
|
* the contract named in the message and drain it. Settled drains ack;
|
|
@@ -36,7 +36,7 @@ const MIN_VISIBILITY_SECONDS = 30;
|
|
|
36
36
|
const unsettledBackoffSeconds = (delaySeconds, deliveryCount) => Math.min(Math.max(delaySeconds * 2 ** Math.max(deliveryCount - 1, 0), MIN_VISIBILITY_SECONDS), 60);
|
|
37
37
|
const vercelQueueTransport = async (delaySeconds) => {
|
|
38
38
|
const queue = await import("@vercel/queue").catch(() => {
|
|
39
|
-
throw new Error("a2/recovery-vercel needs the '@vercel/queue' package (optional peer dependency) — install it, or inject a transport");
|
|
39
|
+
throw new Error("experimental-a2/recovery-vercel needs the '@vercel/queue' package (optional peer dependency) — install it, or inject a transport");
|
|
40
40
|
});
|
|
41
41
|
return {
|
|
42
42
|
send: (topic, message, options) => queue.send(topic, message, options),
|
|
@@ -73,10 +73,10 @@ function vercelQueues(options = {}) {
|
|
|
73
73
|
}
|
|
74
74
|
};
|
|
75
75
|
const successorDueAt = (message) => {
|
|
76
|
-
const minimumDueAt = clock.now().getTime() + DRAIN_TIMINGS.
|
|
76
|
+
const minimumDueAt = clock.now().getTime() + DRAIN_TIMINGS.claimTtlMs + DRAIN_TIMINGS.recoveryGraceMs;
|
|
77
77
|
if (message.dueAt === void 0) return minimumDueAt;
|
|
78
|
-
const beats = Math.max(1, Math.ceil((minimumDueAt - message.dueAt) / DRAIN_TIMINGS.
|
|
79
|
-
return message.dueAt + beats * DRAIN_TIMINGS.
|
|
78
|
+
const beats = Math.max(1, Math.ceil((minimumDueAt - message.dueAt) / DRAIN_TIMINGS.claimHeartbeatMs));
|
|
79
|
+
return message.dueAt + beats * DRAIN_TIMINGS.claimHeartbeatMs;
|
|
80
80
|
};
|
|
81
81
|
return {
|
|
82
82
|
async arm({ contract, sessionId, dueAt }) {
|
|
@@ -97,11 +97,10 @@ function vercelQueues(options = {}) {
|
|
|
97
97
|
const nextDueAt = successorDueAt(message);
|
|
98
98
|
const result = internals ? await internals.recoveryDrain(message.sessionId, { recoveryDueAt: nextDueAt }) : {
|
|
99
99
|
...await server.drain(message.sessionId),
|
|
100
|
-
outcome: "unknown"
|
|
101
|
-
processed: 0
|
|
100
|
+
outcome: "unknown"
|
|
102
101
|
};
|
|
103
102
|
if (result.settled) return;
|
|
104
|
-
if (result.outcome === "busy"
|
|
103
|
+
if (result.outcome === "busy") {
|
|
105
104
|
if (result.recoveryArm) await result.recoveryArm;
|
|
106
105
|
else await sendWatchdog({
|
|
107
106
|
contract: message.contract,
|