bunqueue 2.8.48 → 2.8.50
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/README.md +19 -9
- package/dist/application/contextFactory.d.ts +4 -2
- package/dist/application/contextFactory.js +3 -0
- package/dist/application/flowFailureRecovery.d.ts +17 -0
- package/dist/application/flowFailureRecovery.js +92 -0
- package/dist/application/operations/ack.d.ts +3 -2
- package/dist/application/operations/ack.js +34 -8
- package/dist/application/operations/flowPush.d.ts +9 -0
- package/dist/application/operations/flowPush.js +112 -0
- package/dist/application/operations/flowTopologyValidation.d.ts +3 -0
- package/dist/application/operations/flowTopologyValidation.js +101 -0
- package/dist/application/operations/flowValidation.d.ts +3 -0
- package/dist/application/operations/flowValidation.js +166 -0
- package/dist/application/operations/push.d.ts +3 -0
- package/dist/application/operations/push.js +2 -2
- package/dist/application/operations/pushInsert.d.ts +7 -1
- package/dist/application/operations/pushInsert.js +18 -8
- package/dist/application/operations/pushLocks.d.ts +1 -0
- package/dist/application/operations/pushLocks.js +5 -1
- package/dist/application/operations/queryOperations.js +7 -6
- package/dist/application/queueManager.d.ts +5 -0
- package/dist/application/queueManager.js +195 -60
- package/dist/cli/commandRouter.d.ts +1 -1
- package/dist/client/flow.d.ts +3 -6
- package/dist/client/flow.js +71 -210
- package/dist/client/flowAtomic.d.ts +9 -0
- package/dist/client/flowAtomic.js +24 -0
- package/dist/client/flowJobCoreMethods.d.ts +28 -0
- package/dist/client/flowJobCoreMethods.js +151 -0
- package/dist/client/flowJobDependencies.d.ts +7 -0
- package/dist/client/flowJobDependencies.js +64 -0
- package/dist/client/flowJobFactory.d.ts +11 -24
- package/dist/client/flowJobFactory.js +91 -360
- package/dist/client/flowJobMoveMethods.d.ts +16 -0
- package/dist/client/flowJobMoveMethods.js +112 -0
- package/dist/client/flowJobTypes.d.ts +29 -0
- package/dist/client/flowJobTypes.js +6 -0
- package/dist/client/flowLegacyPlan.d.ts +16 -0
- package/dist/client/flowLegacyPlan.js +118 -0
- package/dist/client/flowOptions.d.ts +10 -0
- package/dist/client/flowOptions.js +44 -0
- package/dist/client/flowPlan.d.ts +16 -0
- package/dist/client/flowPlan.js +93 -0
- package/dist/client/flowPush.js +4 -1
- package/dist/client/flowReader.d.ts +10 -0
- package/dist/client/flowReader.js +147 -0
- package/dist/client/jobHelpers.js +2 -0
- package/dist/client/workflow/clock.d.ts +3 -1
- package/dist/client/workflow/clock.js +18 -1
- package/dist/client/workflow/compensationChild.d.ts +30 -0
- package/dist/client/workflow/compensationChild.js +65 -0
- package/dist/client/workflow/compensationPass.d.ts +16 -0
- package/dist/client/workflow/compensationPass.js +96 -0
- package/dist/client/workflow/compensationSupport.d.ts +24 -0
- package/dist/client/workflow/compensationSupport.js +57 -0
- package/dist/client/workflow/compensator.d.ts +11 -50
- package/dist/client/workflow/compensator.js +22 -374
- package/dist/client/workflow/definitionGuard.d.ts +11 -0
- package/dist/client/workflow/definitionGuard.js +22 -0
- package/dist/client/workflow/engine.d.ts +2 -2
- package/dist/client/workflow/engine.js +8 -8
- package/dist/client/workflow/eventTypes.d.ts +44 -0
- package/dist/client/workflow/eventTypes.js +2 -0
- package/dist/client/workflow/executionTypes.d.ts +133 -0
- package/dist/client/workflow/executionTypes.js +2 -0
- package/dist/client/workflow/executor.d.ts +4 -17
- package/dist/client/workflow/executor.js +50 -208
- package/dist/client/workflow/executorLifecycle.d.ts +18 -0
- package/dist/client/workflow/executorLifecycle.js +69 -0
- package/dist/client/workflow/executorNodes.d.ts +17 -0
- package/dist/client/workflow/executorNodes.js +138 -0
- package/dist/client/workflow/identity.d.ts +2 -0
- package/dist/client/workflow/identity.js +8 -0
- package/dist/client/workflow/index.d.ts +1 -1
- package/dist/client/workflow/loops.d.ts +3 -5
- package/dist/client/workflow/loops.js +25 -23
- package/dist/client/workflow/mapRunner.d.ts +4 -0
- package/dist/client/workflow/mapRunner.js +45 -0
- package/dist/client/workflow/recovery.js +10 -3
- package/dist/client/workflow/runner.d.ts +2 -21
- package/dist/client/workflow/runner.js +39 -104
- package/dist/client/workflow/runnerTiming.d.ts +14 -0
- package/dist/client/workflow/runnerTiming.js +66 -0
- package/dist/client/workflow/stepTypes.d.ts +171 -0
- package/dist/client/workflow/stepTypes.js +3 -0
- package/dist/client/workflow/store.d.ts +17 -17
- package/dist/client/workflow/store.js +65 -100
- package/dist/client/workflow/storeExecutionCodec.d.ts +11 -0
- package/dist/client/workflow/storeExecutionCodec.js +34 -0
- package/dist/client/workflow/storeListing.d.ts +11 -0
- package/dist/client/workflow/storeListing.js +45 -0
- package/dist/client/workflow/storeMaintenance.d.ts +4 -0
- package/dist/client/workflow/storeMaintenance.js +40 -0
- package/dist/client/workflow/storeSignals.d.ts +9 -0
- package/dist/client/workflow/storeSignals.js +38 -2
- package/dist/client/workflow/subWorkflowRunner.d.ts +13 -0
- package/dist/client/workflow/subWorkflowRunner.js +40 -0
- package/dist/client/workflow/types.d.ts +4 -356
- package/dist/client/workflow/types.js +1 -3
- package/dist/client/workflow/waitFor.js +39 -26
- package/dist/client/workflow/workflow.d.ts +16 -59
- package/dist/client/workflow/workflow.js +53 -179
- package/dist/client/workflow/workflowDecisions.d.ts +11 -0
- package/dist/client/workflow/workflowDecisions.js +27 -0
- package/dist/client/workflow/workflowDefinition.d.ts +16 -0
- package/dist/client/workflow/workflowDefinition.js +123 -0
- package/dist/client/workflow/workflowIntrospection.d.ts +5 -0
- package/dist/client/workflow/workflowIntrospection.js +46 -0
- package/dist/client/workflow/workflowValidation.d.ts +44 -0
- package/dist/client/workflow/workflowValidation.js +143 -0
- package/dist/domain/types/command.d.ts +7 -1
- package/dist/domain/types/flow.d.ts +25 -0
- package/dist/domain/types/flow.js +1 -0
- package/dist/infrastructure/persistence/schema.d.ts +2 -2
- package/dist/infrastructure/persistence/schema.js +52 -3
- package/dist/infrastructure/persistence/sqlite.d.ts +16 -0
- package/dist/infrastructure/persistence/sqlite.js +129 -7
- package/dist/infrastructure/persistence/sqliteBatch.js +10 -4
- package/dist/infrastructure/persistence/sqliteSerializer.d.ts +2 -0
- package/dist/infrastructure/persistence/sqliteSerializer.js +18 -5
- package/dist/infrastructure/persistence/statements.d.ts +4 -0
- package/dist/infrastructure/persistence/statements.js +8 -2
- package/dist/infrastructure/server/handlerRoutes.js +3 -0
- package/dist/infrastructure/server/handlers/advanced.js +2 -2
- package/dist/infrastructure/server/handlers/flow.d.ts +7 -0
- package/dist/infrastructure/server/handlers/flow.js +11 -0
- package/dist/infrastructure/server/handlers/index.d.ts +1 -0
- package/dist/infrastructure/server/handlers/index.js +1 -0
- package/package.json +4 -2
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable workflow-definition identity and sealing.
|
|
3
|
+
*
|
|
4
|
+
* Executions persist the hash produced here. It describes the durable graph, while
|
|
5
|
+
* `revision` lets callers explicitly mark handler/condition changes that cannot be
|
|
6
|
+
* inferred safely from closures.
|
|
7
|
+
*/
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
function stepShape(def) {
|
|
10
|
+
return {
|
|
11
|
+
name: def.name,
|
|
12
|
+
retry: def.retry,
|
|
13
|
+
timeout: def.timeout,
|
|
14
|
+
compensates: def.compensate !== undefined,
|
|
15
|
+
validatesInput: def.inputSchema !== undefined,
|
|
16
|
+
validatesOutput: def.outputSchema !== undefined,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function nodeShape(node) {
|
|
20
|
+
if (node.type === 'step')
|
|
21
|
+
return { type: node.type, step: stepShape(node.def) };
|
|
22
|
+
if (node.type === 'branch') {
|
|
23
|
+
return {
|
|
24
|
+
type: node.type,
|
|
25
|
+
paths: [...node.def.paths.entries()]
|
|
26
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
27
|
+
.map(([name, steps]) => [name, steps.map(stepShape)]),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (node.type === 'parallel') {
|
|
31
|
+
return { type: node.type, steps: node.def.steps.map(stepShape) };
|
|
32
|
+
}
|
|
33
|
+
if (node.type === 'subWorkflow') {
|
|
34
|
+
return {
|
|
35
|
+
type: node.type,
|
|
36
|
+
name: node.name,
|
|
37
|
+
timeout: node.timeout,
|
|
38
|
+
pollInterval: node.pollInterval,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (node.type === 'waitFor') {
|
|
42
|
+
return { type: node.type, event: node.event, timeout: node.timeout ?? null };
|
|
43
|
+
}
|
|
44
|
+
if (node.type === 'doUntil' || node.type === 'doWhile') {
|
|
45
|
+
return {
|
|
46
|
+
type: node.type,
|
|
47
|
+
maxIterations: node.def.maxIterations,
|
|
48
|
+
steps: node.def.steps.map(stepShape),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (node.type === 'forEach') {
|
|
52
|
+
return {
|
|
53
|
+
type: node.type,
|
|
54
|
+
maxIterations: node.def.maxIterations,
|
|
55
|
+
step: stepShape(node.def.step),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (node.type === 'map')
|
|
59
|
+
return { type: node.type, name: node.def.name };
|
|
60
|
+
return { type: 'pivot' };
|
|
61
|
+
}
|
|
62
|
+
export function workflowDefinitionHash(workflow) {
|
|
63
|
+
const definition = {
|
|
64
|
+
name: workflow.name,
|
|
65
|
+
revision: workflow.revision,
|
|
66
|
+
nodes: workflow.nodes.map(nodeShape),
|
|
67
|
+
};
|
|
68
|
+
return createHash('sha256').update(JSON.stringify(definition)).digest('hex');
|
|
69
|
+
}
|
|
70
|
+
function sealMap(map) {
|
|
71
|
+
const immutable = () => {
|
|
72
|
+
throw new Error('A registered workflow definition is sealed');
|
|
73
|
+
};
|
|
74
|
+
Object.defineProperties(map, {
|
|
75
|
+
set: { value: immutable },
|
|
76
|
+
delete: { value: immutable },
|
|
77
|
+
clear: { value: immutable },
|
|
78
|
+
});
|
|
79
|
+
Object.freeze(map);
|
|
80
|
+
}
|
|
81
|
+
function sealStep(def) {
|
|
82
|
+
Object.freeze(def);
|
|
83
|
+
}
|
|
84
|
+
function sealNode(node) {
|
|
85
|
+
if (node.type === 'step')
|
|
86
|
+
sealStep(node.def);
|
|
87
|
+
else if (node.type === 'branch') {
|
|
88
|
+
for (const steps of node.def.paths.values()) {
|
|
89
|
+
for (const step of steps)
|
|
90
|
+
sealStep(step);
|
|
91
|
+
Object.freeze(steps);
|
|
92
|
+
}
|
|
93
|
+
sealMap(node.def.paths);
|
|
94
|
+
Object.freeze(node.def);
|
|
95
|
+
}
|
|
96
|
+
else if (node.type === 'parallel') {
|
|
97
|
+
for (const step of node.def.steps)
|
|
98
|
+
sealStep(step);
|
|
99
|
+
Object.freeze(node.def.steps);
|
|
100
|
+
Object.freeze(node.def);
|
|
101
|
+
}
|
|
102
|
+
else if (node.type === 'doUntil' || node.type === 'doWhile') {
|
|
103
|
+
for (const step of node.def.steps)
|
|
104
|
+
sealStep(step);
|
|
105
|
+
Object.freeze(node.def.steps);
|
|
106
|
+
Object.freeze(node.def);
|
|
107
|
+
}
|
|
108
|
+
else if (node.type === 'forEach') {
|
|
109
|
+
sealStep(node.def.step);
|
|
110
|
+
Object.freeze(node.def);
|
|
111
|
+
}
|
|
112
|
+
else if (node.type === 'map') {
|
|
113
|
+
Object.freeze(node.def);
|
|
114
|
+
}
|
|
115
|
+
Object.freeze(node);
|
|
116
|
+
}
|
|
117
|
+
export function sealWorkflowDefinition(workflow) {
|
|
118
|
+
const hash = workflowDefinitionHash(workflow);
|
|
119
|
+
for (const node of workflow.nodes)
|
|
120
|
+
sealNode(node);
|
|
121
|
+
Object.freeze(workflow.nodes);
|
|
122
|
+
return hash;
|
|
123
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { WorkflowNode } from './types';
|
|
2
|
+
/** Names that produce indexed records such as `name:0`. */
|
|
3
|
+
export declare function indexedStepNames(nodes: readonly WorkflowNode[]): string[];
|
|
4
|
+
/** Every record name declared by a workflow graph. */
|
|
5
|
+
export declare function stepNames(nodes: readonly WorkflowNode[]): string[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Names that produce indexed records such as `name:0`. */
|
|
2
|
+
export function indexedStepNames(nodes) {
|
|
3
|
+
const names = [];
|
|
4
|
+
for (const node of nodes) {
|
|
5
|
+
if (node.type === 'doUntil' || node.type === 'doWhile') {
|
|
6
|
+
for (const step of node.def.steps)
|
|
7
|
+
names.push(step.name);
|
|
8
|
+
}
|
|
9
|
+
else if (node.type === 'forEach') {
|
|
10
|
+
names.push(node.def.step.name);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return names;
|
|
14
|
+
}
|
|
15
|
+
/** Every record name declared by a workflow graph. */
|
|
16
|
+
export function stepNames(nodes) {
|
|
17
|
+
const names = [];
|
|
18
|
+
for (const node of nodes) {
|
|
19
|
+
if (node.type === 'step')
|
|
20
|
+
names.push(node.def.name);
|
|
21
|
+
else if (node.type === 'branch') {
|
|
22
|
+
for (const steps of node.def.paths.values()) {
|
|
23
|
+
for (const step of steps)
|
|
24
|
+
names.push(step.name);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else if (node.type === 'parallel') {
|
|
28
|
+
for (const step of node.def.steps)
|
|
29
|
+
names.push(step.name);
|
|
30
|
+
}
|
|
31
|
+
else if (node.type === 'subWorkflow') {
|
|
32
|
+
names.push(`sub:${node.name}`);
|
|
33
|
+
}
|
|
34
|
+
else if (node.type === 'doUntil' || node.type === 'doWhile') {
|
|
35
|
+
for (const step of node.def.steps)
|
|
36
|
+
names.push(step.name);
|
|
37
|
+
}
|
|
38
|
+
else if (node.type === 'forEach') {
|
|
39
|
+
names.push(node.def.step.name);
|
|
40
|
+
}
|
|
41
|
+
else if (node.type === 'map') {
|
|
42
|
+
names.push(node.def.name);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return names;
|
|
46
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { StepDefinition, WorkflowNode } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Extract step definitions from an inline builder. Inline nodes run inside one
|
|
4
|
+
* `wf:step` job, so only plain steps have executable semantics there.
|
|
5
|
+
*/
|
|
6
|
+
export declare function onlySteps(sub: {
|
|
7
|
+
nodes: readonly WorkflowNode[];
|
|
8
|
+
}, where: string, label?: string): StepDefinition[];
|
|
9
|
+
/** Why an event name cannot be used as a gate, or `null` when it can. */
|
|
10
|
+
export declare function unusableEventName(event: unknown): string | null;
|
|
11
|
+
/**
|
|
12
|
+
* A delivered signal is retained by event name rather than consumed, so two gates
|
|
13
|
+
* sharing a name would both be opened by one delivery.
|
|
14
|
+
*/
|
|
15
|
+
export declare function assertNoDuplicateWaitFor(wf: {
|
|
16
|
+
name: string;
|
|
17
|
+
nodes: readonly {
|
|
18
|
+
type: string;
|
|
19
|
+
event?: string;
|
|
20
|
+
}[];
|
|
21
|
+
}): void;
|
|
22
|
+
type RegistrableWorkflow = {
|
|
23
|
+
name: string;
|
|
24
|
+
nodes: readonly WorkflowNode[];
|
|
25
|
+
getStepNames(): string[];
|
|
26
|
+
getIndexedStepNames(): string[];
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Reject both engine namespace collisions and names colliding with the
|
|
30
|
+
* `name:index` records generated by loops.
|
|
31
|
+
*/
|
|
32
|
+
export declare function assertNoIndexCollision(wf: RegistrableWorkflow): void;
|
|
33
|
+
/**
|
|
34
|
+
* `retry` is the total number of attempts. Zero/non-integral values would make the
|
|
35
|
+
* runner skip the handler and fabricate a failure record.
|
|
36
|
+
*/
|
|
37
|
+
export declare function assertUsableRetry(step: string, retry: number | undefined): void;
|
|
38
|
+
/** Timeout values are finite milliseconds; zero deliberately means no forward bound. */
|
|
39
|
+
export declare function assertUsableTimeout(where: string, timeout: number | undefined): void;
|
|
40
|
+
/** Polling delays and child deadlines cannot be disabled or spin at zero. */
|
|
41
|
+
export declare function assertPositiveDuration(where: string, duration: number | undefined): void;
|
|
42
|
+
/** Loop limits must bound the number of body executions exactly. */
|
|
43
|
+
export declare function assertUsableIterations(where: string, maxIterations: number | undefined): void;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract step definitions from an inline builder. Inline nodes run inside one
|
|
3
|
+
* `wf:step` job, so only plain steps have executable semantics there.
|
|
4
|
+
*/
|
|
5
|
+
export function onlySteps(sub, where, label) {
|
|
6
|
+
const rejected = sub.nodes.filter((node) => node.type !== 'step');
|
|
7
|
+
if (rejected.length > 0) {
|
|
8
|
+
const kinds = [...new Set(rejected.map((node) => node.type))].join(', ');
|
|
9
|
+
const which = label ? ` (path "${label}")` : '';
|
|
10
|
+
throw new Error(`${where} accepts step() nodes only${which}, but received: ${kinds}. ` +
|
|
11
|
+
`Move those nodes to the top level of the workflow, or extract them into a ` +
|
|
12
|
+
`separate workflow and call it with subWorkflow().`);
|
|
13
|
+
}
|
|
14
|
+
return sub.nodes
|
|
15
|
+
.filter((node) => node.type === 'step')
|
|
16
|
+
.map((node) => node.def);
|
|
17
|
+
}
|
|
18
|
+
/** Why an event name cannot be used as a gate, or `null` when it can. */
|
|
19
|
+
export function unusableEventName(event) {
|
|
20
|
+
if (typeof event !== 'string' || event.length === 0)
|
|
21
|
+
return 'with no event name';
|
|
22
|
+
if (event === '__proto__') {
|
|
23
|
+
return 'named "__proto__", which cannot be stored as a signal key and would never receive its signal';
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A delivered signal is retained by event name rather than consumed, so two gates
|
|
29
|
+
* sharing a name would both be opened by one delivery.
|
|
30
|
+
*/
|
|
31
|
+
export function assertNoDuplicateWaitFor(wf) {
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
for (const node of wf.nodes) {
|
|
34
|
+
if (node.type !== 'waitFor')
|
|
35
|
+
continue;
|
|
36
|
+
const bad = unusableEventName(node.event);
|
|
37
|
+
if (bad !== null)
|
|
38
|
+
throw new Error(`Workflow "${wf.name}" has a waitFor gate ${bad}`);
|
|
39
|
+
const event = node.event;
|
|
40
|
+
if (seen.has(event)) {
|
|
41
|
+
throw new Error(`Workflow "${wf.name}" waits for the event "${event}" more than once. ` +
|
|
42
|
+
`One signal would open every one of those gates, because a delivered signal ` +
|
|
43
|
+
`is never consumed. Give each gate its own event name.`);
|
|
44
|
+
}
|
|
45
|
+
seen.add(event);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Engine records use `__*` for bookkeeping and `sub:*` for child handles. A user
|
|
50
|
+
* step in either namespace would be skipped or misclassified during unwind.
|
|
51
|
+
*/
|
|
52
|
+
function assertNoReservedStepNames(wf) {
|
|
53
|
+
for (const name of userStepNames(wf.nodes)) {
|
|
54
|
+
if (name.startsWith('__') || name.startsWith('sub:')) {
|
|
55
|
+
throw new Error(`Step name "${name}" in workflow "${wf.name}" uses a reserved engine namespace. ` +
|
|
56
|
+
`Names beginning with "__" or "sub:" are reserved for workflow bookkeeping.`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Reject both engine namespace collisions and names colliding with the
|
|
62
|
+
* `name:index` records generated by loops.
|
|
63
|
+
*/
|
|
64
|
+
export function assertNoIndexCollision(wf) {
|
|
65
|
+
assertNoReservedStepNames(wf);
|
|
66
|
+
const generators = wf.getIndexedStepNames();
|
|
67
|
+
if (generators.length === 0)
|
|
68
|
+
return;
|
|
69
|
+
for (const declared of wf.getStepNames()) {
|
|
70
|
+
for (const base of generators) {
|
|
71
|
+
if (declared !== base && new RegExp(`^${escapeRe(base)}:\\d+$`).test(declared)) {
|
|
72
|
+
throw new Error(`Step "${declared}" in "${wf.name}" collides with the per-iteration names ` +
|
|
73
|
+
`reserved by the loop step "${base}" ("${base}:0", "${base}:1", ...). ` +
|
|
74
|
+
`Rename one of them.`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** All user-declared record names, excluding legitimate generated subworkflow handles. */
|
|
80
|
+
function userStepNames(nodes) {
|
|
81
|
+
const names = [];
|
|
82
|
+
for (const node of nodes) {
|
|
83
|
+
if (node.type === 'step')
|
|
84
|
+
names.push(node.def.name);
|
|
85
|
+
else if (node.type === 'branch') {
|
|
86
|
+
for (const steps of node.def.paths.values()) {
|
|
87
|
+
for (const step of steps)
|
|
88
|
+
names.push(step.name);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (node.type === 'parallel') {
|
|
92
|
+
for (const step of node.def.steps)
|
|
93
|
+
names.push(step.name);
|
|
94
|
+
}
|
|
95
|
+
else if (node.type === 'doUntil' || node.type === 'doWhile') {
|
|
96
|
+
for (const step of node.def.steps)
|
|
97
|
+
names.push(step.name);
|
|
98
|
+
}
|
|
99
|
+
else if (node.type === 'forEach')
|
|
100
|
+
names.push(node.def.step.name);
|
|
101
|
+
else if (node.type === 'map')
|
|
102
|
+
names.push(node.def.name);
|
|
103
|
+
}
|
|
104
|
+
return names;
|
|
105
|
+
}
|
|
106
|
+
function escapeRe(value) {
|
|
107
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* `retry` is the total number of attempts. Zero/non-integral values would make the
|
|
111
|
+
* runner skip the handler and fabricate a failure record.
|
|
112
|
+
*/
|
|
113
|
+
export function assertUsableRetry(step, retry) {
|
|
114
|
+
if (retry === undefined)
|
|
115
|
+
return;
|
|
116
|
+
if (!Number.isSafeInteger(retry) || retry < 1) {
|
|
117
|
+
throw new Error(`Step "${step}" declares retry: ${retry}. retry is the number of attempts, so it must be an integer of 1 or more (1 means a single attempt with no retry).`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** Timeout values are finite milliseconds; zero deliberately means no forward bound. */
|
|
121
|
+
export function assertUsableTimeout(where, timeout) {
|
|
122
|
+
if (timeout === undefined)
|
|
123
|
+
return;
|
|
124
|
+
if (!Number.isFinite(timeout) || timeout < 0) {
|
|
125
|
+
throw new Error(`${where} timeout must be a finite number of milliseconds at least 0`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Polling delays and child deadlines cannot be disabled or spin at zero. */
|
|
129
|
+
export function assertPositiveDuration(where, duration) {
|
|
130
|
+
if (duration === undefined)
|
|
131
|
+
return;
|
|
132
|
+
if (!Number.isFinite(duration) || duration <= 0) {
|
|
133
|
+
throw new Error(`${where} must be a finite number of milliseconds greater than 0`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Loop limits must bound the number of body executions exactly. */
|
|
137
|
+
export function assertUsableIterations(where, maxIterations) {
|
|
138
|
+
if (maxIterations === undefined)
|
|
139
|
+
return;
|
|
140
|
+
if (!Number.isSafeInteger(maxIterations) || maxIterations < 1) {
|
|
141
|
+
throw new Error(`${where} maxIterations must be an integer of 1 or more`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* All supported commands for TCP/HTTP protocol
|
|
4
4
|
*/
|
|
5
5
|
import type { JobInput, JobState } from './job';
|
|
6
|
+
import type { AtomicFlowJobInput } from './flow';
|
|
6
7
|
import type { CronJobOptions } from './cron';
|
|
7
8
|
/** Base command interface */
|
|
8
9
|
interface BaseCommand {
|
|
@@ -81,6 +82,11 @@ export interface PushBatchCommand extends BaseCommand {
|
|
|
81
82
|
readonly queue: string;
|
|
82
83
|
readonly jobs: JobInput[];
|
|
83
84
|
}
|
|
85
|
+
/** Atomically commit a fully-resolved multi-queue flow graph. */
|
|
86
|
+
export interface PushFlowCommand extends BaseCommand {
|
|
87
|
+
readonly cmd: 'PUSHF';
|
|
88
|
+
readonly jobs: AtomicFlowJobInput[];
|
|
89
|
+
}
|
|
84
90
|
export interface PullCommand extends BaseCommand {
|
|
85
91
|
readonly cmd: 'PULL';
|
|
86
92
|
readonly queue: string;
|
|
@@ -510,7 +516,7 @@ export interface HelloCommand extends BaseCommand {
|
|
|
510
516
|
readonly capabilities?: 'pipelining'[];
|
|
511
517
|
}
|
|
512
518
|
/** Union of all commands */
|
|
513
|
-
export type Command = PushCommand | PushBatchCommand | PullCommand | PullBatchCommand | AckCommand | AckBatchCommand | FailCommand | GetJobCommand | GetStateCommand | GetResultCommand | GetJobsCommand | GetJobCountsCommand | GetCountsPerPriorityCommand | GetJobByCustomIdCommand | CountCommand | GetProgressCommand | CancelCommand | ProgressCommand | UpdateCommand | ChangePriorityCommand | PromoteCommand | WaitJobCommand | MoveToDelayedCommand | DiscardCommand | PauseCommand | ResumeCommand | IsPausedCommand | DrainCommand | ObliterateCommand | ListQueuesCommand | CleanCommand | DlqCommand | RetryDlqCommand | PurgeDlqCommand | RetryCompletedCommand | RateLimitCommand | SetConcurrencyCommand | RateLimitClearCommand | ClearConcurrencyCommand | SetStallConfigCommand | GetStallConfigCommand | SetDlqConfigCommand | GetDlqConfigCommand | CronCommand | CronDeleteCommand | CronListCommand | AddLogCommand | GetLogsCommand | HeartbeatCommand | JobHeartbeatCommand | JobHeartbeatBatchCommand | PingCommand | RegisterWorkerCommand | UnregisterWorkerCommand | ListWorkersCommand | AddWebhookCommand | RemoveWebhookCommand | ListWebhooksCommand | StatsCommand | MetricsCommand | PrometheusCommand | CronGetCommand | GetChildrenValuesCommand | StorageStatusCommand | ClearLogsCommand | ExtendLockCommand | ExtendLocksCommand | ChangeDelayCommand | SetWebhookEnabledCommand | CompactMemoryCommand | UpdateParentCommand | MoveToWaitCommand | PromoteJobsCommand | DashboardOverviewCommand | DashboardQueuesCommand | DashboardQueueCommand | AuthCommand | GetFailedChildrenValuesCommand | GetIgnoredChildrenFailuresCommand | RemoveChildDependencyCommand | RemoveUnprocessedChildrenCommand | HelloCommand;
|
|
519
|
+
export type Command = PushCommand | PushBatchCommand | PushFlowCommand | PullCommand | PullBatchCommand | AckCommand | AckBatchCommand | FailCommand | GetJobCommand | GetStateCommand | GetResultCommand | GetJobsCommand | GetJobCountsCommand | GetCountsPerPriorityCommand | GetJobByCustomIdCommand | CountCommand | GetProgressCommand | CancelCommand | ProgressCommand | UpdateCommand | ChangePriorityCommand | PromoteCommand | WaitJobCommand | MoveToDelayedCommand | DiscardCommand | PauseCommand | ResumeCommand | IsPausedCommand | DrainCommand | ObliterateCommand | ListQueuesCommand | CleanCommand | DlqCommand | RetryDlqCommand | PurgeDlqCommand | RetryCompletedCommand | RateLimitCommand | SetConcurrencyCommand | RateLimitClearCommand | ClearConcurrencyCommand | SetStallConfigCommand | GetStallConfigCommand | SetDlqConfigCommand | GetDlqConfigCommand | CronCommand | CronDeleteCommand | CronListCommand | AddLogCommand | GetLogsCommand | HeartbeatCommand | JobHeartbeatCommand | JobHeartbeatBatchCommand | PingCommand | RegisterWorkerCommand | UnregisterWorkerCommand | ListWorkersCommand | AddWebhookCommand | RemoveWebhookCommand | ListWebhooksCommand | StatsCommand | MetricsCommand | PrometheusCommand | CronGetCommand | GetChildrenValuesCommand | StorageStatusCommand | ClearLogsCommand | ExtendLockCommand | ExtendLocksCommand | ChangeDelayCommand | SetWebhookEnabledCommand | CompactMemoryCommand | UpdateParentCommand | MoveToWaitCommand | PromoteJobsCommand | DashboardOverviewCommand | DashboardQueuesCommand | DashboardQueueCommand | AuthCommand | GetFailedChildrenValuesCommand | GetIgnoredChildrenFailuresCommand | RemoveChildDependencyCommand | RemoveUnprocessedChildrenCommand | HelloCommand;
|
|
514
520
|
/** Extract command type */
|
|
515
521
|
export type CommandType = Command['cmd'];
|
|
516
522
|
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Job, JobId, JobInput } from './job';
|
|
2
|
+
/** One fully-resolved job in an atomic flow commit. */
|
|
3
|
+
export interface AtomicFlowJobInput {
|
|
4
|
+
readonly id: JobId;
|
|
5
|
+
readonly queue: string;
|
|
6
|
+
readonly input: JobInput;
|
|
7
|
+
}
|
|
8
|
+
/** Wire-neutral batch consumed by embedded and TCP FlowProducer paths. */
|
|
9
|
+
export interface AtomicFlowBatchInput {
|
|
10
|
+
readonly jobs: AtomicFlowJobInput[];
|
|
11
|
+
}
|
|
12
|
+
/** Authoritative snapshots returned after a successful flow commit. */
|
|
13
|
+
export interface AtomicFlowBatchResult {
|
|
14
|
+
readonly jobs: Job[];
|
|
15
|
+
}
|
|
16
|
+
export type FlowFailureMode = 'fail' | 'remove' | 'ignore' | 'continue';
|
|
17
|
+
/** Durable recovery record for a terminal child failure. */
|
|
18
|
+
export interface FlowFailureRecord {
|
|
19
|
+
readonly parentId: JobId;
|
|
20
|
+
readonly childId: JobId;
|
|
21
|
+
readonly childQueue: string;
|
|
22
|
+
readonly mode: FlowFailureMode;
|
|
23
|
+
readonly error: string;
|
|
24
|
+
readonly createdAt: number;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
/** SQLite PRAGMA settings for optimal performance */
|
|
5
5
|
export declare const PRAGMA_SETTINGS = "\nPRAGMA journal_mode = WAL;\nPRAGMA synchronous = NORMAL;\nPRAGMA cache_size = -64000;\nPRAGMA temp_store = MEMORY;\nPRAGMA mmap_size = 268435456;\nPRAGMA page_size = 4096;\nPRAGMA busy_timeout = 5000;\n";
|
|
6
6
|
/** Main schema creation */
|
|
7
|
-
export declare const SCHEMA = "\n-- Jobs table (using UUIDv7 for job IDs)\n-- Uses BLOB for data fields (MessagePack serialization for ~2-3x faster than JSON)\nCREATE TABLE IF NOT EXISTS jobs (\n id TEXT PRIMARY KEY,\n queue TEXT NOT NULL,\n data BLOB NOT NULL,\n priority INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL,\n run_at INTEGER NOT NULL,\n started_at INTEGER,\n completed_at INTEGER,\n attempts INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT 3,\n backoff INTEGER NOT NULL DEFAULT 1000,\n ttl INTEGER,\n timeout INTEGER,\n unique_key TEXT,\n custom_id TEXT,\n depends_on BLOB,\n parent_id TEXT,\n children_ids BLOB,\n tags BLOB,\n state TEXT NOT NULL DEFAULT 'waiting',\n lifo INTEGER NOT NULL DEFAULT 0,\n group_id TEXT,\n progress INTEGER DEFAULT 0,\n progress_msg TEXT,\n remove_on_complete INTEGER DEFAULT 0,\n remove_on_fail INTEGER DEFAULT 0,\n stall_timeout INTEGER,\n last_heartbeat INTEGER,\n stall_count INTEGER NOT NULL DEFAULT 0,\n timeline BLOB,\n stacktrace BLOB\n);\n\n-- Indexes for common queries\nCREATE INDEX IF NOT EXISTS idx_jobs_queue_state\n ON jobs(queue, state);\n-- Stable createdAt/id pagination for unfiltered and logical-state queue views\nCREATE INDEX IF NOT EXISTS idx_jobs_queue_created\n ON jobs(queue, created_at, id);\nCREATE INDEX IF NOT EXISTS idx_jobs_queue_state_created\n ON jobs(queue, state, created_at, id);\nCREATE INDEX IF NOT EXISTS idx_jobs_run_at\n ON jobs(run_at) WHERE state IN ('waiting', 'delayed');\nCREATE INDEX IF NOT EXISTS idx_jobs_unique\n ON jobs(queue, unique_key) WHERE unique_key IS NOT NULL;\nCREATE INDEX IF NOT EXISTS idx_jobs_custom_id\n ON jobs(custom_id) WHERE custom_id IS NOT NULL;\nCREATE INDEX IF NOT EXISTS idx_jobs_parent\n ON jobs(parent_id) WHERE parent_id IS NOT NULL;\n\n-- Job results storage (BLOB for MessagePack)\nCREATE TABLE IF NOT EXISTS job_results (\n job_id TEXT PRIMARY KEY,\n result BLOB,\n completed_at INTEGER NOT NULL\n);\n\n-- Dead letter queue (BLOB for MessagePack - stores full DlqEntry)\nCREATE TABLE IF NOT EXISTS dlq (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n job_id TEXT NOT NULL,\n queue TEXT NOT NULL,\n entry BLOB NOT NULL,\n entered_at INTEGER NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_dlq_queue ON dlq(queue);\nCREATE INDEX IF NOT EXISTS idx_dlq_job_id ON dlq(job_id);\nCREATE INDEX IF NOT EXISTS idx_dlq_entered_at ON dlq(entered_at);\n\n-- Performance indexes for high-throughput operations\n-- Stall detection: runs every 5s, needs fast lookup of active jobs by started_at\nCREATE INDEX IF NOT EXISTS idx_jobs_state_started\n ON jobs(state, started_at) WHERE state = 'active';\n\n-- Group operations: fast lookup by group_id\nCREATE INDEX IF NOT EXISTS idx_jobs_group_id\n ON jobs(group_id) WHERE group_id IS NOT NULL;\n\n-- Pending jobs: compound index for priority-ordered retrieval\nCREATE INDEX IF NOT EXISTS idx_jobs_pending_priority\n ON jobs(queue, state, priority DESC, run_at ASC) WHERE state IN ('waiting', 'delayed');\n\n-- Completed jobs: index for recovery ordering (issue #84)\nCREATE INDEX IF NOT EXISTS idx_jobs_completed_order\n ON jobs(completed_at DESC) WHERE state = 'completed';\n\n-- Cron jobs (BLOB for MessagePack)\nCREATE TABLE IF NOT EXISTS cron_jobs (\n name TEXT PRIMARY KEY,\n queue TEXT NOT NULL,\n data BLOB NOT NULL,\n schedule TEXT,\n repeat_every INTEGER,\n priority INTEGER NOT NULL DEFAULT 0,\n next_run INTEGER NOT NULL,\n executions INTEGER NOT NULL DEFAULT 0,\n max_limit INTEGER,\n timezone TEXT,\n unique_key TEXT,\n dedup BLOB,\n skip_missed_on_restart INTEGER NOT NULL DEFAULT 0,\n skip_if_no_worker INTEGER NOT NULL DEFAULT 0,\n prevent_overlap INTEGER NOT NULL DEFAULT 1,\n job_options BLOB\n);\n\n-- Queue state persistence (optional)\nCREATE TABLE IF NOT EXISTS queue_state (\n name TEXT PRIMARY KEY,\n paused INTEGER NOT NULL DEFAULT 0,\n rate_limit INTEGER,\n concurrency_limit INTEGER,\n rate_limit_duration INTEGER,\n rate_limit_expires_at INTEGER,\n stall_enabled INTEGER,\n stall_interval INTEGER,\n max_stalls INTEGER,\n stall_grace_period INTEGER,\n dlq_config BLOB\n);\n";
|
|
7
|
+
export declare const SCHEMA = "\n-- Jobs table (using UUIDv7 for job IDs)\n-- Uses BLOB for data fields (MessagePack serialization for ~2-3x faster than JSON)\nCREATE TABLE IF NOT EXISTS jobs (\n id TEXT PRIMARY KEY,\n queue TEXT NOT NULL,\n data BLOB NOT NULL,\n priority INTEGER NOT NULL DEFAULT 0,\n created_at INTEGER NOT NULL,\n run_at INTEGER NOT NULL,\n started_at INTEGER,\n completed_at INTEGER,\n attempts INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT 3,\n backoff INTEGER NOT NULL DEFAULT 1000,\n ttl INTEGER,\n timeout INTEGER,\n unique_key TEXT,\n custom_id TEXT,\n depends_on BLOB,\n parent_id TEXT,\n children_ids BLOB,\n tags BLOB,\n state TEXT NOT NULL DEFAULT 'waiting',\n lifo INTEGER NOT NULL DEFAULT 0,\n group_id TEXT,\n progress INTEGER DEFAULT 0,\n progress_msg TEXT,\n remove_on_complete INTEGER DEFAULT 0,\n remove_on_fail INTEGER DEFAULT 0,\n fail_parent_on_failure INTEGER NOT NULL DEFAULT 0,\n remove_dependency_on_failure INTEGER NOT NULL DEFAULT 0,\n continue_parent_on_failure INTEGER NOT NULL DEFAULT 0,\n ignore_dependency_on_failure INTEGER NOT NULL DEFAULT 0,\n stall_timeout INTEGER,\n last_heartbeat INTEGER,\n stall_count INTEGER NOT NULL DEFAULT 0,\n timeline BLOB,\n stacktrace BLOB\n);\n\n-- Indexes for common queries\nCREATE INDEX IF NOT EXISTS idx_jobs_queue_state\n ON jobs(queue, state);\n-- Stable createdAt/id pagination for unfiltered and logical-state queue views\nCREATE INDEX IF NOT EXISTS idx_jobs_queue_created\n ON jobs(queue, created_at, id);\nCREATE INDEX IF NOT EXISTS idx_jobs_queue_state_created\n ON jobs(queue, state, created_at, id);\nCREATE INDEX IF NOT EXISTS idx_jobs_run_at\n ON jobs(run_at) WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed');\nCREATE INDEX IF NOT EXISTS idx_jobs_unique\n ON jobs(queue, unique_key) WHERE unique_key IS NOT NULL;\nCREATE INDEX IF NOT EXISTS idx_jobs_custom_id\n ON jobs(custom_id) WHERE custom_id IS NOT NULL;\nCREATE INDEX IF NOT EXISTS idx_jobs_parent\n ON jobs(parent_id) WHERE parent_id IS NOT NULL;\n\n-- Durable outbox/state for terminal child failure propagation.\nCREATE TABLE IF NOT EXISTS flow_failures (\n parent_id TEXT NOT NULL,\n child_id TEXT NOT NULL,\n child_queue TEXT NOT NULL,\n mode TEXT NOT NULL,\n error TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n PRIMARY KEY (parent_id, child_id)\n);\nCREATE INDEX IF NOT EXISTS idx_flow_failures_parent ON flow_failures(parent_id);\n\n-- Job results storage (BLOB for MessagePack)\nCREATE TABLE IF NOT EXISTS job_results (\n job_id TEXT PRIMARY KEY,\n result BLOB,\n completed_at INTEGER NOT NULL\n);\n\n-- Dead letter queue (BLOB for MessagePack - stores full DlqEntry)\nCREATE TABLE IF NOT EXISTS dlq (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n job_id TEXT NOT NULL,\n queue TEXT NOT NULL,\n entry BLOB NOT NULL,\n entered_at INTEGER NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_dlq_queue ON dlq(queue);\nCREATE INDEX IF NOT EXISTS idx_dlq_job_id ON dlq(job_id);\nCREATE INDEX IF NOT EXISTS idx_dlq_entered_at ON dlq(entered_at);\n\n-- Performance indexes for high-throughput operations\n-- Stall detection: runs every 5s, needs fast lookup of active jobs by started_at\nCREATE INDEX IF NOT EXISTS idx_jobs_state_started\n ON jobs(state, started_at) WHERE state = 'active';\n\n-- Group operations: fast lookup by group_id\nCREATE INDEX IF NOT EXISTS idx_jobs_group_id\n ON jobs(group_id) WHERE group_id IS NOT NULL;\n\n-- Pending jobs: compound index for priority-ordered retrieval\nCREATE INDEX IF NOT EXISTS idx_jobs_pending_priority\n ON jobs(queue, state, priority DESC, run_at ASC) WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed');\n\n-- Completed jobs: index for recovery ordering (issue #84)\nCREATE INDEX IF NOT EXISTS idx_jobs_completed_order\n ON jobs(completed_at DESC) WHERE state = 'completed';\n\n-- Cron jobs (BLOB for MessagePack)\nCREATE TABLE IF NOT EXISTS cron_jobs (\n name TEXT PRIMARY KEY,\n queue TEXT NOT NULL,\n data BLOB NOT NULL,\n schedule TEXT,\n repeat_every INTEGER,\n priority INTEGER NOT NULL DEFAULT 0,\n next_run INTEGER NOT NULL,\n executions INTEGER NOT NULL DEFAULT 0,\n max_limit INTEGER,\n timezone TEXT,\n unique_key TEXT,\n dedup BLOB,\n skip_missed_on_restart INTEGER NOT NULL DEFAULT 0,\n skip_if_no_worker INTEGER NOT NULL DEFAULT 0,\n prevent_overlap INTEGER NOT NULL DEFAULT 1,\n job_options BLOB\n);\n\n-- Queue state persistence (optional)\nCREATE TABLE IF NOT EXISTS queue_state (\n name TEXT PRIMARY KEY,\n paused INTEGER NOT NULL DEFAULT 0,\n rate_limit INTEGER,\n concurrency_limit INTEGER,\n rate_limit_duration INTEGER,\n rate_limit_expires_at INTEGER,\n stall_enabled INTEGER,\n stall_interval INTEGER,\n max_stalls INTEGER,\n stall_grace_period INTEGER,\n dlq_config BLOB\n);\n";
|
|
8
8
|
/** Migration version table */
|
|
9
9
|
export declare const MIGRATION_TABLE = "\nCREATE TABLE IF NOT EXISTS migrations (\n version INTEGER PRIMARY KEY,\n applied_at INTEGER NOT NULL\n);\n";
|
|
10
10
|
/** Current schema version */
|
|
11
|
-
export declare const SCHEMA_VERSION =
|
|
11
|
+
export declare const SCHEMA_VERSION = 27;
|
|
12
12
|
/** All migrations in order */
|
|
13
13
|
export declare const MIGRATIONS: Record<number, string>;
|
|
@@ -42,6 +42,10 @@ CREATE TABLE IF NOT EXISTS jobs (
|
|
|
42
42
|
progress_msg TEXT,
|
|
43
43
|
remove_on_complete INTEGER DEFAULT 0,
|
|
44
44
|
remove_on_fail INTEGER DEFAULT 0,
|
|
45
|
+
fail_parent_on_failure INTEGER NOT NULL DEFAULT 0,
|
|
46
|
+
remove_dependency_on_failure INTEGER NOT NULL DEFAULT 0,
|
|
47
|
+
continue_parent_on_failure INTEGER NOT NULL DEFAULT 0,
|
|
48
|
+
ignore_dependency_on_failure INTEGER NOT NULL DEFAULT 0,
|
|
45
49
|
stall_timeout INTEGER,
|
|
46
50
|
last_heartbeat INTEGER,
|
|
47
51
|
stall_count INTEGER NOT NULL DEFAULT 0,
|
|
@@ -58,7 +62,7 @@ CREATE INDEX IF NOT EXISTS idx_jobs_queue_created
|
|
|
58
62
|
CREATE INDEX IF NOT EXISTS idx_jobs_queue_state_created
|
|
59
63
|
ON jobs(queue, state, created_at, id);
|
|
60
64
|
CREATE INDEX IF NOT EXISTS idx_jobs_run_at
|
|
61
|
-
ON jobs(run_at) WHERE state IN ('waiting', 'delayed');
|
|
65
|
+
ON jobs(run_at) WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed');
|
|
62
66
|
CREATE INDEX IF NOT EXISTS idx_jobs_unique
|
|
63
67
|
ON jobs(queue, unique_key) WHERE unique_key IS NOT NULL;
|
|
64
68
|
CREATE INDEX IF NOT EXISTS idx_jobs_custom_id
|
|
@@ -66,6 +70,18 @@ CREATE INDEX IF NOT EXISTS idx_jobs_custom_id
|
|
|
66
70
|
CREATE INDEX IF NOT EXISTS idx_jobs_parent
|
|
67
71
|
ON jobs(parent_id) WHERE parent_id IS NOT NULL;
|
|
68
72
|
|
|
73
|
+
-- Durable outbox/state for terminal child failure propagation.
|
|
74
|
+
CREATE TABLE IF NOT EXISTS flow_failures (
|
|
75
|
+
parent_id TEXT NOT NULL,
|
|
76
|
+
child_id TEXT NOT NULL,
|
|
77
|
+
child_queue TEXT NOT NULL,
|
|
78
|
+
mode TEXT NOT NULL,
|
|
79
|
+
error TEXT NOT NULL,
|
|
80
|
+
created_at INTEGER NOT NULL,
|
|
81
|
+
PRIMARY KEY (parent_id, child_id)
|
|
82
|
+
);
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_flow_failures_parent ON flow_failures(parent_id);
|
|
84
|
+
|
|
69
85
|
-- Job results storage (BLOB for MessagePack)
|
|
70
86
|
CREATE TABLE IF NOT EXISTS job_results (
|
|
71
87
|
job_id TEXT PRIMARY KEY,
|
|
@@ -97,7 +113,7 @@ CREATE INDEX IF NOT EXISTS idx_jobs_group_id
|
|
|
97
113
|
|
|
98
114
|
-- Pending jobs: compound index for priority-ordered retrieval
|
|
99
115
|
CREATE INDEX IF NOT EXISTS idx_jobs_pending_priority
|
|
100
|
-
ON jobs(queue, state, priority DESC, run_at ASC) WHERE state IN ('waiting', 'delayed');
|
|
116
|
+
ON jobs(queue, state, priority DESC, run_at ASC) WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed');
|
|
101
117
|
|
|
102
118
|
-- Completed jobs: index for recovery ordering (issue #84)
|
|
103
119
|
CREATE INDEX IF NOT EXISTS idx_jobs_completed_order
|
|
@@ -146,7 +162,7 @@ CREATE TABLE IF NOT EXISTS migrations (
|
|
|
146
162
|
);
|
|
147
163
|
`;
|
|
148
164
|
/** Current schema version */
|
|
149
|
-
export const SCHEMA_VERSION =
|
|
165
|
+
export const SCHEMA_VERSION = 27;
|
|
150
166
|
/** All migrations in order */
|
|
151
167
|
export const MIGRATIONS = {
|
|
152
168
|
1: SCHEMA,
|
|
@@ -240,5 +256,38 @@ ALTER TABLE queue_state ADD COLUMN stall_grace_period INTEGER;
|
|
|
240
256
|
// Migration 22: Persist the complete per-queue DLQ policy atomically.
|
|
241
257
|
22: `
|
|
242
258
|
ALTER TABLE queue_state ADD COLUMN dlq_config BLOB;
|
|
259
|
+
`,
|
|
260
|
+
// Migrations 23-26: flow failure policy must survive active-job recovery.
|
|
261
|
+
23: `
|
|
262
|
+
ALTER TABLE jobs ADD COLUMN fail_parent_on_failure INTEGER NOT NULL DEFAULT 0;
|
|
263
|
+
`,
|
|
264
|
+
24: `
|
|
265
|
+
ALTER TABLE jobs ADD COLUMN remove_dependency_on_failure INTEGER NOT NULL DEFAULT 0;
|
|
266
|
+
`,
|
|
267
|
+
25: `
|
|
268
|
+
ALTER TABLE jobs ADD COLUMN continue_parent_on_failure INTEGER NOT NULL DEFAULT 0;
|
|
269
|
+
`,
|
|
270
|
+
26: `
|
|
271
|
+
ALTER TABLE jobs ADD COLUMN ignore_dependency_on_failure INTEGER NOT NULL DEFAULT 0;
|
|
272
|
+
`,
|
|
273
|
+
27: `
|
|
274
|
+
CREATE TABLE IF NOT EXISTS flow_failures (
|
|
275
|
+
parent_id TEXT NOT NULL,
|
|
276
|
+
child_id TEXT NOT NULL,
|
|
277
|
+
child_queue TEXT NOT NULL,
|
|
278
|
+
mode TEXT NOT NULL,
|
|
279
|
+
error TEXT NOT NULL,
|
|
280
|
+
created_at INTEGER NOT NULL,
|
|
281
|
+
PRIMARY KEY (parent_id, child_id)
|
|
282
|
+
);
|
|
283
|
+
CREATE INDEX IF NOT EXISTS idx_flow_failures_parent ON flow_failures(parent_id);
|
|
284
|
+
|
|
285
|
+
DROP INDEX IF EXISTS idx_jobs_run_at;
|
|
286
|
+
CREATE INDEX idx_jobs_run_at
|
|
287
|
+
ON jobs(run_at) WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed');
|
|
288
|
+
|
|
289
|
+
DROP INDEX IF EXISTS idx_jobs_pending_priority;
|
|
290
|
+
CREATE INDEX idx_jobs_pending_priority
|
|
291
|
+
ON jobs(queue, state, priority DESC, run_at ASC) WHERE state IN ('waiting', 'prioritized', 'waiting-children', 'delayed');
|
|
243
292
|
`,
|
|
244
293
|
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Uses MessagePack for ~2-3x faster serialization than JSON
|
|
5
5
|
*/
|
|
6
6
|
import type { Job, JobId, JobTimelineEntry } from '../../domain/types/job';
|
|
7
|
+
import type { FlowFailureRecord } from '../../domain/types/flow';
|
|
7
8
|
import type { StallConfig } from '../../domain/types/stall';
|
|
8
9
|
import type { CronJob } from '../../domain/types/cron';
|
|
9
10
|
import { type DlqConfig, type DlqEntry } from '../../domain/types/dlq';
|
|
@@ -98,6 +99,11 @@ export declare class SqliteStorage {
|
|
|
98
99
|
markFailed(job: Job, error: string | null): void;
|
|
99
100
|
/** Save DLQ entry with full metadata */
|
|
100
101
|
saveDlqEntry(entry: DlqEntry): void;
|
|
102
|
+
/**
|
|
103
|
+
* Commit a terminal child outcome, its optional DLQ row, and its flow outbox
|
|
104
|
+
* record together. Recovery therefore never observes only half the transition.
|
|
105
|
+
*/
|
|
106
|
+
commitFailedJob(jobId: JobId, entry: DlqEntry | null, flowFailure: FlowFailureRecord | null): void;
|
|
101
107
|
/** Delete DLQ entry by job ID */
|
|
102
108
|
deleteDlqEntry(jobId: JobId): void;
|
|
103
109
|
/** Clear all DLQ entries for a queue */
|
|
@@ -129,6 +135,16 @@ export declare class SqliteStorage {
|
|
|
129
135
|
updateRunAt(jobId: JobId, runAt: number): void;
|
|
130
136
|
/** Update a job's children_ids blob and parent_id */
|
|
131
137
|
updateJobChildrenIds(jobId: JobId, childrenIds: JobId[]): void;
|
|
138
|
+
/** Persist both sides of a legacy parent link in one SQLite transaction. */
|
|
139
|
+
updateFlowLink(child: Pick<Job, 'id' | 'parentId' | 'data'>, parent: Pick<Job, 'id' | 'childrenIds' | 'dependsOn' | 'data'>, parentState: 'waiting-children' | 'waiting' | 'prioritized' | 'delayed'): void;
|
|
140
|
+
/** Persist both sides of a removed parent/child relationship atomically. */
|
|
141
|
+
removeFlowLink(child: Pick<Job, 'id' | 'parentId' | 'data'>, parent: Pick<Job, 'id' | 'childrenIds' | 'dependsOn' | 'data' | 'runAt'>, parentState: 'waiting-children' | 'waiting' | 'prioritized'): void;
|
|
142
|
+
/** Record terminal child propagation before applying its parent-side effect. */
|
|
143
|
+
saveFlowFailure(record: FlowFailureRecord): void;
|
|
144
|
+
loadFlowFailures(): FlowFailureRecord[];
|
|
145
|
+
deleteFlowFailure(parentId: JobId, childId?: JobId): void;
|
|
146
|
+
/** Persist dependency removal/promotion as one parent-side recovery checkpoint. */
|
|
147
|
+
updateFlowParentResolution(job: Pick<Job, 'id' | 'dependsOn' | 'runAt' | 'priority'>): void;
|
|
132
148
|
getJob(id: JobId): Job | null;
|
|
133
149
|
storeResult(jobId: JobId, result: unknown): void;
|
|
134
150
|
getResult(jobId: JobId): unknown;
|