experimental-a2 0.17.0 → 0.18.1
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 +32 -0
- package/README.md +1 -1
- package/dist/{actor-DBbe5xeE.d.ts → actor-C7b-1arn.d.ts} +5 -5
- package/dist/{actor-DBbe5xeE.d.ts.map → actor-C7b-1arn.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-vpGtcaCB.d.ts → ai-BMLlI2Ff.d.ts} +3 -3
- package/dist/{ai-vpGtcaCB.d.ts.map → ai-BMLlI2Ff.d.ts.map} +1 -1
- package/dist/{ai-UrWfKeuv.js → ai-D6mGlO8j.js} +2 -2
- package/dist/{ai-UrWfKeuv.js.map → ai-D6mGlO8j.js.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 +692 -0
- package/dist/ai-fx.js.map +1 -0
- package/dist/ai-harness-B2suP2Qh.d.ts +26 -0
- package/dist/ai-harness-B2suP2Qh.d.ts.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-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 +950 -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 +113 -40
- package/dist/ai-server.js.map +1 -1
- package/dist/ai-tool-input-DmSqMUQi.js +40 -0
- package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
- 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-BhHLoNKF.js} +3 -3
- package/dist/{client-COaiaDub.js.map → client-BhHLoNKF.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-CscSY7K_.js} +11 -3
- package/dist/internal-CscSY7K_.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 +3 -3
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +7 -7
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +9 -10
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Rj64fNvX.js → server-D-Y4pqwu.js} +126 -67
- package/dist/server-D-Y4pqwu.js.map +1 -0
- package/dist/{server-CGM8WzRG.d.ts → server-DYAATFuT.d.ts} +42 -7
- package/dist/server-DYAATFuT.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-C-QwTXV1.d.ts} +2 -2
- package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
- package/dist/{store-Gpkq7WBo.d.ts → store-CyLUy86v.d.ts} +53 -5
- package/dist/store-CyLUy86v.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 +56 -11
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-4_y8lETm.js → store-polling-7WJV0YWP.js} +2 -2
- package/dist/{store-polling-4_y8lETm.js.map → store-polling-7WJV0YWP.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 +106 -25
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-B1ixeqIS.js} +3 -3
- package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-B1ixeqIS.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-CM0AfLY0.js} +271 -101
- package/dist/store-redis-notify-CM0AfLY0.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 +66 -21
- 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-BKxH1vbP.js} +2 -2
- package/dist/{wire-DjfHOvdW.js.map → wire-BKxH1vbP.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 +109 -145
- package/docs/reference/01-core.mdx +139 -0
- package/docs/reference/02-server.mdx +860 -0
- package/docs/reference/03-client.mdx +301 -0
- package/docs/reference/04-ai.mdx +731 -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 +142 -0
- package/docs/server/04-routes.mdx +243 -0
- package/docs/server/05-production.mdx +323 -0
- package/docs/server/06-guarantees.mdx +132 -0
- package/examples/playground/DEPLOY.md +3 -5
- package/examples/playground/app/a0/[appId]/page.tsx +12 -13
- 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/devtools.tsx +2 -8
- 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 +98 -54
- 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 +271 -90
- package/src/ai-fx.browser.ts +5 -0
- package/src/ai-fx.ts +912 -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 +1322 -0
- package/src/ai-sdk-step.ts +2 -2
- package/src/ai-server.ts +3 -1
- package/src/ai-tool-input.ts +45 -0
- package/src/internal.ts +13 -0
- package/src/libfx.d.ts +34 -0
- package/src/reducer.ts +1 -1
- package/src/scheduler-qstash.ts +5 -5
- package/src/scheduler-vercel.ts +14 -15
- package/src/server.ts +233 -29
- package/src/store-memory.ts +92 -16
- package/src/store-postgres.ts +156 -25
- package/src/store-redis-core.ts +322 -110
- package/src/store-redis-notify.ts +2 -0
- package/src/store-sqlite.ts +121 -28
- package/src/store.ts +55 -3
- 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'
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
asSchema,
|
|
3
|
+
InvalidToolInputError,
|
|
4
|
+
type ToolSet,
|
|
5
|
+
TypeValidationError,
|
|
6
|
+
} from 'ai'
|
|
7
|
+
|
|
8
|
+
export type SchemaInput =
|
|
9
|
+
{ success: true; value: unknown } | { success: false; errorText: string }
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A call's input through its tool's schema, as the AI SDK step reads it: the
|
|
13
|
+
* schema's output with its defaults, or the AI SDK's error text. A name the
|
|
14
|
+
* tool set does not own has no schema.
|
|
15
|
+
*/
|
|
16
|
+
export async function schemaInput({
|
|
17
|
+
tools,
|
|
18
|
+
toolName,
|
|
19
|
+
value,
|
|
20
|
+
}: {
|
|
21
|
+
tools: ToolSet
|
|
22
|
+
toolName: string
|
|
23
|
+
value: unknown
|
|
24
|
+
}): Promise<SchemaInput> {
|
|
25
|
+
const schema = Object.hasOwn(tools, toolName)
|
|
26
|
+
? asSchema(tools[toolName]!.inputSchema)
|
|
27
|
+
: undefined
|
|
28
|
+
if (schema?.validate === undefined) return { success: true, value }
|
|
29
|
+
let cause: unknown
|
|
30
|
+
try {
|
|
31
|
+
const result = await schema.validate(value)
|
|
32
|
+
if (result.success) return { success: true, value: result.value }
|
|
33
|
+
cause = result.error
|
|
34
|
+
} catch (error) {
|
|
35
|
+
cause = error
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
errorText: new InvalidToolInputError({
|
|
40
|
+
toolName,
|
|
41
|
+
toolInput: JSON.stringify(value),
|
|
42
|
+
cause: TypeValidationError.wrap({ value, cause }),
|
|
43
|
+
}).message,
|
|
44
|
+
}
|
|
45
|
+
}
|
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
|
|
|
@@ -267,12 +274,18 @@ export const DRAIN_TIMINGS: {
|
|
|
267
274
|
watchdogGraceMs: number
|
|
268
275
|
/** Maximum time a top-level append waits for its initial scheduler arm. */
|
|
269
276
|
schedulerArmTimeoutMs: number
|
|
277
|
+
/** First retry delay after a caught failure, doubling per failure. */
|
|
278
|
+
failureRetryBaseMs: number
|
|
279
|
+
/** Ceiling of the caught-failure retry delay. */
|
|
280
|
+
failureRetryMaxMs: number
|
|
270
281
|
} = {
|
|
271
282
|
claimTtlMs: 5_000,
|
|
272
283
|
claimHeartbeatMs: 2_000,
|
|
273
284
|
renewRetryMs: 400,
|
|
274
285
|
watchdogGraceMs: 1_000,
|
|
275
286
|
schedulerArmTimeoutMs: 2_000,
|
|
287
|
+
failureRetryBaseMs: 1_000,
|
|
288
|
+
failureRetryMaxMs: 30_000,
|
|
276
289
|
}
|
|
277
290
|
|
|
278
291
|
/**
|
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/scheduler-qstash.ts
CHANGED
|
@@ -912,7 +912,7 @@ export function qstash(options: QStashSchedulerOptions = {}): A2Scheduler {
|
|
|
912
912
|
}
|
|
913
913
|
if (result.settled) return response.ok()
|
|
914
914
|
|
|
915
|
-
if (result.outcome === 'busy') {
|
|
915
|
+
if (result.outcome === 'busy' || result.outcome === 'stalled') {
|
|
916
916
|
if (result.schedulerArm) await result.schedulerArm
|
|
917
917
|
else {
|
|
918
918
|
await sendTask({
|
|
@@ -926,10 +926,10 @@ export function qstash(options: QStashSchedulerOptions = {}): A2Scheduler {
|
|
|
926
926
|
return response.ok()
|
|
927
927
|
}
|
|
928
928
|
|
|
929
|
-
//
|
|
930
|
-
//
|
|
931
|
-
//
|
|
932
|
-
//
|
|
929
|
+
// A drain whose outcome is unknown hands its work to a fresh durable
|
|
930
|
+
// message: QStash's per-message retry default can be lower than A2's
|
|
931
|
+
// failure budget. If this publish fails, the throw keeps the current
|
|
932
|
+
// delivery retryable.
|
|
933
933
|
await sendTask({
|
|
934
934
|
version: 1,
|
|
935
935
|
kind: 'drain',
|
package/src/scheduler-vercel.ts
CHANGED
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
* the configured deployment scope for sending and receiving.
|
|
15
15
|
* - `handler(...servers)` is the delivery route: look up the server for
|
|
16
16
|
* the contract named in the message and drain it. Settled drains ack;
|
|
17
|
-
* contention
|
|
18
|
-
* the current message and redelivers with backoff.
|
|
17
|
+
* contention and handler failure arm a later watchdog before acking.
|
|
19
18
|
*
|
|
20
19
|
* `@vercel/queue` is an optional peer dependency, imported lazily. The
|
|
21
20
|
* `transport` option is the injection seam — tests run a real little
|
|
@@ -44,12 +43,12 @@ import {
|
|
|
44
43
|
} from './server.ts'
|
|
45
44
|
|
|
46
45
|
/**
|
|
47
|
-
* The "not settled yet" signal for a
|
|
48
|
-
* not a transport failure. The default transport classifies
|
|
49
|
-
* `@vercel/queue`'s `retry` option and reschedules the delivery quietly
|
|
46
|
+
* The "not settled yet" signal for a drain whose outcome is unknown — an
|
|
47
|
+
* expected state, not a transport failure. The default transport classifies
|
|
48
|
+
* it in `@vercel/queue`'s `retry` option and reschedules the delivery quietly
|
|
50
49
|
* (`{ afterSeconds }` → visibility change → 200 response, no error log)
|
|
51
|
-
* instead of letting it surface as a thrown 500. Busy
|
|
52
|
-
* fresh watchdog instead. Custom transports should treat this class the
|
|
50
|
+
* instead of letting it surface as a thrown 500. Busy and stalled deliveries
|
|
51
|
+
* use a fresh watchdog instead. Custom transports should treat this class the
|
|
53
52
|
* same way when their protocol has a polite reschedule.
|
|
54
53
|
*/
|
|
55
54
|
export class DrainUnsettled extends Error {}
|
|
@@ -354,11 +353,12 @@ export function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {
|
|
|
354
353
|
}
|
|
355
354
|
if (result.settled) return
|
|
356
355
|
|
|
357
|
-
if (result.outcome === 'busy') {
|
|
358
|
-
// The live owner
|
|
359
|
-
// Ensure a later slot here
|
|
360
|
-
// durable delivery. If the queue is down, the
|
|
361
|
-
// current message unacked for visibility-timeout
|
|
356
|
+
if (result.outcome === 'busy' || result.outcome === 'stalled') {
|
|
357
|
+
// The live owner arms its own claim windows, and the store holds
|
|
358
|
+
// a failed event until its retry time. Ensure a later slot here
|
|
359
|
+
// as well before relinquishing this durable delivery. If the queue is down, the
|
|
360
|
+
// throw leaves the current message unacked for visibility-timeout
|
|
361
|
+
// redelivery.
|
|
362
362
|
if (result.schedulerArm) await result.schedulerArm
|
|
363
363
|
else {
|
|
364
364
|
await sendTask({
|
|
@@ -372,9 +372,8 @@ export function vercelQueues(options: VercelQueuesOptions = {}): A2Scheduler {
|
|
|
372
372
|
return
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
-
//
|
|
376
|
-
// transport retry path.
|
|
377
|
-
// above, so retries stop there.
|
|
375
|
+
// A drain whose outcome is unknown keeps the current durable message
|
|
376
|
+
// and uses the transport retry path.
|
|
378
377
|
throw new DrainUnsettled(
|
|
379
378
|
`a2 scheduler: session '${task.sessionId}' of '${task.contract}' not settled yet (${result.outcome}) — rescheduled for redelivery`,
|
|
380
379
|
)
|
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,48 @@ 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
|
+
/**
|
|
209
|
+
* Failed attempts since the event was appended or last retried: caught
|
|
210
|
+
* handler failures, and attempts that ended before they finished.
|
|
211
|
+
*/
|
|
212
|
+
failureCount: number
|
|
213
|
+
/**
|
|
214
|
+
* The last error a handler threw, stringified; null when every failed
|
|
215
|
+
* attempt ended before it finished.
|
|
216
|
+
*/
|
|
217
|
+
lastError: string | null
|
|
218
|
+
failedAt: Date
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Dead-letter resolution; root sessions only. */
|
|
222
|
+
export type SessionDeadLetters<D extends EventDefs> = {
|
|
223
|
+
/** The session's dead-lettered events, oldest first. */
|
|
224
|
+
deadLetters(): Promise<DeadLetter<D>[]>
|
|
225
|
+
/**
|
|
226
|
+
* Atomically resolve one dead-lettered event, then wake the session like
|
|
227
|
+
* an append. Any other event is left alone and reports `redriven: false`.
|
|
228
|
+
*/
|
|
229
|
+
redrive(
|
|
230
|
+
index: number,
|
|
231
|
+
options: SessionRedriveOptions,
|
|
232
|
+
): Promise<SessionRedriveResult>
|
|
233
|
+
}
|
|
234
|
+
|
|
191
235
|
/**
|
|
192
236
|
* The presence members of a session — intersected in via
|
|
193
237
|
* `WithPresence`, so they exist exactly when the contract declares
|
|
@@ -293,7 +337,9 @@ export type A2Server<
|
|
|
293
337
|
(request: Request): Promise<Response>
|
|
294
338
|
(request: Request, options: ServerFetchOptions<D, P>): Promise<Response>
|
|
295
339
|
}
|
|
296
|
-
session(
|
|
340
|
+
session(
|
|
341
|
+
id: string,
|
|
342
|
+
): Session<D, SessionAppend<D>, P, PublicState<R>> & SessionDeadLetters<D>
|
|
297
343
|
/**
|
|
298
344
|
* Process every currently eligible event. `settled` means nothing
|
|
299
345
|
* actionable remains, including work blocked behind a dead letter.
|
|
@@ -459,6 +505,13 @@ export type ServerOptions<
|
|
|
459
505
|
|
|
460
506
|
const MAX_FAILURES = 10
|
|
461
507
|
|
|
508
|
+
/** How long an event waits after its `failureCount`th failed attempt. */
|
|
509
|
+
const failureRetryDelayMs = (failureCount: number): number =>
|
|
510
|
+
Math.min(
|
|
511
|
+
DRAIN_TIMINGS.failureRetryBaseMs * 2 ** Math.max(failureCount - 1, 0),
|
|
512
|
+
DRAIN_TIMINGS.failureRetryMaxMs,
|
|
513
|
+
)
|
|
514
|
+
|
|
462
515
|
/** Abort a lost attempt's handler and release its drain slot. */
|
|
463
516
|
function endSuperseded(
|
|
464
517
|
entry: {
|
|
@@ -487,6 +540,7 @@ type ClaimWindow = {
|
|
|
487
540
|
expiresAtMs: number
|
|
488
541
|
watchdogAtMs: number
|
|
489
542
|
deadlineCapped: boolean
|
|
543
|
+
pastDeadline: boolean
|
|
490
544
|
}
|
|
491
545
|
|
|
492
546
|
type DrainSignal = {
|
|
@@ -561,9 +615,11 @@ function pruneAbortHub(hub: AbortHub): void {
|
|
|
561
615
|
}
|
|
562
616
|
|
|
563
617
|
/**
|
|
564
|
-
* Deadlines tighten the claim/watchdog window
|
|
565
|
-
*
|
|
566
|
-
*
|
|
618
|
+
* Deadlines tighten the claim/watchdog window, and a passed deadline ends
|
|
619
|
+
* claiming: a claim then could not run, only lapse and be claimed again until
|
|
620
|
+
* the platform ends the process. A platform timeout otherwise follows the
|
|
621
|
+
* same path as any other process death: the in-flight event remains pending
|
|
622
|
+
* and a fresh invocation retries it.
|
|
567
623
|
*/
|
|
568
624
|
function nextClaimWindow(): ClaimWindow {
|
|
569
625
|
const nowMs = Date.now()
|
|
@@ -579,6 +635,7 @@ function nextClaimWindow(): ClaimWindow {
|
|
|
579
635
|
expiresAtMs,
|
|
580
636
|
watchdogAtMs: expiresAtMs + DRAIN_TIMINGS.watchdogGraceMs,
|
|
581
637
|
deadlineCapped,
|
|
638
|
+
pastDeadline: deadlineMs !== null && deadlineTtlMs <= 0,
|
|
582
639
|
}
|
|
583
640
|
}
|
|
584
641
|
|
|
@@ -603,6 +660,27 @@ const devDefaultStore = retryableLazy(() =>
|
|
|
603
660
|
import('./store-sqlite.ts').then((m) => m.sqlite()),
|
|
604
661
|
)
|
|
605
662
|
|
|
663
|
+
/** Waits for a scheduler arm for at most `schedulerArmTimeoutMs`. */
|
|
664
|
+
async function joinSchedulerArm(arm: Promise<void>): Promise<void> {
|
|
665
|
+
let timeout: ReturnType<typeof setTimeout> | null = null
|
|
666
|
+
try {
|
|
667
|
+
await Promise.race([
|
|
668
|
+
arm,
|
|
669
|
+
new Promise<never>((_, reject) => {
|
|
670
|
+
timeout = setTimeout(() => {
|
|
671
|
+
reject(
|
|
672
|
+
new Error(
|
|
673
|
+
`a2 scheduler arm timed out after ${DRAIN_TIMINGS.schedulerArmTimeoutMs}ms`,
|
|
674
|
+
),
|
|
675
|
+
)
|
|
676
|
+
}, DRAIN_TIMINGS.schedulerArmTimeoutMs)
|
|
677
|
+
}),
|
|
678
|
+
])
|
|
679
|
+
} finally {
|
|
680
|
+
if (timeout) clearTimeout(timeout)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
606
684
|
function environment(): 'development' | 'test' | 'production' {
|
|
607
685
|
const env =
|
|
608
686
|
typeof process === 'undefined' ? undefined : process.env?.['NODE_ENV']
|
|
@@ -1081,26 +1159,12 @@ export function createServer<
|
|
|
1081
1159
|
await dispatchArm
|
|
1082
1160
|
}
|
|
1083
1161
|
if (initialArm) {
|
|
1084
|
-
let timeout: ReturnType<typeof setTimeout> | null = null
|
|
1085
1162
|
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
|
-
])
|
|
1163
|
+
await joinSchedulerArm(initialArm)
|
|
1098
1164
|
} catch (err) {
|
|
1099
1165
|
span.setAttribute('a2.append.armed', false)
|
|
1100
1166
|
span.recordError(err)
|
|
1101
1167
|
if (requireDurableInitialArm) throw err
|
|
1102
|
-
} finally {
|
|
1103
|
-
if (timeout) clearTimeout(timeout)
|
|
1104
1168
|
}
|
|
1105
1169
|
}
|
|
1106
1170
|
return appended
|
|
@@ -2483,6 +2547,102 @@ export function createServer<
|
|
|
2483
2547
|
return { cleared: result.cleared, index: result.index }
|
|
2484
2548
|
}
|
|
2485
2549
|
|
|
2550
|
+
// ── dead letters ─────────────────────────────────────────────────
|
|
2551
|
+
|
|
2552
|
+
const redrivingStore = async (
|
|
2553
|
+
method: 'deadLetters' | 'redrive',
|
|
2554
|
+
): Promise<A2Store & Required<Pick<A2Store, 'deadLetters' | 'redrive'>>> => {
|
|
2555
|
+
const store = await resolveStore()
|
|
2556
|
+
if (!store.deadLetters || !store.redrive) {
|
|
2557
|
+
throw new TypeError(
|
|
2558
|
+
`session.${method} requires a store that supports redriving dead letters`,
|
|
2559
|
+
)
|
|
2560
|
+
}
|
|
2561
|
+
return store as A2Store & Required<Pick<A2Store, 'deadLetters' | 'redrive'>>
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
const listDeadLetters = async (
|
|
2565
|
+
sessionId: string,
|
|
2566
|
+
): Promise<DeadLetter<D>[]> => {
|
|
2567
|
+
const store = await redrivingStore('deadLetters')
|
|
2568
|
+
let rows: StoredEvent[]
|
|
2569
|
+
try {
|
|
2570
|
+
rows = await store.deadLetters(nsId(sessionId))
|
|
2571
|
+
} catch (err) {
|
|
2572
|
+
throw asStoreUnavailable(err)
|
|
2573
|
+
}
|
|
2574
|
+
return rows.flatMap((row) =>
|
|
2575
|
+
row.failedAt === null
|
|
2576
|
+
? []
|
|
2577
|
+
: [
|
|
2578
|
+
{
|
|
2579
|
+
...toPublic(row),
|
|
2580
|
+
lane: row.lane,
|
|
2581
|
+
attemptCount: row.attemptCount,
|
|
2582
|
+
failureCount: row.failureCount,
|
|
2583
|
+
lastError: row.lastError,
|
|
2584
|
+
failedAt: row.failedAt,
|
|
2585
|
+
},
|
|
2586
|
+
],
|
|
2587
|
+
)
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
const redriveEvent = async (
|
|
2591
|
+
sessionId: string,
|
|
2592
|
+
index: number,
|
|
2593
|
+
redriveOptions: SessionRedriveOptions,
|
|
2594
|
+
): Promise<SessionRedriveResult> => {
|
|
2595
|
+
assertRedriveIndex(index)
|
|
2596
|
+
const action = redriveOptions?.action
|
|
2597
|
+
if (action !== 'retry' && action !== 'skip') {
|
|
2598
|
+
throw new TypeError("redrive requires action 'retry' or 'skip'")
|
|
2599
|
+
}
|
|
2600
|
+
return telemetry.span(
|
|
2601
|
+
'a2.redrive',
|
|
2602
|
+
{
|
|
2603
|
+
'a2.contract': name,
|
|
2604
|
+
'a2.session_id': sessionId,
|
|
2605
|
+
'a2.redrive.index': index,
|
|
2606
|
+
'a2.redrive.action': action,
|
|
2607
|
+
},
|
|
2608
|
+
async (span) => {
|
|
2609
|
+
const store = await redrivingStore('redrive')
|
|
2610
|
+
let result: StoreRedriveResult
|
|
2611
|
+
try {
|
|
2612
|
+
result = await store.redrive({
|
|
2613
|
+
sessionId: nsId(sessionId),
|
|
2614
|
+
index,
|
|
2615
|
+
action,
|
|
2616
|
+
})
|
|
2617
|
+
} catch (err) {
|
|
2618
|
+
throw asStoreUnavailable(err)
|
|
2619
|
+
}
|
|
2620
|
+
span.setAttribute('a2.redrive.redriven', result.redriven)
|
|
2621
|
+
// Healing like a top-level append, even when nothing was redriven, so
|
|
2622
|
+
// repeating a redrive whose wakeup was lost resumes the session.
|
|
2623
|
+
if (result.hasPending) {
|
|
2624
|
+
const watchdogAt = scheduler
|
|
2625
|
+
? schedulerSlot(nextClaimWindow().watchdogAtMs)
|
|
2626
|
+
: undefined
|
|
2627
|
+
const arm =
|
|
2628
|
+
watchdogAt === undefined
|
|
2629
|
+
? null
|
|
2630
|
+
: startSchedulerArm(sessionId, watchdogAt)
|
|
2631
|
+
scheduleDrain(sessionId, watchdogAt)
|
|
2632
|
+
if (arm) {
|
|
2633
|
+
try {
|
|
2634
|
+
await joinSchedulerArm(arm)
|
|
2635
|
+
} catch (err) {
|
|
2636
|
+
span.setAttribute('a2.redrive.armed', false)
|
|
2637
|
+
span.recordError(err)
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
return { redriven: result.redriven }
|
|
2642
|
+
},
|
|
2643
|
+
)
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2486
2646
|
const makeSession = <Append>(
|
|
2487
2647
|
id: string,
|
|
2488
2648
|
append: Append,
|
|
@@ -2584,7 +2744,7 @@ export function createServer<
|
|
|
2584
2744
|
generatedIds,
|
|
2585
2745
|
)
|
|
2586
2746
|
}
|
|
2587
|
-
|
|
2747
|
+
const ctx: HandlerContext<D> = {
|
|
2588
2748
|
event: toPublic(trigger),
|
|
2589
2749
|
attempt,
|
|
2590
2750
|
// The handler-scoped `setPresence` stamps `seen` with the
|
|
@@ -2597,6 +2757,9 @@ export function createServer<
|
|
|
2597
2757
|
}),
|
|
2598
2758
|
signal,
|
|
2599
2759
|
}
|
|
2760
|
+
if (attempt > 1 && trigger.lastFailedAttempt === attempt - 1)
|
|
2761
|
+
failedPreviousAttempt.add(ctx)
|
|
2762
|
+
return ctx
|
|
2600
2763
|
}
|
|
2601
2764
|
|
|
2602
2765
|
const abortHubs = new Map<string, AbortHub>()
|
|
@@ -2922,6 +3085,25 @@ export function createServer<
|
|
|
2922
3085
|
return tracked
|
|
2923
3086
|
}
|
|
2924
3087
|
|
|
3088
|
+
/** Records an event the store dead-lettered on takeover; it never runs. */
|
|
3089
|
+
const reportExhausted = (event: StoredEvent): Promise<void> =>
|
|
3090
|
+
telemetry.span(
|
|
3091
|
+
'a2.event',
|
|
3092
|
+
{
|
|
3093
|
+
'a2.contract': name,
|
|
3094
|
+
'a2.session_id': sessionId,
|
|
3095
|
+
'a2.event.type': event.type,
|
|
3096
|
+
'a2.event.index': event.index,
|
|
3097
|
+
'a2.event.id': event.id,
|
|
3098
|
+
'a2.event.attempt': event.attemptCount,
|
|
3099
|
+
'a2.event.handled': handlers.has(event.type),
|
|
3100
|
+
...(event.lane === null ? {} : { 'a2.event.lane': event.lane }),
|
|
3101
|
+
},
|
|
3102
|
+
async (eventSpan) => {
|
|
3103
|
+
eventSpan.setAttribute('a2.event.outcome', 'dead_lettered')
|
|
3104
|
+
},
|
|
3105
|
+
)
|
|
3106
|
+
|
|
2925
3107
|
const runEvent = async (
|
|
2926
3108
|
event: StoredEvent,
|
|
2927
3109
|
supersedeSignal: AbortSignal,
|
|
@@ -2981,9 +3163,8 @@ export function createServer<
|
|
|
2981
3163
|
if (supersedeSignal.aborted && err === supersedeSignal.reason) {
|
|
2982
3164
|
// The handler surrendered to its own abort — identity, not
|
|
2983
3165
|
// code, so it cannot arrive from another context. A lapse
|
|
2984
|
-
// is not a handler failure: no completion
|
|
2985
|
-
//
|
|
2986
|
-
// retries.
|
|
3166
|
+
// is not a handler failure: no completion; the claim simply
|
|
3167
|
+
// expires, and the claim that takes it over counts it.
|
|
2987
3168
|
eventSpan.setAttribute('a2.event.outcome', 'surrendered')
|
|
2988
3169
|
return {
|
|
2989
3170
|
status: 'surrendered',
|
|
@@ -3016,6 +3197,7 @@ export function createServer<
|
|
|
3016
3197
|
// budget before dead-lettering.
|
|
3017
3198
|
maxFailures:
|
|
3018
3199
|
err instanceof NonRetriableError ? 1 : MAX_FAILURES,
|
|
3200
|
+
retryAfterMs: failureRetryDelayMs(event.failureCount + 1),
|
|
3019
3201
|
})
|
|
3020
3202
|
commitSpan.setAttribute('a2.commit.outcome', result.outcome)
|
|
3021
3203
|
return result
|
|
@@ -3147,6 +3329,10 @@ export function createServer<
|
|
|
3147
3329
|
let claim: StoreClaimAvailableResult | undefined
|
|
3148
3330
|
if (shouldClaim) {
|
|
3149
3331
|
const window = nextClaimWindow()
|
|
3332
|
+
if (window.pastDeadline) {
|
|
3333
|
+
outcome = 'busy'
|
|
3334
|
+
break
|
|
3335
|
+
}
|
|
3150
3336
|
claim = await telemetry.span(
|
|
3151
3337
|
'a2.claim',
|
|
3152
3338
|
{ 'a2.contract': name, 'a2.session_id': sessionId },
|
|
@@ -3155,6 +3341,7 @@ export function createServer<
|
|
|
3155
3341
|
sessionId: ns,
|
|
3156
3342
|
holder,
|
|
3157
3343
|
ttlMs: window.ttlMs,
|
|
3344
|
+
maxFailures: MAX_FAILURES,
|
|
3158
3345
|
...(window.deadlineCapped
|
|
3159
3346
|
? { expiresAtMs: window.expiresAtMs }
|
|
3160
3347
|
: {}),
|
|
@@ -3171,11 +3358,16 @@ export function createServer<
|
|
|
3171
3358
|
},
|
|
3172
3359
|
)
|
|
3173
3360
|
if (claim.outcome === 'claimed') {
|
|
3174
|
-
|
|
3361
|
+
// A takeover that spent the last failed attempt comes back
|
|
3362
|
+
// dead-lettered instead of claimed.
|
|
3363
|
+
const runnable = claim.events.filter(
|
|
3364
|
+
(event) => event.failedAt === null,
|
|
3365
|
+
)
|
|
3366
|
+
for (const event of claim.events)
|
|
3367
|
+
if (event.failedAt !== null) await reportExhausted(event)
|
|
3368
|
+
if (runnable.length > 0) requestWatchdog(window.watchdogAtMs)
|
|
3175
3369
|
if (window.deadlineCapped) stopRenewing = true
|
|
3176
|
-
for (const event of
|
|
3177
|
-
start(event, window.expiresAtMs)
|
|
3178
|
-
}
|
|
3370
|
+
for (const event of runnable) start(event, window.expiresAtMs)
|
|
3179
3371
|
shouldClaim = false
|
|
3180
3372
|
}
|
|
3181
3373
|
}
|
|
@@ -3276,7 +3468,9 @@ export function createServer<
|
|
|
3276
3468
|
)
|
|
3277
3469
|
}
|
|
3278
3470
|
|
|
3279
|
-
const session = (
|
|
3471
|
+
const session = (
|
|
3472
|
+
id: string,
|
|
3473
|
+
): Session<D, SessionAppend<D>, P> & SessionDeadLetters<D> => {
|
|
3280
3474
|
assertSessionId(id)
|
|
3281
3475
|
const appendInline = async (...events: AppendInput<D>[]) =>
|
|
3282
3476
|
appendExternal(id, 'inline', events)
|
|
@@ -3285,7 +3479,11 @@ export function createServer<
|
|
|
3285
3479
|
const append = Object.assign(appendInline, {
|
|
3286
3480
|
dispatch: appendDispatch,
|
|
3287
3481
|
}) as SessionAppend<D>
|
|
3288
|
-
return makeSession(id, append, null)
|
|
3482
|
+
return Object.assign(makeSession(id, append, null), {
|
|
3483
|
+
deadLetters: () => listDeadLetters(id),
|
|
3484
|
+
redrive: (index: number, redriveOptions: SessionRedriveOptions) =>
|
|
3485
|
+
redriveEvent(id, index, redriveOptions),
|
|
3486
|
+
})
|
|
3289
3487
|
}
|
|
3290
3488
|
|
|
3291
3489
|
const historyPage = async (
|
|
@@ -3393,6 +3591,12 @@ function assertStateIndex(value: number): void {
|
|
|
3393
3591
|
}
|
|
3394
3592
|
}
|
|
3395
3593
|
|
|
3594
|
+
function assertRedriveIndex(value: number): void {
|
|
3595
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
3596
|
+
throw new TypeError('redrive requires a positive safe-integer index')
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3396
3600
|
function assertClearIndex(value: number): void {
|
|
3397
3601
|
if (!Number.isSafeInteger(value) || value < 0) {
|
|
3398
3602
|
throw new TypeError('ifLastIndex must be a non-negative safe integer')
|