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.
Files changed (92) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/dist/ai-B4YhEnfw.d.ts +333 -0
  3. package/dist/ai-server.d.ts +48 -9
  4. package/dist/ai-server.js +121 -49
  5. package/dist/ai.d.ts +2 -303
  6. package/dist/ai.js +231 -86
  7. package/dist/cli-B3VuxoDe.js +597 -0
  8. package/dist/cli-bin.d.ts +1 -0
  9. package/dist/cli-bin.js +5 -0
  10. package/dist/cli.d.ts +19 -0
  11. package/dist/cli.js +2 -0
  12. package/dist/client-BrfDXQ8A.d.ts +155 -0
  13. package/dist/client-Bt4tAKi9.js +798 -0
  14. package/dist/client.d.ts +2 -90
  15. package/dist/client.js +1 -409
  16. package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +10 -2
  17. package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +62 -8
  18. package/dist/devtools-J_jZ2vQf.d.ts +151 -0
  19. package/dist/devtools-kJJaORn-.js +338 -0
  20. package/dist/devtools-server.browser.js +1 -1
  21. package/dist/devtools-server.d.ts +2 -2
  22. package/dist/devtools-server.js +224 -43
  23. package/dist/devtools.d.ts +2 -0
  24. package/dist/devtools.js +2 -0
  25. package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +4 -4
  26. package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +1 -1
  27. package/dist/http.d.ts +71 -13
  28. package/dist/http.js +302 -41
  29. package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +2 -2
  30. package/dist/index.d.ts +5 -5
  31. package/dist/index.js +2 -2
  32. package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +2 -1
  33. package/dist/internal-aEotMzu_.js +209 -0
  34. package/dist/otel.d.ts +1 -1
  35. package/dist/platform-B4TnJtWu.js +32 -0
  36. package/dist/react.d.ts +37 -14
  37. package/dist/react.js +26 -15
  38. package/dist/scheduler-qstash.d.ts +78 -0
  39. package/dist/scheduler-qstash.js +499 -0
  40. package/dist/scheduler-task-BpzhPnRS.js +54 -0
  41. package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +17 -24
  42. package/dist/scheduler-vercel.js +226 -0
  43. package/dist/server-CcNnFnoW.js +1405 -0
  44. package/dist/server-YtPq7hjw.d.ts +260 -0
  45. package/dist/server.d.ts +4 -155
  46. package/dist/server.js +2 -2
  47. package/dist/{log-ldf5g8Cx.d.ts → store-C3sNAaBT.d.ts} +111 -35
  48. package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +1 -1
  49. package/dist/store-codec-DTG0Ftek.js +8 -0
  50. package/dist/store-memory.d.ts +11 -0
  51. package/dist/{log-memory.js → store-memory.js} +127 -24
  52. package/dist/{log-polling-6COoN60V.js → store-polling-DgrrAE3d.js} +7 -6
  53. package/dist/{log-postgres.d.ts → store-postgres.d.ts} +6 -6
  54. package/dist/{log-postgres.js → store-postgres.js} +158 -24
  55. package/dist/{log-redis.js → store-redis-core-DWqx3F47.js} +294 -156
  56. package/dist/store-redis-http.d.ts +21 -0
  57. package/dist/store-redis-http.js +70 -0
  58. package/dist/store-redis.d.ts +37 -0
  59. package/dist/store-redis.js +298 -0
  60. package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +6 -6
  61. package/dist/{log-sqlite.js → store-sqlite.js} +116 -22
  62. package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +1 -1
  63. package/dist/testing.browser.d.ts +1 -0
  64. package/dist/testing.browser.js +4 -0
  65. package/dist/testing.d.ts +31 -0
  66. package/dist/testing.js +101 -0
  67. package/dist/wire-DCUZBUlT.js +222 -0
  68. package/docs/01-quickstart.mdx +4 -5
  69. package/docs/concepts/01-contracts.mdx +21 -17
  70. package/docs/concepts/02-handlers.mdx +7 -7
  71. package/docs/concepts/03-durability.mdx +26 -29
  72. package/docs/concepts/04-state.mdx +18 -21
  73. package/docs/guides/01-timers.mdx +154 -54
  74. package/docs/guides/02-cancellation.mdx +30 -4
  75. package/docs/guides/03-react.mdx +20 -21
  76. package/docs/guides/04-local-first.mdx +1 -1
  77. package/docs/guides/05-production.mdx +321 -60
  78. package/docs/guides/06-ai-agents.mdx +249 -49
  79. package/docs/guides/07-devtools.mdx +137 -12
  80. package/docs/guides/08-application-data.mdx +1 -1
  81. package/docs/guides/09-presence.mdx +284 -0
  82. package/docs/guides/10-transports.mdx +131 -0
  83. package/docs/index.mdx +22 -46
  84. package/docs/reference/01-api.mdx +751 -102
  85. package/docs/reference/02-errors.mdx +10 -5
  86. package/package.json +23 -6
  87. package/dist/internal-D6wNxTck.js +0 -36
  88. package/dist/log-memory.d.ts +0 -11
  89. package/dist/log-redis.d.ts +0 -31
  90. package/dist/recovery-vercel.js +0 -119
  91. package/dist/server-DJgD2YWP.js +0 -877
  92. package/dist/wire-BVsgR8o9.js +0 -62
