experimental-a2 0.6.0 → 0.8.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 +41 -0
- package/dist/ai-server.d.ts +4 -5
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +20 -17
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +334 -2
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +1 -1
- package/dist/client.d.ts +202 -2
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1025 -1
- package/dist/client.js.map +1 -0
- package/dist/errors-BQuJpe82.js.map +1 -1
- package/dist/index.d.ts +22 -3
- package/dist/index.d.ts.map +1 -0
- package/dist/{internal-DstsI6Re.js → internal-DRXJ56EI.js} +5 -28
- package/dist/internal-DRXJ56EI.js.map +1 -0
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.js +4 -5
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +4 -4
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Duw6MVlB.js → server-B2XNevQA.js} +830 -131
- package/dist/server-B2XNevQA.js.map +1 -0
- package/dist/{server-DpvjhdoE.d.ts → server-DjPhHnbI.d.ts} +71 -50
- package/dist/server-DjPhHnbI.d.ts.map +1 -0
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/{store-DysUkTH3.d.ts → store-RJO35BMj.d.ts} +24 -62
- package/dist/store-RJO35BMj.d.ts.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +80 -78
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
- package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +231 -182
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DT01r4GZ.js} +213 -161
- package/dist/store-redis-core-DT01r4GZ.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -4
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +4 -5
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +104 -91
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
- package/dist/wire-B6te_wns.js.map +1 -0
- package/docs/concepts/02-handlers.mdx +4 -0
- package/docs/concepts/04-state.mdx +57 -9
- package/docs/guides/03-react.mdx +20 -28
- package/docs/guides/05-production.mdx +9 -11
- package/docs/guides/06-ai-agents.mdx +10 -15
- package/docs/guides/09-presence.mdx +14 -19
- package/docs/guides/10-transports.mdx +104 -86
- package/docs/reference/01-api.mdx +182 -293
- package/docs/reference/02-errors.mdx +5 -7
- package/package.json +1 -14
- package/src/ai-server.ts +36 -15
- package/src/client.ts +2 -2
- package/src/errors.ts +1 -0
- package/src/internal.ts +3 -62
- package/src/push-envelope.ts +24 -21
- package/src/scheduler-qstash.ts +3 -3
- package/src/scheduler-vercel.ts +2 -2
- package/src/server-fetch.ts +344 -0
- package/src/server.ts +315 -312
- package/src/session-socket.ts +36 -20
- package/src/sse.ts +2 -2
- package/src/store-memory.ts +138 -101
- package/src/store-postgres.ts +355 -238
- package/src/store-redis-core.ts +247 -237
- package/src/store-redis-http.ts +1 -2
- package/src/store-redis.ts +1 -2
- package/src/store-sqlite.ts +191 -153
- package/src/store.ts +24 -66
- package/src/wire.ts +2 -1
- package/dist/ai-D_PGS-JR.d.ts +0 -334
- package/dist/ai-D_PGS-JR.d.ts.map +0 -1
- package/dist/cli-B3VuxoDe.js +0 -599
- package/dist/cli-B3VuxoDe.js.map +0 -1
- package/dist/cli-bin.d.ts +0 -1
- package/dist/cli-bin.js +0 -7
- package/dist/cli-bin.js.map +0 -1
- package/dist/cli.d.ts +0 -20
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -2
- package/dist/client-CdMqi7mC.d.ts +0 -202
- package/dist/client-CdMqi7mC.d.ts.map +0 -1
- package/dist/client-Dj5d3SP_.js +0 -1026
- package/dist/client-Dj5d3SP_.js.map +0 -1
- package/dist/devtools-J_jZ2vQf.d.ts +0 -152
- package/dist/devtools-J_jZ2vQf.d.ts.map +0 -1
- package/dist/devtools-kJJaORn-.js +0 -340
- package/dist/devtools-kJJaORn-.js.map +0 -1
- package/dist/devtools-server.browser.d.ts +0 -1
- package/dist/devtools-server.browser.js +0 -6
- package/dist/devtools-server.browser.js.map +0 -1
- package/dist/devtools-server.d.ts +0 -23
- package/dist/devtools-server.d.ts.map +0 -1
- package/dist/devtools-server.js +0 -1270
- package/dist/devtools-server.js.map +0 -1
- package/dist/devtools.d.ts +0 -2
- package/dist/devtools.js +0 -2
- package/dist/errors-W6nwJ-fm.d.ts +0 -21
- package/dist/errors-W6nwJ-fm.d.ts.map +0 -1
- package/dist/http.d.ts +0 -151
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js +0 -706
- package/dist/http.js.map +0 -1
- package/dist/inspection-DaxB5jM2.js +0 -13
- package/dist/inspection-DaxB5jM2.js.map +0 -1
- package/dist/internal-DstsI6Re.js.map +0 -1
- package/dist/platform-B4TnJtWu.js +0 -34
- package/dist/platform-B4TnJtWu.js.map +0 -1
- package/dist/server-DpvjhdoE.d.ts.map +0 -1
- package/dist/server-Duw6MVlB.js.map +0 -1
- package/dist/store-DysUkTH3.d.ts.map +0 -1
- package/dist/store-redis-core-BFLwz0Wj.js.map +0 -1
- package/dist/testing.browser.d.ts +0 -1
- package/dist/testing.browser.js +0 -6
- package/dist/testing.browser.js.map +0 -1
- package/dist/testing.d.ts +0 -32
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js +0 -103
- package/dist/testing.js.map +0 -1
- package/dist/wire-BFQmSJ-9.js.map +0 -1
- package/docs/guides/07-devtools.mdx +0 -229
- package/src/cli-bin.ts +0 -5
- package/src/cli.ts +0 -1046
- package/src/devtools-app.ts +0 -989
- package/src/devtools-server.browser.ts +0 -5
- package/src/devtools-server.ts +0 -604
- package/src/devtools.ts +0 -716
- package/src/http.ts +0 -394
- package/src/inspection.ts +0 -39
- package/src/testing.browser.ts +0 -5
- package/src/testing.ts +0 -185
package/src/server.ts
CHANGED
|
@@ -28,11 +28,9 @@ import {
|
|
|
28
28
|
markSchedulerSendFailure,
|
|
29
29
|
nullProtoRecord,
|
|
30
30
|
serverInternals,
|
|
31
|
-
serverSchedulerBindings,
|
|
32
31
|
type DrainOutcome,
|
|
33
32
|
type DrainResult,
|
|
34
33
|
} from './internal.ts'
|
|
35
|
-
import { InspectionUnsupportedError, serverInspection } from './inspection.ts'
|
|
36
34
|
import { defaultSleep } from './store-polling.ts'
|
|
37
35
|
import type {
|
|
38
36
|
A2Store,
|
|
@@ -40,12 +38,21 @@ import type {
|
|
|
40
38
|
Event,
|
|
41
39
|
EventCause,
|
|
42
40
|
StoreClaimAvailableResult,
|
|
41
|
+
StoreSnapshotWrite,
|
|
43
42
|
StoreStateRead,
|
|
44
43
|
PresenceRow,
|
|
45
44
|
ReturnedEvent,
|
|
46
45
|
StoredEvent,
|
|
47
46
|
} from './store.ts'
|
|
48
47
|
import type { Reducer } from './reducer.ts'
|
|
48
|
+
import {
|
|
49
|
+
createServerFetch,
|
|
50
|
+
type A2Operation,
|
|
51
|
+
type A2PushEvent,
|
|
52
|
+
type A2PushPresence,
|
|
53
|
+
type ServerFetchOptions,
|
|
54
|
+
type UpgradeWebSocket,
|
|
55
|
+
} from './server-fetch.ts'
|
|
49
56
|
import { validateSync } from './validate.ts'
|
|
50
57
|
import {
|
|
51
58
|
deterministicEventId,
|
|
@@ -66,45 +73,6 @@ import {
|
|
|
66
73
|
type A2Telemetry,
|
|
67
74
|
} from './telemetry.ts'
|
|
68
75
|
|
|
69
|
-
/**
|
|
70
|
-
* Events that arrived over the wire through `parsePushBody` — already
|
|
71
|
-
* envelope-validated, headed for schema validation inside `append`.
|
|
72
|
-
* The brand lets the documented push route hand them straight to
|
|
73
|
-
* `session.append` without weakening typed appends for app code: a
|
|
74
|
-
* hand-written `{ type: string }` literal still fails to compile.
|
|
75
|
-
*/
|
|
76
|
-
export type PushedEvent = {
|
|
77
|
-
type: string
|
|
78
|
-
payload: unknown
|
|
79
|
-
id?: string
|
|
80
|
-
readonly '~a2.pushed': true
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* A presence patch that arrived over the wire through `parsePushBody`
|
|
85
|
-
* — same provenance brand as `PushedEvent`, so the documented route
|
|
86
|
-
* hands it whole to `session.setPresence` while a hand-written
|
|
87
|
-
* untyped patch still fails to compile. Field validation happens
|
|
88
|
-
* inside `setPresence`.
|
|
89
|
-
*/
|
|
90
|
-
export type PushedPresence = {
|
|
91
|
-
participant: string
|
|
92
|
-
values: Record<string, unknown>
|
|
93
|
-
seen?: number
|
|
94
|
-
/** The sender's LWW stamp in epoch ms; receipt time when absent. */
|
|
95
|
-
at?: number
|
|
96
|
-
readonly '~a2.pushed': true
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export type PushValidationContext = {
|
|
100
|
-
sessionId: string
|
|
101
|
-
events: readonly PushedEvent[]
|
|
102
|
-
/** The whole pushed patch — present exactly on the presence-plane
|
|
103
|
-
* invocation, so the callback authorizes the participant id and can
|
|
104
|
-
* apply size or cardinality policy. */
|
|
105
|
-
presence?: PushedPresence
|
|
106
|
-
}
|
|
107
|
-
|
|
108
76
|
/** What every handler receives. */
|
|
109
77
|
export type HandlerContext<
|
|
110
78
|
D extends EventDefs,
|
|
@@ -142,11 +110,9 @@ export type Lane<
|
|
|
142
110
|
K extends keyof D & string = keyof D & string,
|
|
143
111
|
> = string | ((context: LaneContext<D, K>) => string)
|
|
144
112
|
|
|
145
|
-
export type SessionDispatch<D extends EventDefs> =
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
(...events: PushedEvent[]): Promise<ContractEvent<D>[]>
|
|
149
|
-
}
|
|
113
|
+
export type SessionDispatch<D extends EventDefs> = (
|
|
114
|
+
...events: AppendInput<D>[]
|
|
115
|
+
) => Promise<ContractEvent<D>[]>
|
|
150
116
|
|
|
151
117
|
export type SessionAppend<D extends EventDefs> = SessionDispatch<D> & {
|
|
152
118
|
/** Commit, then hand pending work directly to configured scheduler. */
|
|
@@ -169,6 +135,10 @@ export type SessionSchedule<D extends EventDefs> = (
|
|
|
169
135
|
...events: AppendInput<D>[]
|
|
170
136
|
) => Promise<void>
|
|
171
137
|
|
|
138
|
+
export type StateOptions = {
|
|
139
|
+
through?: number | 'latest'
|
|
140
|
+
}
|
|
141
|
+
|
|
172
142
|
/**
|
|
173
143
|
* The presence members of a session — intersected in via
|
|
174
144
|
* `WithPresence`, so they exist exactly when the contract declares
|
|
@@ -192,16 +162,12 @@ export type SessionPresence<D extends EventDefs, P extends PresenceDefs> = {
|
|
|
192
162
|
* is the sender's LWW stamp in epoch ms; omitted, receipt time
|
|
193
163
|
* stands in (single writer, so receipt order is sender order).
|
|
194
164
|
*/
|
|
195
|
-
setPresence(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
at?: number
|
|
202
|
-
}
|
|
203
|
-
| PushedPresence,
|
|
204
|
-
): Promise<void>
|
|
165
|
+
setPresence(patch: {
|
|
166
|
+
participant: string
|
|
167
|
+
values: PresencePatch<P>['values']
|
|
168
|
+
seen?: number
|
|
169
|
+
at?: number
|
|
170
|
+
}): Promise<void>
|
|
205
171
|
/** The current map, expired values pruned — a point-in-time read. */
|
|
206
172
|
presence(): Promise<PresenceMap<P>>
|
|
207
173
|
}
|
|
@@ -216,11 +182,13 @@ export type Session<
|
|
|
216
182
|
append: Append
|
|
217
183
|
schedule: SessionSchedule<D>
|
|
218
184
|
history(options?: { gte?: number; lte?: number }): Promise<ContractEvent<D>[]>
|
|
219
|
-
state<S>(
|
|
185
|
+
state<S>(
|
|
186
|
+
reducer: Reducer<D, S>,
|
|
187
|
+
options?: StateOptions,
|
|
188
|
+
): Promise<{ state: S; index: number }>
|
|
220
189
|
/**
|
|
221
190
|
* A live feed of this session's events, starting after `startAfter`
|
|
222
|
-
* (exclusive). Server-side only
|
|
223
|
-
* exposes it over SSE as the route's stream lane.
|
|
191
|
+
* (exclusive). Server-side only. `server.fetch` exposes it over SSE.
|
|
224
192
|
*/
|
|
225
193
|
stream(opts?: { startAfter?: number }): AsyncIterable<ContractEvent<D>>
|
|
226
194
|
}
|
|
@@ -255,6 +223,10 @@ export type A2Server<
|
|
|
255
223
|
> = {
|
|
256
224
|
/** The contract this server implements. */
|
|
257
225
|
readonly contract: Contract<D, P>
|
|
226
|
+
readonly fetch: {
|
|
227
|
+
(request: Request): Promise<Response>
|
|
228
|
+
(request: Request, options: ServerFetchOptions<D, P>): Promise<Response>
|
|
229
|
+
}
|
|
258
230
|
session(id: string): Session<D, SessionAppend<D>, P>
|
|
259
231
|
/**
|
|
260
232
|
* Process every currently eligible event. `settled` means nothing
|
|
@@ -263,6 +235,15 @@ export type A2Server<
|
|
|
263
235
|
drain(sessionId: string): Promise<{ settled: boolean }>
|
|
264
236
|
}
|
|
265
237
|
|
|
238
|
+
export type {
|
|
239
|
+
A2Operation,
|
|
240
|
+
A2PushEvent,
|
|
241
|
+
A2PushPresence,
|
|
242
|
+
ServerFetchOptions,
|
|
243
|
+
UpgradeWebSocket,
|
|
244
|
+
}
|
|
245
|
+
export type { A2Socket } from './session-socket.ts'
|
|
246
|
+
|
|
266
247
|
/**
|
|
267
248
|
* Deliver one authenticated scheduler append through an A2 server's ordinary
|
|
268
249
|
* top-level append path. Custom scheduler adapters call this after validating
|
|
@@ -336,8 +317,6 @@ export type ServerOptions<
|
|
|
336
317
|
scheduler?: A2Scheduler
|
|
337
318
|
/** Optional instrumentation — e.g. `otel()` from `experimental-a2/otel`. */
|
|
338
319
|
telemetry?: A2Telemetry
|
|
339
|
-
/** Validate events that came through `parsePushBody` before writing them. */
|
|
340
|
-
validatePush?: (context: PushValidationContext) => void | PromiseLike<void>
|
|
341
320
|
/**
|
|
342
321
|
* Presence-plane policy — valid only when the contract declares
|
|
343
322
|
* presence fields (TypeError at construction otherwise). `ttlMs` is
|
|
@@ -994,26 +973,85 @@ export function createServer<
|
|
|
994
973
|
store: A2Store,
|
|
995
974
|
sessionId: string,
|
|
996
975
|
reducerName: string,
|
|
976
|
+
throughIndex?: number,
|
|
977
|
+
snapshotThroughIndex?: number,
|
|
997
978
|
): Promise<StoreStateRead> => {
|
|
998
979
|
try {
|
|
999
|
-
return await store.readState(
|
|
980
|
+
return await store.readState(
|
|
981
|
+
nsId(sessionId),
|
|
982
|
+
reducerName,
|
|
983
|
+
throughIndex === undefined && snapshotThroughIndex === undefined
|
|
984
|
+
? undefined
|
|
985
|
+
: {
|
|
986
|
+
...(throughIndex === undefined ? {} : { throughIndex }),
|
|
987
|
+
...(snapshotThroughIndex === undefined
|
|
988
|
+
? {}
|
|
989
|
+
: { snapshotThroughIndex }),
|
|
990
|
+
},
|
|
991
|
+
)
|
|
1000
992
|
} catch {
|
|
1001
993
|
try {
|
|
1002
|
-
return {
|
|
994
|
+
return {
|
|
995
|
+
headIndex: null,
|
|
996
|
+
snapshot: null,
|
|
997
|
+
events: await store.read(
|
|
998
|
+
nsId(sessionId),
|
|
999
|
+
throughIndex === undefined ? undefined : { throughIndex },
|
|
1000
|
+
),
|
|
1001
|
+
}
|
|
1003
1002
|
} catch (err) {
|
|
1004
1003
|
throw asStoreUnavailable(err)
|
|
1005
1004
|
}
|
|
1006
1005
|
}
|
|
1007
1006
|
}
|
|
1008
1007
|
|
|
1008
|
+
type PendingStateRead = {
|
|
1009
|
+
sessionId: string
|
|
1010
|
+
throughIndex?: number
|
|
1011
|
+
pinEventIndex?: number
|
|
1012
|
+
span: A2SpanHandle
|
|
1013
|
+
resolve: (result: { state: unknown; index: number }) => void
|
|
1014
|
+
reject: (reason: unknown) => void
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
type StateReadPlan = {
|
|
1018
|
+
sessionId: string
|
|
1019
|
+
entries: PendingStateRead[]
|
|
1020
|
+
throughIndex?: number
|
|
1021
|
+
snapshotThroughIndex?: number
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const planStateReads = (batch: PendingStateRead[]): StateReadPlan[] => {
|
|
1025
|
+
const bySession = new Map<string, PendingStateRead[]>()
|
|
1026
|
+
for (const entry of batch) {
|
|
1027
|
+
const entries = bySession.get(entry.sessionId)
|
|
1028
|
+
if (entries) entries.push(entry)
|
|
1029
|
+
else bySession.set(entry.sessionId, [entry])
|
|
1030
|
+
}
|
|
1031
|
+
const plans: StateReadPlan[] = []
|
|
1032
|
+
for (const [sessionId, entries] of bySession) {
|
|
1033
|
+
const boundaries = entries.flatMap((entry) =>
|
|
1034
|
+
entry.throughIndex === undefined ? [] : [entry.throughIndex],
|
|
1035
|
+
)
|
|
1036
|
+
const plan: StateReadPlan = { sessionId, entries }
|
|
1037
|
+
if (boundaries.length === entries.length) {
|
|
1038
|
+
plan.throughIndex = Math.max(...boundaries)
|
|
1039
|
+
}
|
|
1040
|
+
if (boundaries.length > 0) {
|
|
1041
|
+
plan.snapshotThroughIndex = Math.min(...boundaries)
|
|
1042
|
+
}
|
|
1043
|
+
plans.push(plan)
|
|
1044
|
+
}
|
|
1045
|
+
return plans
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1009
1048
|
// The cache is untrusted: schema rejection refolds from the raw store.
|
|
1010
|
-
const
|
|
1049
|
+
const foldStatePlan = async (
|
|
1011
1050
|
store: A2Store,
|
|
1012
|
-
|
|
1013
|
-
|
|
1051
|
+
reducer: Reducer<D, unknown>,
|
|
1052
|
+
plan: StateReadPlan,
|
|
1014
1053
|
stateRead: StoreStateRead,
|
|
1015
|
-
|
|
1016
|
-
): Promise<{ state: S; index: number; folded: number }> => {
|
|
1054
|
+
): Promise<void> => {
|
|
1017
1055
|
let state = cloneInitial(reducer.initialState)
|
|
1018
1056
|
let index = 0
|
|
1019
1057
|
let snapshotOutcome = 'miss'
|
|
@@ -1029,44 +1067,101 @@ export function createServer<
|
|
|
1029
1067
|
if (result.issues) {
|
|
1030
1068
|
snapshotOutcome = 'rejected'
|
|
1031
1069
|
} else {
|
|
1032
|
-
state = result.value
|
|
1070
|
+
state = result.value
|
|
1033
1071
|
index = snap.index
|
|
1034
1072
|
snapshotOutcome = 'hit'
|
|
1035
1073
|
}
|
|
1036
1074
|
} else {
|
|
1037
|
-
state = snap.state
|
|
1075
|
+
state = snap.state
|
|
1038
1076
|
index = snap.index
|
|
1039
1077
|
snapshotOutcome = 'hit'
|
|
1040
1078
|
}
|
|
1041
1079
|
}
|
|
1042
|
-
span.setAttribute('a2.state.snapshot', snapshotOutcome)
|
|
1043
|
-
|
|
1044
1080
|
if (snapshotOutcome === 'rejected') {
|
|
1045
1081
|
try {
|
|
1046
|
-
rows = await store.read(
|
|
1082
|
+
rows = await store.read(
|
|
1083
|
+
nsId(plan.sessionId),
|
|
1084
|
+
plan.throughIndex === undefined
|
|
1085
|
+
? undefined
|
|
1086
|
+
: { throughIndex: plan.throughIndex },
|
|
1087
|
+
)
|
|
1047
1088
|
} catch (err) {
|
|
1048
1089
|
throw asStoreUnavailable(err)
|
|
1049
1090
|
}
|
|
1050
1091
|
}
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1092
|
+
const ordered = plan.entries.toSorted(
|
|
1093
|
+
(a, b) =>
|
|
1094
|
+
(a.throughIndex ?? Number.POSITIVE_INFINITY) -
|
|
1095
|
+
(b.throughIndex ?? Number.POSITIVE_INFINITY),
|
|
1096
|
+
)
|
|
1097
|
+
const writes = new Map<
|
|
1098
|
+
number,
|
|
1099
|
+
{ state: unknown; pinEventIndexes: Set<number> }
|
|
1100
|
+
>()
|
|
1101
|
+
let rowPosition = 0
|
|
1102
|
+
for (const entry of ordered) {
|
|
1103
|
+
const target = entry.throughIndex ?? Number.POSITIVE_INFINITY
|
|
1104
|
+
while (rowPosition < rows.length && rows[rowPosition]!.index <= target) {
|
|
1105
|
+
const row = rows[rowPosition]!
|
|
1106
|
+
state = reducer.fold(state, toPublic(row) as never)
|
|
1107
|
+
index = row.index
|
|
1108
|
+
rowPosition += 1
|
|
1109
|
+
}
|
|
1110
|
+
const resultState = cloneInitial(state)
|
|
1111
|
+
const folded = rowPosition
|
|
1112
|
+
entry.span.setAttribute('a2.state.snapshot', snapshotOutcome)
|
|
1113
|
+
entry.span.setAttribute('a2.state.folded', folded)
|
|
1114
|
+
entry.span.setAttribute('a2.state.index', index)
|
|
1115
|
+
if (index > 0 && (folded > 0 || entry.pinEventIndex !== undefined)) {
|
|
1116
|
+
const advancesHead =
|
|
1117
|
+
stateRead.headIndex === null || index > stateRead.headIndex
|
|
1118
|
+
const repairsHead =
|
|
1119
|
+
snapshotOutcome === 'rejected' && index === stateRead.headIndex
|
|
1120
|
+
if (
|
|
1121
|
+
!advancesHead &&
|
|
1122
|
+
!repairsHead &&
|
|
1123
|
+
entry.pinEventIndex === undefined
|
|
1124
|
+
) {
|
|
1125
|
+
entry.resolve({ state: resultState, index })
|
|
1126
|
+
continue
|
|
1127
|
+
}
|
|
1128
|
+
let write = writes.get(index)
|
|
1129
|
+
if (!write) {
|
|
1130
|
+
write = {
|
|
1131
|
+
state: cloneInitial(resultState),
|
|
1132
|
+
pinEventIndexes: new Set(),
|
|
1133
|
+
}
|
|
1134
|
+
writes.set(index, write)
|
|
1135
|
+
}
|
|
1136
|
+
if (entry.pinEventIndex !== undefined) {
|
|
1137
|
+
write.pinEventIndexes.add(entry.pinEventIndex)
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
entry.resolve({ state: resultState, index })
|
|
1141
|
+
}
|
|
1142
|
+
if (writes.size > 0) {
|
|
1143
|
+
const snapshots: StoreSnapshotWrite[] = []
|
|
1144
|
+
for (const [snapshotIndex, write] of writes) {
|
|
1145
|
+
const snapshot: StoreSnapshotWrite = {
|
|
1146
|
+
index: snapshotIndex,
|
|
1147
|
+
state: write.state,
|
|
1148
|
+
}
|
|
1149
|
+
if (write.pinEventIndexes.size > 0) {
|
|
1150
|
+
snapshot.pinEventIndexes = [...write.pinEventIndexes]
|
|
1151
|
+
}
|
|
1152
|
+
snapshots.push(snapshot)
|
|
1153
|
+
}
|
|
1154
|
+
const persistence = Promise.resolve()
|
|
1155
|
+
.then(() =>
|
|
1156
|
+
store.putSnapshots(nsId(plan.sessionId), reducer.name, snapshots),
|
|
1157
|
+
)
|
|
1158
|
+
.catch(() => {})
|
|
1159
|
+
track(persistence)
|
|
1160
|
+
platformWaitUntil(persistence)
|
|
1054
1161
|
}
|
|
1055
|
-
span.setAttribute('a2.state.folded', rows.length)
|
|
1056
|
-
span.setAttribute('a2.state.index', index)
|
|
1057
|
-
return { state, index, folded: rows.length }
|
|
1058
1162
|
}
|
|
1059
1163
|
|
|
1060
1164
|
// ── same-tick state-read coalescing ──────────────────────────────
|
|
1061
|
-
// Concurrent state() calls for one reducer name coalesce into a
|
|
1062
|
-
// single store.readStates() round trip when the adapter has one.
|
|
1063
|
-
// Unlike DataLoader there is deliberately NO result cache: a batch
|
|
1064
|
-
// exists only between enqueue and flush, and nothing is shared after
|
|
1065
|
-
// distribution — every state() call still observes a fresh frontier.
|
|
1066
|
-
type PendingStateRead = {
|
|
1067
|
-
sessionId: string
|
|
1068
|
-
resolve: (read: StoreStateRead | Promise<StoreStateRead>) => void
|
|
1069
|
-
}
|
|
1070
1165
|
const pendingStateReads = new Map<string, PendingStateRead[]>()
|
|
1071
1166
|
|
|
1072
1167
|
// The flush microtask voids this promise, so the function must be
|
|
@@ -1074,40 +1169,62 @@ export function createServer<
|
|
|
1074
1169
|
// synchronously throwing adapter or a read structuredClone rejects.
|
|
1075
1170
|
const flushStateReads = async (
|
|
1076
1171
|
store: A2Store,
|
|
1077
|
-
|
|
1172
|
+
reducer: Reducer<D, unknown>,
|
|
1078
1173
|
batch: PendingStateRead[],
|
|
1079
1174
|
): Promise<void> => {
|
|
1175
|
+
const plans = planStateReads(batch)
|
|
1080
1176
|
let reads: StoreStateRead[] | null
|
|
1081
1177
|
try {
|
|
1082
1178
|
reads = store.readStates
|
|
1083
1179
|
? await store.readStates(
|
|
1084
|
-
|
|
1085
|
-
|
|
1180
|
+
plans.map((plan) => ({
|
|
1181
|
+
sessionId: nsId(plan.sessionId),
|
|
1182
|
+
...(plan.throughIndex === undefined
|
|
1183
|
+
? {}
|
|
1184
|
+
: { throughIndex: plan.throughIndex }),
|
|
1185
|
+
...(plan.snapshotThroughIndex === undefined
|
|
1186
|
+
? {}
|
|
1187
|
+
: { snapshotThroughIndex: plan.snapshotThroughIndex }),
|
|
1188
|
+
})),
|
|
1189
|
+
reducer.name,
|
|
1190
|
+
)
|
|
1191
|
+
: await Promise.all(
|
|
1192
|
+
plans.map((plan) =>
|
|
1193
|
+
readCachedState(
|
|
1194
|
+
store,
|
|
1195
|
+
plan.sessionId,
|
|
1196
|
+
reducer.name,
|
|
1197
|
+
plan.throughIndex,
|
|
1198
|
+
plan.snapshotThroughIndex,
|
|
1199
|
+
),
|
|
1200
|
+
),
|
|
1086
1201
|
)
|
|
1087
|
-
: null
|
|
1088
1202
|
} catch {
|
|
1089
1203
|
reads = null
|
|
1090
1204
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1205
|
+
if (!reads || reads.length !== plans.length) {
|
|
1206
|
+
try {
|
|
1207
|
+
reads = await Promise.all(
|
|
1208
|
+
plans.map((plan) =>
|
|
1209
|
+
readCachedState(
|
|
1210
|
+
store,
|
|
1211
|
+
plan.sessionId,
|
|
1212
|
+
reducer.name,
|
|
1213
|
+
plan.throughIndex,
|
|
1214
|
+
plan.snapshotThroughIndex,
|
|
1215
|
+
),
|
|
1216
|
+
),
|
|
1217
|
+
)
|
|
1218
|
+
} catch (err) {
|
|
1219
|
+
for (const entry of batch) entry.reject(err)
|
|
1220
|
+
return
|
|
1097
1221
|
}
|
|
1098
|
-
return
|
|
1099
1222
|
}
|
|
1100
|
-
|
|
1101
|
-
// payload objects, exactly as two separate state() calls would not.
|
|
1102
|
-
const seen = new Set<string>()
|
|
1103
|
-
for (const [position, entry] of batch.entries()) {
|
|
1104
|
-
const shared = seen.has(entry.sessionId)
|
|
1105
|
-
seen.add(entry.sessionId)
|
|
1223
|
+
for (const [position, plan] of plans.entries()) {
|
|
1106
1224
|
try {
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
entry.resolve(readCachedState(store, entry.sessionId, reducerName))
|
|
1225
|
+
await foldStatePlan(store, reducer, plan, reads[position]!)
|
|
1226
|
+
} catch (err) {
|
|
1227
|
+
for (const entry of plan.entries) entry.reject(err)
|
|
1111
1228
|
}
|
|
1112
1229
|
}
|
|
1113
1230
|
}
|
|
@@ -1115,9 +1232,13 @@ export function createServer<
|
|
|
1115
1232
|
const enqueueStateRead = (
|
|
1116
1233
|
store: A2Store,
|
|
1117
1234
|
sessionId: string,
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1235
|
+
reducer: Reducer<D, unknown>,
|
|
1236
|
+
throughIndex?: number,
|
|
1237
|
+
pinEventIndex?: number,
|
|
1238
|
+
span?: A2SpanHandle,
|
|
1239
|
+
): Promise<{ state: unknown; index: number }> =>
|
|
1240
|
+
new Promise((resolve, reject) => {
|
|
1241
|
+
const reducerName = reducer.name
|
|
1121
1242
|
let batch = pendingStateReads.get(reducerName)
|
|
1122
1243
|
if (!batch) {
|
|
1123
1244
|
const opened: PendingStateRead[] = []
|
|
@@ -1133,16 +1254,25 @@ export function createServer<
|
|
|
1133
1254
|
// the last. A deeper-staggered caller just opens the next batch.
|
|
1134
1255
|
queueMicrotask(() => {
|
|
1135
1256
|
pendingStateReads.delete(reducerName)
|
|
1136
|
-
void flushStateReads(store,
|
|
1257
|
+
void flushStateReads(store, reducer, opened)
|
|
1137
1258
|
})
|
|
1138
1259
|
batch = opened
|
|
1139
1260
|
}
|
|
1140
|
-
batch.push({
|
|
1261
|
+
batch.push({
|
|
1262
|
+
sessionId,
|
|
1263
|
+
...(throughIndex === undefined ? {} : { throughIndex }),
|
|
1264
|
+
...(pinEventIndex === undefined ? {} : { pinEventIndex }),
|
|
1265
|
+
span: span!,
|
|
1266
|
+
resolve,
|
|
1267
|
+
reject,
|
|
1268
|
+
})
|
|
1141
1269
|
})
|
|
1142
1270
|
|
|
1143
1271
|
const readState = async <S>(
|
|
1144
1272
|
sessionId: string,
|
|
1145
1273
|
reducer: Reducer<D, S>,
|
|
1274
|
+
throughIndex?: number,
|
|
1275
|
+
pinEventIndex?: number,
|
|
1146
1276
|
): Promise<{ state: S; index: number }> => {
|
|
1147
1277
|
const store = await resolveStore()
|
|
1148
1278
|
return telemetry.span(
|
|
@@ -1152,36 +1282,15 @@ export function createServer<
|
|
|
1152
1282
|
'a2.session_id': sessionId,
|
|
1153
1283
|
'a2.state.reducer': reducer.name,
|
|
1154
1284
|
},
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
? await enqueueStateRead(store, sessionId, reducer.name)
|
|
1158
|
-
: await readCachedState(store, sessionId, reducer.name)
|
|
1159
|
-
const { state, index, folded } = await foldStateRead(
|
|
1285
|
+
(span) =>
|
|
1286
|
+
enqueueStateRead(
|
|
1160
1287
|
store,
|
|
1161
1288
|
sessionId,
|
|
1162
|
-
reducer,
|
|
1163
|
-
|
|
1289
|
+
reducer as Reducer<D, unknown>,
|
|
1290
|
+
throughIndex,
|
|
1291
|
+
pinEventIndex,
|
|
1164
1292
|
span,
|
|
1165
|
-
)
|
|
1166
|
-
// Write-back is a disposable cache. Copy before returning so caller
|
|
1167
|
-
// mutations cannot race the background persistence.
|
|
1168
|
-
if (folded > 0) {
|
|
1169
|
-
const snapshotState = cloneInitial(state)
|
|
1170
|
-
const write = Promise.resolve()
|
|
1171
|
-
.then(() =>
|
|
1172
|
-
store.putSnapshot(
|
|
1173
|
-
nsId(sessionId),
|
|
1174
|
-
reducer.name,
|
|
1175
|
-
index,
|
|
1176
|
-
snapshotState,
|
|
1177
|
-
),
|
|
1178
|
-
)
|
|
1179
|
-
.catch(() => {})
|
|
1180
|
-
track(write)
|
|
1181
|
-
platformWaitUntil(write)
|
|
1182
|
-
}
|
|
1183
|
-
return { state, index }
|
|
1184
|
-
},
|
|
1293
|
+
) as Promise<{ state: S; index: number }>,
|
|
1185
1294
|
)
|
|
1186
1295
|
}
|
|
1187
1296
|
|
|
@@ -1205,11 +1314,11 @@ export function createServer<
|
|
|
1205
1314
|
const payloads = events.map((event) =>
|
|
1206
1315
|
cloneScheduledPayload(event.payload),
|
|
1207
1316
|
)
|
|
1208
|
-
const snapshottedEvents = events.map((event, index) =>
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1317
|
+
const snapshottedEvents = events.map((event, index) =>
|
|
1318
|
+
event.id === undefined
|
|
1319
|
+
? { type: event.type, payload: payloads[index] }
|
|
1320
|
+
: { type: event.type, payload: payloads[index], id: event.id },
|
|
1321
|
+
) as AppendInput<D>[]
|
|
1213
1322
|
const validated = validateEvents(
|
|
1214
1323
|
sessionId,
|
|
1215
1324
|
structuredClone(snapshottedEvents),
|
|
@@ -1295,22 +1404,6 @@ export function createServer<
|
|
|
1295
1404
|
},
|
|
1296
1405
|
fallbackSeen: number,
|
|
1297
1406
|
): Promise<void> => {
|
|
1298
|
-
// The push-route provenance seam, mirroring appendExternal: a
|
|
1299
|
-
// branded patch came from the wire, so `validatePush` runs before
|
|
1300
|
-
// field validation and the broadcast — with `events: []`, the
|
|
1301
|
-
// documented presence-only shape.
|
|
1302
|
-
if (Reflect.get(patch, '~a2.pushed') === true) {
|
|
1303
|
-
// Frozen before the callback: this is the authorization seam,
|
|
1304
|
-
// and an authorization hook must not be able to rewrite
|
|
1305
|
-
// authorship or values on its way through (assignment throws
|
|
1306
|
-
// under strict mode, failing the push loudly).
|
|
1307
|
-
Object.freeze(patch.values)
|
|
1308
|
-
await options.validatePush?.({
|
|
1309
|
-
sessionId,
|
|
1310
|
-
events: [],
|
|
1311
|
-
presence: Object.freeze(patch as PushedPresence),
|
|
1312
|
-
})
|
|
1313
|
-
}
|
|
1314
1407
|
if (
|
|
1315
1408
|
typeof patch.participant !== 'string' ||
|
|
1316
1409
|
patch.participant.length === 0
|
|
@@ -1636,7 +1729,18 @@ export function createServer<
|
|
|
1636
1729
|
schedule: makeSchedule(id, trigger),
|
|
1637
1730
|
history: async (bounds?: { gte?: number; lte?: number }) =>
|
|
1638
1731
|
(await readHistory(id, bounds)).map(toPublic),
|
|
1639
|
-
state: <S>(reducer: Reducer<D, S
|
|
1732
|
+
state: <S>(reducer: Reducer<D, S>, stateOptions?: StateOptions) => {
|
|
1733
|
+
const through = stateOptions?.through ?? trigger?.index
|
|
1734
|
+
if (through !== undefined && through !== 'latest') {
|
|
1735
|
+
assertStateIndex(through)
|
|
1736
|
+
}
|
|
1737
|
+
return readState(
|
|
1738
|
+
id,
|
|
1739
|
+
reducer,
|
|
1740
|
+
through === 'latest' ? undefined : through,
|
|
1741
|
+
trigger && through !== 'latest' ? trigger.index : undefined,
|
|
1742
|
+
)
|
|
1743
|
+
},
|
|
1640
1744
|
stream: (opts?: { startAfter?: number; presence?: boolean }) => {
|
|
1641
1745
|
const startAfter = opts?.startAfter ?? 0
|
|
1642
1746
|
assertStreamIndex(startAfter)
|
|
@@ -2240,16 +2344,38 @@ export function createServer<
|
|
|
2240
2344
|
shouldClaim = false
|
|
2241
2345
|
}
|
|
2242
2346
|
}
|
|
2347
|
+
if (
|
|
2348
|
+
claim?.outcome === 'claimed' &&
|
|
2349
|
+
[...active.values()].some((entry) => entry.lapsed)
|
|
2350
|
+
) {
|
|
2351
|
+
shouldClaim = true
|
|
2352
|
+
continue
|
|
2353
|
+
}
|
|
2243
2354
|
if (active.size > 0) {
|
|
2244
2355
|
const wake = signal?.wait(signalVersion)
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2356
|
+
const hasLapsed = [...active.values()].some(
|
|
2357
|
+
(entry) => entry.lapsed,
|
|
2358
|
+
)
|
|
2359
|
+
const retry =
|
|
2360
|
+
claim?.outcome === 'busy' && hasLapsed
|
|
2361
|
+
? defaultSleep(
|
|
2362
|
+
Math.max(0, claim.retryAt.getTime() - Date.now()),
|
|
2363
|
+
)
|
|
2364
|
+
: undefined
|
|
2365
|
+
const retryDue = await Promise.race([
|
|
2366
|
+
...[...active.values()].map((entry) =>
|
|
2367
|
+
entry.execution.then(() => false),
|
|
2368
|
+
),
|
|
2369
|
+
...(wake ? [wake.promise.then(() => false)] : []),
|
|
2370
|
+
...(retry ? [retry.promise.then(() => true)] : []),
|
|
2248
2371
|
])
|
|
2249
2372
|
wake?.cancel()
|
|
2373
|
+
retry?.cancel()
|
|
2250
2374
|
shouldClaim =
|
|
2375
|
+
retryDue ||
|
|
2251
2376
|
claimAgain ||
|
|
2252
2377
|
active.size === 0 ||
|
|
2378
|
+
hasLapsed ||
|
|
2253
2379
|
(signal !== undefined && signal.version !== signalVersion)
|
|
2254
2380
|
claimAgain = false
|
|
2255
2381
|
continue
|
|
@@ -2298,19 +2424,12 @@ export function createServer<
|
|
|
2298
2424
|
const appendExternal = async (
|
|
2299
2425
|
sessionId: string,
|
|
2300
2426
|
mode: 'inline' | 'dispatch',
|
|
2301
|
-
events:
|
|
2427
|
+
events: AppendInput<D>[],
|
|
2302
2428
|
requireDurableInitialArm = false,
|
|
2303
2429
|
): Promise<ContractEvent<D>[]> => {
|
|
2304
|
-
const pushed = events.filter(
|
|
2305
|
-
(event): event is PushedEvent =>
|
|
2306
|
-
Reflect.get(event, '~a2.pushed') === true,
|
|
2307
|
-
)
|
|
2308
|
-
if (pushed.length > 0) {
|
|
2309
|
-
await options.validatePush?.({ sessionId, events: pushed })
|
|
2310
|
-
}
|
|
2311
2430
|
return appendCore(
|
|
2312
2431
|
sessionId,
|
|
2313
|
-
events
|
|
2432
|
+
events,
|
|
2314
2433
|
'external',
|
|
2315
2434
|
mode,
|
|
2316
2435
|
(_rows, watchdogDueAt) => scheduleDrain(sessionId, watchdogDueAt),
|
|
@@ -2320,21 +2439,23 @@ export function createServer<
|
|
|
2320
2439
|
)
|
|
2321
2440
|
}
|
|
2322
2441
|
|
|
2442
|
+
const session = (id: string): Session<D, SessionAppend<D>, P> => {
|
|
2443
|
+
assertSessionId(id)
|
|
2444
|
+
const appendInline = async (...events: AppendInput<D>[]) =>
|
|
2445
|
+
appendExternal(id, 'inline', events)
|
|
2446
|
+
const appendDispatch = async (...events: AppendInput<D>[]) =>
|
|
2447
|
+
appendExternal(id, 'dispatch', events)
|
|
2448
|
+
const append = Object.assign(appendInline, {
|
|
2449
|
+
dispatch: appendDispatch,
|
|
2450
|
+
}) as SessionAppend<D>
|
|
2451
|
+
return makeSession(id, append, null)
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
const fetch = createServerFetch({ contract: serverContract, session })
|
|
2323
2455
|
const self: A2Server<D, P> = {
|
|
2324
2456
|
contract: serverContract,
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
const appendInline = async (
|
|
2328
|
-
...events: Array<AppendInput<D> | PushedEvent>
|
|
2329
|
-
) => appendExternal(id, 'inline', events)
|
|
2330
|
-
const appendDispatch = async (
|
|
2331
|
-
...events: Array<AppendInput<D> | PushedEvent>
|
|
2332
|
-
) => appendExternal(id, 'dispatch', events)
|
|
2333
|
-
const append = Object.assign(appendInline, {
|
|
2334
|
-
dispatch: appendDispatch,
|
|
2335
|
-
}) as SessionAppend<D>
|
|
2336
|
-
return makeSession(id, append, null)
|
|
2337
|
-
},
|
|
2457
|
+
fetch,
|
|
2458
|
+
session,
|
|
2338
2459
|
|
|
2339
2460
|
async drain(sessionId) {
|
|
2340
2461
|
assertSessionId(sessionId)
|
|
@@ -2363,126 +2484,6 @@ export function createServer<
|
|
|
2363
2484
|
await appendExternal(sessionId, 'inline', [...events], true)
|
|
2364
2485
|
},
|
|
2365
2486
|
})
|
|
2366
|
-
serverSchedulerBindings.set(self, { scheduler })
|
|
2367
|
-
|
|
2368
|
-
serverInspection.set(self, {
|
|
2369
|
-
async listSessions(inspectionOptions) {
|
|
2370
|
-
const store = await resolveStore()
|
|
2371
|
-
if (!store.inspect) {
|
|
2372
|
-
throw new InspectionUnsupportedError(
|
|
2373
|
-
'this store backend does not support inspection',
|
|
2374
|
-
)
|
|
2375
|
-
}
|
|
2376
|
-
const page = await store.inspect.listSessions({
|
|
2377
|
-
prefix: `${name}${NS}`,
|
|
2378
|
-
limit: inspectionOptions.limit,
|
|
2379
|
-
...(inspectionOptions.cursor !== undefined
|
|
2380
|
-
? { cursor: inspectionOptions.cursor }
|
|
2381
|
-
: {}),
|
|
2382
|
-
})
|
|
2383
|
-
return {
|
|
2384
|
-
cursor: page.cursor,
|
|
2385
|
-
// oxlint-disable-next-line oxc/no-map-spread -- inspection results remain adapter-owned
|
|
2386
|
-
sessions: page.sessions.map((session) => {
|
|
2387
|
-
const publicSession = { ...session }
|
|
2388
|
-
publicSession.sessionId = stripNs(session.sessionId)
|
|
2389
|
-
return publicSession
|
|
2390
|
-
}),
|
|
2391
|
-
}
|
|
2392
|
-
},
|
|
2393
|
-
async readSessionPage(sessionId, inspectionOptions) {
|
|
2394
|
-
assertSessionId(sessionId)
|
|
2395
|
-
if (
|
|
2396
|
-
!Number.isSafeInteger(inspectionOptions.afterIndex) ||
|
|
2397
|
-
inspectionOptions.afterIndex < 0 ||
|
|
2398
|
-
!Number.isSafeInteger(inspectionOptions.limit) ||
|
|
2399
|
-
inspectionOptions.limit < 1 ||
|
|
2400
|
-
(inspectionOptions.throughIndex !== undefined &&
|
|
2401
|
-
(!Number.isSafeInteger(inspectionOptions.throughIndex) ||
|
|
2402
|
-
inspectionOptions.throughIndex < 0))
|
|
2403
|
-
) {
|
|
2404
|
-
throw new TypeError('invalid inspection event page bounds')
|
|
2405
|
-
}
|
|
2406
|
-
const store = await resolveStore()
|
|
2407
|
-
if (!store.inspect) {
|
|
2408
|
-
throw new InspectionUnsupportedError(
|
|
2409
|
-
'this store backend does not support inspection',
|
|
2410
|
-
)
|
|
2411
|
-
}
|
|
2412
|
-
const ns = nsId(sessionId)
|
|
2413
|
-
const snapshots =
|
|
2414
|
-
inspectionOptions.afterIndex === 0
|
|
2415
|
-
? await store.inspect.listSnapshots(ns)
|
|
2416
|
-
: []
|
|
2417
|
-
const page = store.inspect.readEvents
|
|
2418
|
-
? await store.inspect.readEvents(ns, inspectionOptions)
|
|
2419
|
-
: await (async () => {
|
|
2420
|
-
const all = await store.read(ns)
|
|
2421
|
-
const currentFrontier = all.at(-1)?.index ?? 0
|
|
2422
|
-
const throughIndex = Math.min(
|
|
2423
|
-
inspectionOptions.throughIndex ?? currentFrontier,
|
|
2424
|
-
currentFrontier,
|
|
2425
|
-
)
|
|
2426
|
-
return {
|
|
2427
|
-
events: all
|
|
2428
|
-
.filter(
|
|
2429
|
-
(event) =>
|
|
2430
|
-
event.index > inspectionOptions.afterIndex &&
|
|
2431
|
-
event.index <= throughIndex,
|
|
2432
|
-
)
|
|
2433
|
-
.slice(0, inspectionOptions.limit),
|
|
2434
|
-
throughIndex,
|
|
2435
|
-
}
|
|
2436
|
-
})()
|
|
2437
|
-
const { events, throughIndex } = page
|
|
2438
|
-
if (
|
|
2439
|
-
!Number.isSafeInteger(throughIndex) ||
|
|
2440
|
-
throughIndex < 0 ||
|
|
2441
|
-
events.length > inspectionOptions.limit ||
|
|
2442
|
-
(inspectionOptions.throughIndex !== undefined &&
|
|
2443
|
-
inspectionOptions.afterIndex > 0 &&
|
|
2444
|
-
throughIndex !== inspectionOptions.throughIndex)
|
|
2445
|
-
) {
|
|
2446
|
-
throw new A2Error(
|
|
2447
|
-
'STORE_UNAVAILABLE',
|
|
2448
|
-
`inspection event page for session '${sessionId}' has invalid bounds`,
|
|
2449
|
-
)
|
|
2450
|
-
}
|
|
2451
|
-
for (let position = 0; position < events.length; position += 1) {
|
|
2452
|
-
if (
|
|
2453
|
-
events[position]!.index !==
|
|
2454
|
-
inspectionOptions.afterIndex + position + 1 ||
|
|
2455
|
-
events[position]!.index > throughIndex
|
|
2456
|
-
) {
|
|
2457
|
-
throw new A2Error(
|
|
2458
|
-
'STORE_UNAVAILABLE',
|
|
2459
|
-
`inspection event page for session '${sessionId}' is not contiguous`,
|
|
2460
|
-
)
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
|
-
if (inspectionOptions.afterIndex < throughIndex && events.length === 0) {
|
|
2464
|
-
throw new A2Error(
|
|
2465
|
-
'STORE_UNAVAILABLE',
|
|
2466
|
-
`inspection event page for session '${sessionId}' ended before index ${throughIndex}`,
|
|
2467
|
-
)
|
|
2468
|
-
}
|
|
2469
|
-
const lastIndex = events.at(-1)?.index ?? inspectionOptions.afterIndex
|
|
2470
|
-
return {
|
|
2471
|
-
// oxlint-disable-next-line oxc/no-map-spread -- inspection results remain adapter-owned
|
|
2472
|
-
events: events.map((event) => {
|
|
2473
|
-
const publicEvent = { ...event }
|
|
2474
|
-
publicEvent.sessionId = sessionId
|
|
2475
|
-
return publicEvent
|
|
2476
|
-
}),
|
|
2477
|
-
snapshots: snapshots.filter(
|
|
2478
|
-
(snapshot) => snapshot.index <= throughIndex,
|
|
2479
|
-
),
|
|
2480
|
-
throughIndex,
|
|
2481
|
-
nextIndex: lastIndex < throughIndex ? lastIndex : null,
|
|
2482
|
-
}
|
|
2483
|
-
},
|
|
2484
|
-
})
|
|
2485
|
-
|
|
2486
2487
|
return self
|
|
2487
2488
|
}
|
|
2488
2489
|
|
|
@@ -2507,6 +2508,12 @@ function assertStreamIndex(value: number): void {
|
|
|
2507
2508
|
}
|
|
2508
2509
|
}
|
|
2509
2510
|
|
|
2511
|
+
function assertStateIndex(value: number): void {
|
|
2512
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
2513
|
+
throw new TypeError('state.through must be a non-negative safe integer')
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2510
2517
|
function assertAppendName(name: string): void {
|
|
2511
2518
|
if (typeof name !== 'string' || name.length === 0) {
|
|
2512
2519
|
throw new TypeError('handler append name must be a non-empty string')
|
|
@@ -2666,7 +2673,6 @@ function cloneInitial<S>(initial: S): S {
|
|
|
2666
2673
|
// transports — re-exported so experimental-a2/server is self-sufficient.
|
|
2667
2674
|
export type {
|
|
2668
2675
|
A2Store,
|
|
2669
|
-
A2StoreInspection,
|
|
2670
2676
|
AppendEvent,
|
|
2671
2677
|
Clock,
|
|
2672
2678
|
Event,
|
|
@@ -2678,9 +2684,6 @@ export type {
|
|
|
2678
2684
|
StoreStateRead,
|
|
2679
2685
|
PresenceRow,
|
|
2680
2686
|
StoredEvent,
|
|
2681
|
-
StoredSessionPage,
|
|
2682
|
-
StoredSessionSummary,
|
|
2683
|
-
StoredSnapshot,
|
|
2684
2687
|
} from './store.ts'
|
|
2685
2688
|
export type {
|
|
2686
2689
|
ScheduledEvent,
|