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/package.json
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "experimental-a2",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Durable sync and reactions for things with a lifecycle: one event log, derived state, and live client per session.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
"a2": "./dist/cli-bin.js"
|
|
9
|
-
},
|
|
10
7
|
"sideEffects": false,
|
|
11
8
|
"engines": {
|
|
12
9
|
"node": ">=22.13"
|
|
@@ -39,7 +36,6 @@
|
|
|
39
36
|
"browser": "./dist/ai-server.browser.js",
|
|
40
37
|
"default": "./dist/ai-server.js"
|
|
41
38
|
},
|
|
42
|
-
"./http": "./dist/http.js",
|
|
43
39
|
"./store-memory": "./dist/store-memory.js",
|
|
44
40
|
"./store-sqlite": "./dist/store-sqlite.js",
|
|
45
41
|
"./store-postgres": "./dist/store-postgres.js",
|
|
@@ -49,15 +45,6 @@
|
|
|
49
45
|
"./scheduler-vercel": "./dist/scheduler-vercel.js",
|
|
50
46
|
"./cache-indexeddb": "./dist/cache-indexeddb.js",
|
|
51
47
|
"./otel": "./dist/otel.js",
|
|
52
|
-
"./devtools": "./dist/devtools.js",
|
|
53
|
-
"./devtools/server": {
|
|
54
|
-
"browser": "./dist/devtools-server.browser.js",
|
|
55
|
-
"default": "./dist/devtools-server.js"
|
|
56
|
-
},
|
|
57
|
-
"./testing": {
|
|
58
|
-
"browser": "./dist/testing.browser.js",
|
|
59
|
-
"default": "./dist/testing.js"
|
|
60
|
-
},
|
|
61
48
|
"./package.json": "./package.json"
|
|
62
49
|
},
|
|
63
50
|
"peerDependencies": {
|
package/src/ai-server.ts
CHANGED
|
@@ -57,11 +57,14 @@ import {
|
|
|
57
57
|
createServer,
|
|
58
58
|
type A2Server,
|
|
59
59
|
type HandlerContext,
|
|
60
|
-
type PushValidationContext,
|
|
61
60
|
type ServerOptions,
|
|
62
61
|
} from './server.ts'
|
|
62
|
+
import {
|
|
63
|
+
setServerFetchHooks,
|
|
64
|
+
type ServerIngressContext,
|
|
65
|
+
} from './server-fetch.ts'
|
|
63
66
|
|
|
64
|
-
|
|
67
|
+
function validateAgentIngress(context: ServerIngressContext): void {
|
|
65
68
|
const rejected = context.events.find((event) => {
|
|
66
69
|
if (event.type !== 'ai.message.created') {
|
|
67
70
|
return !(
|
|
@@ -203,7 +206,7 @@ export type CreateAgentServerOptions<
|
|
|
203
206
|
D extends AIEventDefs<M> & EventDefs,
|
|
204
207
|
T extends ToolSet = ToolSet,
|
|
205
208
|
> = CreateHandlersOptions<M, D, T> &
|
|
206
|
-
Omit<ServerOptions<D>, 'contract' | 'handlers'
|
|
209
|
+
Omit<ServerOptions<D>, 'contract' | 'handlers'> & {
|
|
207
210
|
handlers?: ServerOptions<D>['handlers']
|
|
208
211
|
}
|
|
209
212
|
|
|
@@ -839,13 +842,16 @@ export function createHandlers<
|
|
|
839
842
|
const scheduleNext = async (
|
|
840
843
|
ctx: Pick<HandlerContext<D>, 'session'>,
|
|
841
844
|
): Promise<AppendInput<D> | void> =>
|
|
842
|
-
requestForNextMessage(
|
|
845
|
+
requestForNextMessage(
|
|
846
|
+
(await ctx.session.state(coordinator, { through: 'latest' })).state,
|
|
847
|
+
)
|
|
843
848
|
|
|
844
849
|
const continueIfReady = async (
|
|
845
850
|
ctx: Pick<HandlerContext<D>, 'session'>,
|
|
846
851
|
generationId: string,
|
|
847
852
|
): Promise<void> => {
|
|
848
|
-
const state = (await ctx.session.state(coordinator))
|
|
853
|
+
const state = (await ctx.session.state(coordinator, { through: 'latest' }))
|
|
854
|
+
.state
|
|
849
855
|
const response = state.response
|
|
850
856
|
if (response?.generation?.generationId !== generationId) {
|
|
851
857
|
return
|
|
@@ -1089,6 +1095,7 @@ export function createHandlers<
|
|
|
1089
1095
|
call,
|
|
1090
1096
|
ctx.session.history,
|
|
1091
1097
|
)
|
|
1098
|
+
if (ctx.signal.aborted) return
|
|
1092
1099
|
const scope: AmbientToolScope = {
|
|
1093
1100
|
contract: options.agent.contract,
|
|
1094
1101
|
context: ctx,
|
|
@@ -1103,7 +1110,8 @@ export function createHandlers<
|
|
|
1103
1110
|
const handleToolCall = async (
|
|
1104
1111
|
ctx: HandlerContext<D, 'ai.tool.called'>,
|
|
1105
1112
|
): Promise<AppendInput<D> | void> => {
|
|
1106
|
-
const state = (await ctx.session.state(coordinator))
|
|
1113
|
+
const state = (await ctx.session.state(coordinator, { through: 'latest' }))
|
|
1114
|
+
.state
|
|
1107
1115
|
const response = state.response
|
|
1108
1116
|
const current = response?.calls.find(
|
|
1109
1117
|
(candidate) => candidate.index === ctx.event.index,
|
|
@@ -1127,7 +1135,8 @@ export function createHandlers<
|
|
|
1127
1135
|
const handleApproval = async (
|
|
1128
1136
|
ctx: HandlerContext<D, 'ai.approval.responded'>,
|
|
1129
1137
|
): Promise<AppendInput<D> | void> => {
|
|
1130
|
-
const state = (await ctx.session.state(coordinator))
|
|
1138
|
+
const state = (await ctx.session.state(coordinator, { through: 'latest' }))
|
|
1139
|
+
.state
|
|
1131
1140
|
const response = state.response
|
|
1132
1141
|
const current = response?.calls.find(
|
|
1133
1142
|
(candidate) =>
|
|
@@ -1160,7 +1169,9 @@ export function createHandlers<
|
|
|
1160
1169
|
const history = await ctx.session.history()
|
|
1161
1170
|
const requestId = ctx.event.id
|
|
1162
1171
|
const request = ctx.event.payload
|
|
1163
|
-
const coordinatorState = (
|
|
1172
|
+
const coordinatorState = (
|
|
1173
|
+
await ctx.session.state(coordinator, { through: 'latest' })
|
|
1174
|
+
).state
|
|
1164
1175
|
if (
|
|
1165
1176
|
coordinatorState.closed ||
|
|
1166
1177
|
coordinatorState.response?.activeRequestId !== requestId
|
|
@@ -1314,7 +1325,9 @@ export function createHandlers<
|
|
|
1314
1325
|
} as AppendInput<D>)
|
|
1315
1326
|
await ctx.session.append('generation-start', ...startEvents)
|
|
1316
1327
|
|
|
1317
|
-
const startedCoordinatorState = (
|
|
1328
|
+
const startedCoordinatorState = (
|
|
1329
|
+
await ctx.session.state(coordinator, { through: 'latest' })
|
|
1330
|
+
).state
|
|
1318
1331
|
if (
|
|
1319
1332
|
startedCoordinatorState.response?.activeRequestId !== requestId ||
|
|
1320
1333
|
startedCoordinatorState.response.generation?.generationId !== generationId
|
|
@@ -1361,8 +1374,12 @@ export function createHandlers<
|
|
|
1361
1374
|
}
|
|
1362
1375
|
}
|
|
1363
1376
|
|
|
1364
|
-
const currentState = (
|
|
1365
|
-
|
|
1377
|
+
const currentState = (
|
|
1378
|
+
await ctx.session.state(options.agent.reducer, { through: 'latest' })
|
|
1379
|
+
).state
|
|
1380
|
+
const currentCoordinatorState = (
|
|
1381
|
+
await ctx.session.state(coordinator, { through: 'latest' })
|
|
1382
|
+
).state
|
|
1366
1383
|
const generationMessages = activeContextMessages(
|
|
1367
1384
|
currentState,
|
|
1368
1385
|
currentCoordinatorState,
|
|
@@ -1378,6 +1395,7 @@ export function createHandlers<
|
|
|
1378
1395
|
options.instructions === undefined
|
|
1379
1396
|
? undefined
|
|
1380
1397
|
: await resolve(options.instructions, resolverContext)
|
|
1398
|
+
if (ctx.signal.aborted) return
|
|
1381
1399
|
const generateContext: AgentGenerateContext<M, D, T> = {
|
|
1382
1400
|
request: ctx.event,
|
|
1383
1401
|
requestId,
|
|
@@ -1538,7 +1556,8 @@ export function createHandlers<
|
|
|
1538
1556
|
const handleRetry = async (
|
|
1539
1557
|
ctx: HandlerContext<D, 'ai.retry.requested'>,
|
|
1540
1558
|
): Promise<AppendInput<D> | void> => {
|
|
1541
|
-
const state = (await ctx.session.state(coordinator))
|
|
1559
|
+
const state = (await ctx.session.state(coordinator, { through: 'latest' }))
|
|
1560
|
+
.state
|
|
1542
1561
|
const response = state.response
|
|
1543
1562
|
if (
|
|
1544
1563
|
response?.status !== 'failed' ||
|
|
@@ -1561,7 +1580,8 @@ export function createHandlers<
|
|
|
1561
1580
|
const handleInputResponse = async (
|
|
1562
1581
|
ctx: HandlerContext<D, 'ai.input.responded'>,
|
|
1563
1582
|
): Promise<void> => {
|
|
1564
|
-
const state = (await ctx.session.state(coordinator))
|
|
1583
|
+
const state = (await ctx.session.state(coordinator, { through: 'latest' }))
|
|
1584
|
+
.state
|
|
1565
1585
|
const response = state.response
|
|
1566
1586
|
if (
|
|
1567
1587
|
response?.responseMessageId !== ctx.event.payload.messageId ||
|
|
@@ -1708,12 +1728,13 @@ export function createAgentServer<
|
|
|
1708
1728
|
...(compaction === undefined ? {} : { compaction }),
|
|
1709
1729
|
...(progress === undefined ? {} : { progress }),
|
|
1710
1730
|
})
|
|
1711
|
-
|
|
1731
|
+
const server = createServer({
|
|
1712
1732
|
...serverOptions,
|
|
1713
1733
|
contract: definition.contract,
|
|
1714
1734
|
handlers: { ...builtIns, ...handlers },
|
|
1715
|
-
validatePush: validateAgentPush,
|
|
1716
1735
|
})
|
|
1736
|
+
setServerFetchHooks(server, { validateIngress: validateAgentIngress })
|
|
1737
|
+
return server
|
|
1717
1738
|
}
|
|
1718
1739
|
|
|
1719
1740
|
export type { Instructions, LanguageModel, ToolSet }
|
package/src/client.ts
CHANGED
|
@@ -808,7 +808,7 @@ const wsTransport = (
|
|
|
808
808
|
settleChannel(
|
|
809
809
|
channel,
|
|
810
810
|
new Error(
|
|
811
|
-
'subscribe not confirmed: is the ws route
|
|
811
|
+
'subscribe not confirmed: is the ws route calling server.fetch with upgradeWebSocket?',
|
|
812
812
|
),
|
|
813
813
|
)
|
|
814
814
|
}, STREAM_TIMINGS.stallTimeoutMs)
|
|
@@ -1562,7 +1562,7 @@ export function createClient<
|
|
|
1562
1562
|
const readHistory = transport.history
|
|
1563
1563
|
if (readHistory === undefined) {
|
|
1564
1564
|
throw new TypeError(
|
|
1565
|
-
'loadHistory requires an http api — the ws transport has no history lane; serve the session
|
|
1565
|
+
'loadHistory requires an http api — the ws transport has no history lane; serve the session with server.fetch and pass its route url as the string or { type: "http" } api',
|
|
1566
1566
|
)
|
|
1567
1567
|
}
|
|
1568
1568
|
const before = loadOptions?.before
|
package/src/errors.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
export type A2ErrorCode =
|
|
10
10
|
| 'INVALID_PAYLOAD' // schema validation failed — thrown before anything is written
|
|
11
|
+
| 'FORBIDDEN' // external ingress was denied by server.fetch authorization
|
|
11
12
|
| 'UNKNOWN_EVENT_TYPE' // event type not in the machine's `events` map
|
|
12
13
|
| 'PARTIAL_DUPLICATE_BATCH' // batch mixed already-appended and fresh events
|
|
13
14
|
| 'SUPERSEDED_ATTEMPT' // handler append from an attempt a recovery claim replaced
|
package/src/internal.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ScheduledEvent } from './scheduler-task.ts'
|
|
7
|
-
import type {
|
|
7
|
+
import type { DrainableServer } from './server.ts'
|
|
8
8
|
|
|
9
9
|
export type DrainOutcome = 'busy' | 'stalled' | 'settled'
|
|
10
10
|
|
|
@@ -67,14 +67,6 @@ export type ServerInternals = {
|
|
|
67
67
|
|
|
68
68
|
export const serverInternals: WeakMap<object, ServerInternals> = new WeakMap()
|
|
69
69
|
|
|
70
|
-
type ServerSchedulerBinding = {
|
|
71
|
-
scheduler: A2Scheduler | undefined
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Scheduler configuration captured only for servers made by `createServer`. */
|
|
75
|
-
export const serverSchedulerBindings: WeakMap<object, ServerSchedulerBinding> =
|
|
76
|
-
new WeakMap()
|
|
77
|
-
|
|
78
70
|
const objectLike = (value: unknown): value is object =>
|
|
79
71
|
(typeof value === 'object' && value !== null) || typeof value === 'function'
|
|
80
72
|
|
|
@@ -232,13 +224,11 @@ const isDrainableServer = (value: unknown): value is DrainableServer => {
|
|
|
232
224
|
|
|
233
225
|
/**
|
|
234
226
|
* Validate the server list captured by a scheduler route at construction time.
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* so only servers made by this A2 copy reach a configured adapter.
|
|
227
|
+
* The seam stays structural so custom scheduler integrations can supply their
|
|
228
|
+
* own drainable targets.
|
|
238
229
|
*/
|
|
239
230
|
export function assertSchedulerTargets(
|
|
240
231
|
servers: readonly unknown[],
|
|
241
|
-
options: { allowStructural: boolean },
|
|
242
232
|
): asserts servers is readonly DrainableServer[] {
|
|
243
233
|
const contracts = new Set<string>()
|
|
244
234
|
for (const target of servers) {
|
|
@@ -252,56 +242,7 @@ export function assertSchedulerTargets(
|
|
|
252
242
|
)
|
|
253
243
|
}
|
|
254
244
|
contracts.add(name)
|
|
255
|
-
|
|
256
|
-
const binding = serverSchedulerBindings.get(target)
|
|
257
|
-
if (!binding) {
|
|
258
|
-
if (!options.allowStructural) {
|
|
259
|
-
throw new TypeError(
|
|
260
|
-
`a2 scheduler: server for contract '${name}' was not created by createServer()`,
|
|
261
|
-
)
|
|
262
|
-
}
|
|
263
|
-
continue
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/** Require every real server to share one exact configured scheduler. */
|
|
269
|
-
export function assertServerSchedulerBindings(
|
|
270
|
-
scheduler: A2Scheduler,
|
|
271
|
-
servers: readonly DrainableServer[],
|
|
272
|
-
): void {
|
|
273
|
-
for (const server of servers) {
|
|
274
|
-
const binding = serverSchedulerBindings.get(server)
|
|
275
|
-
if (!binding?.scheduler) {
|
|
276
|
-
throw new TypeError(
|
|
277
|
-
`a2 scheduler: server for contract '${server.contract.name}' has no scheduler configured`,
|
|
278
|
-
)
|
|
279
|
-
}
|
|
280
|
-
if (binding.scheduler !== scheduler) {
|
|
281
|
-
throw new TypeError(
|
|
282
|
-
`a2 scheduler: server for contract '${server.contract.name}' is configured with a different scheduler instance`,
|
|
283
|
-
)
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/** Resolve the scheduler privately bound to a real A2 server. */
|
|
289
|
-
export function schedulerForServer(server: unknown): A2Scheduler {
|
|
290
|
-
if (!isDrainableServer(server)) {
|
|
291
|
-
throw new TypeError('a2 scheduler: expected a server from createServer()')
|
|
292
|
-
}
|
|
293
|
-
const binding = serverSchedulerBindings.get(server)
|
|
294
|
-
if (!binding) {
|
|
295
|
-
throw new TypeError(
|
|
296
|
-
`a2 scheduler: server for contract '${server.contract.name}' was not created by createServer()`,
|
|
297
|
-
)
|
|
298
|
-
}
|
|
299
|
-
if (!binding.scheduler) {
|
|
300
|
-
throw new TypeError(
|
|
301
|
-
`a2 scheduler: server for contract '${server.contract.name}' has no scheduler configured`,
|
|
302
|
-
)
|
|
303
245
|
}
|
|
304
|
-
return binding.scheduler
|
|
305
246
|
}
|
|
306
247
|
|
|
307
248
|
/**
|
package/src/push-envelope.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The push envelope's trust boundary
|
|
3
|
-
*
|
|
2
|
+
* The push envelope's trust boundary for `{ sessionId, events, presence? }`,
|
|
3
|
+
* shared verbatim by the HTTP
|
|
4
4
|
* POST lane and the socket's push/presence frames so INVALID_PAYLOAD
|
|
5
|
-
* shapes are one implementation. Internal module
|
|
6
|
-
* socket handler) compose it; it is not part of the public surface.
|
|
5
|
+
* shapes are one implementation. Internal module, not public API.
|
|
7
6
|
*/
|
|
8
7
|
|
|
9
8
|
import { A2Error } from './errors.ts'
|
|
10
9
|
import { MAX_DATE_MS, RESERVED_PARTICIPANT_IDS } from './internal.ts'
|
|
11
|
-
|
|
10
|
+
|
|
11
|
+
export type ParsedPushEvent = {
|
|
12
|
+
type: string
|
|
13
|
+
payload: unknown
|
|
14
|
+
id?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ParsedPushPresence = {
|
|
18
|
+
participant: string
|
|
19
|
+
values: Record<string, unknown>
|
|
20
|
+
seen?: number
|
|
21
|
+
at?: number
|
|
22
|
+
}
|
|
12
23
|
|
|
13
24
|
export const invalidPushBody = (message: string): A2Error =>
|
|
14
25
|
new A2Error('INVALID_PAYLOAD', `malformed push body: ${message}`)
|
|
@@ -17,18 +28,13 @@ const invalid = invalidPushBody
|
|
|
17
28
|
|
|
18
29
|
export type PushBody = {
|
|
19
30
|
sessionId: string
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* for a presence-only push. */
|
|
23
|
-
events: PushedEvent[]
|
|
24
|
-
/** Branded like `events`: `session.setPresence` accepts it whole;
|
|
25
|
-
* field validation still happens inside `setPresence`. */
|
|
26
|
-
presence?: PushedPresence
|
|
31
|
+
events: ParsedPushEvent[]
|
|
32
|
+
presence?: ParsedPushPresence
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
export function parsePresenceSibling(
|
|
30
36
|
value: unknown,
|
|
31
|
-
):
|
|
37
|
+
): ParsedPushPresence | undefined {
|
|
32
38
|
if (value === undefined) return undefined
|
|
33
39
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
34
40
|
throw invalid('presence must be an object when present')
|
|
@@ -68,14 +74,12 @@ export function parsePresenceSibling(
|
|
|
68
74
|
} = { participant, values: values as Record<string, unknown> }
|
|
69
75
|
if (seen !== undefined) out.seen = seen
|
|
70
76
|
if (at !== undefined) out.at = at
|
|
71
|
-
|
|
72
|
-
return out as PushedPresence
|
|
77
|
+
return out
|
|
73
78
|
}
|
|
74
79
|
|
|
75
|
-
/** The events half of the envelope
|
|
76
|
-
*
|
|
77
|
-
|
|
78
|
-
export function parsePushEvents(events: unknown): PushedEvent[] {
|
|
80
|
+
/** The events half of the envelope, shared by the POST body and socket
|
|
81
|
+
* `push` frames so INVALID_PAYLOAD shapes have one implementation. */
|
|
82
|
+
export function parsePushEvents(events: unknown): ParsedPushEvent[] {
|
|
79
83
|
if (!Array.isArray(events) || events.length === 0) {
|
|
80
84
|
throw invalid('events must be a non-empty array')
|
|
81
85
|
}
|
|
@@ -95,8 +99,7 @@ export function parsePushEvents(events: unknown): PushedEvent[] {
|
|
|
95
99
|
payload,
|
|
96
100
|
}
|
|
97
101
|
if (id !== undefined) out.id = id
|
|
98
|
-
|
|
99
|
-
return out as PushedEvent
|
|
102
|
+
return out
|
|
100
103
|
})
|
|
101
104
|
}
|
|
102
105
|
|
package/src/scheduler-qstash.ts
CHANGED
|
@@ -94,7 +94,7 @@ export type QStashTransport = {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
export type QStashSchedulerOptions = {
|
|
97
|
-
/** Exact URL mounted with `
|
|
97
|
+
/** Exact URL mounted with `scheduler.handler(...)`. Inferred in local dev and on Vercel. */
|
|
98
98
|
url?: string
|
|
99
99
|
/** QStash API token. Default: `QSTASH_TOKEN`. */
|
|
100
100
|
token?: string
|
|
@@ -819,7 +819,7 @@ export function qstash(options: QStashSchedulerOptions = {}): A2Scheduler {
|
|
|
819
819
|
},
|
|
820
820
|
|
|
821
821
|
handler(...servers: DrainableServer[]) {
|
|
822
|
-
assertSchedulerTargets(servers
|
|
822
|
+
assertSchedulerTargets(servers)
|
|
823
823
|
const byContract = new Map<string, DrainableServer>()
|
|
824
824
|
for (const server of servers) {
|
|
825
825
|
byContract.set(server.contract.name, server)
|
|
@@ -851,7 +851,7 @@ export function qstash(options: QStashSchedulerOptions = {}): A2Scheduler {
|
|
|
851
851
|
const server = byContract.get(task.contract)
|
|
852
852
|
if (!server) {
|
|
853
853
|
throw new Error(
|
|
854
|
-
`a2 scheduler: no server for contract '${task.contract}' — pass it to
|
|
854
|
+
`a2 scheduler: no server for contract '${task.contract}' — pass it to scheduler.handler(...)`,
|
|
855
855
|
)
|
|
856
856
|
}
|
|
857
857
|
const internals = serverInternals.get(server)
|
package/src/scheduler-vercel.ts
CHANGED
|
@@ -368,7 +368,7 @@ export function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {
|
|
|
368
368
|
},
|
|
369
369
|
|
|
370
370
|
handler(...servers: DrainableServer[]) {
|
|
371
|
-
assertSchedulerTargets(servers
|
|
371
|
+
assertSchedulerTargets(servers)
|
|
372
372
|
const byContract = new Map<string, DrainableServer>()
|
|
373
373
|
for (const server of servers) {
|
|
374
374
|
byContract.set(server.contract.name, server)
|
|
@@ -386,7 +386,7 @@ export function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {
|
|
|
386
386
|
// wiring bug — throw so it redelivers and stays visible in
|
|
387
387
|
// queue observability instead of vanishing on an ack.
|
|
388
388
|
throw new Error(
|
|
389
|
-
`a2 scheduler: no server for contract '${task.contract}' — pass it to
|
|
389
|
+
`a2 scheduler: no server for contract '${task.contract}' — pass it to scheduler.handler(...)`,
|
|
390
390
|
)
|
|
391
391
|
}
|
|
392
392
|
const internals = serverInternals.get(server)
|