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.
Files changed (55) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/ai-server.browser.js +2 -2
  3. package/dist/ai-server.d.ts +19 -7
  4. package/dist/ai-server.js +730 -96
  5. package/dist/ai.d.ts +32 -11
  6. package/dist/ai.js +253 -75
  7. package/dist/client.d.ts +1 -1
  8. package/dist/client.js +4 -4
  9. package/dist/{contract-B0kAXoaL.js → contract-CG_adnu_.js} +2 -1
  10. package/dist/{contract-DL8btVd9.d.ts → contract-C_3dIIEU.d.ts} +4 -1
  11. package/dist/devtools-server.browser.js +2 -2
  12. package/dist/devtools-server.js +1 -1
  13. package/dist/http.d.ts +1 -1
  14. package/dist/http.js +4 -3
  15. package/dist/idempotent-replay-BMyHrP0L.js +19 -0
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +1 -1
  18. package/dist/{internal-Dm8Ejnud.js → internal-D6wNxTck.js} +3 -3
  19. package/dist/{log-Dg1I8NRr.d.ts → log-ldf5g8Cx.d.ts} +74 -56
  20. package/dist/log-memory.d.ts +1 -1
  21. package/dist/log-memory.js +173 -96
  22. package/dist/{log-polling-RO7kclzR.js → log-polling-6COoN60V.js} +1 -1
  23. package/dist/log-postgres.d.ts +1 -1
  24. package/dist/log-postgres.js +235 -192
  25. package/dist/log-redis.d.ts +1 -1
  26. package/dist/log-redis.js +453 -263
  27. package/dist/log-sqlite.d.ts +1 -1
  28. package/dist/log-sqlite.js +216 -127
  29. package/dist/otel.d.ts +1 -1
  30. package/dist/otel.js +1 -1
  31. package/dist/react.d.ts +1 -1
  32. package/dist/react.js +1 -1
  33. package/dist/recovery-vercel.d.ts +2 -2
  34. package/dist/recovery-vercel.js +9 -10
  35. package/dist/server-DJgD2YWP.js +877 -0
  36. package/dist/server.browser.js +4 -4
  37. package/dist/server.d.ts +46 -27
  38. package/dist/server.js +1 -1
  39. package/dist/{telemetry-C78al20p.d.ts → telemetry-Cso0qyHQ.d.ts} +1 -1
  40. package/dist/{wire-2QpU1EtJ.js → wire-BVsgR8o9.js} +1 -1
  41. package/docs/01-quickstart.mdx +7 -7
  42. package/docs/concepts/01-contracts.mdx +22 -22
  43. package/docs/concepts/02-handlers.mdx +223 -89
  44. package/docs/concepts/03-durability.mdx +199 -112
  45. package/docs/concepts/04-state.mdx +27 -1
  46. package/docs/guides/01-timers.mdx +4 -4
  47. package/docs/guides/02-cancellation.mdx +32 -4
  48. package/docs/guides/05-production.mdx +61 -27
  49. package/docs/guides/06-ai-agents.mdx +151 -70
  50. package/docs/guides/07-devtools.mdx +6 -3
  51. package/docs/guides/08-application-data.mdx +5 -6
  52. package/docs/index.mdx +30 -14
  53. package/docs/reference/01-api.mdx +305 -70
  54. package/package.json +31 -31
  55. package/dist/server-DYsnKTTy.js +0 -780
@@ -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 { i as EventDefs, n as Contract, r as ContractEvent, s as Reducer, t as AppendInput } from "./contract-DL8btVd9.js";
2
- import { a as Event, c as IdSource, d as LogStateRead, f as StoredEvent, h as StoredSnapshot, i as Clock, l as LogClaimResult, m as StoredSessionSummary, n as A2LogInspection, o as EventCause, p as StoredSessionPage, r as AppendEvent, s as FailAttemptResult, t as A2Log, u as LogHandoffResult } from "./log-Dg1I8NRr.js";
3
- import { i as A2Telemetry } from "./telemetry-C78al20p.js";
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
- /** Append what happens next to this session's log. Atomic per call. */
25
- append: (...events: AppendInput<D>[]) => Promise<ContractEvent<D>[]>;
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
- /** A handle on one instance of the machine. Creating it does no I/O. */
33
- type Session<D extends EventDefs> = {
34
- readonly sessionId: string;
35
- append: {
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. Lease holders move the watchdog alongside
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 the session's pending events in order. `settled` means the
90
- * drain left nothing actionable: every event processed, or the
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>) => boolean); };
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: AbortSpec<D, K>;
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 LogClaimResult, type LogHandoffResult, type LogStateRead, PushedEvent, RecoverableServer, ServerOptions, Session, type StoredEvent, type StoredSessionPage, type StoredSessionSummary, type StoredSnapshot, createServer };
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-DYsnKTTy.js";
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.
@@ -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, append }) => {
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
- await append({ type: 'shop.notified', payload: {} })
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 `append` inside a handler is already deduplicated. If the handler
80
- crashes after appending and runs again, the second append is a no-op,
81
- [here's how](/concepts/handlers#handlers-can-run-twice).
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 orders, recovers, folds, and streams.
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
- appended the next; the log shows the whole story, in order. This is what
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, ordering, state, and live stream.
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
- nothing but schemas. The server implements it, [reducers](/concepts/state)
41
- derive from it, and the browser types its pushes off it. One artifact,
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
- Handlers and storage bind to the contract with `createServer` from
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, append }) => {
71
+ created: async ({ event }) => {
72
72
  // your side effect, then:
73
- await append({ type: 'shop.notified', payload: {} })
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, its own ordering, its own
105
- state, and its own live stream. The id is yours: use whatever identifies the
106
- subject in your system.
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
- Sessions are also the unit of ordering: events within one session are
112
- processed serially, in log order. Different sessions never wait for each
113
- other. More in [Handlers](/concepts/handlers#ordering).
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 must be ordered, folded, recovered, or viewed
119
- together. Split things that should progress independently.
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, which arms a durable, delayed drain
134
- for every top-level append, giving crash recovery and handler retries in one
135
- mechanism. The handler starts without waiting for recovery, and a successful
136
- initial arm is durable before `append` returns. It changes how fast A2 heals,
137
- not how it works. Everything in these concept pages holds with or without it.
138
- Covered in [Going to production](/guides/production).
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).