bazis 0.131.0 → 0.133.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/cli/templates/process.ts +1 -1
- package/core/infra/connectors/codex/CodexAppServerClient.ts +1 -1
- package/core/workflow/{runtime_module/config/Runtime.config.ts → contracts/Workflow.config.ts} +15 -6
- package/core/workflow/contracts/WorkflowExtensions.ts +40 -0
- package/core/workflow/contracts/WorkflowInputValidator.ts +6 -5
- package/core/workflow/definitions_module/WorkflowDefinitions.module.ts +16 -3
- package/core/workflow/definitions_module/access/Definitions.permissions.ts +4 -3
- package/core/workflow/definitions_module/access/Folder.references.ts +37 -0
- package/core/workflow/definitions_module/access/Process.references.ts +1 -1
- package/core/workflow/definitions_module/access/ProcessFolder.lookup.ts +20 -0
- package/core/workflow/definitions_module/code/CatalogImport.ts +95 -0
- package/core/workflow/definitions_module/code/CodeProcessPublisher.ts +27 -16
- package/core/workflow/definitions_module/code/codeProcesses.ts +7 -3
- package/core/workflow/definitions_module/code/defineProcess.ts +10 -4
- package/core/workflow/definitions_module/http/Processes.controller.ts +66 -23
- package/core/workflow/definitions_module/model/Definitions.dbContext.ts +2 -2
- package/core/workflow/definitions_module/model/WfProcess.model.ts +19 -5
- package/core/workflow/definitions_module/model/WfVersion.model.ts +1 -1
- package/core/workflow/definitions_module/services/IWorkflowDefinitions.service.ts +35 -10
- package/core/workflow/definitions_module/services/WorkflowDefinitions.service.ts +90 -28
- package/core/workflow/generated/deps.ts +15 -3
- package/core/workflow/index.ts +4 -2
- package/core/workflow/internal/WorkflowLanguages.ts +35 -0
- package/core/workflow/internal/canonicalJson.ts +8 -0
- package/core/workflow/internal/http.ts +20 -1
- package/core/workflow/runtime_module/WorkflowRuntime.module.ts +4 -2
- package/core/workflow/runtime_module/access/Runtime.permissions.ts +5 -3
- package/core/workflow/runtime_module/background/WorkflowRetention.ts +3 -3
- package/core/workflow/runtime_module/background/WorkflowWorker.ts +2 -2
- package/core/workflow/runtime_module/events/Runtime.events.ts +6 -6
- package/core/workflow/runtime_module/http/Instances.controller.ts +30 -13
- package/core/workflow/runtime_module/internal/ChildCalls.ts +186 -0
- package/core/workflow/runtime_module/internal/GraphCache.ts +7 -1
- package/core/workflow/runtime_module/internal/RunLimits.ts +124 -0
- package/core/workflow/runtime_module/internal/Schedules.ts +2 -2
- package/core/workflow/runtime_module/internal/TimerStep.ts +11 -1
- package/core/workflow/runtime_module/internal/ToolStep.ts +43 -12
- package/core/workflow/runtime_module/internal/WorkflowEngine.ts +559 -123
- package/core/workflow/runtime_module/internal/WorkflowJobs.ts +39 -10
- package/core/workflow/runtime_module/model/Runtime.dbContext.ts +2 -2
- package/core/workflow/runtime_module/model/WfHistory.model.ts +2 -2
- package/core/workflow/runtime_module/model/WfInstance.model.ts +34 -6
- package/core/workflow/runtime_module/model/WfJob.model.ts +29 -4
- package/core/workflow/runtime_module/model/WfSubscription.model.ts +1 -1
- package/core/workflow/runtime_module/model/WfToken.model.ts +15 -3
- package/core/workflow/runtime_module/services/IWorkflowRuntime.service.ts +43 -4
- package/core/workflow/runtime_module/services/WorkflowRuntime.service.ts +56 -13
- package/core/workflow/tasks_module/events/TaskEvents.ts +4 -8
- package/core/workflow/tasks_module/http/Tasks.controller.ts +14 -13
- package/core/workflow/tasks_module/model/Tasks.dbContext.ts +2 -2
- package/core/workflow/tasks_module/model/WfTask.model.ts +8 -4
- package/core/workflow/tasks_module/services/IWorkflowTasks.service.ts +3 -3
- package/core/workflow/tasks_module/services/WorkflowTasks.service.ts +4 -4
- package/library/openapi/codegen.ts +3 -0
- package/library/validation/types/ValidatorOptions.ts +2 -0
- package/library/workflow/ProcessDocument.ts +73 -23
- package/library/workflow/expressionSyntax.ts +243 -0
- package/library/workflow/expressions.ts +347 -0
- package/library/workflow/formFields.ts +118 -33
- package/library/workflow/index.ts +48 -9
- package/library/workflow/localized.ts +35 -0
- package/library/workflow/masking.ts +72 -0
- package/library/workflow/mermaid.ts +39 -13
- package/library/workflow/nodeKinds.ts +147 -17
- package/library/workflow/palette.ts +110 -0
- package/library/workflow/paths.ts +37 -6
- package/library/workflow/structure.ts +311 -0
- package/library/workflow/upgradeDocument.ts +84 -0
- package/library/workflow/validateProcess.ts +246 -52
- package/package.json +1 -1
- package/library/workflow/conditions.ts +0 -44
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { inputSpecOf, validateForm } from "../../../../library/workflow";
|
|
2
|
+
import { WorkflowError } from "../../contracts/WorkflowError";
|
|
3
|
+
import type { IWorkflowDefinitions, VersionDetails } from "../../definitions_module/services/IWorkflowDefinitions.service";
|
|
4
|
+
import type { RuntimeDbContext } from "../model/Runtime.dbContext";
|
|
5
|
+
import type { WfJob } from "../model/WfJob.model";
|
|
6
|
+
import type { WorkflowEngine } from "./WorkflowEngine";
|
|
7
|
+
|
|
8
|
+
/** How many waiting instances of a process one pass looks at. */
|
|
9
|
+
const RELEASE_BATCH = 1000;
|
|
10
|
+
|
|
11
|
+
/** What a whole run is limited by and what follows its failure: `runTimeout`, `maxConcurrent`, `sameKey: "queue"`, `errorHandler`. */
|
|
12
|
+
export class RunLimits {
|
|
13
|
+
constructor(
|
|
14
|
+
private readonly db: RuntimeDbContext,
|
|
15
|
+
private readonly engine: WorkflowEngine,
|
|
16
|
+
private readonly definitions: IWorkflowDefinitions,
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
/** The run's time is out. A `failed` instance waits for a person; its retry plans the job again. */
|
|
20
|
+
async runTimeout(job: WfJob): Promise<void> {
|
|
21
|
+
const done = job.instanceId === null ? null : await this.engine.inStep(job.instanceId, async (step) => {
|
|
22
|
+
await this.dropJob(job);
|
|
23
|
+
if (step.instance.status === "running") await this.engine.timeOut(step);
|
|
24
|
+
return true;
|
|
25
|
+
});
|
|
26
|
+
if (done === null) await this.dropJob(job);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A place may have come free: instances of the process that wait for one start, oldest first. */
|
|
30
|
+
async release(job: WfJob): Promise<void> {
|
|
31
|
+
await this.releaseProcess((job.payload as { processKey: string }).processKey);
|
|
32
|
+
await this.dropJob(job);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Every process with waiting instances: the safety net for what no ending instance tells, such as a new version without a limit. */
|
|
36
|
+
async releaseWaiting(): Promise<void> {
|
|
37
|
+
const rows = await this.db.instances.asNoTracking().where((i) => i.status.eq("pending")).select((i) => ({ processKey: i.processKey })).toList();
|
|
38
|
+
for (const key of new Set(rows.map((row) => row.processKey))) await this.releaseProcess(key);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private async releaseProcess(processKey: string): Promise<void> {
|
|
42
|
+
const waiting = await this.db.instances.asNoTracking().where((i) => i.processKey.eq(processKey).and(i.status.eq("pending"))).orderBy((i) => i.id).take(RELEASE_BATCH).toList();
|
|
43
|
+
if (waiting.length === 0) return;
|
|
44
|
+
// Graphs live in the Definitions store, out of reach inside the transaction; `inStep` finds them cached.
|
|
45
|
+
const graphs = new Map(await Promise.all(waiting.map(async (row) => [row.id, await this.engine.graphOf(row.versionId)] as const)));
|
|
46
|
+
await this.db.transactionScope(async () => {
|
|
47
|
+
await this.engine.lockProcess(processKey);
|
|
48
|
+
let running = await this.engine.runningOf(processKey);
|
|
49
|
+
for (const row of waiting) {
|
|
50
|
+
const { maxConcurrent, sameKey } = graphs.get(row.id)!.document.settings ?? {};
|
|
51
|
+
if (maxConcurrent !== undefined && running >= maxConcurrent) continue;
|
|
52
|
+
if (sameKey === "queue" && row.businessKey !== null && await this.keyTaken(processKey, row.businessKey, row.id)) continue;
|
|
53
|
+
const released = await this.engine.inStep(row.id, async (step) => {
|
|
54
|
+
if (step.instance.status !== "pending") return false;
|
|
55
|
+
await this.engine.release(step);
|
|
56
|
+
return true;
|
|
57
|
+
});
|
|
58
|
+
if (released === true) running += 1;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Whether an instance older than this one still works on the key: it goes first. */
|
|
64
|
+
private async keyTaken(processKey: string, businessKey: string, id: string): Promise<boolean> {
|
|
65
|
+
const same = await this.db.instances.asNoTracking()
|
|
66
|
+
.where((i) => i.processKey.eq(processKey).and(i.businessKey.eq(businessKey)).and(i.status.in(["pending", "running", "failed"])).and(i.id.lt(id)))
|
|
67
|
+
.count();
|
|
68
|
+
return same > 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** An `errorHandler` job: the handler starts with `$event` telling what happened. What keeps it from starting goes to the history of the instance that failed. */
|
|
72
|
+
async errorHandler(job: WfJob): Promise<void> {
|
|
73
|
+
const failed = job.instanceId === null ? undefined : await this.db.instances.asNoTracking().firstOrDefault((i) => i.id.eq(job.instanceId!));
|
|
74
|
+
if (!failed) {
|
|
75
|
+
await this.dropJob(job);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const process = (job.payload as { process: string }).process;
|
|
79
|
+
const note = (data: Record<string, unknown>) => this.engine.inStep(failed.id, async (step) => {
|
|
80
|
+
await this.dropJob(job);
|
|
81
|
+
this.engine.history(step, null, null, "handler", { process, ...data });
|
|
82
|
+
});
|
|
83
|
+
let version: VersionDetails;
|
|
84
|
+
try {
|
|
85
|
+
version = await this.definitions.latest(process);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (!(error instanceof WorkflowError)) throw error;
|
|
88
|
+
await note({ error: error.message });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const start = Object.entries(version.document.nodes).find(([, node]) => node.kind === "manualStart");
|
|
92
|
+
if (!start) {
|
|
93
|
+
await note({ error: "the process has no manual start" });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const { value, issues } = validateForm(inputSpecOf(version.document), {});
|
|
97
|
+
if (issues.length > 0) {
|
|
98
|
+
await note({ error: "the process needs input", issues });
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const graph = this.engine.cache(version);
|
|
102
|
+
// In one transaction with the job, so that a repeat of the job does not start a second handler.
|
|
103
|
+
await this.engine.inStep(failed.id, async (step) => {
|
|
104
|
+
await this.dropJob(job);
|
|
105
|
+
const { id } = await this.engine.begin(graph, start[0], {
|
|
106
|
+
input: value,
|
|
107
|
+
partition: failed.partition,
|
|
108
|
+
businessKey: null,
|
|
109
|
+
startedVia: "errorHandler",
|
|
110
|
+
actor: { kind: "system", id: `process:${failed.processKey}` },
|
|
111
|
+
event: {
|
|
112
|
+
key: "workflow.instance-stopped",
|
|
113
|
+
payload: { instanceId: failed.id, processKey: failed.processKey, version: failed.version, partition: failed.partition, businessKey: failed.businessKey, status: failed.status, node: failed.error?.node ?? null, code: failed.error?.code ?? null, message: failed.error?.message ?? null },
|
|
114
|
+
},
|
|
115
|
+
cause: { instanceId: failed.id, processKey: failed.processKey },
|
|
116
|
+
});
|
|
117
|
+
this.engine.history(step, null, null, "handler", { process, instanceId: id });
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private async dropJob(job: WfJob): Promise<void> {
|
|
122
|
+
await this.db.jobs.asNoTracking().where((j) => j.id.eq(job.id)).executeDelete();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parseCron } from "../../../../library/workflow";
|
|
1
|
+
import { inputSpecOf, parseCron, validateForm } from "../../../../library/workflow";
|
|
2
2
|
import type { IWorkflowDefinitions, VersionDetails } from "../../definitions_module/services/IWorkflowDefinitions.service";
|
|
3
3
|
import type { RuntimeDbContext } from "../model/Runtime.dbContext";
|
|
4
4
|
import { WfJob } from "../model/WfJob.model";
|
|
@@ -65,7 +65,7 @@ export class Schedules {
|
|
|
65
65
|
else await this.db.jobs.asNoTracking().where((j) => j.id.eq(job.id)).executeUpdate({ dueAt: next, lockedUntil: null, attempts: 0, lastError: null });
|
|
66
66
|
for (const [nodeId, cron] of scheduleNodes(latest)) {
|
|
67
67
|
if (!cron.matches(due)) continue;
|
|
68
|
-
await this.engine.begin(graph, nodeId, { input: {}, partition: null, businessKey: null, startedVia: "schedule", actor: SCHEDULE_ACTOR });
|
|
68
|
+
await this.engine.begin(graph, nodeId, { input: validateForm(inputSpecOf(latest.document), {}).value, partition: null, businessKey: null, startedVia: "schedule", actor: SCHEDULE_ACTOR });
|
|
69
69
|
}
|
|
70
70
|
});
|
|
71
71
|
}
|
|
@@ -2,7 +2,7 @@ import type { RuntimeDbContext } from "../model/Runtime.dbContext";
|
|
|
2
2
|
import type { WfJob } from "../model/WfJob.model";
|
|
3
3
|
import type { WorkflowEngine } from "./WorkflowEngine";
|
|
4
4
|
|
|
5
|
-
/** One `timer` job: a `timerCatch` step or a boundary timer is due. A closed token drops it. */
|
|
5
|
+
/** One `timer` job: a `timerCatch` step or a boundary timer is due. A closed token drops it. A `loop` job works the same way. */
|
|
6
6
|
export class TimerStep {
|
|
7
7
|
constructor(
|
|
8
8
|
private readonly db: RuntimeDbContext,
|
|
@@ -18,4 +18,14 @@ export class TimerStep {
|
|
|
18
18
|
});
|
|
19
19
|
if (fired === null) await this.db.jobs.asNoTracking().where((j) => j.id.eq(job.id)).executeDelete();
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
async continueLoop(job: WfJob): Promise<void> {
|
|
23
|
+
const went = job.instanceId === null || job.tokenId === null
|
|
24
|
+
? null
|
|
25
|
+
: await this.engine.inStep(job.instanceId, async (step) => {
|
|
26
|
+
await this.db.jobs.asNoTracking().where((j) => j.id.eq(job.id)).executeDelete();
|
|
27
|
+
return await this.engine.continueLoop(step, job.tokenId!);
|
|
28
|
+
});
|
|
29
|
+
if (went === null) await this.db.jobs.asNoTracking().where((j) => j.id.eq(job.id)).executeDelete();
|
|
30
|
+
}
|
|
21
31
|
}
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import type { AgentToolResult } from "../../../agent";
|
|
2
|
-
import { durationMs, resolveInput } from "../../../../library/workflow";
|
|
1
|
+
import type { AgentSchemaContract, AgentToolResult } from "../../../agent";
|
|
2
|
+
import { durationMs, maskByMapping, maskBySchema, masksPersonal, personalPaths, resolveInput, type ProcessDocument } from "../../../../library/workflow";
|
|
3
3
|
import type { WorkflowToolMetadata } from "../../contracts/types";
|
|
4
4
|
import type { WorkflowTools } from "../../internal/WorkflowTools";
|
|
5
5
|
import type { RuntimeDbContext } from "../model/Runtime.dbContext";
|
|
6
6
|
import type { WfJob } from "../model/WfJob.model";
|
|
7
|
-
import { dataOf, type WorkflowEngine } from "./WorkflowEngine";
|
|
7
|
+
import { dataOf, runActorOf, type WorkflowEngine } from "./WorkflowEngine";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* One `tool` job: call the service task's Tool and apply the result. The Tool
|
|
11
11
|
* runs outside a transaction, the result is applied in a step transaction; a
|
|
12
12
|
* token closed meanwhile (a cancel) drops it. The Tool runs on behalf of who
|
|
13
|
-
* started the instance, with the token id as its idempotency key: after a crash
|
|
13
|
+
* started the instance or of the process (`runAs`), with the token id as its idempotency key: after a crash
|
|
14
14
|
* between the call and the step the call may repeat (TOOLS-001 §7).
|
|
15
15
|
*/
|
|
16
|
+
/** How long a claimed job belongs to its worker; if it dies, another one takes the job after that. */
|
|
17
|
+
export const LEASE_MS = 5 * 60_000;
|
|
18
|
+
|
|
19
|
+
const TIMEOUT_CODES: ReadonlySet<string> = new Set(["TOOL_TIMEOUT", "TOOL_TIMEOUT_OUTCOME_UNKNOWN"]);
|
|
20
|
+
|
|
16
21
|
export class ToolStep {
|
|
17
22
|
constructor(
|
|
18
23
|
private readonly db: RuntimeDbContext,
|
|
@@ -30,21 +35,29 @@ export class ToolStep {
|
|
|
30
35
|
const graph = await this.engine.graphOf(instance.versionId);
|
|
31
36
|
const node = graph.document.nodes[token.nodeId];
|
|
32
37
|
const name = String(node?.config?.tool ?? "");
|
|
33
|
-
const input = resolveInput(node?.config?.input ?? {}, dataOf(instance));
|
|
38
|
+
const input = resolveInput(node?.config?.input ?? {}, dataOf(instance, this.engine.now(), token));
|
|
39
|
+
const timeoutMs = stepTimeoutMs(node?.config?.timeout, graph.document);
|
|
40
|
+
// A step may run longer than the lease: another worker must not take the job and call the Tool again meanwhile.
|
|
41
|
+
if (timeoutMs !== undefined && timeoutMs >= LEASE_MS) {
|
|
42
|
+
await this.db.jobs.asNoTracking().where((j) => j.id.eq(job.id)).executeUpdate({ lockedUntil: new Date(this.engine.now().getTime() + timeoutMs + LEASE_MS) });
|
|
43
|
+
}
|
|
34
44
|
const runner = this.tools.bound;
|
|
35
45
|
const result: AgentToolResult = runner === undefined
|
|
36
46
|
? { callId: token.id, name, status: "error", error: { code: "TOOL_NOT_FOUND", message: `Tool ${name} is not available to processes` }, metadata: {} } as AgentToolResult
|
|
37
47
|
: await runner.run(name, input, {
|
|
38
48
|
idempotencyKey: token.id,
|
|
39
|
-
actor:
|
|
49
|
+
actor: runActorOf(graph.document, instance),
|
|
40
50
|
metadata: { instanceId: instance.id, processKey: instance.processKey, nodeId: token.nodeId, partition: instance.partition, test: false } satisfies WorkflowToolMetadata,
|
|
41
51
|
signal,
|
|
52
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
42
53
|
});
|
|
43
54
|
|
|
44
|
-
const
|
|
55
|
+
const timedOut = TIMEOUT_CODES.has(result.error?.code ?? "");
|
|
56
|
+
const reason = timedOut ? "timeout" : reasonOf(result);
|
|
57
|
+
const payloads = graph.document.settings?.log?.toolPayloads === true ? this.payloads(graph.document, name, node?.config?.input, input, result) : {};
|
|
45
58
|
await this.engine.inStep(instance.id, async (step) => {
|
|
46
59
|
const current = step.tokens.find((t) => t.id === token.id);
|
|
47
|
-
const retryAt = current?.status === "waiting" && step.instance.status === "running" && result.status === "error" && reason === undefined
|
|
60
|
+
const retryAt = current?.status === "waiting" && step.instance.status === "running" && result.status === "error" && (reason === undefined || timedOut)
|
|
48
61
|
? nextAttempt(node?.config?.retry, job.attempts, this.engine.now())
|
|
49
62
|
: undefined;
|
|
50
63
|
if (retryAt !== undefined) {
|
|
@@ -59,6 +72,7 @@ export class ToolStep {
|
|
|
59
72
|
status: result.status,
|
|
60
73
|
attempt: job.attempts,
|
|
61
74
|
durationMs: result.durationMs ?? null,
|
|
75
|
+
...payloads,
|
|
62
76
|
...(result.error ? { error: { code: result.error.code ?? null, message: result.error.message, ...(reason === undefined ? {} : { reason }) } } : {}),
|
|
63
77
|
...(retryAt === undefined ? {} : { retryAt: retryAt.toISOString() }),
|
|
64
78
|
});
|
|
@@ -74,6 +88,15 @@ export class ToolStep {
|
|
|
74
88
|
}
|
|
75
89
|
});
|
|
76
90
|
}
|
|
91
|
+
|
|
92
|
+
/** The input and output for the history under `log.toolPayloads`; personal data is masked unless `log.maskPersonal` is `false`. */
|
|
93
|
+
private payloads(document: ProcessDocument, name: string, mapping: unknown, input: unknown, result: AgentToolResult): Record<string, unknown> {
|
|
94
|
+
if (!masksPersonal(document)) return { input, output: result.output ?? null };
|
|
95
|
+
const contract = this.tools.bound?.describe(name);
|
|
96
|
+
const schemaOf = (side: AgentSchemaContract | undefined) => (side?.kind === "json-schema" ? side.schema : undefined);
|
|
97
|
+
const masked = maskByMapping(mapping ?? {}, maskBySchema(input, schemaOf(contract?.input)), personalPaths(document));
|
|
98
|
+
return { input: masked, output: maskBySchema(result.output ?? null, schemaOf(contract?.output)) };
|
|
99
|
+
}
|
|
77
100
|
}
|
|
78
101
|
|
|
79
102
|
/** The business reason of a `ToolError`, if the Tool gave one. */
|
|
@@ -83,15 +106,23 @@ function reasonOf(result: AgentToolResult): string | undefined {
|
|
|
83
106
|
return typeof reason === "string" ? reason : undefined;
|
|
84
107
|
}
|
|
85
108
|
|
|
109
|
+
/** How long a step's Tool may run: its `timeout`, else `settings.stepTimeout`; `undefined` leaves the Tool's own limit. */
|
|
110
|
+
function stepTimeoutMs(timeout: unknown, document: ProcessDocument): number | undefined {
|
|
111
|
+
const value = typeof timeout === "string" ? timeout : document.settings?.stepTimeout;
|
|
112
|
+
return value === undefined ? undefined : durationMs(value);
|
|
113
|
+
}
|
|
114
|
+
|
|
86
115
|
/**
|
|
87
116
|
* When to try a failed call again under the node's `retry` policy, or
|
|
88
|
-
* `undefined` when the attempts are used up. The pause
|
|
89
|
-
*
|
|
117
|
+
* `undefined` when the attempts are used up. The pause starts at `backoff` (10
|
|
118
|
+
* seconds by default), grows by `factor` (2 by default) unless `growth` is
|
|
119
|
+
* `fixed`, and stops growing at a day.
|
|
90
120
|
*/
|
|
91
121
|
export function nextAttempt(policy: unknown, attempts: number, now: Date): Date | undefined {
|
|
92
122
|
if (typeof policy !== "object" || policy === null) return undefined;
|
|
93
|
-
const { maxAttempts, backoff } = policy as { maxAttempts?: unknown; backoff?: unknown };
|
|
123
|
+
const { maxAttempts, backoff, growth, factor } = policy as { maxAttempts?: unknown; backoff?: unknown; growth?: unknown; factor?: unknown };
|
|
94
124
|
if (typeof maxAttempts !== "number" || attempts >= maxAttempts) return undefined;
|
|
95
125
|
const base = typeof backoff === "string" ? durationMs(backoff) ?? 10_000 : 10_000;
|
|
96
|
-
|
|
126
|
+
const scale = growth === "fixed" ? 1 : (typeof factor === "number" ? factor : 2) ** Math.max(attempts - 1, 0);
|
|
127
|
+
return new Date(now.getTime() + Math.min(base * scale, 86_400_000));
|
|
97
128
|
}
|