experimental-a2 0.6.0 → 0.7.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 +22 -0
- package/dist/ai-server.d.ts +4 -5
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +8 -7
- 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-286j79Mt.js} +708 -79
- package/dist/server-286j79Mt.js.map +1 -0
- package/dist/{server-DpvjhdoE.d.ts → server-DgXmORIq.d.ts} +67 -49
- package/dist/server-DgXmORIq.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-flRz1OWh.d.ts} +2 -57
- package/dist/store-flRz1OWh.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 +1 -59
- 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.js +1 -81
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DEYO8Ryv.js} +48 -134
- package/dist/store-redis-core-DEYO8Ryv.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +2 -3
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +3 -4
- 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 +1 -72
- 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/guides/03-react.mdx +20 -28
- package/docs/guides/05-production.mdx +9 -11
- package/docs/guides/06-ai-agents.mdx +8 -14
- package/docs/guides/09-presence.mdx +14 -19
- package/docs/guides/10-transports.mdx +104 -86
- package/docs/reference/01-api.mdx +143 -277
- package/docs/reference/02-errors.mdx +5 -7
- package/package.json +1 -14
- package/src/ai-server.ts +9 -5
- 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 +73 -225
- package/src/session-socket.ts +36 -20
- package/src/sse.ts +2 -2
- package/src/store-memory.ts +0 -81
- package/src/store-postgres.ts +0 -100
- package/src/store-redis-core.ts +47 -211
- package/src/store-redis-http.ts +0 -1
- package/src/store-redis.ts +0 -1
- package/src/store-sqlite.ts +0 -119
- package/src/store.ts +0 -60
- 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,
|
|
@@ -46,6 +44,14 @@ import type {
|
|
|
46
44
|
StoredEvent,
|
|
47
45
|
} from './store.ts'
|
|
48
46
|
import type { Reducer } from './reducer.ts'
|
|
47
|
+
import {
|
|
48
|
+
createServerFetch,
|
|
49
|
+
type A2Operation,
|
|
50
|
+
type A2PushEvent,
|
|
51
|
+
type A2PushPresence,
|
|
52
|
+
type ServerFetchOptions,
|
|
53
|
+
type UpgradeWebSocket,
|
|
54
|
+
} from './server-fetch.ts'
|
|
49
55
|
import { validateSync } from './validate.ts'
|
|
50
56
|
import {
|
|
51
57
|
deterministicEventId,
|
|
@@ -66,45 +72,6 @@ import {
|
|
|
66
72
|
type A2Telemetry,
|
|
67
73
|
} from './telemetry.ts'
|
|
68
74
|
|
|
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
75
|
/** What every handler receives. */
|
|
109
76
|
export type HandlerContext<
|
|
110
77
|
D extends EventDefs,
|
|
@@ -142,11 +109,9 @@ export type Lane<
|
|
|
142
109
|
K extends keyof D & string = keyof D & string,
|
|
143
110
|
> = string | ((context: LaneContext<D, K>) => string)
|
|
144
111
|
|
|
145
|
-
export type SessionDispatch<D extends EventDefs> =
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
(...events: PushedEvent[]): Promise<ContractEvent<D>[]>
|
|
149
|
-
}
|
|
112
|
+
export type SessionDispatch<D extends EventDefs> = (
|
|
113
|
+
...events: AppendInput<D>[]
|
|
114
|
+
) => Promise<ContractEvent<D>[]>
|
|
150
115
|
|
|
151
116
|
export type SessionAppend<D extends EventDefs> = SessionDispatch<D> & {
|
|
152
117
|
/** Commit, then hand pending work directly to configured scheduler. */
|
|
@@ -192,16 +157,12 @@ export type SessionPresence<D extends EventDefs, P extends PresenceDefs> = {
|
|
|
192
157
|
* is the sender's LWW stamp in epoch ms; omitted, receipt time
|
|
193
158
|
* stands in (single writer, so receipt order is sender order).
|
|
194
159
|
*/
|
|
195
|
-
setPresence(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
at?: number
|
|
202
|
-
}
|
|
203
|
-
| PushedPresence,
|
|
204
|
-
): Promise<void>
|
|
160
|
+
setPresence(patch: {
|
|
161
|
+
participant: string
|
|
162
|
+
values: PresencePatch<P>['values']
|
|
163
|
+
seen?: number
|
|
164
|
+
at?: number
|
|
165
|
+
}): Promise<void>
|
|
205
166
|
/** The current map, expired values pruned — a point-in-time read. */
|
|
206
167
|
presence(): Promise<PresenceMap<P>>
|
|
207
168
|
}
|
|
@@ -219,8 +180,7 @@ export type Session<
|
|
|
219
180
|
state<S>(reducer: Reducer<D, S>): Promise<{ state: S; index: number }>
|
|
220
181
|
/**
|
|
221
182
|
* 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.
|
|
183
|
+
* (exclusive). Server-side only. `server.fetch` exposes it over SSE.
|
|
224
184
|
*/
|
|
225
185
|
stream(opts?: { startAfter?: number }): AsyncIterable<ContractEvent<D>>
|
|
226
186
|
}
|
|
@@ -255,6 +215,10 @@ export type A2Server<
|
|
|
255
215
|
> = {
|
|
256
216
|
/** The contract this server implements. */
|
|
257
217
|
readonly contract: Contract<D, P>
|
|
218
|
+
readonly fetch: {
|
|
219
|
+
(request: Request): Promise<Response>
|
|
220
|
+
(request: Request, options: ServerFetchOptions<D, P>): Promise<Response>
|
|
221
|
+
}
|
|
258
222
|
session(id: string): Session<D, SessionAppend<D>, P>
|
|
259
223
|
/**
|
|
260
224
|
* Process every currently eligible event. `settled` means nothing
|
|
@@ -263,6 +227,15 @@ export type A2Server<
|
|
|
263
227
|
drain(sessionId: string): Promise<{ settled: boolean }>
|
|
264
228
|
}
|
|
265
229
|
|
|
230
|
+
export type {
|
|
231
|
+
A2Operation,
|
|
232
|
+
A2PushEvent,
|
|
233
|
+
A2PushPresence,
|
|
234
|
+
ServerFetchOptions,
|
|
235
|
+
UpgradeWebSocket,
|
|
236
|
+
}
|
|
237
|
+
export type { A2Socket } from './session-socket.ts'
|
|
238
|
+
|
|
266
239
|
/**
|
|
267
240
|
* Deliver one authenticated scheduler append through an A2 server's ordinary
|
|
268
241
|
* top-level append path. Custom scheduler adapters call this after validating
|
|
@@ -336,8 +309,6 @@ export type ServerOptions<
|
|
|
336
309
|
scheduler?: A2Scheduler
|
|
337
310
|
/** Optional instrumentation — e.g. `otel()` from `experimental-a2/otel`. */
|
|
338
311
|
telemetry?: A2Telemetry
|
|
339
|
-
/** Validate events that came through `parsePushBody` before writing them. */
|
|
340
|
-
validatePush?: (context: PushValidationContext) => void | PromiseLike<void>
|
|
341
312
|
/**
|
|
342
313
|
* Presence-plane policy — valid only when the contract declares
|
|
343
314
|
* presence fields (TypeError at construction otherwise). `ttlMs` is
|
|
@@ -1295,22 +1266,6 @@ export function createServer<
|
|
|
1295
1266
|
},
|
|
1296
1267
|
fallbackSeen: number,
|
|
1297
1268
|
): 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
1269
|
if (
|
|
1315
1270
|
typeof patch.participant !== 'string' ||
|
|
1316
1271
|
patch.participant.length === 0
|
|
@@ -2240,16 +2195,38 @@ export function createServer<
|
|
|
2240
2195
|
shouldClaim = false
|
|
2241
2196
|
}
|
|
2242
2197
|
}
|
|
2198
|
+
if (
|
|
2199
|
+
claim?.outcome === 'claimed' &&
|
|
2200
|
+
[...active.values()].some((entry) => entry.lapsed)
|
|
2201
|
+
) {
|
|
2202
|
+
shouldClaim = true
|
|
2203
|
+
continue
|
|
2204
|
+
}
|
|
2243
2205
|
if (active.size > 0) {
|
|
2244
2206
|
const wake = signal?.wait(signalVersion)
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2207
|
+
const hasLapsed = [...active.values()].some(
|
|
2208
|
+
(entry) => entry.lapsed,
|
|
2209
|
+
)
|
|
2210
|
+
const retry =
|
|
2211
|
+
claim?.outcome === 'busy' && hasLapsed
|
|
2212
|
+
? defaultSleep(
|
|
2213
|
+
Math.max(0, claim.retryAt.getTime() - Date.now()),
|
|
2214
|
+
)
|
|
2215
|
+
: undefined
|
|
2216
|
+
const retryDue = await Promise.race([
|
|
2217
|
+
...[...active.values()].map((entry) =>
|
|
2218
|
+
entry.execution.then(() => false),
|
|
2219
|
+
),
|
|
2220
|
+
...(wake ? [wake.promise.then(() => false)] : []),
|
|
2221
|
+
...(retry ? [retry.promise.then(() => true)] : []),
|
|
2248
2222
|
])
|
|
2249
2223
|
wake?.cancel()
|
|
2224
|
+
retry?.cancel()
|
|
2250
2225
|
shouldClaim =
|
|
2226
|
+
retryDue ||
|
|
2251
2227
|
claimAgain ||
|
|
2252
2228
|
active.size === 0 ||
|
|
2229
|
+
hasLapsed ||
|
|
2253
2230
|
(signal !== undefined && signal.version !== signalVersion)
|
|
2254
2231
|
claimAgain = false
|
|
2255
2232
|
continue
|
|
@@ -2298,19 +2275,12 @@ export function createServer<
|
|
|
2298
2275
|
const appendExternal = async (
|
|
2299
2276
|
sessionId: string,
|
|
2300
2277
|
mode: 'inline' | 'dispatch',
|
|
2301
|
-
events:
|
|
2278
|
+
events: AppendInput<D>[],
|
|
2302
2279
|
requireDurableInitialArm = false,
|
|
2303
2280
|
): 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
2281
|
return appendCore(
|
|
2312
2282
|
sessionId,
|
|
2313
|
-
events
|
|
2283
|
+
events,
|
|
2314
2284
|
'external',
|
|
2315
2285
|
mode,
|
|
2316
2286
|
(_rows, watchdogDueAt) => scheduleDrain(sessionId, watchdogDueAt),
|
|
@@ -2320,21 +2290,23 @@ export function createServer<
|
|
|
2320
2290
|
)
|
|
2321
2291
|
}
|
|
2322
2292
|
|
|
2293
|
+
const session = (id: string): Session<D, SessionAppend<D>, P> => {
|
|
2294
|
+
assertSessionId(id)
|
|
2295
|
+
const appendInline = async (...events: AppendInput<D>[]) =>
|
|
2296
|
+
appendExternal(id, 'inline', events)
|
|
2297
|
+
const appendDispatch = async (...events: AppendInput<D>[]) =>
|
|
2298
|
+
appendExternal(id, 'dispatch', events)
|
|
2299
|
+
const append = Object.assign(appendInline, {
|
|
2300
|
+
dispatch: appendDispatch,
|
|
2301
|
+
}) as SessionAppend<D>
|
|
2302
|
+
return makeSession(id, append, null)
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
const fetch = createServerFetch({ contract: serverContract, session })
|
|
2323
2306
|
const self: A2Server<D, P> = {
|
|
2324
2307
|
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
|
-
},
|
|
2308
|
+
fetch,
|
|
2309
|
+
session,
|
|
2338
2310
|
|
|
2339
2311
|
async drain(sessionId) {
|
|
2340
2312
|
assertSessionId(sessionId)
|
|
@@ -2363,126 +2335,6 @@ export function createServer<
|
|
|
2363
2335
|
await appendExternal(sessionId, 'inline', [...events], true)
|
|
2364
2336
|
},
|
|
2365
2337
|
})
|
|
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
2338
|
return self
|
|
2487
2339
|
}
|
|
2488
2340
|
|
|
@@ -2666,7 +2518,6 @@ function cloneInitial<S>(initial: S): S {
|
|
|
2666
2518
|
// transports — re-exported so experimental-a2/server is self-sufficient.
|
|
2667
2519
|
export type {
|
|
2668
2520
|
A2Store,
|
|
2669
|
-
A2StoreInspection,
|
|
2670
2521
|
AppendEvent,
|
|
2671
2522
|
Clock,
|
|
2672
2523
|
Event,
|
|
@@ -2678,9 +2529,6 @@ export type {
|
|
|
2678
2529
|
StoreStateRead,
|
|
2679
2530
|
PresenceRow,
|
|
2680
2531
|
StoredEvent,
|
|
2681
|
-
StoredSessionPage,
|
|
2682
|
-
StoredSessionSummary,
|
|
2683
|
-
StoredSnapshot,
|
|
2684
2532
|
} from './store.ts'
|
|
2685
2533
|
export type {
|
|
2686
2534
|
ScheduledEvent,
|
package/src/session-socket.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { A2Error } from './errors.ts'
|
|
|
11
11
|
import { SOCKET_TIMINGS, STREAM_TIMINGS } from './internal.ts'
|
|
12
12
|
import type { Event } from './store.ts'
|
|
13
13
|
import type { EventDefs, PresencePatch, PresenceSnapshot } from './contract.ts'
|
|
14
|
-
import type {
|
|
14
|
+
import type { ParsedPushEvent, ParsedPushPresence } from './push-envelope.ts'
|
|
15
|
+
import type { Session } from './server.ts'
|
|
15
16
|
import { parsePresenceSibling, parsePushEvents } from './push-envelope.ts'
|
|
16
17
|
import {
|
|
17
18
|
SOCKET_PING_FRAME,
|
|
@@ -71,16 +72,15 @@ const MAX_MALFORMED_FRAMES = 5
|
|
|
71
72
|
* The pump's runtime view of a session. The presence members are
|
|
72
73
|
* conditional on the public surface (`WithPresence`) and therefore
|
|
73
74
|
* invisible on a generic `Session<D>`, so capability is checked at
|
|
74
|
-
* runtime (`setPresence` present)
|
|
75
|
-
* route sample. The casts live in `sessionsSocket` and `handle`.
|
|
75
|
+
* runtime (`setPresence` present). The cast stays inside the transport.
|
|
76
76
|
*/
|
|
77
77
|
export type SocketSession = {
|
|
78
78
|
stream(opts: {
|
|
79
79
|
startAfter: number
|
|
80
80
|
presence?: boolean
|
|
81
81
|
}): AsyncIterable<Event | PresencePatch | PresenceSnapshot>
|
|
82
|
-
append(...events:
|
|
83
|
-
setPresence?(patch:
|
|
82
|
+
append(...events: ParsedPushEvent[]): Promise<Event[]>
|
|
83
|
+
setPresence?(patch: ParsedPushPresence): Promise<void>
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
@@ -238,7 +238,8 @@ export type SessionsSocketResolve<D extends EventDefs> = (
|
|
|
238
238
|
sessionId: string,
|
|
239
239
|
/** The entry's resume frontier — `stream({ startAfter })`. */
|
|
240
240
|
startAfter: number,
|
|
241
|
-
) =>
|
|
241
|
+
) =>
|
|
242
|
+
Pick<Session<D>, 'stream'> | null | Promise<Pick<Session<D>, 'stream'> | null>
|
|
242
243
|
|
|
243
244
|
export type SessionsSocketOptions = {
|
|
244
245
|
/** The `stream()` presence opt-in for every session on the socket —
|
|
@@ -247,17 +248,11 @@ export type SessionsSocketOptions = {
|
|
|
247
248
|
/** Epoch ms: close cleanly (code 1000) at this time, ahead of a known
|
|
248
249
|
* platform deadline, so clients reconnect on our schedule. */
|
|
249
250
|
deadline?: number
|
|
250
|
-
/**
|
|
251
|
-
* Per-frame push gate — policy's seam on the up lane, the socket
|
|
252
|
-
* counterpart of a POST route's checks (`handle`'s `before` rides
|
|
253
|
-
* it). Runs after the frame parses and before `append`: return an
|
|
254
|
-
* `A2Error` to answer the push with that error ack (nothing
|
|
255
|
-
* appended), `null` to allow. Absent, every push on an accepted
|
|
256
|
-
* subscription is allowed — the prior behavior, unchanged.
|
|
257
|
-
*/
|
|
251
|
+
/** Runs after frame parsing and before append or presence I/O. */
|
|
258
252
|
gatePush?(
|
|
259
253
|
sessionId: string,
|
|
260
|
-
events:
|
|
254
|
+
events: ParsedPushEvent[],
|
|
255
|
+
presence?: ParsedPushPresence,
|
|
261
256
|
): A2Error | null | Promise<A2Error | null>
|
|
262
257
|
}
|
|
263
258
|
|
|
@@ -359,11 +354,14 @@ export function sessionsSocket<D extends EventDefs>(
|
|
|
359
354
|
id: string,
|
|
360
355
|
index: number,
|
|
361
356
|
): Promise<void> => {
|
|
362
|
-
let session: Session<D> | null
|
|
357
|
+
let session: Pick<Session<D>, 'stream'> | null
|
|
363
358
|
try {
|
|
364
359
|
session = await resolve(id, index)
|
|
365
360
|
} catch {
|
|
366
|
-
|
|
361
|
+
if (!shell.torn()) {
|
|
362
|
+
shell.send(socketUnsubscribedFor(id, 'stream failed'))
|
|
363
|
+
}
|
|
364
|
+
return
|
|
367
365
|
}
|
|
368
366
|
if (shell.torn()) return
|
|
369
367
|
if (session === null) {
|
|
@@ -419,7 +417,8 @@ export function sessionsSocket<D extends EventDefs>(
|
|
|
419
417
|
}
|
|
420
418
|
try {
|
|
421
419
|
const events = parsePushEvents(frame['events'])
|
|
422
|
-
const denial =
|
|
420
|
+
const denial =
|
|
421
|
+
(await options?.gatePush?.(sessionId, events, undefined)) ?? null
|
|
423
422
|
if (denial !== null) {
|
|
424
423
|
shell.send(socketErrorAckFor(req, denial, sessionId))
|
|
425
424
|
return
|
|
@@ -442,11 +441,28 @@ export function sessionsSocket<D extends EventDefs>(
|
|
|
442
441
|
shell.malformedFrame('presence frame on a presence-less session')
|
|
443
442
|
return
|
|
444
443
|
}
|
|
444
|
+
let patch: ParsedPushPresence | undefined
|
|
445
445
|
try {
|
|
446
|
-
|
|
447
|
-
if (patch) await subscription.target.setPresence(patch)
|
|
446
|
+
patch = parsePresenceSibling(frame)
|
|
448
447
|
} catch {
|
|
449
448
|
shell.malformedFrame('invalid presence frame')
|
|
449
|
+
return
|
|
450
|
+
}
|
|
451
|
+
if (!patch) return
|
|
452
|
+
try {
|
|
453
|
+
const denial = (await options?.gatePush?.(sessionId, [], patch)) ?? null
|
|
454
|
+
if (denial !== null) return
|
|
455
|
+
await subscription.target.setPresence(patch)
|
|
456
|
+
} catch (error) {
|
|
457
|
+
const failure = asA2Error(error)
|
|
458
|
+
if (
|
|
459
|
+
failure.code === 'INVALID_PAYLOAD' ||
|
|
460
|
+
failure.code === 'UNKNOWN_PRESENCE_FIELD'
|
|
461
|
+
) {
|
|
462
|
+
shell.malformedFrame('invalid presence frame')
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
shell.shutdown(1011, 'internal error')
|
|
450
466
|
}
|
|
451
467
|
}
|
|
452
468
|
|
package/src/sse.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The SSE response protocol — framing, heartbeat, deadline, and
|
|
3
|
-
* teardown for the live stream lane. Internal module: `
|
|
4
|
-
*
|
|
3
|
+
* teardown for the live stream lane. Internal module: `server.fetch` is
|
|
4
|
+
* the sanctioned caller; the framing rules stay
|
|
5
5
|
* documented on `sseResponse` because the client's parser (and the
|
|
6
6
|
* stall watchdog) depend on them.
|
|
7
7
|
*/
|
package/src/store-memory.ts
CHANGED
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
type IdSource,
|
|
22
22
|
type PresenceRow,
|
|
23
23
|
type StoredEvent,
|
|
24
|
-
type StoredSessionSummary,
|
|
25
24
|
type StoreStateRead,
|
|
26
25
|
} from './store.ts'
|
|
27
26
|
|
|
@@ -648,86 +647,6 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
648
647
|
},
|
|
649
648
|
},
|
|
650
649
|
|
|
651
|
-
inspect: {
|
|
652
|
-
async listSessions(inspectionOptions) {
|
|
653
|
-
const ids = [...sessions.keys()]
|
|
654
|
-
.filter(
|
|
655
|
-
(sessionId) =>
|
|
656
|
-
sessionId.startsWith(inspectionOptions.prefix) &&
|
|
657
|
-
(inspectionOptions.cursor === undefined ||
|
|
658
|
-
sessionId > inspectionOptions.cursor),
|
|
659
|
-
)
|
|
660
|
-
.toSorted()
|
|
661
|
-
const visible = ids.slice(0, inspectionOptions.limit)
|
|
662
|
-
const summaries = visible.map((sessionId): StoredSessionSummary => {
|
|
663
|
-
const rows = sessions.get(sessionId)!
|
|
664
|
-
const first = rows[0]!
|
|
665
|
-
let updatedAt = first.createdAt
|
|
666
|
-
let pendingCount = 0
|
|
667
|
-
let failedCount = 0
|
|
668
|
-
let attemptCount = 0
|
|
669
|
-
let failureCount = 0
|
|
670
|
-
for (const row of rows) {
|
|
671
|
-
attemptCount += row.attemptCount
|
|
672
|
-
failureCount += row.failureCount
|
|
673
|
-
if (row.processedAt === null && row.failedAt === null) {
|
|
674
|
-
pendingCount += 1
|
|
675
|
-
}
|
|
676
|
-
if (row.failedAt !== null) failedCount += 1
|
|
677
|
-
for (const timestamp of [
|
|
678
|
-
row.createdAt,
|
|
679
|
-
row.firstClaimedAt,
|
|
680
|
-
row.lastClaimedAt,
|
|
681
|
-
row.lastFailedAt,
|
|
682
|
-
row.processedAt,
|
|
683
|
-
row.failedAt,
|
|
684
|
-
]) {
|
|
685
|
-
if (timestamp && timestamp > updatedAt) updatedAt = timestamp
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
return {
|
|
689
|
-
sessionId,
|
|
690
|
-
eventCount: rows.length,
|
|
691
|
-
pendingCount,
|
|
692
|
-
failedCount,
|
|
693
|
-
attemptCount,
|
|
694
|
-
failureCount,
|
|
695
|
-
firstEventAt: new Date(first.createdAt),
|
|
696
|
-
updatedAt: new Date(updatedAt),
|
|
697
|
-
}
|
|
698
|
-
})
|
|
699
|
-
return {
|
|
700
|
-
sessions: summaries,
|
|
701
|
-
cursor: ids.length > visible.length ? (visible.at(-1) ?? null) : null,
|
|
702
|
-
}
|
|
703
|
-
},
|
|
704
|
-
async readEvents(sessionId, pageOptions) {
|
|
705
|
-
const rows = sessions.get(sessionId) ?? []
|
|
706
|
-
const throughIndex = Math.min(
|
|
707
|
-
pageOptions.throughIndex ?? rows.length,
|
|
708
|
-
rows.length,
|
|
709
|
-
)
|
|
710
|
-
return {
|
|
711
|
-
events: rows
|
|
712
|
-
.slice(pageOptions.afterIndex, throughIndex)
|
|
713
|
-
.slice(0, pageOptions.limit)
|
|
714
|
-
.map(toStored),
|
|
715
|
-
throughIndex,
|
|
716
|
-
}
|
|
717
|
-
},
|
|
718
|
-
async listSnapshots(sessionId) {
|
|
719
|
-
const prefix = `${sessionId}\u0000`
|
|
720
|
-
return [...snapshots.entries()]
|
|
721
|
-
.filter(([key]) => key.startsWith(prefix))
|
|
722
|
-
.map(([key, snapshot]) => ({
|
|
723
|
-
reducerName: key.slice(prefix.length),
|
|
724
|
-
index: snapshot.index,
|
|
725
|
-
updatedAt: new Date(snapshot.updatedAt),
|
|
726
|
-
}))
|
|
727
|
-
.toSorted((a, b) => a.reducerName.localeCompare(b.reducerName))
|
|
728
|
-
},
|
|
729
|
-
},
|
|
730
|
-
|
|
731
650
|
stream(sessionId, opts) {
|
|
732
651
|
const startAfter = opts?.startAfter ?? 0
|
|
733
652
|
return {
|