experimental-a2 0.2.0 → 0.4.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 +169 -0
- package/dist/ai-B4YhEnfw.d.ts +333 -0
- package/dist/ai-server.d.ts +48 -9
- package/dist/ai-server.js +121 -49
- package/dist/ai.d.ts +2 -303
- package/dist/ai.js +231 -86
- package/dist/cli-B3VuxoDe.js +597 -0
- package/dist/cli-bin.d.ts +1 -0
- package/dist/cli-bin.js +5 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +2 -0
- package/dist/client-BrfDXQ8A.d.ts +155 -0
- package/dist/client-Bt4tAKi9.js +798 -0
- package/dist/client.d.ts +2 -90
- package/dist/client.js +1 -409
- package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +10 -2
- package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +62 -8
- package/dist/devtools-J_jZ2vQf.d.ts +151 -0
- package/dist/devtools-kJJaORn-.js +338 -0
- package/dist/devtools-server.browser.js +1 -1
- package/dist/devtools-server.d.ts +2 -2
- package/dist/devtools-server.js +224 -43
- package/dist/devtools.d.ts +2 -0
- package/dist/devtools.js +2 -0
- package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +4 -4
- package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +1 -1
- package/dist/http.d.ts +71 -13
- package/dist/http.js +302 -41
- package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +2 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +2 -1
- package/dist/internal-aEotMzu_.js +209 -0
- package/dist/otel.d.ts +1 -1
- package/dist/platform-B4TnJtWu.js +32 -0
- package/dist/react.d.ts +37 -14
- package/dist/react.js +26 -15
- package/dist/scheduler-qstash.d.ts +78 -0
- package/dist/scheduler-qstash.js +499 -0
- package/dist/scheduler-task-BpzhPnRS.js +54 -0
- package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +17 -24
- package/dist/scheduler-vercel.js +226 -0
- package/dist/server-CcNnFnoW.js +1405 -0
- package/dist/server-YtPq7hjw.d.ts +260 -0
- package/dist/server.d.ts +4 -155
- package/dist/server.js +2 -2
- package/dist/{log-ldf5g8Cx.d.ts → store-C3sNAaBT.d.ts} +111 -35
- package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +1 -1
- package/dist/store-codec-DTG0Ftek.js +8 -0
- package/dist/store-memory.d.ts +11 -0
- package/dist/{log-memory.js → store-memory.js} +127 -24
- package/dist/{log-polling-6COoN60V.js → store-polling-DgrrAE3d.js} +7 -6
- package/dist/{log-postgres.d.ts → store-postgres.d.ts} +6 -6
- package/dist/{log-postgres.js → store-postgres.js} +158 -24
- package/dist/{log-redis.js → store-redis-core-DWqx3F47.js} +294 -156
- package/dist/store-redis-http.d.ts +21 -0
- package/dist/store-redis-http.js +70 -0
- package/dist/store-redis.d.ts +37 -0
- package/dist/store-redis.js +298 -0
- package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +6 -6
- package/dist/{log-sqlite.js → store-sqlite.js} +116 -22
- package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +1 -1
- package/dist/testing.browser.d.ts +1 -0
- package/dist/testing.browser.js +4 -0
- package/dist/testing.d.ts +31 -0
- package/dist/testing.js +101 -0
- package/dist/wire-DCUZBUlT.js +222 -0
- package/docs/01-quickstart.mdx +4 -5
- package/docs/concepts/01-contracts.mdx +21 -17
- package/docs/concepts/02-handlers.mdx +7 -7
- package/docs/concepts/03-durability.mdx +26 -29
- package/docs/concepts/04-state.mdx +18 -21
- package/docs/guides/01-timers.mdx +154 -54
- package/docs/guides/02-cancellation.mdx +30 -4
- package/docs/guides/03-react.mdx +20 -21
- package/docs/guides/04-local-first.mdx +1 -1
- package/docs/guides/05-production.mdx +321 -60
- package/docs/guides/06-ai-agents.mdx +249 -49
- package/docs/guides/07-devtools.mdx +137 -12
- package/docs/guides/08-application-data.mdx +1 -1
- package/docs/guides/09-presence.mdx +284 -0
- package/docs/guides/10-transports.mdx +131 -0
- package/docs/index.mdx +22 -46
- package/docs/reference/01-api.mdx +751 -102
- package/docs/reference/02-errors.mdx +10 -5
- package/package.json +23 -6
- package/dist/internal-D6wNxTck.js +0 -36
- package/dist/log-memory.d.ts +0 -11
- package/dist/log-redis.d.ts +0 -31
- package/dist/recovery-vercel.js +0 -119
- package/dist/server-DJgD2YWP.js +0 -877
- package/dist/wire-BVsgR8o9.js +0 -62
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { t as A2Error } from "./errors-
|
|
1
|
+
import { t as A2Error } from "./errors-BQuJpe82.js";
|
|
2
|
+
import { n as pollingStream } from "./store-polling-DgrrAE3d.js";
|
|
2
3
|
import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
|
|
3
|
-
import { t as idempotentReplay } from "./idempotent-replay-
|
|
4
|
-
import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./
|
|
5
|
-
import { t as
|
|
6
|
-
//#region src/
|
|
4
|
+
import { t as idempotentReplay } from "./idempotent-replay-DuqEkYA7.js";
|
|
5
|
+
import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
|
|
6
|
+
import { t as decodeReturnedEventIds } from "./store-codec-DTG0Ftek.js";
|
|
7
|
+
//#region src/store-postgres.ts
|
|
7
8
|
/**
|
|
8
|
-
* experimental-a2/
|
|
9
|
+
* experimental-a2/store-postgres — postgres store backend (the production path).
|
|
9
10
|
*
|
|
10
|
-
* Implements the
|
|
11
|
+
* Implements the A2Store interface over any client that speaks the tiny
|
|
11
12
|
* `PostgresClient` shape below — a `pg` Pool matches it structurally
|
|
12
13
|
* (and is what `connectionString` creates, lazily; `pg` is an optional
|
|
13
14
|
* peer dependency). The conformance suite in test/conformance is the
|
|
@@ -69,6 +70,17 @@ create table if not exists a2_snapshots (
|
|
|
69
70
|
updated_at bigint not null,
|
|
70
71
|
primary key (session_id, reducer_name)
|
|
71
72
|
);
|
|
73
|
+
|
|
74
|
+
create table if not exists a2_presence (
|
|
75
|
+
ns text not null,
|
|
76
|
+
participant text not null,
|
|
77
|
+
field text not null,
|
|
78
|
+
value jsonb not null,
|
|
79
|
+
seen bigint not null,
|
|
80
|
+
at bigint not null,
|
|
81
|
+
expires_at bigint not null,
|
|
82
|
+
primary key (ns, participant, field)
|
|
83
|
+
);
|
|
72
84
|
`;
|
|
73
85
|
const asNumber = (value) => Number(value);
|
|
74
86
|
const toDate = (ms) => ms === null || ms === void 0 ? null : new Date(asNumber(ms));
|
|
@@ -94,7 +106,7 @@ const toStored = (row) => ({
|
|
|
94
106
|
lane: row["lane"] ?? null,
|
|
95
107
|
processedAt: toDate(row["processed_at"]),
|
|
96
108
|
processedByAttempt: row["processed_by_attempt"] === null || row["processed_by_attempt"] === void 0 ? null : asNumber(row["processed_by_attempt"]),
|
|
97
|
-
returnedEventIds: row["returned_event_ids"] === null || row["returned_event_ids"] === void 0 ? null :
|
|
109
|
+
returnedEventIds: row["returned_event_ids"] === null || row["returned_event_ids"] === void 0 ? null : decodeReturnedEventIds(row["returned_event_ids"]),
|
|
98
110
|
firstClaimedAt: toDate(row["first_claimed_at"]),
|
|
99
111
|
lastClaimedAt: toDate(row["last_claimed_at"]),
|
|
100
112
|
attemptCount: asNumber(row["attempt_count"]),
|
|
@@ -119,7 +131,7 @@ const wrap = async (fn) => {
|
|
|
119
131
|
return await fn();
|
|
120
132
|
} catch (err) {
|
|
121
133
|
if (err instanceof A2Error || err instanceof TypeError) throw err;
|
|
122
|
-
throw new A2Error("
|
|
134
|
+
throw new A2Error("STORE_UNAVAILABLE", "postgres store operation failed", { cause: err });
|
|
123
135
|
}
|
|
124
136
|
};
|
|
125
137
|
const jsonb = (value) => JSON.stringify(value) ?? "null";
|
|
@@ -151,7 +163,7 @@ function postgres(options = {}) {
|
|
|
151
163
|
let c = options.client;
|
|
152
164
|
if (!c) {
|
|
153
165
|
c = new (await (import("pg").catch(() => {
|
|
154
|
-
throw new A2Error("
|
|
166
|
+
throw new A2Error("STORE_NOT_CONFIGURED", "experimental-a2/store-postgres with a connectionString needs the 'pg' package (optional peer dependency) — install it, or inject a client");
|
|
155
167
|
}))).default.Pool({ connectionString: options.connectionString });
|
|
156
168
|
owned = c;
|
|
157
169
|
}
|
|
@@ -230,6 +242,19 @@ function postgres(options = {}) {
|
|
|
230
242
|
throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
|
|
231
243
|
}
|
|
232
244
|
}
|
|
245
|
+
const causeIndexes = [...new Set(events.filter((e) => e.cause !== void 0).map((e) => e.cause.index))];
|
|
246
|
+
if (causeIndexes.length > 0) {
|
|
247
|
+
const { rows: parents } = await query(`select idx, attempt_count, failed_at from a2_events
|
|
248
|
+
where session_id = $1 and idx = any($2::bigint[])
|
|
249
|
+
for update`, [sessionId, causeIndexes]);
|
|
250
|
+
const byIndex = new Map(parents.map((row) => [asNumber(row["idx"]), row]));
|
|
251
|
+
for (const e of events) {
|
|
252
|
+
if (!e.cause) continue;
|
|
253
|
+
const parent = byIndex.get(e.cause.index);
|
|
254
|
+
if (!parent) throw new TypeError(`no event at index ${e.cause.index} in session '${sessionId}'`);
|
|
255
|
+
if (asNumber(parent["attempt_count"]) !== e.cause.attempt || parent["failed_at"] !== null) throw new A2Error("SUPERSEDED_ATTEMPT", `attempt ${e.cause.attempt} no longer owns event ${e.cause.index} in session '${sessionId}'`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
233
258
|
const { rows: maxRows } = await query(`select coalesce(max(idx), 0) as max,
|
|
234
259
|
exists(
|
|
235
260
|
select 1 from a2_events pending
|
|
@@ -305,6 +330,10 @@ function postgres(options = {}) {
|
|
|
305
330
|
params.push(opts.afterIndex);
|
|
306
331
|
conditions.push(`idx > $${params.length}`);
|
|
307
332
|
}
|
|
333
|
+
if (opts?.throughIndex !== void 0) {
|
|
334
|
+
params.push(opts.throughIndex);
|
|
335
|
+
conditions.push(`idx <= $${params.length}`);
|
|
336
|
+
}
|
|
308
337
|
const { rows } = await c.query(`select * from a2_events where ${conditions.join(" and ")} order by idx`, params);
|
|
309
338
|
return rows.map(toStored);
|
|
310
339
|
});
|
|
@@ -356,27 +385,56 @@ function postgres(options = {}) {
|
|
|
356
385
|
};
|
|
357
386
|
}));
|
|
358
387
|
},
|
|
359
|
-
async renewClaims({ sessionId, holder,
|
|
360
|
-
if (
|
|
388
|
+
async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {
|
|
389
|
+
if (claims.length === 0) return {
|
|
390
|
+
renewed: [],
|
|
391
|
+
superseded: []
|
|
392
|
+
};
|
|
361
393
|
return wrap(async () => {
|
|
362
394
|
const c = await client();
|
|
363
395
|
const now = clock.now().getTime();
|
|
364
396
|
const expiresAt = expiresAtMs ?? now + ttlMs;
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
397
|
+
const indexes = claims.map((claim) => claim.index);
|
|
398
|
+
const attempts = new Map(claims.map((claim) => [claim.index, claim.attempt]));
|
|
399
|
+
const { rows } = await c.query(`with listed as (
|
|
400
|
+
select idx, attempt
|
|
401
|
+
from unnest($3::bigint[], $6::int[]) as listed(idx, attempt)
|
|
402
|
+
),
|
|
403
|
+
renewed as (
|
|
404
|
+
update a2_events event set claim_expires_at = $1
|
|
405
|
+
from listed
|
|
406
|
+
where event.session_id = $2
|
|
407
|
+
and event.idx = listed.idx
|
|
408
|
+
and event.attempt_count = listed.attempt
|
|
409
|
+
and event.processed_at is null
|
|
410
|
+
and event.failed_at is null
|
|
411
|
+
and event.claim_holder = $4
|
|
412
|
+
and event.claim_expires_at > $5
|
|
413
|
+
returning event.idx
|
|
414
|
+
)
|
|
415
|
+
select event.idx, event.attempt_count,
|
|
416
|
+
exists(select 1 from renewed where renewed.idx = event.idx)
|
|
417
|
+
as renewed
|
|
418
|
+
from a2_events event
|
|
419
|
+
where event.session_id = $2 and event.idx = any($3::bigint[])`, [
|
|
373
420
|
expiresAt,
|
|
374
421
|
sessionId,
|
|
375
422
|
indexes,
|
|
376
423
|
holder,
|
|
377
|
-
now
|
|
424
|
+
now,
|
|
425
|
+
claims.map((claim) => claim.attempt)
|
|
378
426
|
]);
|
|
379
|
-
|
|
427
|
+
const renewed = [];
|
|
428
|
+
const superseded = [];
|
|
429
|
+
for (const row of rows) {
|
|
430
|
+
const index = asNumber(row["idx"]);
|
|
431
|
+
if (row["renewed"] === true) renewed.push(index);
|
|
432
|
+
else if (asNumber(row["attempt_count"]) > (attempts.get(index) ?? 0)) superseded.push(index);
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
renewed: renewed.toSorted((a, b) => a - b),
|
|
436
|
+
superseded: superseded.toSorted((a, b) => a - b)
|
|
437
|
+
};
|
|
380
438
|
});
|
|
381
439
|
},
|
|
382
440
|
async completeAttempt({ sessionId, index, attempt, events }) {
|
|
@@ -390,7 +448,7 @@ function postgres(options = {}) {
|
|
|
390
448
|
if (new Set(ids).size !== ids.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains the same event id more than once");
|
|
391
449
|
if (parent["processed_at"] !== null) {
|
|
392
450
|
if (asNumber(parent["processed_by_attempt"]) !== attempt) return { outcome: "superseded" };
|
|
393
|
-
const returnedIds = parent["returned_event_ids"] === null || parent["returned_event_ids"] === void 0 ? null :
|
|
451
|
+
const returnedIds = parent["returned_event_ids"] === null || parent["returned_event_ids"] === void 0 ? null : decodeReturnedEventIds(parent["returned_event_ids"]);
|
|
394
452
|
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");
|
|
395
453
|
if (events.length === 0) return {
|
|
396
454
|
outcome: "completed",
|
|
@@ -592,6 +650,63 @@ function postgres(options = {}) {
|
|
|
592
650
|
]);
|
|
593
651
|
});
|
|
594
652
|
},
|
|
653
|
+
presence: {
|
|
654
|
+
async set(ns, participant, values, meta) {
|
|
655
|
+
const entries = Object.entries(values);
|
|
656
|
+
if (entries.length === 0) return;
|
|
657
|
+
await wrap(async () => {
|
|
658
|
+
const c = await client();
|
|
659
|
+
const atMs = meta.at.getTime();
|
|
660
|
+
const expiresAtMs = clock.now().getTime() + meta.ttlMs;
|
|
661
|
+
for (const [field, value] of entries) await (value === null ? c.query(`delete from a2_presence
|
|
662
|
+
where ns = $1 and participant = $2 and field = $3
|
|
663
|
+
and at <= $4`, [
|
|
664
|
+
ns,
|
|
665
|
+
participant,
|
|
666
|
+
field,
|
|
667
|
+
atMs
|
|
668
|
+
]) : c.query(`insert into a2_presence
|
|
669
|
+
(ns, participant, field, value, seen, at, expires_at)
|
|
670
|
+
values ($1, $2, $3, $4::jsonb, $5, $6, $7)
|
|
671
|
+
on conflict (ns, participant, field) do update set
|
|
672
|
+
value = excluded.value,
|
|
673
|
+
seen = excluded.seen,
|
|
674
|
+
at = excluded.at,
|
|
675
|
+
expires_at = excluded.expires_at
|
|
676
|
+
where a2_presence.at <= excluded.at`, [
|
|
677
|
+
ns,
|
|
678
|
+
participant,
|
|
679
|
+
field,
|
|
680
|
+
jsonb(value),
|
|
681
|
+
meta.seen,
|
|
682
|
+
atMs,
|
|
683
|
+
expiresAtMs
|
|
684
|
+
]));
|
|
685
|
+
});
|
|
686
|
+
},
|
|
687
|
+
async read(ns) {
|
|
688
|
+
return wrap(async () => {
|
|
689
|
+
const c = await client();
|
|
690
|
+
const now = clock.now().getTime();
|
|
691
|
+
const { rows } = await c.query(`with swept as (
|
|
692
|
+
delete from a2_presence
|
|
693
|
+
where ns = $1 and expires_at <= $2
|
|
694
|
+
)
|
|
695
|
+
select participant, field, value, seen, at, expires_at
|
|
696
|
+
from a2_presence
|
|
697
|
+
where ns = $1 and expires_at > $2
|
|
698
|
+
order by participant, field`, [ns, now]);
|
|
699
|
+
return rows.map((row) => ({
|
|
700
|
+
participant: row["participant"],
|
|
701
|
+
field: row["field"],
|
|
702
|
+
value: row["value"],
|
|
703
|
+
seen: asNumber(row["seen"]),
|
|
704
|
+
at: new Date(asNumber(row["at"])),
|
|
705
|
+
expiresAt: new Date(asNumber(row["expires_at"]))
|
|
706
|
+
}));
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
},
|
|
595
710
|
inspect: {
|
|
596
711
|
async listSessions(inspectionOptions) {
|
|
597
712
|
return wrap(async () => {
|
|
@@ -641,6 +756,25 @@ function postgres(options = {}) {
|
|
|
641
756
|
};
|
|
642
757
|
});
|
|
643
758
|
},
|
|
759
|
+
async readEvents(sessionId, pageOptions) {
|
|
760
|
+
return wrap(async () => {
|
|
761
|
+
const c = await client();
|
|
762
|
+
const currentFrontier = asNumber((await c.query("select coalesce(max(idx), 0) as frontier from a2_events where session_id = $1", [sessionId])).rows[0]?.["frontier"] ?? 0);
|
|
763
|
+
const throughIndex = Math.min(pageOptions.throughIndex ?? currentFrontier, currentFrontier);
|
|
764
|
+
const { rows } = await c.query(`select * from a2_events
|
|
765
|
+
where session_id = $1 and idx > $2 and idx <= $3
|
|
766
|
+
order by idx limit $4`, [
|
|
767
|
+
sessionId,
|
|
768
|
+
pageOptions.afterIndex,
|
|
769
|
+
throughIndex,
|
|
770
|
+
pageOptions.limit
|
|
771
|
+
]);
|
|
772
|
+
return {
|
|
773
|
+
events: rows.map(toStored),
|
|
774
|
+
throughIndex
|
|
775
|
+
};
|
|
776
|
+
});
|
|
777
|
+
},
|
|
644
778
|
async listSnapshots(sessionId) {
|
|
645
779
|
return wrap(async () => {
|
|
646
780
|
const { rows } = await (await client()).query(`select reducer_name, up_to_index, updated_at
|
|
@@ -658,7 +792,7 @@ function postgres(options = {}) {
|
|
|
658
792
|
const { rows } = await (await client()).query("select * from a2_events where session_id = $1 and idx > $2 order by idx", [sessionId, afterIndex]);
|
|
659
793
|
return rows.map(toStored);
|
|
660
794
|
});
|
|
661
|
-
return pollingStream(readAfter, opts?.
|
|
795
|
+
return pollingStream(readAfter, opts?.startAfter !== void 0 ? { startAfter: opts.startAfter } : {});
|
|
662
796
|
},
|
|
663
797
|
async close() {
|
|
664
798
|
const current = connection.peek();
|