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,23 @@
|
|
|
1
|
+
//#region src/errors.ts
|
|
2
|
+
var A2Error = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
/** e.g. the Standard Schema issues for INVALID_PAYLOAD */
|
|
5
|
+
details;
|
|
6
|
+
constructor(code, message, options) {
|
|
7
|
+
super(message, options && "cause" in options ? { cause: options.cause } : void 0);
|
|
8
|
+
this.name = "A2Error";
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.details = options?.details;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Wrap an arbitrary thrown value as LOG_UNAVAILABLE, preserving the
|
|
15
|
+
* original as `cause`. A2Errors pass through untouched — they already
|
|
16
|
+
* carry their own meaning.
|
|
17
|
+
*/
|
|
18
|
+
function asLogUnavailable(err) {
|
|
19
|
+
if (err instanceof A2Error) return err;
|
|
20
|
+
return new A2Error("LOG_UNAVAILABLE", "the log backend failed", { cause: err });
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { asLogUnavailable as n, A2Error as t };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/errors.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* a2 errors — one class, discriminated by `code`.
|
|
4
|
+
*
|
|
5
|
+
* See specs/a2-implementation.md §10 and docs/reference/02-errors.mdx.
|
|
6
|
+
* A single class serializes cleanly across the push-route boundary and
|
|
7
|
+
* keeps `instanceof` checks working after bundling.
|
|
8
|
+
*/
|
|
9
|
+
type A2ErrorCode = "INVALID_PAYLOAD" | "UNKNOWN_EVENT_TYPE" | "PARTIAL_DUPLICATE_BATCH" | "LOG_UNAVAILABLE" | "LOG_NOT_CONFIGURED";
|
|
10
|
+
declare class A2Error extends Error {
|
|
11
|
+
readonly code: A2ErrorCode;
|
|
12
|
+
/** e.g. the Standard Schema issues for INVALID_PAYLOAD */
|
|
13
|
+
readonly details: unknown;
|
|
14
|
+
constructor(code: A2ErrorCode, message: string, options?: {
|
|
15
|
+
details?: unknown;
|
|
16
|
+
cause?: unknown;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { A2ErrorCode as n, A2Error as t };
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { a as Event } from "./log-Dg1I8NRr.js";
|
|
2
|
+
import { PushedEvent } from "./server.js";
|
|
3
|
+
import { t as A2Error } from "./errors-xL_JTXsY.js";
|
|
4
|
+
//#region src/http.d.ts
|
|
5
|
+
/** The push envelope: what a client POSTs to append events. */
|
|
6
|
+
type PushBody = {
|
|
7
|
+
sessionId: string;
|
|
8
|
+
/** Branded: `session.append` accepts these directly (the push route
|
|
9
|
+
* path); schema validation still happens inside `append`. */
|
|
10
|
+
events: PushedEvent[];
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Validate the push envelope — `{ sessionId, events }` — throwing
|
|
14
|
+
* `INVALID_PAYLOAD` on a malformed body. Payload validation against the
|
|
15
|
+
* machine's schemas happens in `append`, not here.
|
|
16
|
+
*/
|
|
17
|
+
declare function parsePushBody(req: Request): Promise<PushBody>;
|
|
18
|
+
/**
|
|
19
|
+
* Pipe a live event iterable into an SSE `Response`. Each event is one
|
|
20
|
+
* frame — `id:` carries the log index, `data:` the JSON event. A
|
|
21
|
+
* disconnecting client cancels the stream, which closes the underlying
|
|
22
|
+
* subscription. Two kinds of comment frames ride along: a `: connected`
|
|
23
|
+
* prelude that flushes headers immediately, and a `: ping` heartbeat
|
|
24
|
+
* every 15 seconds so clients (and proxies) can tell a quiet stream
|
|
25
|
+
* from a dead connection — the session client's stall watchdog counts
|
|
26
|
+
* on it.
|
|
27
|
+
*/
|
|
28
|
+
declare function sseResponse(iterable: AsyncIterable<Event>): Response;
|
|
29
|
+
/**
|
|
30
|
+
* Serialize an error into the documented wire shape,
|
|
31
|
+
* `{ error: { code, message, details } }`, with the mapped status
|
|
32
|
+
* (400 for caller bugs, 503 for LOG_UNAVAILABLE). Non-A2Errors become a
|
|
33
|
+
* 503 LOG_UNAVAILABLE — from the client's perspective an unknown server
|
|
34
|
+
* failure is retryable-once, not a protocol contract.
|
|
35
|
+
*/
|
|
36
|
+
declare function errorResponse(error: unknown): Response;
|
|
37
|
+
/**
|
|
38
|
+
* The other half of the wire pair: rebuild an `A2Error` from a response
|
|
39
|
+
* body, or null if the body isn't one. Client `push` uses it so both
|
|
40
|
+
* sides branch on identical codes.
|
|
41
|
+
*/
|
|
42
|
+
declare function deserializeError(body: unknown): A2Error | null;
|
|
43
|
+
//#endregion
|
|
44
|
+
export { PushBody, deserializeError, errorResponse, parsePushBody, sseResponse };
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { t as A2Error } from "./errors-BJRMd-h6.js";
|
|
2
|
+
import { r as STREAM_TIMINGS } from "./internal-Dm8Ejnud.js";
|
|
3
|
+
import { a as eventToWire, n as errorStatus, r as errorToWire, t as errorFromWire } from "./wire-2QpU1EtJ.js";
|
|
4
|
+
//#region src/http.ts
|
|
5
|
+
/**
|
|
6
|
+
* a2/http — route-side transport helpers (specs/a2-api.md §9–10).
|
|
7
|
+
*
|
|
8
|
+
* `sseResponse` pipes a `session.stream()` iterable into a server-sent
|
|
9
|
+
* events Response; `parsePushBody` validates the push envelope;
|
|
10
|
+
* `errorResponse`/`deserializeError` are the A2Error wire pair the push
|
|
11
|
+
* route and the client's `push` share.
|
|
12
|
+
*/
|
|
13
|
+
const invalid = (message) => new A2Error("INVALID_PAYLOAD", `malformed push body: ${message}`);
|
|
14
|
+
/**
|
|
15
|
+
* Validate the push envelope — `{ sessionId, events }` — throwing
|
|
16
|
+
* `INVALID_PAYLOAD` on a malformed body. Payload validation against the
|
|
17
|
+
* machine's schemas happens in `append`, not here.
|
|
18
|
+
*/
|
|
19
|
+
async function parsePushBody(req) {
|
|
20
|
+
let body;
|
|
21
|
+
try {
|
|
22
|
+
body = await req.json();
|
|
23
|
+
} catch (cause) {
|
|
24
|
+
throw new A2Error("INVALID_PAYLOAD", "push body is not valid JSON", { cause });
|
|
25
|
+
}
|
|
26
|
+
if (body === null || typeof body !== "object") throw invalid("expected an object");
|
|
27
|
+
const { sessionId, events } = body;
|
|
28
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) throw invalid("sessionId must be a non-empty string");
|
|
29
|
+
if (!Array.isArray(events) || events.length === 0) throw invalid("events must be a non-empty array");
|
|
30
|
+
return {
|
|
31
|
+
sessionId,
|
|
32
|
+
events: events.map((event, i) => {
|
|
33
|
+
if (event === null || typeof event !== "object") throw invalid(`events[${i}] must be an object`);
|
|
34
|
+
const { type, payload, id } = event;
|
|
35
|
+
if (typeof type !== "string" || type.length === 0) throw invalid(`events[${i}].type must be a non-empty string`);
|
|
36
|
+
if (id !== void 0 && typeof id !== "string") throw invalid(`events[${i}].id must be a string when present`);
|
|
37
|
+
const out = {
|
|
38
|
+
type,
|
|
39
|
+
payload
|
|
40
|
+
};
|
|
41
|
+
if (id !== void 0) out.id = id;
|
|
42
|
+
return out;
|
|
43
|
+
})
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Pipe a live event iterable into an SSE `Response`. Each event is one
|
|
48
|
+
* frame — `id:` carries the log index, `data:` the JSON event. A
|
|
49
|
+
* disconnecting client cancels the stream, which closes the underlying
|
|
50
|
+
* subscription. Two kinds of comment frames ride along: a `: connected`
|
|
51
|
+
* prelude that flushes headers immediately, and a `: ping` heartbeat
|
|
52
|
+
* every 15 seconds so clients (and proxies) can tell a quiet stream
|
|
53
|
+
* from a dead connection — the session client's stall watchdog counts
|
|
54
|
+
* on it.
|
|
55
|
+
*/
|
|
56
|
+
function sseResponse(iterable) {
|
|
57
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
58
|
+
const encoder = new TextEncoder();
|
|
59
|
+
let heartbeat;
|
|
60
|
+
const stopHeartbeat = () => {
|
|
61
|
+
if (heartbeat !== void 0) clearInterval(heartbeat);
|
|
62
|
+
heartbeat = void 0;
|
|
63
|
+
};
|
|
64
|
+
const stream = new ReadableStream({
|
|
65
|
+
start(controller) {
|
|
66
|
+
controller.enqueue(encoder.encode(": connected\n\n"));
|
|
67
|
+
heartbeat = setInterval(() => {
|
|
68
|
+
try {
|
|
69
|
+
controller.enqueue(encoder.encode(": ping\n\n"));
|
|
70
|
+
} catch {
|
|
71
|
+
stopHeartbeat();
|
|
72
|
+
}
|
|
73
|
+
}, STREAM_TIMINGS.sseHeartbeatMs);
|
|
74
|
+
heartbeat.unref?.();
|
|
75
|
+
},
|
|
76
|
+
async pull(controller) {
|
|
77
|
+
const { value, done } = await iterator.next();
|
|
78
|
+
if (done) {
|
|
79
|
+
stopHeartbeat();
|
|
80
|
+
controller.close();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
controller.enqueue(encoder.encode(`id: ${value.index}\ndata: ${JSON.stringify(eventToWire(value))}\n\n`));
|
|
84
|
+
},
|
|
85
|
+
async cancel() {
|
|
86
|
+
stopHeartbeat();
|
|
87
|
+
await iterator.return?.();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
return new Response(stream, {
|
|
91
|
+
status: 200,
|
|
92
|
+
headers: {
|
|
93
|
+
"content-type": "text/event-stream",
|
|
94
|
+
"cache-control": "no-cache, no-transform",
|
|
95
|
+
connection: "keep-alive"
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Serialize an error into the documented wire shape,
|
|
101
|
+
* `{ error: { code, message, details } }`, with the mapped status
|
|
102
|
+
* (400 for caller bugs, 503 for LOG_UNAVAILABLE). Non-A2Errors become a
|
|
103
|
+
* 503 LOG_UNAVAILABLE — from the client's perspective an unknown server
|
|
104
|
+
* failure is retryable-once, not a protocol contract.
|
|
105
|
+
*/
|
|
106
|
+
function errorResponse(error) {
|
|
107
|
+
const a2error = error instanceof A2Error ? error : new A2Error("LOG_UNAVAILABLE", "internal error", { cause: error });
|
|
108
|
+
return Response.json(errorToWire(a2error), { status: errorStatus(a2error.code) });
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The other half of the wire pair: rebuild an `A2Error` from a response
|
|
112
|
+
* body, or null if the body isn't one. Client `push` uses it so both
|
|
113
|
+
* sides branch on identical codes.
|
|
114
|
+
*/
|
|
115
|
+
function deserializeError(body) {
|
|
116
|
+
return errorFromWire(body);
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
export { deserializeError, errorResponse, parsePushBody, sseResponse };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { a as ReducerOptions, c as ReducerBuilder, i as EventDefs, l as StandardSchemaV1, n as Contract, o as contract, r as ContractEvent, s as Reducer, t as AppendInput } from "./contract-DL8btVd9.js";
|
|
2
|
+
import { a as Event } from "./log-Dg1I8NRr.js";
|
|
3
|
+
import { i as A2Telemetry, n as A2SpanHandle, r as A2SpanName, t as A2AttributeValue } from "./telemetry-C78al20p.js";
|
|
4
|
+
import { n as A2ErrorCode, t as A2Error } from "./errors-xL_JTXsY.js";
|
|
5
|
+
export { type A2AttributeValue, A2Error, type A2ErrorCode, type A2SpanHandle, type A2SpanName, type A2Telemetry, type AppendInput, type Contract, type ContractEvent, type Event, type EventDefs, type Reducer, type ReducerBuilder, type ReducerOptions, type StandardSchemaV1, contract };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//#region src/inspection.ts
|
|
2
|
+
const serverInspection = /* @__PURE__ */ new WeakMap();
|
|
3
|
+
/** Owned live-inspection cadence; mutable only for white-box tests. */
|
|
4
|
+
const DEVTOOLS_TIMINGS = {
|
|
5
|
+
activeMs: 250,
|
|
6
|
+
idleMs: 1e3,
|
|
7
|
+
heartbeatMs: 15e3
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
export { serverInspection as n, DEVTOOLS_TIMINGS as t };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region src/internal.ts
|
|
2
|
+
const serverInternals = /* @__PURE__ */ new WeakMap();
|
|
3
|
+
/**
|
|
4
|
+
* Drain timing knobs. Mutable only as a white-box test seam — lease
|
|
5
|
+
* heartbeats run on real timers, so tests shrink these to keep suites
|
|
6
|
+
* fast. Production always uses the defaults.
|
|
7
|
+
*/
|
|
8
|
+
const DRAIN_TIMINGS = {
|
|
9
|
+
leaseTtlMs: 5e3,
|
|
10
|
+
leaseHeartbeatMs: 2e3,
|
|
11
|
+
recoveryGraceMs: 1e3,
|
|
12
|
+
recoveryArmTimeoutMs: 2e3
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Stream liveness knobs — the server-side SSE heartbeat and the
|
|
16
|
+
* client-side stall watchdog. Paired by design: the client declares a
|
|
17
|
+
* connection dead after roughly two missed heartbeats. Mutable only as
|
|
18
|
+
* a white-box test seam; production always uses the defaults.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Poll cadence for the poll-based log streams (sqlite, postgres) —
|
|
22
|
+
* owned, not configurable: adaptive polling removed the operator's
|
|
23
|
+
* reason to tune it (the floor keeps active streams smooth; the
|
|
24
|
+
* ceiling bounds idle cost). Mutable only as a white-box test seam so
|
|
25
|
+
* suites don't wait out real idle gaps.
|
|
26
|
+
*/
|
|
27
|
+
const POLL_TIMINGS = {
|
|
28
|
+
activeFloorMs: 25,
|
|
29
|
+
idleCeilingMs: 250
|
|
30
|
+
};
|
|
31
|
+
const STREAM_TIMINGS = {
|
|
32
|
+
sseHeartbeatMs: 15e3,
|
|
33
|
+
stallTimeoutMs: 35e3
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
export { serverInternals as i, POLL_TIMINGS as n, STREAM_TIMINGS as r, DRAIN_TIMINGS as t };
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
//#region src/log.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The A2Log interface — the storage contract every log backend
|
|
4
|
+
* implements. See specs/a2-implementation.md §2–3.
|
|
5
|
+
*
|
|
6
|
+
* This is the whole storage contract: append, read, the drain markers,
|
|
7
|
+
* the failure markers, leases, snapshots, and the live stream. Recovery
|
|
8
|
+
* needs nothing extra — the armed queue message is its own state, and
|
|
9
|
+
* the log is the only thing it consults.
|
|
10
|
+
*/
|
|
11
|
+
/** A stored event, as the public API exposes it. */
|
|
12
|
+
type Event = {
|
|
13
|
+
id: string;
|
|
14
|
+
type: string;
|
|
15
|
+
payload: unknown;
|
|
16
|
+
/** Position in the session's log, from 1. */
|
|
17
|
+
index: number;
|
|
18
|
+
sessionId: string;
|
|
19
|
+
createdAt: Date;
|
|
20
|
+
};
|
|
21
|
+
/** The handler dispatch whose append first persisted a child event. */
|
|
22
|
+
type EventCause = {
|
|
23
|
+
index: number;
|
|
24
|
+
attempt: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* What the log stores: immutable event history, including its causal edge,
|
|
28
|
+
* plus derived drain and failure bookkeeping. The bookkeeping is disposable;
|
|
29
|
+
* the event and its cause are not.
|
|
30
|
+
*/
|
|
31
|
+
type StoredEvent = Event & {
|
|
32
|
+
/** Same-session handler dispatch that appended this event; null means root or legacy unknown. */
|
|
33
|
+
cause: EventCause | null;
|
|
34
|
+
/** Adapter clock time recorded by completion or manual skip; null while pending. */
|
|
35
|
+
processedAt: Date | null;
|
|
36
|
+
/** Dispatch attempt that completed this event; null for pending or administrative completion. */
|
|
37
|
+
processedByAttempt: number | null;
|
|
38
|
+
/** Adapter clock time recorded by the first durable dispatch claim. */
|
|
39
|
+
firstClaimedAt: Date | null;
|
|
40
|
+
/** Adapter clock time recorded by the most recent durable dispatch claim. */
|
|
41
|
+
lastClaimedAt: Date | null;
|
|
42
|
+
/** Durable dispatch claims, including claims abandoned by hard kills. */
|
|
43
|
+
attemptCount: number;
|
|
44
|
+
/** Caught handler failures. This alone drives dead-lettering. */
|
|
45
|
+
failureCount: number;
|
|
46
|
+
/** Adapter clock time recorded by the most recent caught handler failure. */
|
|
47
|
+
lastFailedAt: Date | null;
|
|
48
|
+
/** Dispatch attempt that produced the most recent caught handler failure. */
|
|
49
|
+
lastFailedAttempt: number | null;
|
|
50
|
+
/** The last handler failure, stringified. */
|
|
51
|
+
lastError: string | null;
|
|
52
|
+
/** Adapter clock time recorded when dead-lettered; null otherwise. */
|
|
53
|
+
failedAt: Date | null;
|
|
54
|
+
};
|
|
55
|
+
/** Durable, read-only session metadata for administrative inspection. */
|
|
56
|
+
type StoredSessionSummary = {
|
|
57
|
+
sessionId: string;
|
|
58
|
+
eventCount: number;
|
|
59
|
+
pendingCount: number;
|
|
60
|
+
failedCount: number;
|
|
61
|
+
/** Durable dispatch claims across every event in the session. */
|
|
62
|
+
attemptCount: number;
|
|
63
|
+
/** Caught handler failures across every event in the session. */
|
|
64
|
+
failureCount: number;
|
|
65
|
+
firstEventAt: Date;
|
|
66
|
+
updatedAt: Date;
|
|
67
|
+
};
|
|
68
|
+
/** Metadata for the latest cached fold of one reducer. */
|
|
69
|
+
type StoredSnapshot = {
|
|
70
|
+
reducerName: string;
|
|
71
|
+
index: number;
|
|
72
|
+
updatedAt: Date;
|
|
73
|
+
};
|
|
74
|
+
/** One consistent cache-plus-tail read for a reducer fold. */
|
|
75
|
+
type LogStateRead = {
|
|
76
|
+
/** The latest cached fold for this reducer, if one exists. */
|
|
77
|
+
snapshot: {
|
|
78
|
+
index: number;
|
|
79
|
+
state: unknown;
|
|
80
|
+
} | null;
|
|
81
|
+
/** Immutable events strictly after `snapshot.index`, or the full log on a miss. */
|
|
82
|
+
events: Event[];
|
|
83
|
+
};
|
|
84
|
+
type StoredSessionPage = {
|
|
85
|
+
sessions: StoredSessionSummary[];
|
|
86
|
+
/** Opaque backend cursor; null means there is no next page. */
|
|
87
|
+
cursor: string | null;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Optional read-only administration implemented by A2's shipped logs.
|
|
91
|
+
* It is separate from the correctness contract so custom logs do not
|
|
92
|
+
* need to expose storage-wide discovery merely to store and drain events.
|
|
93
|
+
*/
|
|
94
|
+
type A2LogInspection = {
|
|
95
|
+
listSessions(options: {
|
|
96
|
+
/** Storage namespace prefix, including the contract separator. */
|
|
97
|
+
prefix: string;
|
|
98
|
+
cursor?: string;
|
|
99
|
+
limit: number;
|
|
100
|
+
}): Promise<StoredSessionPage>;
|
|
101
|
+
listSnapshots(sessionId: string): Promise<StoredSnapshot[]>;
|
|
102
|
+
};
|
|
103
|
+
/** The result of atomically claiming the next event in a drain. */
|
|
104
|
+
type LogClaimResult = {
|
|
105
|
+
outcome: "claimed";
|
|
106
|
+
event: StoredEvent;
|
|
107
|
+
} | {
|
|
108
|
+
outcome: "busy";
|
|
109
|
+
} | {
|
|
110
|
+
outcome: "settled";
|
|
111
|
+
};
|
|
112
|
+
/** A completion may lose to a newer claim or an earlier completion. */
|
|
113
|
+
type LogHandoffResult = LogClaimResult | {
|
|
114
|
+
outcome: "superseded";
|
|
115
|
+
};
|
|
116
|
+
/** The result of atomically recording a caught handler failure. */
|
|
117
|
+
type FailAttemptResult = {
|
|
118
|
+
outcome: "failed" | "dead_lettered" | "superseded";
|
|
119
|
+
failureCount: number;
|
|
120
|
+
};
|
|
121
|
+
/** Input to `A2Log.append` — already validated by the machine. */
|
|
122
|
+
type AppendEvent = {
|
|
123
|
+
type: string;
|
|
124
|
+
payload: unknown;
|
|
125
|
+
/** Caller-supplied idempotency key; generated when absent. */
|
|
126
|
+
id?: string;
|
|
127
|
+
/** Internal causal edge supplied atomically by `ctx.append`. */
|
|
128
|
+
cause?: EventCause;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* An injectable clock. Adapters take one so tests can drive lease
|
|
132
|
+
* expiry, failure timestamps, and (later) stuck-session detection
|
|
133
|
+
* deterministically — against real storage, no mocking.
|
|
134
|
+
*/
|
|
135
|
+
type Clock = {
|
|
136
|
+
now(): Date;
|
|
137
|
+
};
|
|
138
|
+
/** An injectable id source for generated event ids. */
|
|
139
|
+
type IdSource = () => string;
|
|
140
|
+
interface A2Log {
|
|
141
|
+
/**
|
|
142
|
+
* Accepts a batch; the batch is atomic — one transaction, consecutive
|
|
143
|
+
* `index`es, all-or-nothing. The idempotency key covers the whole
|
|
144
|
+
* operation, not each item: if *every* event's `id` already exists in
|
|
145
|
+
* this session, this is a retry of a committed batch whose ack was
|
|
146
|
+
* lost — return the existing rows as success. If only *some* ids
|
|
147
|
+
* exist, the caller mixed an already-sent batch with fresh events —
|
|
148
|
+
* always a caller bug — so throw `A2Error('PARTIAL_DUPLICATE_BATCH')`.
|
|
149
|
+
*/
|
|
150
|
+
append(sessionId: string, events: AppendEvent[]): Promise<StoredEvent[]>;
|
|
151
|
+
/** Events for one session, oldest first. */
|
|
152
|
+
read(sessionId: string, opts?: {
|
|
153
|
+
afterIndex?: number;
|
|
154
|
+
unprocessedOnly?: boolean;
|
|
155
|
+
}): Promise<StoredEvent[]>;
|
|
156
|
+
/**
|
|
157
|
+
* Atomically selects the first pending event, checks its scope, acquires the
|
|
158
|
+
* session lease, increments its durable dispatch count, and records the
|
|
159
|
+
* operation's clock time. A claimed event's `attemptCount` is the 1-based
|
|
160
|
+
* attempt passed to its handler.
|
|
161
|
+
*/
|
|
162
|
+
claimNext(options: {
|
|
163
|
+
sessionId: string;
|
|
164
|
+
holder: string;
|
|
165
|
+
ttlMs: number;
|
|
166
|
+
expiresAtMs?: number;
|
|
167
|
+
/** Inclusive causal-tree frontier. Omit for a full-session drain. */
|
|
168
|
+
maxIndex?: number;
|
|
169
|
+
}): Promise<LogClaimResult>;
|
|
170
|
+
/**
|
|
171
|
+
* Atomically marks one event processed only if its attempt is still current
|
|
172
|
+
* and it has no completion marker. While this holder still owns a live lease,
|
|
173
|
+
* claims the next pending event. Completion and any claimed successor record
|
|
174
|
+
* the same adapter clock value.
|
|
175
|
+
*/
|
|
176
|
+
completeAndClaimNext(options: {
|
|
177
|
+
sessionId: string;
|
|
178
|
+
holder: string;
|
|
179
|
+
completedIndex: number;
|
|
180
|
+
attempt: number;
|
|
181
|
+
/** Inclusive causal-tree frontier. Omit for a full-session drain. */
|
|
182
|
+
maxIndex?: number;
|
|
183
|
+
}): Promise<LogHandoffResult>;
|
|
184
|
+
/** The drain-completion marker and drain idempotency check. */
|
|
185
|
+
markProcessed(sessionId: string, index: number): Promise<void>;
|
|
186
|
+
/**
|
|
187
|
+
* Atomically records a caught failure for one claimed attempt. A stale
|
|
188
|
+
* attempt cannot poison a processed event or a newer dispatch. Accepted
|
|
189
|
+
* failures record the operation's clock time.
|
|
190
|
+
*/
|
|
191
|
+
failAttempt(options: {
|
|
192
|
+
sessionId: string;
|
|
193
|
+
index: number;
|
|
194
|
+
attempt: number;
|
|
195
|
+
error: string;
|
|
196
|
+
maxFailures: number;
|
|
197
|
+
}): Promise<FailAttemptResult>;
|
|
198
|
+
/** Dead-letters the event. */
|
|
199
|
+
markFailed(sessionId: string, index: number): Promise<void>;
|
|
200
|
+
/**
|
|
201
|
+
* Reads a reducer snapshot and its event tail as one consistent adapter
|
|
202
|
+
* operation. On a cache miss, `snapshot` is null and `events` is the full
|
|
203
|
+
* log. The snapshot is untrusted; core may reject it and issue a full
|
|
204
|
+
* `read()` when its state schema no longer accepts the cached value.
|
|
205
|
+
*/
|
|
206
|
+
readState(sessionId: string, reducerName: string): Promise<LogStateRead>;
|
|
207
|
+
/**
|
|
208
|
+
* Writes a disposable reducer cache. Guard this operation so a slower
|
|
209
|
+
* concurrent writer can never clobber a further-along snapshot
|
|
210
|
+
* (`where up_to_index < excluded.up_to_index`).
|
|
211
|
+
*/
|
|
212
|
+
putSnapshot(sessionId: string, reducerName: string, index: number, state: unknown): Promise<void>;
|
|
213
|
+
/** Read-only operational data used by `a2/devtools/server`. */
|
|
214
|
+
inspect?: A2LogInspection;
|
|
215
|
+
/**
|
|
216
|
+
* One lease per session — serializes processing order for a session,
|
|
217
|
+
* not individual events. TTL-bounded, never held indefinitely.
|
|
218
|
+
* Re-acquiring with the same holder renews the TTL.
|
|
219
|
+
*/
|
|
220
|
+
lease: {
|
|
221
|
+
acquire(options: {
|
|
222
|
+
sessionId: string;
|
|
223
|
+
holder: string;
|
|
224
|
+
ttlMs: number;
|
|
225
|
+
/** Optional absolute expiry used for platform-deadline lease windows. */
|
|
226
|
+
expiresAtMs?: number;
|
|
227
|
+
}): Promise<boolean>;
|
|
228
|
+
release(options: {
|
|
229
|
+
sessionId: string;
|
|
230
|
+
holder: string;
|
|
231
|
+
}): Promise<void>;
|
|
232
|
+
};
|
|
233
|
+
/**
|
|
234
|
+
* A live feed of one session's events, starting after `startAt`
|
|
235
|
+
* (exclusive). Transport is the backend's choice — in-process pub/sub,
|
|
236
|
+
* polling, LISTEN/NOTIFY — callers never branch on which. The iterable
|
|
237
|
+
* ends when the consumer calls `return()` (e.g. a disconnecting SSE
|
|
238
|
+
* client) and must deliver events appended after subscription.
|
|
239
|
+
*/
|
|
240
|
+
stream(sessionId: string, opts?: {
|
|
241
|
+
startAt?: number;
|
|
242
|
+
}): AsyncIterable<Event>;
|
|
243
|
+
}
|
|
244
|
+
//#endregion
|
|
245
|
+
export { Event as a, IdSource as c, LogStateRead as d, StoredEvent as f, StoredSnapshot as h, Clock as i, LogClaimResult as l, StoredSessionSummary as m, A2LogInspection as n, EventCause as o, StoredSessionPage as p, AppendEvent as r, FailAttemptResult as s, A2Log as t, LogHandoffResult as u };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { c as IdSource, i as Clock, t as A2Log } from "./log-Dg1I8NRr.js";
|
|
2
|
+
//#region src/log-memory.d.ts
|
|
3
|
+
type MemoryLogOptions = {
|
|
4
|
+
/** Injectable clock — every stored timestamp comes from here. */
|
|
5
|
+
clock?: Clock;
|
|
6
|
+
/** Injectable id source for generated event ids. */
|
|
7
|
+
ids?: IdSource;
|
|
8
|
+
};
|
|
9
|
+
declare function memory(options?: MemoryLogOptions): A2Log;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { MemoryLogOptions, memory };
|