@@ -1,19 +1,11 @@
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, dispatch claims,
7
- * failure markers, 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
- */
1
+ import { c as PresencePatch } from "./contract-jIfaR085.js";
2
+ //#region src/store.d.ts
11
3
  /** A stored event, as the public API exposes it. */
12
4
  type Event = {
13
5
  id: string;
14
6
  type: string;
15
7
  payload: unknown;
16
- /** Position in the session's log, from 1. */
8
+ /** Position in the session's event log, from 1. */
17
9
  index: number;
18
10
  sessionId: string;
19
11
  createdAt: Date;
@@ -26,7 +18,7 @@ type EventCause = {
26
18
  batchSize?: number;
27
19
  };
28
20
  /**
29
- * What the log stores: immutable event history, including its causal edge,
21
+ * What the store persists: immutable event history, including its causal edge,
30
22
  * plus derived dispatch and failure bookkeeping. The bookkeeping is disposable;
31
23
  * the event and its cause are not.
32
24
  */
@@ -75,6 +67,21 @@ type StoredSessionSummary = {
75
67
  firstEventAt: Date;
76
68
  updatedAt: Date;
77
69
  };
70
+ /**
71
+ * One live presence value, as `A2Store.presence.read` returns it. The
72
+ * row is the LWW unit — one participant's one field. `at` is the
73
+ * sender's stamp, the LWW comparator. `expiresAt` is the storage's
74
+ * own clock at the applied set plus its `ttlMs` — never derived from
75
+ * the sender stamp; a row is live strictly before it.
76
+ */
77
+ type PresenceRow = {
78
+ participant: string;
79
+ field: string;
80
+ value: unknown;
81
+ seen: number;
82
+ at: Date;
83
+ expiresAt: Date;
84
+ };
78
85
  /** Metadata for the latest cached fold of one reducer. */
79
86
  type StoredSnapshot = {
80
87
  reducerName: string;
@@ -82,7 +89,7 @@ type StoredSnapshot = {
82
89
  updatedAt: Date;
83
90
  };
84
91
  /** One consistent cache-plus-tail read for a reducer fold. */
85
- type LogStateRead = {
92
+ type StoreStateRead = {
86
93
  /** The latest cached fold for this reducer, if one exists. */
87
94
  snapshot: {
88
95
  index: number;
@@ -96,22 +103,37 @@ type StoredSessionPage = {
96
103
  /** Opaque backend cursor; null means there is no next page. */
97
104
  cursor: string | null;
98
105
  };
106
+ type StoredEventPage = {
107
+ events: StoredEvent[];
108
+ /** Fixed inclusive log frontier shared by every page in one inspection. */
109
+ throughIndex: number;
110
+ };
99
111
  /**
100
- * Optional read-only administration implemented by A2's shipped logs.
101
- * It is separate from the correctness contract so custom logs do not
112
+ * Optional read-only administration implemented by A2's shipped stores.
113
+ * It is separate from the correctness contract so custom stores do not
102
114
  * need to expose storage-wide discovery merely to process events.
103
115
  */
104
- type A2LogInspection = {
116
+ type A2StoreInspection = {
105
117
  listSessions(options: {
106
118
  /** Storage namespace prefix, including the contract separator. */
107
119
  prefix: string;
108
120
  cursor?: string;
109
121
  limit: number;
110
122
  }): Promise<StoredSessionPage>;
123
+ /**
124
+ * A bounded event page for read-only administration. The first page captures
125
+ * `throughIndex`; callers pass it back so concurrent appends cannot extend a
126
+ * walk indefinitely. Optional for custom stores; A2 falls back to `read()`.
127
+ */
128
+ readEvents?(sessionId: string, options: {
129
+ afterIndex: number;
130
+ throughIndex?: number;
131
+ limit: number;
132
+ }): Promise<StoredEventPage>;
111
133
  listSnapshots(sessionId: string): Promise<StoredSnapshot[]>;
112
134
  };
113
135
  /** The result of atomically claiming every currently eligible event. */
114
- type LogClaimAvailableResult = {
136
+ type StoreClaimAvailableResult = {
115
137
  outcome: "claimed";
116
138
  events: StoredEvent[];
117
139
  } | {
@@ -127,12 +149,19 @@ type CompleteAttemptResult = {
127
149
  } | {
128
150
  outcome: "superseded";
129
151
  };
152
+ /** The result of one claim-renewal heartbeat, both lists in append order. */
153
+ type RenewClaimsResult = {
154
+ /** Listed claims that remain owned by the holder. */
155
+ renewed: number[];
156
+ /** Listed claims whose event a newer attempt has durably taken. */
157
+ superseded: number[];
158
+ };
130
159
  /** The result of atomically recording a caught handler failure. */
131
160
  type FailAttemptResult = {
132
161
  outcome: "failed" | "dead_lettered" | "superseded";
133
162
  failureCount: number;
134
163
  };
135
- /** Input to `A2Log.append` — already validated by the machine. */
164
+ /** Input to `A2Store.append` — already validated by the machine. */
136
165
  type AppendEvent = {
137
166
  type: string;
138
167
  payload: unknown;
@@ -145,12 +174,12 @@ type AppendEvent = {
145
174
  /** Internal hint: settle this event in the append transaction; no handler is registered. */
146
175
  settled?: true;
147
176
  };
148
- /** A handler-returned event with the deterministic id recovery requires. */
177
+ /** A handler-returned event with the deterministic id retries require. */
149
178
  type ReturnedEvent = AppendEvent & {
150
179
  id: string;
151
180
  };
152
181
  /** The rows written by an append and the session's pending state. */
153
- type LogAppendResult = {
182
+ type StoreAppendResult = {
154
183
  events: StoredEvent[];
155
184
  /** Whether the session contains an event without a completion marker. */
156
185
  hasPending: boolean;
@@ -165,7 +194,7 @@ type Clock = {
165
194
  };
166
195
  /** An injectable id source for generated event ids. */
167
196
  type IdSource = () => string;
168
- interface A2Log {
197
+ interface A2Store {
169
198
  /**
170
199
  * Accepts a batch; the batch is atomic — one transaction, consecutive
171
200
  * `index`es, all-or-nothing. The idempotency key covers the whole
@@ -178,17 +207,27 @@ interface A2Log {
178
207
  * operation, with no dispatch claim or `processedByAttempt`. The result's
179
208
  * `hasPending` reflects the whole session in the same atomic operation,
180
209
  * including older events and idempotent retries.
210
+ *
211
+ * An event carrying `cause` is a handler append, fenced by attempt
212
+ * currency: accept it only while `cause.attempt` is still the parent
213
+ * event's latest attempt and the parent is not dead-lettered; otherwise
214
+ * throw `A2Error('SUPERSEDED_ATTEMPT')` and write nothing. The check is
215
+ * part of this atomic operation and must serialize against a concurrent
216
+ * `claimAvailable` — an unlocked read of the parent admits write skew.
217
+ * The idempotent-replay path runs first, so a batch whose ids all exist
218
+ * replays regardless of the current attempt.
181
219
  */
182
- append(sessionId: string, events: AppendEvent[]): Promise<LogAppendResult>;
183
- /** Events for one session, oldest first. */
220
+ append(sessionId: string, events: AppendEvent[]): Promise<StoreAppendResult>;
221
+ /** Events for one session, oldest first. Bounds form `(afterIndex, throughIndex]`. */
184
222
  read(sessionId: string, opts?: {
185
223
  afterIndex?: number;
224
+ throughIndex?: number;
186
225
  }): Promise<StoredEvent[]>;
187
226
  /**
188
227
  * Atomically claims every eligible pending event. Unlaned events are all
189
228
  * independently eligible. Within a lane, only the lowest-index unfinished
190
229
  * event is eligible. A live claim produces `busy` only when it is the sole
191
- * remaining obstacle to actionable work. Claimed events are returned in log
230
+ * remaining obstacle to actionable work. Claimed events are returned in event-log
192
231
  * order. Excluded rows remain lane barriers.
193
232
  */
194
233
  claimAvailable(options: {
@@ -197,19 +236,26 @@ interface A2Log {
197
236
  ttlMs: number;
198
237
  expiresAtMs?: number;
199
238
  excludeIndexes?: readonly number[];
200
- }): Promise<LogClaimAvailableResult>;
239
+ }): Promise<StoreClaimAvailableResult>;
201
240
  /**
202
- * Renews the listed live claims still owned by `holder`. Missing, expired,
203
- * completed, failed, or superseded claims are omitted from the result. The
204
- * renewed indexes are returned in log order.
241
+ * Renews the listed live claims still owned by `holder`. Each listed claim
242
+ * carries the attempt ordinal the holder owns. `renewed` lists the claims
243
+ * that remain owned after the operation; `superseded` lists the claims
244
+ * whose event's `attemptCount` has durably passed the listed attempt. An
245
+ * expired claim no successor has taken appears in neither list — its
246
+ * attempt may still complete (`completeAttempt` is the fence), so it is
247
+ * not reported as lost. Renewal never revives an expired claim.
205
248
  */
206
249
  renewClaims(options: {
207
250
  sessionId: string;
208
251
  holder: string;
209
- indexes: number[];
252
+ claims: readonly {
253
+ index: number;
254
+ attempt: number;
255
+ }[];
210
256
  ttlMs: number;
211
257
  expiresAtMs?: number;
212
- }): Promise<number[]>;
258
+ }): Promise<RenewClaimsResult>;
213
259
  /**
214
260
  * Atomically completes one current attempt and appends its returned events.
215
261
  * Retrying a committed completion with the same attempt and deterministic
@@ -239,25 +285,55 @@ interface A2Log {
239
285
  * log. The snapshot is untrusted; core may reject it and issue a full
240
286
  * `read()` when its state schema no longer accepts the cached value.
241
287
  */
242
- readState(sessionId: string, reducerName: string): Promise<LogStateRead>;
288
+ readState(sessionId: string, reducerName: string): Promise<StoreStateRead>;
243
289
  /**
244
290
  * Writes a disposable reducer cache. Guard this operation so a slower
245
291
  * concurrent writer can never clobber a further-along snapshot
246
292
  * (`where up_to_index < excluded.up_to_index`).
247
293
  */
248
294
  putSnapshot(sessionId: string, reducerName: string, index: number, state: unknown): Promise<void>;
295
+ /**
296
+ * Optional ephemeral-plane capability (specs/a2-implementation.md
297
+ * §15.1) — optional like snapshots are. Values arrive already
298
+ * validated by core; adapters store them opaquely. Presence never
299
+ * touches the event log: no index, no history row, no recovery arm.
300
+ */
301
+ presence?: {
302
+ /**
303
+ * Field-wise last-writer-wins merge of one participant's values:
304
+ * a field whose existing row has a strictly newer `at` (the
305
+ * sender's stamp) is left untouched; `null` deletes the row. Every
306
+ * applied write refreshes that field's expiry to the storage's own
307
+ * clock plus `ttlMs` — the sender stamp orders writes but never
308
+ * anchors their lifetime.
309
+ */
310
+ set(ns: string, participant: string, values: Record<string, unknown | null>, meta: {
311
+ seen: number;
312
+ at: Date;
313
+ ttlMs: number;
314
+ }): Promise<void>;
315
+ /** The current map, pruned of rows at or past their `expiresAt`. */
316
+ read(ns: string): Promise<PresenceRow[]>;
317
+ /**
318
+ * Push-tier patch delivery; present only on backends with a real
319
+ * broadcast primitive. Without it the backend is the degraded
320
+ * tier: live feeds surface presence by re-reading on their
321
+ * existing poll cadence.
322
+ */
323
+ subscribe?(ns: string, onPatch: (patch: PresencePatch) => void): () => void;
324
+ };
249
325
  /** Read-only operational data used by `experimental-a2/devtools/server`. */
250
- inspect?: A2LogInspection;
326
+ inspect?: A2StoreInspection;
251
327
  /**
252
- * A live feed of one session's events, starting after `startAt`
328
+ * A live feed of one session's events, starting after `startAfter`
253
329
  * (exclusive). Transport is the backend's choice — in-process pub/sub,
254
330
  * polling, LISTEN/NOTIFY — callers never branch on which. The iterable
255
331
  * ends when the consumer calls `return()` (e.g. a disconnecting SSE
256
332
  * client) and must deliver events appended after subscription.
257
333
  */
258
334
  stream(sessionId: string, opts?: {
259
- startAt?: number;
335
+ startAfter?: number;
260
336
  }): AsyncIterable<Event>;
261
337
  }
262
338
  //#endregion
263
- export { Event as a, IdSource as c, LogStateRead as d, StoredEvent as f, StoredSnapshot as h, Clock as i, LogAppendResult as l, StoredSessionSummary as m, A2LogInspection as n, EventCause as o, StoredSessionPage as p, AppendEvent as r, FailAttemptResult as s, A2Log as t, LogClaimAvailableResult as u };
339
+ export { Event as a, IdSource as c, StoreClaimAvailableResult as d, StoreStateRead as f, StoredSnapshot as g, StoredSessionSummary as h, Clock as i, PresenceRow as l, StoredSessionPage as m, A2StoreInspection as n, EventCause as o, StoredEvent as p, AppendEvent as r, FailAttemptResult as s, A2Store as t, StoreAppendResult as u };
@@ -1,4 +1,4 @@
1
- //#region src/log.ts
1
+ //#region src/store.ts
2
2
  const SYSTEM_CLOCK = { now: () => /* @__PURE__ */ new Date() };
3
3
  const RANDOM_IDS = () => crypto.randomUUID();
4
4
  //#endregion
@@ -0,0 +1,8 @@
1
+ //#region src/store-codec.ts
2
+ const decodeReturnedEventIds = (value) => {
3
+ const decoded = typeof value === "string" ? JSON.parse(value) : value;
4
+ if (!Array.isArray(decoded) || decoded.some((eventId) => typeof eventId !== "string")) throw new TypeError("stored event has invalid returned event ids");
5
+ return decoded;
6
+ };
7
+ //#endregion
8
+ export { decodeReturnedEventIds as t };
@@ -0,0 +1,11 @@
1
+ import { c as IdSource, i as Clock, t as A2Store } from "./store-C3sNAaBT.js";
2
+ //#region src/store-memory.d.ts
3
+ type MemoryStoreOptions = {
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?: MemoryStoreOptions): A2Store;
10
+ //#endregion
11
+ export { MemoryStoreOptions, memory };
@@ -1,14 +1,8 @@
1
- import { t as A2Error } from "./errors-BJRMd-h6.js";
2
- import { t as idempotentReplay } from "./idempotent-replay-BMyHrP0L.js";
3
- import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./log-yJbXUf72.js";
4
- //#region src/log-memory.ts
5
- /**
6
- * experimental-a2/log-memory — in-memory log backend (the test default).
7
- *
8
- * Implements the A2Log interface with zero dependencies. See
9
- * specs/a2-implementation.md §3 for the contract; the conformance suite
10
- * in test/conformance is the executable version of it.
11
- */
1
+ import { t as A2Error } from "./errors-BQuJpe82.js";
2
+ import { g as nullProtoRecord } from "./internal-aEotMzu_.js";
3
+ import { t as idempotentReplay } from "./idempotent-replay-DuqEkYA7.js";
4
+ import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
5
+ //#region src/store-memory.ts
12
6
  const toStored = (row) => ({
13
7
  id: row.id,
14
8
  type: row.type,
@@ -50,6 +44,9 @@ function memory(options = {}) {
50
44
  /** Keyed by `${sessionId}\u0000${reducerName}` — a pure cache. */
51
45
  const snapshots = /* @__PURE__ */ new Map();
52
46
  const streamSubscribers = /* @__PURE__ */ new Map();
47
+ /** ns → participant → field → latest surviving write. */
48
+ const presenceRows = /* @__PURE__ */ new Map();
49
+ const presenceSubscribers = /* @__PURE__ */ new Map();
53
50
  const notifyStreams = (sessionId, rows) => {
54
51
  const subs = streamSubscribers.get(sessionId);
55
52
  if (!subs) return;
@@ -173,16 +170,23 @@ function memory(options = {}) {
173
170
  if (existing.length === events.length) return appendResult(sessionId, idempotentReplay(events, existing.map(toStored)));
174
171
  throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
175
172
  }
173
+ const rows = sessions.get(sessionId) ?? [];
174
+ for (const event of events) {
175
+ if (!event.cause) continue;
176
+ const parent = rows[event.cause.index - 1];
177
+ if (!parent) throw new TypeError(`no event at index ${event.cause.index} in session '${sessionId}'`);
178
+ if (parent.attemptCount !== event.cause.attempt || parent.failedAt !== null) throw new A2Error("SUPERSEDED_ATTEMPT", `attempt ${event.cause.attempt} no longer owns event ${event.cause.index} in session '${sessionId}'`);
179
+ }
176
180
  const now = clock.now();
177
181
  const inserted = insert(sessionId, events, now);
178
182
  notifyStreams(sessionId, inserted);
179
183
  return appendResult(sessionId, inserted.map(toStored));
180
184
  },
181
185
  async read(sessionId, opts) {
182
- return (sessions.get(sessionId) ?? []).filter((row) => {
183
- if (opts?.afterIndex !== void 0 && row.index <= opts.afterIndex) return false;
184
- return true;
185
- }).map(toStored);
186
+ const rows = sessions.get(sessionId) ?? [];
187
+ const start = Math.max(0, opts?.afterIndex ?? 0);
188
+ const end = Math.max(0, opts?.throughIndex ?? rows.length);
189
+ return rows.slice(start, end).map(toStored);
186
190
  },
187
191
  async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs, excludeIndexes }) {
188
192
  const claimedAt = clock.now();
@@ -209,16 +213,28 @@ function memory(options = {}) {
209
213
  events: eligible.map((row) => claim(row, holder, claimedAt, expiresAt))
210
214
  };
211
215
  },
212
- async renewClaims({ sessionId, holder, indexes, ttlMs, expiresAtMs }) {
216
+ async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {
213
217
  const now = clock.now();
214
218
  const expiresAt = new Date(expiresAtMs ?? now.getTime() + ttlMs);
215
- const requested = new Set(indexes);
219
+ const requested = new Map(claims.map((c) => [c.index, c.attempt]));
216
220
  const renewed = [];
217
- for (const row of sessions.get(sessionId) ?? []) if (requested.has(row.index) && row.processedAt === null && row.failedAt === null && row.claimHolder === holder && row.claimExpiresAt !== null && row.claimExpiresAt > now) {
218
- row.claimExpiresAt = new Date(expiresAt);
219
- renewed.push(row.index);
221
+ const superseded = [];
222
+ for (const row of sessions.get(sessionId) ?? []) {
223
+ const attempt = requested.get(row.index);
224
+ if (attempt === void 0) continue;
225
+ if (row.attemptCount > attempt) {
226
+ superseded.push(row.index);
227
+ continue;
228
+ }
229
+ if (row.processedAt === null && row.failedAt === null && row.claimHolder === holder && row.claimExpiresAt !== null && row.claimExpiresAt > now) {
230
+ row.claimExpiresAt = new Date(expiresAt);
231
+ renewed.push(row.index);
232
+ }
220
233
  }
221
- return renewed;
234
+ return {
235
+ renewed,
236
+ superseded
237
+ };
222
238
  },
223
239
  async completeAttempt({ sessionId, index, attempt, events }) {
224
240
  const parent = find(sessionId, index);
@@ -309,6 +325,85 @@ function memory(options = {}) {
309
325
  updatedAt: clock.now()
310
326
  });
311
327
  },
328
+ presence: {
329
+ async set(ns, participant, values, meta) {
330
+ let participants = presenceRows.get(ns);
331
+ if (!participants) {
332
+ participants = /* @__PURE__ */ new Map();
333
+ presenceRows.set(ns, participants);
334
+ }
335
+ let fields = participants.get(participant);
336
+ if (!fields) {
337
+ fields = /* @__PURE__ */ new Map();
338
+ participants.set(participant, fields);
339
+ }
340
+ const expiresAtMs = clock.now().getTime() + meta.ttlMs;
341
+ const applied = nullProtoRecord();
342
+ let appliedCount = 0;
343
+ for (const [field, value] of Object.entries(values)) {
344
+ const existing = fields.get(field);
345
+ if (existing && existing.at > meta.at) continue;
346
+ if (value === null) fields.delete(field);
347
+ else fields.set(field, {
348
+ value: structuredClone(value),
349
+ seen: meta.seen,
350
+ at: new Date(meta.at),
351
+ expiresAt: new Date(expiresAtMs)
352
+ });
353
+ applied[field] = structuredClone(value);
354
+ appliedCount += 1;
355
+ }
356
+ if (fields.size === 0) participants.delete(participant);
357
+ if (participants.size === 0) presenceRows.delete(ns);
358
+ if (appliedCount === 0) return;
359
+ const subs = presenceSubscribers.get(ns);
360
+ if (!subs) return;
361
+ const patch = {
362
+ participant,
363
+ values: applied,
364
+ seen: meta.seen,
365
+ at: new Date(meta.at)
366
+ };
367
+ for (const listener of subs) listener(patch);
368
+ },
369
+ async read(ns) {
370
+ const participants = presenceRows.get(ns);
371
+ if (!participants) return [];
372
+ const now = clock.now();
373
+ const rows = [];
374
+ for (const [participant, fields] of participants) {
375
+ for (const [field, entry] of fields) {
376
+ if (entry.expiresAt <= now) {
377
+ fields.delete(field);
378
+ continue;
379
+ }
380
+ rows.push({
381
+ participant,
382
+ field,
383
+ value: structuredClone(entry.value),
384
+ seen: entry.seen,
385
+ at: new Date(entry.at),
386
+ expiresAt: new Date(entry.expiresAt)
387
+ });
388
+ }
389
+ if (fields.size === 0) participants.delete(participant);
390
+ }
391
+ if (participants.size === 0) presenceRows.delete(ns);
392
+ return rows;
393
+ },
394
+ subscribe(ns, onPatch) {
395
+ let subs = presenceSubscribers.get(ns);
396
+ if (!subs) {
397
+ subs = /* @__PURE__ */ new Set();
398
+ presenceSubscribers.set(ns, subs);
399
+ }
400
+ subs.add(onPatch);
401
+ return () => {
402
+ subs.delete(onPatch);
403
+ if (subs.size === 0) presenceSubscribers.delete(ns);
404
+ };
405
+ }
406
+ },
312
407
  inspect: {
313
408
  async listSessions(inspectionOptions) {
314
409
  const ids = [...sessions.keys()].filter((sessionId) => sessionId.startsWith(inspectionOptions.prefix) && (inspectionOptions.cursor === void 0 || sessionId > inspectionOptions.cursor)).toSorted();
@@ -350,6 +445,14 @@ function memory(options = {}) {
350
445
  cursor: ids.length > visible.length ? visible.at(-1) ?? null : null
351
446
  };
352
447
  },
448
+ async readEvents(sessionId, pageOptions) {
449
+ const rows = sessions.get(sessionId) ?? [];
450
+ const throughIndex = Math.min(pageOptions.throughIndex ?? rows.length, rows.length);
451
+ return {
452
+ events: rows.slice(pageOptions.afterIndex, throughIndex).slice(0, pageOptions.limit).map(toStored),
453
+ throughIndex
454
+ };
455
+ },
353
456
  async listSnapshots(sessionId) {
354
457
  const prefix = `${sessionId}\u0000`;
355
458
  return [...snapshots.entries()].filter(([key]) => key.startsWith(prefix)).map(([key, snapshot]) => ({
@@ -360,9 +463,9 @@ function memory(options = {}) {
360
463
  }
361
464
  },
362
465
  stream(sessionId, opts) {
363
- const startAt = opts?.startAt ?? 0;
466
+ const startAfter = opts?.startAfter ?? 0;
364
467
  return { [Symbol.asyncIterator]() {
365
- let last = startAt;
468
+ let last = startAfter;
366
469
  const buffer = [];
367
470
  let wake = null;
368
471
  let closed = false;
@@ -377,7 +480,7 @@ function memory(options = {}) {
377
480
  }
378
481
  subs.add(onRow);
379
482
  const existing = sessions.get(sessionId) ?? [];
380
- buffer.unshift(...existing.filter((row) => row.index > startAt));
483
+ buffer.unshift(...existing.filter((row) => row.index > startAfter));
381
484
  const unsubscribe = () => {
382
485
  subs.delete(onRow);
383
486
  if (subs.size === 0) streamSubscribers.delete(sessionId);
@@ -1,8 +1,8 @@
1
- import { n as POLL_TIMINGS } from "./internal-D6wNxTck.js";
2
- //#region src/log-polling.ts
1
+ import { i as POLL_TIMINGS } from "./internal-aEotMzu_.js";
2
+ //#region src/store-polling.ts
3
3
  /**
4
4
  * The shared polling stream — how poll-based backends (sqlite,
5
- * postgres) implement `A2Log.stream`.
5
+ * postgres) implement `A2Store.stream`.
6
6
  *
7
7
  * The cadence is activity-adaptive: while events are flowing, polls
8
8
  * run at a tight floor (25ms) so streams feel pushed — a token
@@ -14,6 +14,7 @@ import { n as POLL_TIMINGS } from "./internal-D6wNxTck.js";
14
14
  * on the cadence — each poll is a catch-up read (`index > last`, in
15
15
  * order), so nothing can be missed, only arrive a beat later.
16
16
  */
17
+ /** Unref'd cancellable timer — shared with the redis safety read. */
17
18
  const defaultSleep = (ms) => {
18
19
  let timer;
19
20
  let wake;
@@ -37,10 +38,10 @@ const floor = () => Math.min(POLL_TIMINGS.activeFloorMs, POLL_TIMINGS.idleCeilin
37
38
  * SSE client), waking any pending sleep immediately.
38
39
  */
39
40
  function pollingStream(readAfter, options) {
40
- const startAt = options.startAt ?? 0;
41
+ const startAfter = options.startAfter ?? 0;
41
42
  const sleep = options.sleep ?? defaultSleep;
42
43
  return { [Symbol.asyncIterator]() {
43
- let last = startAt;
44
+ let last = startAfter;
44
45
  let buffer = [];
45
46
  let closed = false;
46
47
  let pending = null;
@@ -80,4 +81,4 @@ function pollingStream(readAfter, options) {
80
81
  } };
81
82
  }
82
83
  //#endregion
83
- export { pollingStream as t };
84
+ export { pollingStream as n, defaultSleep as t };
@@ -1,5 +1,5 @@
1
- import { c as IdSource, i as Clock, t as A2Log } from "./log-ldf5g8Cx.js";
2
- //#region src/log-postgres.d.ts
1
+ import { c as IdSource, i as Clock, t as A2Store } from "./store-C3sNAaBT.js";
2
+ //#region src/store-postgres.d.ts
3
3
  /** The result shape this backend reads: just rows. */
4
4
  type PostgresQueryResult = {
5
5
  rows: Record<string, unknown>[];
@@ -20,7 +20,7 @@ type PostgresClient = {
20
20
  connect?(): Promise<PostgresConnection>;
21
21
  end?(): Promise<void>;
22
22
  };
23
- type PostgresLogOptions = {
23
+ type PostgresStoreOptions = {
24
24
  /** Creates a `pg` Pool lazily (requires the optional peer dep `pg`). */
25
25
  connectionString?: string | undefined;
26
26
  /** Bring your own client — a `pg` Pool, PGlite, a pooler wrapper. */
@@ -30,11 +30,11 @@ type PostgresLogOptions = {
30
30
  /** Injectable id source for generated event ids. */
31
31
  ids?: IdSource;
32
32
  };
33
- type PostgresLog = A2Log & {
33
+ type PostgresStore = A2Store & {
34
34
  /** End the underlying pool/client (when this backend created it, or
35
35
  * the injected client has `end`). */
36
36
  close(): Promise<void>;
37
37
  };
38
- declare function postgres(options?: PostgresLogOptions): PostgresLog;
38
+ declare function postgres(options?: PostgresStoreOptions): PostgresStore;
39
39
  //#endregion
40
- export { PostgresClient, PostgresConnection, PostgresLog, PostgresLogOptions, PostgresQueryResult, postgres };
40
+ export { PostgresClient, PostgresConnection, PostgresQueryResult, PostgresStore, PostgresStoreOptions, postgres };