experimental-a2 0.3.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.
- package/CHANGELOG.md +156 -0
- package/dist/ai-B4YhEnfw.d.ts +333 -0
- package/dist/ai-server.d.ts +48 -9
- package/dist/ai-server.js +121 -49
- package/dist/ai.d.ts +2 -303
- package/dist/ai.js +231 -86
- package/dist/cli-B3VuxoDe.js +597 -0
- package/dist/cli-bin.d.ts +1 -0
- package/dist/cli-bin.js +5 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +2 -0
- package/dist/client-BrfDXQ8A.d.ts +155 -0
- package/dist/client-Bt4tAKi9.js +798 -0
- package/dist/client.d.ts +2 -90
- package/dist/client.js +1 -409
- package/dist/{contract-CG_adnu_.js → contract-48bUMgcL.js} +10 -2
- package/dist/{contract-C_3dIIEU.d.ts → contract-jIfaR085.d.ts} +62 -8
- package/dist/devtools-J_jZ2vQf.d.ts +151 -0
- package/dist/devtools-kJJaORn-.js +338 -0
- package/dist/devtools-server.browser.js +1 -1
- package/dist/devtools-server.d.ts +2 -2
- package/dist/devtools-server.js +224 -43
- package/dist/devtools.d.ts +2 -0
- package/dist/devtools.js +2 -0
- package/dist/{errors-BJRMd-h6.js → errors-BQuJpe82.js} +4 -4
- package/dist/{errors-xL_JTXsY.d.ts → errors-W6nwJ-fm.d.ts} +1 -1
- package/dist/http.d.ts +71 -13
- package/dist/http.js +302 -41
- package/dist/{idempotent-replay-BMyHrP0L.js → idempotent-replay-DuqEkYA7.js} +2 -2
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/{inspection-E7qbD0Xj.js → inspection-DaxB5jM2.js} +2 -1
- package/dist/internal-aEotMzu_.js +209 -0
- package/dist/otel.d.ts +1 -1
- package/dist/platform-B4TnJtWu.js +32 -0
- package/dist/react.d.ts +37 -14
- package/dist/react.js +26 -15
- package/dist/scheduler-qstash.d.ts +78 -0
- package/dist/scheduler-qstash.js +499 -0
- package/dist/scheduler-task-BpzhPnRS.js +54 -0
- package/dist/{recovery-vercel.d.ts → scheduler-vercel.d.ts} +17 -24
- package/dist/scheduler-vercel.js +226 -0
- package/dist/server-CcNnFnoW.js +1405 -0
- package/dist/server-YtPq7hjw.d.ts +260 -0
- package/dist/server.d.ts +4 -155
- package/dist/server.js +2 -2
- package/dist/{log-ldf5g8Cx.d.ts → store-C3sNAaBT.d.ts} +111 -35
- package/dist/{log-yJbXUf72.js → store-N8PXxDAS.js} +1 -1
- package/dist/store-codec-DTG0Ftek.js +8 -0
- package/dist/store-memory.d.ts +11 -0
- package/dist/{log-memory.js → store-memory.js} +127 -24
- package/dist/{log-polling-DZ1MiKLg.js → store-polling-DgrrAE3d.js} +5 -5
- package/dist/{log-postgres.d.ts → store-postgres.d.ts} +6 -6
- package/dist/{log-postgres.js → store-postgres.js} +158 -24
- package/dist/{log-redis-core-CyJ5L8yR.js → store-redis-core-DWqx3F47.js} +269 -66
- package/dist/{log-redis-http.d.ts → store-redis-http.d.ts} +7 -7
- package/dist/{log-redis-http.js → store-redis-http.js} +20 -12
- package/dist/{log-redis.d.ts → store-redis.d.ts} +6 -6
- package/dist/{log-redis.js → store-redis.js} +99 -39
- package/dist/{log-sqlite.d.ts → store-sqlite.d.ts} +6 -6
- package/dist/{log-sqlite.js → store-sqlite.js} +116 -22
- package/dist/{telemetry-Cso0qyHQ.d.ts → telemetry-BjYHTfh2.d.ts} +1 -1
- package/dist/testing.browser.d.ts +1 -0
- package/dist/testing.browser.js +4 -0
- package/dist/testing.d.ts +31 -0
- package/dist/testing.js +101 -0
- package/dist/wire-DCUZBUlT.js +222 -0
- package/docs/01-quickstart.mdx +3 -3
- package/docs/concepts/01-contracts.mdx +18 -13
- package/docs/concepts/02-handlers.mdx +7 -7
- package/docs/concepts/03-durability.mdx +21 -21
- package/docs/concepts/04-state.mdx +18 -21
- package/docs/guides/01-timers.mdx +154 -49
- package/docs/guides/02-cancellation.mdx +30 -4
- package/docs/guides/03-react.mdx +20 -21
- package/docs/guides/04-local-first.mdx +1 -1
- package/docs/guides/05-production.mdx +310 -61
- package/docs/guides/06-ai-agents.mdx +249 -49
- package/docs/guides/07-devtools.mdx +137 -12
- package/docs/guides/08-application-data.mdx +1 -1
- package/docs/guides/09-presence.mdx +284 -0
- package/docs/guides/10-transports.mdx +131 -0
- package/docs/index.mdx +18 -14
- package/docs/reference/01-api.mdx +751 -103
- package/docs/reference/02-errors.mdx +10 -5
- package/package.json +23 -7
- package/dist/internal-gCd5qMry.js +0 -44
- package/dist/log-memory.d.ts +0 -11
- package/dist/recovery-vercel.js +0 -119
- package/dist/server-BcLa4RFL.js +0 -877
- package/dist/wire-BVsgR8o9.js +0 -62
|
@@ -1,19 +1,11 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
|
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
|
|
101
|
-
* It is separate from the correctness contract so custom
|
|
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
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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<
|
|
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<
|
|
239
|
+
}): Promise<StoreClaimAvailableResult>;
|
|
201
240
|
/**
|
|
202
|
-
* Renews the listed live claims still owned by `holder`.
|
|
203
|
-
*
|
|
204
|
-
*
|
|
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
|
-
|
|
252
|
+
claims: readonly {
|
|
253
|
+
index: number;
|
|
254
|
+
attempt: number;
|
|
255
|
+
}[];
|
|
210
256
|
ttlMs: number;
|
|
211
257
|
expiresAtMs?: number;
|
|
212
|
-
}): Promise<
|
|
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<
|
|
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?:
|
|
326
|
+
inspect?: A2StoreInspection;
|
|
251
327
|
/**
|
|
252
|
-
* A live feed of one session's events, starting after `
|
|
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
|
-
|
|
335
|
+
startAfter?: number;
|
|
260
336
|
}): AsyncIterable<Event>;
|
|
261
337
|
}
|
|
262
338
|
//#endregion
|
|
263
|
-
export { Event as a, IdSource as c,
|
|
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 };
|
|
@@ -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-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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,
|
|
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
|
|
219
|
+
const requested = new Map(claims.map((c) => [c.index, c.attempt]));
|
|
216
220
|
const renewed = [];
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
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
|
|
466
|
+
const startAfter = opts?.startAfter ?? 0;
|
|
364
467
|
return { [Symbol.asyncIterator]() {
|
|
365
|
-
let last =
|
|
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 >
|
|
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 {
|
|
2
|
-
//#region src/
|
|
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 `
|
|
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
|
|
@@ -38,10 +38,10 @@ const floor = () => Math.min(POLL_TIMINGS.activeFloorMs, POLL_TIMINGS.idleCeilin
|
|
|
38
38
|
* SSE client), waking any pending sleep immediately.
|
|
39
39
|
*/
|
|
40
40
|
function pollingStream(readAfter, options) {
|
|
41
|
-
const
|
|
41
|
+
const startAfter = options.startAfter ?? 0;
|
|
42
42
|
const sleep = options.sleep ?? defaultSleep;
|
|
43
43
|
return { [Symbol.asyncIterator]() {
|
|
44
|
-
let last =
|
|
44
|
+
let last = startAfter;
|
|
45
45
|
let buffer = [];
|
|
46
46
|
let closed = false;
|
|
47
47
|
let pending = null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { c as IdSource, i as Clock, t as
|
|
2
|
-
//#region src/
|
|
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
|
|
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
|
|
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?:
|
|
38
|
+
declare function postgres(options?: PostgresStoreOptions): PostgresStore;
|
|
39
39
|
//#endregion
|
|
40
|
-
export { PostgresClient, PostgresConnection,
|
|
40
|
+
export { PostgresClient, PostgresConnection, PostgresQueryResult, PostgresStore, PostgresStoreOptions, postgres };
|