experimental-a2 0.17.0 → 0.18.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 +21 -0
- package/README.md +1 -1
- package/dist/{actor-DBbe5xeE.d.ts → actor-Bjce9_5U.d.ts} +5 -5
- package/dist/{actor-DBbe5xeE.d.ts.map → actor-Bjce9_5U.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +2 -2
- package/dist/{ai-UrWfKeuv.js → ai-BpOGm-zB.js} +2 -2
- package/dist/{ai-UrWfKeuv.js.map → ai-BpOGm-zB.js.map} +1 -1
- package/dist/{ai-vpGtcaCB.d.ts → ai-DrUaMyRt.d.ts} +3 -3
- package/dist/{ai-vpGtcaCB.d.ts.map → ai-DrUaMyRt.d.ts.map} +1 -1
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.browser.d.ts +1 -0
- package/dist/ai-fx.browser.js +6 -0
- package/dist/ai-fx.browser.js.map +1 -0
- package/dist/ai-fx.d.ts +10 -0
- package/dist/ai-fx.d.ts.map +1 -0
- package/dist/ai-fx.js +634 -0
- package/dist/ai-fx.js.map +1 -0
- package/dist/ai-harness-BuLh_EYi.js +273 -0
- package/dist/ai-harness-BuLh_EYi.js.map +1 -0
- package/dist/ai-harness-DvsPeJeB.d.ts +26 -0
- package/dist/ai-harness-DvsPeJeB.d.ts.map +1 -0
- package/dist/ai-pi.browser.d.ts +1 -0
- package/dist/ai-pi.browser.js +6 -0
- package/dist/ai-pi.browser.js.map +1 -0
- package/dist/ai-pi.d.ts +17 -0
- package/dist/ai-pi.d.ts.map +1 -0
- package/dist/ai-pi.js +904 -0
- package/dist/ai-pi.js.map +1 -0
- package/dist/ai-react.d.ts +5 -5
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +47 -25
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +106 -37
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/bounded-cache-CPHWF-8h.js +41 -0
- package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
- package/dist/{client-COaiaDub.js → client-BReKqwZC.js} +3 -3
- package/dist/{client-COaiaDub.js.map → client-BReKqwZC.js.map} +1 -1
- package/dist/{client-qRnRUzBS.d.ts → client-h_DKFAjj.d.ts} +3 -3
- package/dist/{client-qRnRUzBS.d.ts.map → client-h_DKFAjj.d.ts.map} +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/{initial-snapshot-BrLso5x_.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +2 -2
- package/dist/{initial-snapshot-BrLso5x_.d.ts.map → initial-snapshot-Bx-c0pDP.d.ts.map} +1 -1
- package/dist/{internal-CbVvEwad.js → internal-B2T3U4Rq.js} +8 -2
- package/dist/internal-B2T3U4Rq.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/{react-BbJ-g6Uu.d.ts → react-BBXgHWFc.d.ts} +4 -4
- package/dist/{react-BbJ-g6Uu.d.ts.map → react-BBXgHWFc.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/reducer-BAXc8JyF.js.map +1 -1
- package/dist/{reducer-BYu6DT7h.d.ts → reducer-CqA59Q6U.d.ts} +2 -2
- package/dist/{reducer-BYu6DT7h.d.ts.map → reducer-CqA59Q6U.d.ts.map} +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +2 -2
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +2 -2
- package/dist/{server-Rj64fNvX.js → server-Dw_k_7kO.js} +93 -60
- package/dist/server-Dw_k_7kO.js.map +1 -0
- package/dist/{server-CGM8WzRG.d.ts → server-En7cbi3v.d.ts} +36 -7
- package/dist/server-En7cbi3v.d.ts.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +1 -1
- package/dist/{session-clear-Dshpuhff.d.ts → session-clear-Bo-XTFEy.d.ts} +2 -2
- package/dist/session-clear-Bo-XTFEy.d.ts.map +1 -0
- package/dist/{store-Gpkq7WBo.d.ts → store-Djdv0bnZ.d.ts} +31 -2
- package/dist/store-Djdv0bnZ.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.js +32 -6
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-4_y8lETm.js → store-polling-B5FcwJfo.js} +2 -2
- package/dist/{store-polling-4_y8lETm.js.map → store-polling-B5FcwJfo.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.js +54 -3
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-BAwS8RWn.js} +3 -3
- package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-BAwS8RWn.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -3
- package/dist/{store-redis-notify-tqWNnMRu.js → store-redis-notify-BRITti_o.js} +187 -68
- package/dist/store-redis-notify-BRITti_o.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.js +32 -2
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{telemetry-cZH-cibj.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
- package/dist/{wire-DjfHOvdW.js → wire-rSUglYo8.js} +2 -2
- package/dist/{wire-DjfHOvdW.js.map → wire-rSUglYo8.js.map} +1 -1
- package/docs/01-quickstart.mdx +49 -83
- package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
- package/docs/actors/01-introduction.mdx +48 -86
- package/docs/actors/02-concurrency.mdx +43 -70
- package/docs/actors/03-timers.mdx +28 -42
- package/docs/actors/04-routes.mdx +110 -150
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/01-introduction.mdx +257 -0
- package/docs/agents/02-tools.mdx +247 -0
- package/docs/agents/03-conversations.mdx +538 -0
- package/docs/agents/04-harnesses.mdx +136 -0
- package/docs/agents/meta.ts +1 -0
- package/docs/client/01-react.mdx +294 -0
- package/docs/client/02-transports.mdx +86 -0
- package/docs/client/03-presence.mdx +224 -0
- package/docs/client/meta.ts +1 -0
- package/docs/contract/01-events.mdx +57 -0
- package/docs/contract/02-state.mdx +142 -0
- package/docs/contract/03-sessions.mdx +175 -0
- package/docs/index.mdx +108 -144
- package/docs/reference/01-core.mdx +139 -0
- package/docs/reference/02-server.mdx +855 -0
- package/docs/reference/03-client.mdx +301 -0
- package/docs/reference/04-ai.mdx +722 -0
- package/docs/reference/05-adapters.mdx +351 -0
- package/docs/reference/06-errors.mdx +138 -0
- package/docs/reference/meta.ts +1 -1
- package/docs/server/01-handlers.mdx +200 -0
- package/docs/server/02-timers.mdx +130 -0
- package/docs/server/03-cancellation.mdx +141 -0
- package/docs/server/04-routes.mdx +243 -0
- package/docs/server/05-production.mdx +322 -0
- package/docs/server/06-guarantees.mdx +123 -0
- package/examples/playground/DEPLOY.md +1 -1
- package/examples/playground/app/a0/[appId]/page.tsx +11 -6
- package/examples/playground/app/a0/app-list.tsx +9 -2
- package/examples/playground/app/a0/app-view.tsx +9 -1
- package/examples/playground/app/a0/apps/[appId]/route.ts +10 -5
- package/examples/playground/app/a0/apps.test.ts +12 -0
- package/examples/playground/app/a0/apps.ts +22 -7
- package/examples/playground/app/a0/chat.tsx +47 -1
- package/examples/playground/app/a0/fx/events/route.ts +9 -0
- package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
- package/examples/playground/app/a0/model.test.ts +18 -1
- package/examples/playground/app/a0/model.ts +47 -10
- package/examples/playground/app/a0/no-photon.cjs +4 -0
- package/examples/playground/app/a0/no-quickjs.cjs +3 -0
- package/examples/playground/app/a0/pi/events/route.ts +9 -0
- package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
- package/examples/playground/app/a0/sandbox-log.ts +10 -4
- package/examples/playground/app/a0/sandbox-session.ts +152 -0
- package/examples/playground/app/a0/scheduler/route.ts +2 -2
- package/examples/playground/app/a0/server.ts +100 -45
- package/examples/playground/app/a0/session.ts +17 -5
- package/examples/playground/app/a0/shell.tsx +11 -3
- package/examples/playground/app/agent/ingress.test.ts +6 -3
- package/examples/playground/app/agent/server.ts +9 -6
- package/examples/playground/app/agent/telemetry.test.ts +71 -50
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +1 -1
- package/examples/playground/app/chat/server.test.ts +30 -26
- package/examples/playground/app/chat/server.ts +46 -46
- package/examples/playground/next.config.ts +17 -5
- package/examples/playground/package.json +4 -1
- package/examples/playground/vercel.json +18 -0
- package/package.json +27 -3
- package/src/ai-agent.ts +261 -87
- package/src/ai-fx.browser.ts +5 -0
- package/src/ai-fx.ts +833 -0
- package/src/ai-harness.ts +518 -0
- package/src/ai-model-metadata.ts +11 -6
- package/src/ai-pi.browser.ts +5 -0
- package/src/ai-pi.ts +1263 -0
- package/src/ai-sdk-step.ts +2 -2
- package/src/ai-server.ts +3 -1
- package/src/internal.ts +7 -0
- package/src/libfx.d.ts +34 -0
- package/src/reducer.ts +1 -1
- package/src/server.ts +177 -19
- package/src/store-memory.ts +42 -7
- package/src/store-postgres.ts +80 -0
- package/src/store-redis-core.ts +210 -79
- package/src/store-redis-notify.ts +2 -0
- package/src/store-sqlite.ts +60 -0
- package/src/store.ts +33 -0
- package/src/telemetry.ts +1 -0
- package/dist/internal-CbVvEwad.js.map +0 -1
- package/dist/server-CGM8WzRG.d.ts.map +0 -1
- package/dist/server-Rj64fNvX.js.map +0 -1
- package/dist/session-clear-Dshpuhff.d.ts.map +0 -1
- package/dist/store-Gpkq7WBo.d.ts.map +0 -1
- package/dist/store-redis-notify-tqWNnMRu.js.map +0 -1
- package/dist/telemetry-cZH-cibj.d.ts.map +0 -1
- package/docs/concepts/01-contracts.mdx +0 -237
- package/docs/concepts/02-handlers.mdx +0 -284
- package/docs/concepts/03-durability.mdx +0 -314
- package/docs/concepts/04-state.mdx +0 -209
- package/docs/guides/01-timers.mdx +0 -193
- package/docs/guides/02-cancellation.mdx +0 -161
- package/docs/guides/03-react.mdx +0 -324
- package/docs/guides/04-local-first.mdx +0 -47
- package/docs/guides/05-production.mdx +0 -534
- package/docs/guides/06-ai-agents.mdx +0 -1408
- package/docs/guides/08-application-data.mdx +0 -113
- package/docs/guides/09-presence.mdx +0 -262
- package/docs/guides/10-transports.mdx +0 -221
- package/docs/reference/01-api.mdx +0 -2358
- package/docs/reference/02-errors.mdx +0 -150
- /package/docs/{concepts → contract}/meta.ts +0 -0
- /package/docs/{guides → server}/meta.ts +0 -0
package/src/ai-sdk-step.ts
CHANGED
|
@@ -8,7 +8,6 @@ import type {
|
|
|
8
8
|
FinishReason,
|
|
9
9
|
InferUIMessageChunk,
|
|
10
10
|
Instructions,
|
|
11
|
-
LanguageModel,
|
|
12
11
|
LanguageModelUsage,
|
|
13
12
|
ModelMessage,
|
|
14
13
|
TextStreamPart,
|
|
@@ -125,7 +124,8 @@ export type AISDKStepSettings<T extends ToolSet> = Partial<
|
|
|
125
124
|
}
|
|
126
125
|
|
|
127
126
|
export type AISDKStepInput<M extends UIMessage, T extends ToolSet> = {
|
|
128
|
-
model
|
|
127
|
+
/** An AI Gateway model ID, resolved through the AI SDK's global provider. */
|
|
128
|
+
model: string
|
|
129
129
|
tools: T
|
|
130
130
|
messages: M[]
|
|
131
131
|
modelMessages?: ModelMessage[]
|
package/src/ai-server.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
export { agent, runWithHandlerContext } from './ai-agent.ts'
|
|
8
8
|
export type {
|
|
9
|
+
AISDKHarness,
|
|
9
10
|
Agent,
|
|
10
11
|
AgentFromContractOptions,
|
|
11
12
|
AgentGenerate,
|
|
@@ -19,8 +20,9 @@ export type {
|
|
|
19
20
|
CompactionOptions,
|
|
20
21
|
CompactionPolicy,
|
|
21
22
|
Instructions,
|
|
22
|
-
LanguageModel,
|
|
23
23
|
ModelSettings,
|
|
24
24
|
Resolvable,
|
|
25
|
+
ToolRecovery,
|
|
25
26
|
ToolSet,
|
|
26
27
|
} from './ai-agent.ts'
|
|
28
|
+
export type { Harness } from './ai-harness.ts'
|
package/src/internal.ts
CHANGED
|
@@ -73,6 +73,13 @@ export type ServerInternals = {
|
|
|
73
73
|
|
|
74
74
|
export const serverInternals: WeakMap<object, ServerInternals> = new WeakMap()
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Handler contexts whose previous attempt ended in a failure the handler
|
|
78
|
+
* threw, as opposed to a claim that lapsed (a crash, a timeout, a frozen
|
|
79
|
+
* process). Only the attempt's own outcome is known, not how far it got.
|
|
80
|
+
*/
|
|
81
|
+
export const failedPreviousAttempt: WeakSet<object> = new WeakSet()
|
|
82
|
+
|
|
76
83
|
const objectLike = (value: unknown): value is object =>
|
|
77
84
|
(typeof value === 'object' && value !== null) || typeof value === 'function'
|
|
78
85
|
|
package/src/libfx.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The subset of libfx's agent API that experimental-a2/ai/fx uses; libfx
|
|
2
|
+
// ships no type declarations.
|
|
3
|
+
declare module 'libfx' {
|
|
4
|
+
export type FxHostTool = {
|
|
5
|
+
name: string
|
|
6
|
+
description: string
|
|
7
|
+
inputSchema: object
|
|
8
|
+
execute(input: unknown, context: { signal: AbortSignal }): unknown
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type FxTurn = AsyncIterable<{ type: string }> & {
|
|
12
|
+
result: Promise<{ stopReason: string }>
|
|
13
|
+
steer(input: string): Promise<void>
|
|
14
|
+
cancel(): void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type FxAgent = {
|
|
18
|
+
prompt(input: string, options?: { signal?: AbortSignal }): FxTurn
|
|
19
|
+
checkpoint(): Promise<Uint8Array>
|
|
20
|
+
close(): Promise<void>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createFxAgent(options: {
|
|
24
|
+
apiKey: string
|
|
25
|
+
model?: string | { id: string; effort?: string; fast?: boolean }
|
|
26
|
+
instructions?: string | string[]
|
|
27
|
+
tools?: FxHostTool[]
|
|
28
|
+
checkpoint?: Uint8Array
|
|
29
|
+
fetch?: (
|
|
30
|
+
input: string | URL | Request,
|
|
31
|
+
init?: RequestInit,
|
|
32
|
+
) => Promise<Response>
|
|
33
|
+
}): Promise<FxAgent>
|
|
34
|
+
}
|
package/src/reducer.ts
CHANGED
|
@@ -62,7 +62,7 @@ export function foldEvent<D extends EventDefs, S>(
|
|
|
62
62
|
* the browser cache) are keyed by it, which makes it the invalidation
|
|
63
63
|
* knob too: changed the fold's logic? Change the name. Snapshots under
|
|
64
64
|
* the old name are ignored and the next read refolds from raw events.
|
|
65
|
-
* See docs/
|
|
65
|
+
* See docs/contract/02-state.mdx.
|
|
66
66
|
*/
|
|
67
67
|
export type Reducer<
|
|
68
68
|
D extends EventDefs = EventDefs,
|
package/src/server.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
DRAIN_TIMINGS,
|
|
26
26
|
POLL_TIMINGS,
|
|
27
27
|
MAX_DATE_MS,
|
|
28
|
+
failedPreviousAttempt,
|
|
28
29
|
RESERVED_PARTICIPANT_IDS,
|
|
29
30
|
STATE_CACHE,
|
|
30
31
|
markSchedulerSendFailure,
|
|
@@ -55,6 +56,7 @@ import type {
|
|
|
55
56
|
StoreClaimAvailableResult,
|
|
56
57
|
StoreClearResult,
|
|
57
58
|
StoreReclaimResult,
|
|
59
|
+
StoreRedriveResult,
|
|
58
60
|
StoreSnapshotWrite,
|
|
59
61
|
StoreStateRead,
|
|
60
62
|
PresenceRow,
|
|
@@ -188,6 +190,42 @@ export type SessionClearOptions = {
|
|
|
188
190
|
|
|
189
191
|
export type SessionClearResult = { cleared: boolean; index: number }
|
|
190
192
|
|
|
193
|
+
export type SessionRedriveOptions = {
|
|
194
|
+
/** `retry` runs the handler again with a fresh failure budget; `skip` settles the event without running it. */
|
|
195
|
+
action: 'retry' | 'skip'
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export type SessionRedriveResult = {
|
|
199
|
+
/** Whether the event was dead-lettered and is now resolved. */
|
|
200
|
+
redriven: boolean
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** A dead-lettered event, with what an operator needs to resolve it. */
|
|
204
|
+
export type DeadLetter<D extends EventDefs> = ContractEvent<D> & {
|
|
205
|
+
lane: string | null
|
|
206
|
+
/** Durable dispatch claims, including claims abandoned by hard kills. */
|
|
207
|
+
attemptCount: number
|
|
208
|
+
/** Caught handler failures since the event was appended or last retried. */
|
|
209
|
+
failureCount: number
|
|
210
|
+
/** The last handler failure, stringified. */
|
|
211
|
+
lastError: string | null
|
|
212
|
+
failedAt: Date
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Dead-letter resolution; root sessions only. */
|
|
216
|
+
export type SessionDeadLetters<D extends EventDefs> = {
|
|
217
|
+
/** The session's dead-lettered events, oldest first. */
|
|
218
|
+
deadLetters(): Promise<DeadLetter<D>[]>
|
|
219
|
+
/**
|
|
220
|
+
* Atomically resolve one dead-lettered event, then wake the session like
|
|
221
|
+
* an append. Any other event is left alone and reports `redriven: false`.
|
|
222
|
+
*/
|
|
223
|
+
redrive(
|
|
224
|
+
index: number,
|
|
225
|
+
options: SessionRedriveOptions,
|
|
226
|
+
): Promise<SessionRedriveResult>
|
|
227
|
+
}
|
|
228
|
+
|
|
191
229
|
/**
|
|
192
230
|
* The presence members of a session — intersected in via
|
|
193
231
|
* `WithPresence`, so they exist exactly when the contract declares
|
|
@@ -293,7 +331,9 @@ export type A2Server<
|
|
|
293
331
|
(request: Request): Promise<Response>
|
|
294
332
|
(request: Request, options: ServerFetchOptions<D, P>): Promise<Response>
|
|
295
333
|
}
|
|
296
|
-
session(
|
|
334
|
+
session(
|
|
335
|
+
id: string,
|
|
336
|
+
): Session<D, SessionAppend<D>, P, PublicState<R>> & SessionDeadLetters<D>
|
|
297
337
|
/**
|
|
298
338
|
* Process every currently eligible event. `settled` means nothing
|
|
299
339
|
* actionable remains, including work blocked behind a dead letter.
|
|
@@ -603,6 +643,27 @@ const devDefaultStore = retryableLazy(() =>
|
|
|
603
643
|
import('./store-sqlite.ts').then((m) => m.sqlite()),
|
|
604
644
|
)
|
|
605
645
|
|
|
646
|
+
/** Waits for a scheduler arm for at most `schedulerArmTimeoutMs`. */
|
|
647
|
+
async function joinSchedulerArm(arm: Promise<void>): Promise<void> {
|
|
648
|
+
let timeout: ReturnType<typeof setTimeout> | null = null
|
|
649
|
+
try {
|
|
650
|
+
await Promise.race([
|
|
651
|
+
arm,
|
|
652
|
+
new Promise<never>((_, reject) => {
|
|
653
|
+
timeout = setTimeout(() => {
|
|
654
|
+
reject(
|
|
655
|
+
new Error(
|
|
656
|
+
`a2 scheduler arm timed out after ${DRAIN_TIMINGS.schedulerArmTimeoutMs}ms`,
|
|
657
|
+
),
|
|
658
|
+
)
|
|
659
|
+
}, DRAIN_TIMINGS.schedulerArmTimeoutMs)
|
|
660
|
+
}),
|
|
661
|
+
])
|
|
662
|
+
} finally {
|
|
663
|
+
if (timeout) clearTimeout(timeout)
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
606
667
|
function environment(): 'development' | 'test' | 'production' {
|
|
607
668
|
const env =
|
|
608
669
|
typeof process === 'undefined' ? undefined : process.env?.['NODE_ENV']
|
|
@@ -1081,26 +1142,12 @@ export function createServer<
|
|
|
1081
1142
|
await dispatchArm
|
|
1082
1143
|
}
|
|
1083
1144
|
if (initialArm) {
|
|
1084
|
-
let timeout: ReturnType<typeof setTimeout> | null = null
|
|
1085
1145
|
try {
|
|
1086
|
-
await
|
|
1087
|
-
initialArm,
|
|
1088
|
-
new Promise<never>((_, reject) => {
|
|
1089
|
-
timeout = setTimeout(() => {
|
|
1090
|
-
reject(
|
|
1091
|
-
new Error(
|
|
1092
|
-
`a2 scheduler arm timed out after ${DRAIN_TIMINGS.schedulerArmTimeoutMs}ms`,
|
|
1093
|
-
),
|
|
1094
|
-
)
|
|
1095
|
-
}, DRAIN_TIMINGS.schedulerArmTimeoutMs)
|
|
1096
|
-
}),
|
|
1097
|
-
])
|
|
1146
|
+
await joinSchedulerArm(initialArm)
|
|
1098
1147
|
} catch (err) {
|
|
1099
1148
|
span.setAttribute('a2.append.armed', false)
|
|
1100
1149
|
span.recordError(err)
|
|
1101
1150
|
if (requireDurableInitialArm) throw err
|
|
1102
|
-
} finally {
|
|
1103
|
-
if (timeout) clearTimeout(timeout)
|
|
1104
1151
|
}
|
|
1105
1152
|
}
|
|
1106
1153
|
return appended
|
|
@@ -2483,6 +2530,102 @@ export function createServer<
|
|
|
2483
2530
|
return { cleared: result.cleared, index: result.index }
|
|
2484
2531
|
}
|
|
2485
2532
|
|
|
2533
|
+
// ── dead letters ─────────────────────────────────────────────────
|
|
2534
|
+
|
|
2535
|
+
const redrivingStore = async (
|
|
2536
|
+
method: 'deadLetters' | 'redrive',
|
|
2537
|
+
): Promise<A2Store & Required<Pick<A2Store, 'deadLetters' | 'redrive'>>> => {
|
|
2538
|
+
const store = await resolveStore()
|
|
2539
|
+
if (!store.deadLetters || !store.redrive) {
|
|
2540
|
+
throw new TypeError(
|
|
2541
|
+
`session.${method} requires a store that supports redriving dead letters`,
|
|
2542
|
+
)
|
|
2543
|
+
}
|
|
2544
|
+
return store as A2Store & Required<Pick<A2Store, 'deadLetters' | 'redrive'>>
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
const listDeadLetters = async (
|
|
2548
|
+
sessionId: string,
|
|
2549
|
+
): Promise<DeadLetter<D>[]> => {
|
|
2550
|
+
const store = await redrivingStore('deadLetters')
|
|
2551
|
+
let rows: StoredEvent[]
|
|
2552
|
+
try {
|
|
2553
|
+
rows = await store.deadLetters(nsId(sessionId))
|
|
2554
|
+
} catch (err) {
|
|
2555
|
+
throw asStoreUnavailable(err)
|
|
2556
|
+
}
|
|
2557
|
+
return rows.flatMap((row) =>
|
|
2558
|
+
row.failedAt === null
|
|
2559
|
+
? []
|
|
2560
|
+
: [
|
|
2561
|
+
{
|
|
2562
|
+
...toPublic(row),
|
|
2563
|
+
lane: row.lane,
|
|
2564
|
+
attemptCount: row.attemptCount,
|
|
2565
|
+
failureCount: row.failureCount,
|
|
2566
|
+
lastError: row.lastError,
|
|
2567
|
+
failedAt: row.failedAt,
|
|
2568
|
+
},
|
|
2569
|
+
],
|
|
2570
|
+
)
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
const redriveEvent = async (
|
|
2574
|
+
sessionId: string,
|
|
2575
|
+
index: number,
|
|
2576
|
+
redriveOptions: SessionRedriveOptions,
|
|
2577
|
+
): Promise<SessionRedriveResult> => {
|
|
2578
|
+
assertRedriveIndex(index)
|
|
2579
|
+
const action = redriveOptions?.action
|
|
2580
|
+
if (action !== 'retry' && action !== 'skip') {
|
|
2581
|
+
throw new TypeError("redrive requires action 'retry' or 'skip'")
|
|
2582
|
+
}
|
|
2583
|
+
return telemetry.span(
|
|
2584
|
+
'a2.redrive',
|
|
2585
|
+
{
|
|
2586
|
+
'a2.contract': name,
|
|
2587
|
+
'a2.session_id': sessionId,
|
|
2588
|
+
'a2.redrive.index': index,
|
|
2589
|
+
'a2.redrive.action': action,
|
|
2590
|
+
},
|
|
2591
|
+
async (span) => {
|
|
2592
|
+
const store = await redrivingStore('redrive')
|
|
2593
|
+
let result: StoreRedriveResult
|
|
2594
|
+
try {
|
|
2595
|
+
result = await store.redrive({
|
|
2596
|
+
sessionId: nsId(sessionId),
|
|
2597
|
+
index,
|
|
2598
|
+
action,
|
|
2599
|
+
})
|
|
2600
|
+
} catch (err) {
|
|
2601
|
+
throw asStoreUnavailable(err)
|
|
2602
|
+
}
|
|
2603
|
+
span.setAttribute('a2.redrive.redriven', result.redriven)
|
|
2604
|
+
// Healing like a top-level append, even when nothing was redriven, so
|
|
2605
|
+
// repeating a redrive whose wakeup was lost resumes the session.
|
|
2606
|
+
if (result.hasPending) {
|
|
2607
|
+
const watchdogAt = scheduler
|
|
2608
|
+
? schedulerSlot(nextClaimWindow().watchdogAtMs)
|
|
2609
|
+
: undefined
|
|
2610
|
+
const arm =
|
|
2611
|
+
watchdogAt === undefined
|
|
2612
|
+
? null
|
|
2613
|
+
: startSchedulerArm(sessionId, watchdogAt)
|
|
2614
|
+
scheduleDrain(sessionId, watchdogAt)
|
|
2615
|
+
if (arm) {
|
|
2616
|
+
try {
|
|
2617
|
+
await joinSchedulerArm(arm)
|
|
2618
|
+
} catch (err) {
|
|
2619
|
+
span.setAttribute('a2.redrive.armed', false)
|
|
2620
|
+
span.recordError(err)
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
return { redriven: result.redriven }
|
|
2625
|
+
},
|
|
2626
|
+
)
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2486
2629
|
const makeSession = <Append>(
|
|
2487
2630
|
id: string,
|
|
2488
2631
|
append: Append,
|
|
@@ -2584,7 +2727,7 @@ export function createServer<
|
|
|
2584
2727
|
generatedIds,
|
|
2585
2728
|
)
|
|
2586
2729
|
}
|
|
2587
|
-
|
|
2730
|
+
const ctx: HandlerContext<D> = {
|
|
2588
2731
|
event: toPublic(trigger),
|
|
2589
2732
|
attempt,
|
|
2590
2733
|
// The handler-scoped `setPresence` stamps `seen` with the
|
|
@@ -2597,6 +2740,9 @@ export function createServer<
|
|
|
2597
2740
|
}),
|
|
2598
2741
|
signal,
|
|
2599
2742
|
}
|
|
2743
|
+
if (attempt > 1 && trigger.lastFailedAttempt === attempt - 1)
|
|
2744
|
+
failedPreviousAttempt.add(ctx)
|
|
2745
|
+
return ctx
|
|
2600
2746
|
}
|
|
2601
2747
|
|
|
2602
2748
|
const abortHubs = new Map<string, AbortHub>()
|
|
@@ -3276,7 +3422,9 @@ export function createServer<
|
|
|
3276
3422
|
)
|
|
3277
3423
|
}
|
|
3278
3424
|
|
|
3279
|
-
const session = (
|
|
3425
|
+
const session = (
|
|
3426
|
+
id: string,
|
|
3427
|
+
): Session<D, SessionAppend<D>, P> & SessionDeadLetters<D> => {
|
|
3280
3428
|
assertSessionId(id)
|
|
3281
3429
|
const appendInline = async (...events: AppendInput<D>[]) =>
|
|
3282
3430
|
appendExternal(id, 'inline', events)
|
|
@@ -3285,7 +3433,11 @@ export function createServer<
|
|
|
3285
3433
|
const append = Object.assign(appendInline, {
|
|
3286
3434
|
dispatch: appendDispatch,
|
|
3287
3435
|
}) as SessionAppend<D>
|
|
3288
|
-
return makeSession(id, append, null)
|
|
3436
|
+
return Object.assign(makeSession(id, append, null), {
|
|
3437
|
+
deadLetters: () => listDeadLetters(id),
|
|
3438
|
+
redrive: (index: number, redriveOptions: SessionRedriveOptions) =>
|
|
3439
|
+
redriveEvent(id, index, redriveOptions),
|
|
3440
|
+
})
|
|
3289
3441
|
}
|
|
3290
3442
|
|
|
3291
3443
|
const historyPage = async (
|
|
@@ -3393,6 +3545,12 @@ function assertStateIndex(value: number): void {
|
|
|
3393
3545
|
}
|
|
3394
3546
|
}
|
|
3395
3547
|
|
|
3548
|
+
function assertRedriveIndex(value: number): void {
|
|
3549
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
3550
|
+
throw new TypeError('redrive requires a positive safe-integer index')
|
|
3551
|
+
}
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3396
3554
|
function assertClearIndex(value: number): void {
|
|
3397
3555
|
if (!Number.isSafeInteger(value) || value < 0) {
|
|
3398
3556
|
throw new TypeError('ifLastIndex must be a non-negative safe integer')
|
package/src/store-memory.ts
CHANGED
|
@@ -259,16 +259,18 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
259
259
|
return row
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
const
|
|
262
|
+
const hasPending = (sessionId: string): boolean => {
|
|
263
263
|
const log = sessions.get(sessionId)
|
|
264
|
-
return
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
(row) => row.index > log!.clearIndex && row.processedAt === null,
|
|
268
|
-
),
|
|
269
|
-
}
|
|
264
|
+
return (log?.rows ?? []).some(
|
|
265
|
+
(row) => row.index > log!.clearIndex && row.processedAt === null,
|
|
266
|
+
)
|
|
270
267
|
}
|
|
271
268
|
|
|
269
|
+
const appendResult = (sessionId: string, events: StoredEvent[]) => ({
|
|
270
|
+
events,
|
|
271
|
+
hasPending: hasPending(sessionId),
|
|
272
|
+
})
|
|
273
|
+
|
|
272
274
|
/** The attempt-currency fence shared by handler appends and clears. */
|
|
273
275
|
const assertCurrentCause = (sessionId: string, cause: EventCause): void => {
|
|
274
276
|
if (cause.index <= clearIndexOf(sessionId)) {
|
|
@@ -719,6 +721,39 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
719
721
|
return { outcome: 'failed', failureCount: row.failureCount }
|
|
720
722
|
},
|
|
721
723
|
|
|
724
|
+
async deadLetters(sessionId) {
|
|
725
|
+
const log = sessions.get(sessionId)
|
|
726
|
+
return (log?.rows ?? [])
|
|
727
|
+
.filter(
|
|
728
|
+
(row) =>
|
|
729
|
+
row.index > log!.clearIndex &&
|
|
730
|
+
row.processedAt === null &&
|
|
731
|
+
row.failedAt !== null,
|
|
732
|
+
)
|
|
733
|
+
.map(toStored)
|
|
734
|
+
},
|
|
735
|
+
|
|
736
|
+
async redrive({ sessionId, index, action }) {
|
|
737
|
+
const row =
|
|
738
|
+
index > clearIndexOf(sessionId) ? rowAt(sessionId, index) : undefined
|
|
739
|
+
if (!row || row.processedAt !== null || row.failedAt === null) {
|
|
740
|
+
return { redriven: false, hasPending: hasPending(sessionId) }
|
|
741
|
+
}
|
|
742
|
+
if (action === 'skip') {
|
|
743
|
+
row.processedAt = clock.now()
|
|
744
|
+
settle(sessionId, row)
|
|
745
|
+
} else {
|
|
746
|
+
row.failedAt = null
|
|
747
|
+
row.failureCount = 0
|
|
748
|
+
const state = dispatchOf(sessionId)
|
|
749
|
+
const queue = row.lane === null ? undefined : state.lanes.get(row.lane)
|
|
750
|
+
if (row.lane === null || queue?.rows[queue.head] === row) {
|
|
751
|
+
state.ready.set(row.index, row)
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return { redriven: true, hasPending: hasPending(sessionId) }
|
|
755
|
+
},
|
|
756
|
+
|
|
722
757
|
async readState(sessionId, reducerName, stateOptions) {
|
|
723
758
|
return readStateOf(
|
|
724
759
|
sessionId,
|
package/src/store-postgres.ts
CHANGED
|
@@ -1364,6 +1364,86 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1364
1364
|
)
|
|
1365
1365
|
},
|
|
1366
1366
|
|
|
1367
|
+
async deadLetters(sessionId) {
|
|
1368
|
+
return wrap(async () => {
|
|
1369
|
+
const c = await client()
|
|
1370
|
+
const { rows } = await c.query(
|
|
1371
|
+
`select * from a2_events
|
|
1372
|
+
where session_id = $1
|
|
1373
|
+
and processed_at is null
|
|
1374
|
+
and failed_at is not null
|
|
1375
|
+
and idx > ${clearIndexOf('$1')}
|
|
1376
|
+
order by idx`,
|
|
1377
|
+
[sessionId],
|
|
1378
|
+
)
|
|
1379
|
+
return rows.map(toStored)
|
|
1380
|
+
})
|
|
1381
|
+
},
|
|
1382
|
+
|
|
1383
|
+
async redrive({ sessionId, index, action }) {
|
|
1384
|
+
return wrap(() =>
|
|
1385
|
+
withTx(async (query) => {
|
|
1386
|
+
// Lane promotion must see every append that committed before it,
|
|
1387
|
+
// so a skip takes the session lock first, in append's order. The
|
|
1388
|
+
// row lock serializes both actions with claims and handler appends.
|
|
1389
|
+
if (action === 'skip') {
|
|
1390
|
+
await query('select pg_advisory_xact_lock(hashtext($1))', [
|
|
1391
|
+
sessionId,
|
|
1392
|
+
])
|
|
1393
|
+
}
|
|
1394
|
+
const { rows } = await query(
|
|
1395
|
+
`select lane from a2_events
|
|
1396
|
+
where session_id = $1 and idx = $2
|
|
1397
|
+
and idx > ${clearIndexOf('$1')}
|
|
1398
|
+
and processed_at is null
|
|
1399
|
+
and failed_at is not null
|
|
1400
|
+
for update`,
|
|
1401
|
+
[sessionId, index],
|
|
1402
|
+
)
|
|
1403
|
+
const row = rows[0]
|
|
1404
|
+
if (row && action === 'retry') {
|
|
1405
|
+
await query(
|
|
1406
|
+
`update a2_events set failed_at = null, failure_count = 0
|
|
1407
|
+
where session_id = $1 and idx = $2`,
|
|
1408
|
+
[sessionId, index],
|
|
1409
|
+
)
|
|
1410
|
+
} else if (row) {
|
|
1411
|
+
await query(
|
|
1412
|
+
`update a2_events set processed_at = $3, lane_ready = false
|
|
1413
|
+
where session_id = $1 and idx = $2`,
|
|
1414
|
+
[sessionId, index, clock.now().getTime()],
|
|
1415
|
+
)
|
|
1416
|
+
const lane = (row['lane'] as string | null) ?? null
|
|
1417
|
+
if (lane !== null) {
|
|
1418
|
+
await query(
|
|
1419
|
+
`update a2_events set lane_ready = true
|
|
1420
|
+
where session_id = $1
|
|
1421
|
+
and idx = (
|
|
1422
|
+
select idx from a2_events
|
|
1423
|
+
where session_id = $1 and lane = $2 and processed_at is null
|
|
1424
|
+
and idx > ${clearIndexOf('$1')}
|
|
1425
|
+
order by idx limit 1
|
|
1426
|
+
)`,
|
|
1427
|
+
[sessionId, lane],
|
|
1428
|
+
)
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
const { rows: status } = await query(
|
|
1432
|
+
`select exists(
|
|
1433
|
+
select 1 from a2_events
|
|
1434
|
+
where session_id = $1 and processed_at is null
|
|
1435
|
+
and idx > ${clearIndexOf('$1')}
|
|
1436
|
+
) as has_pending`,
|
|
1437
|
+
[sessionId],
|
|
1438
|
+
)
|
|
1439
|
+
return {
|
|
1440
|
+
redriven: row !== undefined,
|
|
1441
|
+
hasPending: status[0]?.['has_pending'] === true,
|
|
1442
|
+
}
|
|
1443
|
+
}),
|
|
1444
|
+
)
|
|
1445
|
+
},
|
|
1446
|
+
|
|
1367
1447
|
async readState(sessionId, reducerName, stateOptions) {
|
|
1368
1448
|
return wrap(
|
|
1369
1449
|
async () =>
|