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/server.browser.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
//#region src/server.browser.ts
|
|
2
2
|
/**
|
|
3
|
-
* The browser build of a2/server. There isn't one — on purpose.
|
|
4
|
-
* Contracts and reducers (from `a2`) and the session client (from
|
|
5
|
-
* `a2/client` / `a2/react`) are the isomorphic surface; the server is
|
|
3
|
+
* The browser build of experimental-a2/server. There isn't one — on purpose.
|
|
4
|
+
* Contracts and reducers (from `experimental-a2`) and the session client (from
|
|
5
|
+
* `experimental-a2/client` / `experimental-a2/react`) are the isomorphic surface; the server is
|
|
6
6
|
* where handlers and storage live. This module existing in a client
|
|
7
7
|
* bundle means a `'use client'` file (or something it imports)
|
|
8
8
|
* value-imported your server module.
|
|
9
9
|
*/
|
|
10
|
-
throw new Error("a2/server is server-only — a client bundle imported it. Import the contract (a2) or the session client (a2/client, a2/react) in browser code instead.");
|
|
10
|
+
throw new Error("experimental-a2/server is server-only — a client bundle imported it. Import the contract (a2) or the session client (experimental-a2/client, experimental-a2/react) in browser code instead.");
|
|
11
11
|
//#endregion
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as Event, c as IdSource, d as LogStateRead, f as StoredEvent, h as StoredSnapshot, i as Clock, l as
|
|
3
|
-
import { i as A2Telemetry } from "./telemetry-
|
|
1
|
+
import { a as EventDefs, c as Reducer, n as Contract, r as ContractEvent, t as AppendInput } from "./contract-C_3dIIEU.js";
|
|
2
|
+
import { a as Event, c as IdSource, d as LogStateRead, f as StoredEvent, h as StoredSnapshot, i as Clock, l as LogAppendResult, m as StoredSessionSummary, n as A2LogInspection, o as EventCause, p as StoredSessionPage, r as AppendEvent, s as FailAttemptResult, t as A2Log, u as LogClaimAvailableResult } from "./log-ldf5g8Cx.js";
|
|
3
|
+
import { i as A2Telemetry } from "./telemetry-Cso0qyHQ.js";
|
|
4
4
|
//#region src/server.d.ts
|
|
5
5
|
/**
|
|
6
6
|
* Events that arrived over the wire through `parsePushBody` — already
|
|
@@ -15,28 +15,43 @@ type PushedEvent = {
|
|
|
15
15
|
id?: string;
|
|
16
16
|
readonly "~a2.pushed": true;
|
|
17
17
|
};
|
|
18
|
+
type PushValidationContext = {
|
|
19
|
+
sessionId: string;
|
|
20
|
+
events: readonly PushedEvent[];
|
|
21
|
+
};
|
|
18
22
|
/** What every handler receives. */
|
|
19
23
|
type HandlerContext<D extends EventDefs, K extends keyof D & string = keyof D & string> = {
|
|
20
24
|
/** The triggering event. */
|
|
21
25
|
event: ContractEvent<D, K>;
|
|
22
26
|
/** Durable, 1-based dispatch ordinal for this event. */
|
|
23
27
|
attempt: number;
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
/** Every past event in this session, oldest first. Always the raw log. */
|
|
27
|
-
history: () => Promise<ContractEvent<D>[]>;
|
|
28
|
+
/** This session, with handler-scoped idempotent append. */
|
|
29
|
+
session: Session<D, HandlerAppend<D>>;
|
|
28
30
|
/** Fires on `abortOn` events (cancellation slice); dormant otherwise. */
|
|
29
31
|
signal: AbortSignal;
|
|
30
32
|
};
|
|
31
|
-
type Handler<D extends EventDefs, K extends keyof D & string = keyof D & string> = (ctx: HandlerContext<D, K>) => Promise<void>;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
(...events: AppendInput<D>[]): Promise<ContractEvent<D>[]>;
|
|
37
|
-
/** The push-route path: events from `parsePushBody`. */
|
|
38
|
-
(...events: PushedEvent[]): Promise<ContractEvent<D>[]>;
|
|
33
|
+
type Handler<D extends EventDefs, K extends keyof D & string = keyof D & string> = (ctx: HandlerContext<D, K>) => Promise<void | AppendInput<D> | readonly AppendInput<D>[]>;
|
|
34
|
+
type LaneContext<D extends EventDefs, K extends keyof D & string = keyof D & string> = {
|
|
35
|
+
sessionId: string;
|
|
36
|
+
event: Pick<ContractEvent<D, K>, "type" | "payload"> & {
|
|
37
|
+
id?: string;
|
|
39
38
|
};
|
|
39
|
+
};
|
|
40
|
+
type Lane<D extends EventDefs, K extends keyof D & string = keyof D & string> = string | ((context: LaneContext<D, K>) => string);
|
|
41
|
+
type SessionDispatch<D extends EventDefs> = {
|
|
42
|
+
(...events: AppendInput<D>[]): Promise<ContractEvent<D>[]>;
|
|
43
|
+
/** The push-route path: events from `parsePushBody`. */
|
|
44
|
+
(...events: PushedEvent[]): Promise<ContractEvent<D>[]>;
|
|
45
|
+
};
|
|
46
|
+
type SessionAppend<D extends EventDefs> = SessionDispatch<D> & {
|
|
47
|
+
/** Commit, then hand pending work directly to configured recovery. */
|
|
48
|
+
dispatch: SessionDispatch<D>;
|
|
49
|
+
};
|
|
50
|
+
type HandlerAppend<D extends EventDefs> = (name: string, ...events: AppendInput<D>[]) => Promise<ContractEvent<D>[]>;
|
|
51
|
+
/** A handle on one instance of the machine. Creating it does no I/O. */
|
|
52
|
+
type Session<D extends EventDefs, Append = SessionAppend<D>> = {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
append: Append;
|
|
40
55
|
history(): Promise<ContractEvent<D>[]>;
|
|
41
56
|
state<S>(reducer: Reducer<D, S>): Promise<{
|
|
42
57
|
state: S;
|
|
@@ -45,7 +60,7 @@ type Session<D extends EventDefs> = {
|
|
|
45
60
|
/**
|
|
46
61
|
* A live feed of this session's events, starting after `startAt`
|
|
47
62
|
* (exclusive). Server-side only — expose it over SSE with
|
|
48
|
-
* `sseResponse` from a2/http.
|
|
63
|
+
* `sseResponse` from experimental-a2/http.
|
|
49
64
|
*/
|
|
50
65
|
stream(opts?: {
|
|
51
66
|
startAt?: number;
|
|
@@ -68,9 +83,9 @@ type RecoverableServer = {
|
|
|
68
83
|
* The recovery seam (a2-implementation.md §7, §9). `arm` puts a
|
|
69
84
|
* delayed, coalesced "drain this session" message on durable
|
|
70
85
|
* infrastructure while inline work starts; `handler` returns the route
|
|
71
|
-
* the transport delivers to.
|
|
86
|
+
* the transport delivers to. Claim holders move the watchdog alongside
|
|
72
87
|
* their renewable execution window. Implementations ship as
|
|
73
|
-
* entry points (`a2/recovery-vercel`); core never imports a transport.
|
|
88
|
+
* entry points (`experimental-a2/recovery-vercel`); core never imports a transport.
|
|
74
89
|
*/
|
|
75
90
|
type A2Recovery = {
|
|
76
91
|
arm(opts: {
|
|
@@ -86,10 +101,8 @@ type A2Server<D extends EventDefs> = {
|
|
|
86
101
|
readonly contract: Contract<D>;
|
|
87
102
|
session(id: string): Session<D>;
|
|
88
103
|
/**
|
|
89
|
-
* Process
|
|
90
|
-
*
|
|
91
|
-
* session is stalled at a dead-lettered event (which recovery
|
|
92
|
-
* transports treat as "stop redelivering").
|
|
104
|
+
* Process every currently eligible event. `settled` means nothing
|
|
105
|
+
* actionable remains, including work blocked behind a dead letter.
|
|
93
106
|
*/
|
|
94
107
|
drain(sessionId: string): Promise<{
|
|
95
108
|
settled: boolean;
|
|
@@ -103,9 +116,13 @@ type A2Server<D extends EventDefs> = {
|
|
|
103
116
|
* without `abortOn` pay nothing. An aborted handler should catch and
|
|
104
117
|
* return normally; throwing means "retry me".
|
|
105
118
|
*/
|
|
106
|
-
type AbortSpec<D extends EventDefs, K extends keyof D & string> = Array<keyof D & string> | { [T in keyof D & string]?: true | ((event: ContractEvent<D, T>, trigger: ContractEvent<D, K
|
|
119
|
+
type AbortSpec<D extends EventDefs, K extends keyof D & string> = Array<keyof D & string> | { [T in keyof D & string]?: true | ((event: ContractEvent<D, T>, trigger: ContractEvent<D, K>, context: {
|
|
120
|
+
attempt: number;
|
|
121
|
+
}) => boolean); };
|
|
107
122
|
type HandlerEntry<D extends EventDefs, K extends keyof D & string = keyof D & string> = Handler<D, K> | {
|
|
108
|
-
abortOn
|
|
123
|
+
abortOn?: AbortSpec<D, K>;
|
|
124
|
+
/** Session-scoped FIFO key, resolved and persisted when the event lands. */
|
|
125
|
+
lane?: Lane<D, K>;
|
|
109
126
|
handler: Handler<D, K>;
|
|
110
127
|
};
|
|
111
128
|
type ServerOptions<D extends EventDefs> = {
|
|
@@ -115,13 +132,15 @@ type ServerOptions<D extends EventDefs> = {
|
|
|
115
132
|
log?: A2Log;
|
|
116
133
|
/**
|
|
117
134
|
* Queue-backed recovery — e.g. `vercelQueues()` from
|
|
118
|
-
* `a2/recovery-vercel`. Absent means append-driven healing only: a
|
|
135
|
+
* `experimental-a2/recovery-vercel`. Absent means append-driven healing only: a
|
|
119
136
|
* working configuration, but a clockless one. Recommended in
|
|
120
137
|
* production.
|
|
121
138
|
*/
|
|
122
139
|
recovery?: A2Recovery;
|
|
123
|
-
/** Optional instrumentation — e.g. `otel()` from `a2/otel`. */
|
|
140
|
+
/** Optional instrumentation — e.g. `otel()` from `experimental-a2/otel`. */
|
|
124
141
|
telemetry?: A2Telemetry;
|
|
142
|
+
/** Validate events that came through `parsePushBody` before writing them. */
|
|
143
|
+
validatePush?: (context: PushValidationContext) => void | PromiseLike<void>;
|
|
125
144
|
/**
|
|
126
145
|
* The reactions, keyed by event type — all present at construction,
|
|
127
146
|
* so a handler can never be silently missing because its module
|
|
@@ -133,4 +152,4 @@ type ServerOptions<D extends EventDefs> = {
|
|
|
133
152
|
/** Implement a contract: bind its vocabulary to storage and reactions. */
|
|
134
153
|
declare function createServer<D extends EventDefs>(options: ServerOptions<D>): A2Server<D>;
|
|
135
154
|
//#endregion
|
|
136
|
-
export { type A2Log, type A2LogInspection, A2Recovery, A2Server, AbortSpec, type AppendEvent, type AppendInput, type Clock, type Contract, type ContractEvent, type Event, type EventCause, type EventDefs, type FailAttemptResult, Handler, HandlerContext, HandlerEntry, type IdSource, type
|
|
155
|
+
export { type A2Log, type A2LogInspection, A2Recovery, A2Server, AbortSpec, type AppendEvent, type AppendInput, type Clock, type Contract, type ContractEvent, type Event, type EventCause, type EventDefs, type FailAttemptResult, Handler, HandlerAppend, HandlerContext, HandlerEntry, type IdSource, Lane, LaneContext, type LogAppendResult, type LogClaimAvailableResult, type LogStateRead, PushValidationContext, PushedEvent, RecoverableServer, ServerOptions, Session, SessionAppend, SessionDispatch, type StoredEvent, type StoredSessionPage, type StoredSessionSummary, type StoredSnapshot, createServer };
|
package/dist/server.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as createServer } from "./server-
|
|
1
|
+
import { t as createServer } from "./server-DJgD2YWP.js";
|
|
2
2
|
export { createServer };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* The A2Telemetry interface — the instrumentation seam. Same philosophy
|
|
4
4
|
* as log backends: the interface lives in core, implementations ship as
|
|
5
|
-
* entry points (`a2/otel` adapts it to OpenTelemetry). Without one,
|
|
5
|
+
* entry points (`experimental-a2/otel` adapts it to OpenTelemetry). Without one,
|
|
6
6
|
* every operation runs through a no-op wrapper.
|
|
7
7
|
*/
|
|
8
8
|
/** Attribute values a2 emits. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as A2Error } from "./errors-BJRMd-h6.js";
|
|
2
2
|
//#region src/wire.ts
|
|
3
3
|
/**
|
|
4
|
-
* The wire format shared by a2/http (server side) and a2/client
|
|
4
|
+
* The wire format shared by experimental-a2/http (server side) and experimental-a2/client
|
|
5
5
|
* (browser side): events as JSON with ISO timestamps, and the A2Error
|
|
6
6
|
* envelope. Internal module — both entry points re-export what's
|
|
7
7
|
* public.
|
package/docs/01-quickstart.mdx
CHANGED
|
@@ -58,10 +58,10 @@ import { createServer } from 'experimental-a2/server'
|
|
|
58
58
|
export const ordersServer = createServer({
|
|
59
59
|
contract: orders,
|
|
60
60
|
handlers: {
|
|
61
|
-
created: async ({ event
|
|
61
|
+
created: async ({ event }) => {
|
|
62
62
|
// your side effect, e.g. email the shop, idempotent via event.id:
|
|
63
63
|
// await sendEmailToShop(event.payload, { idempotencyKey: event.id })
|
|
64
|
-
|
|
64
|
+
return { type: 'shop.notified', payload: {} }
|
|
65
65
|
},
|
|
66
66
|
},
|
|
67
67
|
})
|
|
@@ -76,9 +76,9 @@ Two things worth noticing:
|
|
|
76
76
|
- The side effect takes an idempotency key. Handlers can run more than
|
|
77
77
|
once (that's what at-least-once delivery means), so external calls
|
|
78
78
|
should be safe to repeat. `event.id` is stable across re-runs. Use it.
|
|
79
|
-
- The
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
- The returned event is committed atomically with successful handler
|
|
80
|
+
completion. If the handler throws or the process dies first,
|
|
81
|
+
`shop.notified` does not appear. A handler can return one event or an array.
|
|
82
82
|
|
|
83
83
|
## 4. Append the first event
|
|
84
84
|
|
|
@@ -101,7 +101,7 @@ export async function POST(req: Request) {
|
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
A session is one instance of the contract. `ordersServer.session('order-42')`
|
|
104
|
-
is order 42's lifecycle: the unit A2
|
|
104
|
+
is order 42's lifecycle: the unit A2 recovers, folds, and streams.
|
|
105
105
|
Creating the handle does no I/O, and the id is yours to choose.
|
|
106
106
|
|
|
107
107
|
Try it:
|
|
@@ -152,7 +152,7 @@ curl localhost:3000/api/orders/order-42
|
|
|
152
152
|
`createdAt`.)
|
|
153
153
|
|
|
154
154
|
That second event is the payoff. You appended one event; the handler
|
|
155
|
-
|
|
155
|
+
returned the next; the log shows the whole story, in order. This is what
|
|
156
156
|
debugging looks like in A2: you read.
|
|
157
157
|
|
|
158
158
|
## 6. Fold state out of the log
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Contracts and sessions
|
|
3
|
-
description: A contract names one kind of lifecycle. A session is one instance, with its own history,
|
|
3
|
+
description: A contract names one kind of lifecycle. A session is one instance, with its own history, state, recovery, and live stream.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Contracts
|
|
@@ -36,10 +36,10 @@ throws `INVALID_PAYLOAD` before anything is written, an unknown type
|
|
|
36
36
|
throws `UNKNOWN_EVENT_TYPE`. What lands in the log is the validated
|
|
37
37
|
output, with defaults applied and transforms run.
|
|
38
38
|
|
|
39
|
-
The contract is a plain, importable, **isomorphic** value. It carries
|
|
40
|
-
|
|
41
|
-
derive from it, and the browser types its pushes
|
|
42
|
-
shared by every side of the wire.
|
|
39
|
+
The contract is a plain, importable, **isomorphic** value. It carries the
|
|
40
|
+
schemas and their type helpers. The server implements it,
|
|
41
|
+
[reducers](/concepts/state) derive from it, and the browser types its pushes
|
|
42
|
+
off it. One artifact, shared by every side of the wire.
|
|
43
43
|
|
|
44
44
|
And that's the entire declaration. No states, no transition table. What
|
|
45
45
|
happens on each event is defined where the contract is served; what
|
|
@@ -55,7 +55,7 @@ convention, not a namespace.
|
|
|
55
55
|
|
|
56
56
|
## Serving a contract
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
Storage and optional handlers bind to the contract with `createServer` from
|
|
59
59
|
`experimental-a2/server`, the entry point that is allowed to touch a log backend
|
|
60
60
|
(and the one that fails loudly if a browser bundle ever imports it):
|
|
61
61
|
|
|
@@ -68,9 +68,9 @@ export const ordersServer = createServer({
|
|
|
68
68
|
contract: orders,
|
|
69
69
|
log: postgres({ connectionString: process.env.DATABASE_URL }),
|
|
70
70
|
handlers: {
|
|
71
|
-
created: async ({ event
|
|
71
|
+
created: async ({ event }) => {
|
|
72
72
|
// your side effect, then:
|
|
73
|
-
|
|
73
|
+
return { type: 'shop.notified', payload: {} }
|
|
74
74
|
},
|
|
75
75
|
},
|
|
76
76
|
})
|
|
@@ -101,22 +101,22 @@ const order = ordersServer.session('order-42')
|
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
A session is one instance of the contract: one order, one signup, one
|
|
104
|
-
chat, one approval, one import. It has its own log,
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
chat, one approval, one import. It has its own ordered log, state, recovery,
|
|
105
|
+
and live stream. The id is yours: use whatever identifies the subject in your
|
|
106
|
+
system.
|
|
107
107
|
|
|
108
108
|
Creating a session does no I/O. It's a handle; nothing loads until you
|
|
109
109
|
append, read, or stream.
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
Handlers are concurrent by default, including within one session. Add a
|
|
112
|
+
[`lane`](/concepts/handlers#lanes) when events share a resource and must run
|
|
113
|
+
one at a time.
|
|
114
114
|
|
|
115
115
|
## Pick the boundary
|
|
116
116
|
|
|
117
117
|
The session is the boundary A2 keeps durable and synchronized. Put events in
|
|
118
|
-
the same session when they
|
|
119
|
-
|
|
118
|
+
the same session when they form one history, fold, recovery unit, or live
|
|
119
|
+
view. Split things that should be read and recovered independently.
|
|
120
120
|
|
|
121
121
|
A contract is not a database table, and a session is not a table or a general
|
|
122
122
|
query boundary. One contract serves every instance of that lifecycle. An
|
|
@@ -130,9 +130,9 @@ usually belong in an application database. See
|
|
|
130
130
|
|
|
131
131
|
## One more option: recovery
|
|
132
132
|
|
|
133
|
-
Servers accept a `recovery` option
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
133
|
+
Servers accept a `recovery` option. When a top-level append leaves or finds
|
|
134
|
+
pending handler work, recovery arms a durable, delayed drain. The handler
|
|
135
|
+
starts without waiting for recovery, and a successful initial arm is durable
|
|
136
|
+
before `append` returns. Event types without handlers settle during append and
|
|
137
|
+
create no recovery work when the session has no older backlog. Covered in
|
|
138
|
+
[Going to production](/guides/production).
|