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,60 @@
|
|
|
1
|
+
import { i as Clock } from "./log-Dg1I8NRr.js";
|
|
2
|
+
import { A2Recovery } from "./server.js";
|
|
3
|
+
//#region src/recovery-vercel.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The "not settled yet" signal for a stalled handler — an expected state,
|
|
6
|
+
* not a transport failure. The default transport classifies it in
|
|
7
|
+
* `@vercel/queue`'s `retry` option and reschedules the delivery quietly
|
|
8
|
+
* (`{ afterSeconds }` → visibility change → 200 response, no error log)
|
|
9
|
+
* instead of letting it surface as a thrown 500. Busy deliveries use a
|
|
10
|
+
* fresh watchdog instead. Custom transports should treat this class the
|
|
11
|
+
* same way when their protocol has a polite reschedule.
|
|
12
|
+
*/
|
|
13
|
+
declare class RecoveryUnsettled extends Error {}
|
|
14
|
+
/** The message a2 puts on the queue. Application state lives in the log. */
|
|
15
|
+
type RecoveryMessage = {
|
|
16
|
+
contract: string;
|
|
17
|
+
sessionId: string;
|
|
18
|
+
/** Whole-second epoch time this watchdog was scheduled to become visible. */
|
|
19
|
+
dueAt?: number;
|
|
20
|
+
};
|
|
21
|
+
type QueueDeliveryMetadata = {
|
|
22
|
+
messageId: string;
|
|
23
|
+
deliveryCount: number;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* What this module needs from a queue — `@vercel/queue`'s `send` and
|
|
27
|
+
* `handleCallback` match it structurally.
|
|
28
|
+
*/
|
|
29
|
+
type QueueTransport = {
|
|
30
|
+
send(topic: string, message: RecoveryMessage, options?: {
|
|
31
|
+
delaySeconds?: number;
|
|
32
|
+
idempotencyKey?: string;
|
|
33
|
+
}): Promise<unknown>;
|
|
34
|
+
/**
|
|
35
|
+
* Wrap the delivery handler into a route. The handler throws
|
|
36
|
+
* `RecoveryUnsettled` when the session needs redelivery — transports
|
|
37
|
+
* with a polite reschedule (visibility change) should use it there
|
|
38
|
+
* instead of surfacing an error; anything else thrown is a genuine
|
|
39
|
+
* failure and should stay loud.
|
|
40
|
+
*/
|
|
41
|
+
handleCallback(handler: (message: RecoveryMessage, metadata: QueueDeliveryMetadata) => Promise<void>): (req: Request) => Promise<Response>;
|
|
42
|
+
/** Is this send error the idempotency-key collision? (= success) */
|
|
43
|
+
isDuplicate(error: unknown): boolean;
|
|
44
|
+
};
|
|
45
|
+
type VercelQueuesOptions = {
|
|
46
|
+
/** Queue topic. One topic serves every contract. Default `'a2'`. */
|
|
47
|
+
topic?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Fallback delay when an arm does not supply an absolute due time.
|
|
50
|
+
* Core A2 arms lease windows explicitly. Default 5.
|
|
51
|
+
*/
|
|
52
|
+
delaySeconds?: number;
|
|
53
|
+
/** Injectable transport (tests). Default: `@vercel/queue`. */
|
|
54
|
+
transport?: QueueTransport;
|
|
55
|
+
/** Injectable clock for due-time calculations (tests). */
|
|
56
|
+
clock?: Clock;
|
|
57
|
+
};
|
|
58
|
+
declare function vercelQueues(options?: VercelQueuesOptions): A2Recovery;
|
|
59
|
+
//#endregion
|
|
60
|
+
export { QueueDeliveryMetadata, QueueTransport, RecoveryMessage, RecoveryUnsettled, VercelQueuesOptions, vercelQueues };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { i as serverInternals, t as DRAIN_TIMINGS } from "./internal-Dm8Ejnud.js";
|
|
2
|
+
import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
|
|
3
|
+
import { n as SYSTEM_CLOCK } from "./log-yJbXUf72.js";
|
|
4
|
+
//#region src/recovery-vercel.ts
|
|
5
|
+
/**
|
|
6
|
+
* a2/recovery-vercel — queue-backed recovery over Vercel Queues.
|
|
7
|
+
*
|
|
8
|
+
* `vercelQueues()` returns an `A2Recovery` (a2-implementation.md §9):
|
|
9
|
+
*
|
|
10
|
+
* - `arm` rounds the requested watchdog time up to a one-second slot.
|
|
11
|
+
* One message per `(contract, session, dueAt)` coalesces appends,
|
|
12
|
+
* lease renewals, and racing callbacks. `DuplicateMessageError` is
|
|
13
|
+
* success because that due-time slot is already durable.
|
|
14
|
+
* - `handler(...servers)` is the delivery route: look up the server for
|
|
15
|
+
* the contract named in the message and drain it. Settled drains ack;
|
|
16
|
+
* contention arms a later watchdog before acking; handler failure keeps
|
|
17
|
+
* the current message and redelivers with backoff.
|
|
18
|
+
*
|
|
19
|
+
* `@vercel/queue` is an optional peer dependency, imported lazily. The
|
|
20
|
+
* `transport` option is the injection seam — tests run a real little
|
|
21
|
+
* in-memory queue through the same interface.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* The "not settled yet" signal for a stalled handler — an expected state,
|
|
25
|
+
* not a transport failure. The default transport classifies it in
|
|
26
|
+
* `@vercel/queue`'s `retry` option and reschedules the delivery quietly
|
|
27
|
+
* (`{ afterSeconds }` → visibility change → 200 response, no error log)
|
|
28
|
+
* instead of letting it surface as a thrown 500. Busy deliveries use a
|
|
29
|
+
* fresh watchdog instead. Custom transports should treat this class the
|
|
30
|
+
* same way when their protocol has a polite reschedule.
|
|
31
|
+
*/
|
|
32
|
+
var RecoveryUnsettled = class extends Error {};
|
|
33
|
+
/** Vercel Queues rejects shorter visibility changes. */
|
|
34
|
+
const MIN_VISIBILITY_SECONDS = 30;
|
|
35
|
+
/** Redelivery backoff for retry states: base, 2×, 4×… bounded. */
|
|
36
|
+
const unsettledBackoffSeconds = (delaySeconds, deliveryCount) => Math.min(Math.max(delaySeconds * 2 ** Math.max(deliveryCount - 1, 0), MIN_VISIBILITY_SECONDS), 60);
|
|
37
|
+
const vercelQueueTransport = async (delaySeconds) => {
|
|
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");
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
send: (topic, message, options) => queue.send(topic, message, options),
|
|
43
|
+
handleCallback: (handler) => queue.handleCallback(handler, {
|
|
44
|
+
visibilityTimeoutSeconds: MIN_VISIBILITY_SECONDS,
|
|
45
|
+
retry: (error, metadata) => error instanceof RecoveryUnsettled ? { afterSeconds: unsettledBackoffSeconds(delaySeconds, metadata.deliveryCount) } : void 0
|
|
46
|
+
}),
|
|
47
|
+
isDuplicate: (error) => error instanceof queue.DuplicateMessageError
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
function vercelQueues(options = {}) {
|
|
51
|
+
const topic = options.topic ?? "a2";
|
|
52
|
+
const delaySeconds = options.delaySeconds ?? 5;
|
|
53
|
+
const clock = options.clock ?? SYSTEM_CLOCK;
|
|
54
|
+
const transport = retryableLazy(() => options.transport ? Promise.resolve(options.transport) : vercelQueueTransport(delaySeconds)).get;
|
|
55
|
+
const sendWatchdog = async (opts) => {
|
|
56
|
+
const t = await transport();
|
|
57
|
+
const nowMs = clock.now().getTime();
|
|
58
|
+
const requestedDueAt = opts.dueAt ?? nowMs + delaySeconds * 1e3;
|
|
59
|
+
const dueAt = Math.ceil(requestedDueAt / 1e3) * 1e3;
|
|
60
|
+
const recoveryId = `${opts.contract}:${opts.sessionId}:watchdog:${dueAt}`;
|
|
61
|
+
try {
|
|
62
|
+
await t.send(topic, {
|
|
63
|
+
contract: opts.contract,
|
|
64
|
+
sessionId: opts.sessionId,
|
|
65
|
+
dueAt
|
|
66
|
+
}, {
|
|
67
|
+
delaySeconds: Math.max(0, Math.ceil((dueAt - nowMs) / 1e3)),
|
|
68
|
+
idempotencyKey: recoveryId
|
|
69
|
+
});
|
|
70
|
+
} catch (err) {
|
|
71
|
+
if (t.isDuplicate(err)) return;
|
|
72
|
+
throw err;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const successorDueAt = (message) => {
|
|
76
|
+
const minimumDueAt = clock.now().getTime() + DRAIN_TIMINGS.leaseTtlMs + DRAIN_TIMINGS.recoveryGraceMs;
|
|
77
|
+
if (message.dueAt === void 0) return minimumDueAt;
|
|
78
|
+
const beats = Math.max(1, Math.ceil((minimumDueAt - message.dueAt) / DRAIN_TIMINGS.leaseHeartbeatMs));
|
|
79
|
+
return message.dueAt + beats * DRAIN_TIMINGS.leaseHeartbeatMs;
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
async arm({ contract, sessionId, dueAt }) {
|
|
83
|
+
await sendWatchdog({
|
|
84
|
+
contract,
|
|
85
|
+
sessionId,
|
|
86
|
+
...dueAt !== void 0 ? { dueAt } : {}
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
handler(...servers) {
|
|
90
|
+
const byContract = /* @__PURE__ */ new Map();
|
|
91
|
+
for (const server of servers) byContract.set(server.contract.name, server);
|
|
92
|
+
return async (req) => {
|
|
93
|
+
return (await transport()).handleCallback(async (message) => {
|
|
94
|
+
const server = byContract.get(message.contract);
|
|
95
|
+
if (!server) throw new Error(`a2 recovery: no server for contract '${message.contract}' — pass it to recovery.handler(...)`);
|
|
96
|
+
const internals = serverInternals.get(server);
|
|
97
|
+
const nextDueAt = successorDueAt(message);
|
|
98
|
+
const result = internals ? await internals.recoveryDrain(message.sessionId, { recoveryDueAt: nextDueAt }) : {
|
|
99
|
+
...await server.drain(message.sessionId),
|
|
100
|
+
outcome: "unknown",
|
|
101
|
+
processed: 0
|
|
102
|
+
};
|
|
103
|
+
if (result.settled) return;
|
|
104
|
+
if (result.outcome === "busy" || result.outcome === "handed_off") {
|
|
105
|
+
if (result.recoveryArm) await result.recoveryArm;
|
|
106
|
+
else await sendWatchdog({
|
|
107
|
+
contract: message.contract,
|
|
108
|
+
sessionId: message.sessionId,
|
|
109
|
+
dueAt: nextDueAt
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
throw new RecoveryUnsettled(`a2 recovery: session '${message.sessionId}' of '${message.contract}' not settled yet (${result.outcome}) — rescheduled for redelivery`);
|
|
114
|
+
})(req);
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { RecoveryUnsettled, vercelQueues };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/retryable-lazy.ts
|
|
2
|
+
const retryableLazy = (initialize) => {
|
|
3
|
+
let current = null;
|
|
4
|
+
const get = () => {
|
|
5
|
+
if (current) return current;
|
|
6
|
+
const attempt = Promise.resolve().then(initialize);
|
|
7
|
+
current = attempt;
|
|
8
|
+
attempt.catch(() => {
|
|
9
|
+
if (current === attempt) current = null;
|
|
10
|
+
});
|
|
11
|
+
return attempt;
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
get,
|
|
15
|
+
peek: () => current
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
export { retryableLazy as t };
|