@ankhorage/flow 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +3 -1
- package/dist/execution.d.ts +5 -0
- package/dist/execution.d.ts.map +1 -0
- package/dist/execution.js +5 -0
- package/dist/execution.js.map +1 -0
- package/dist/features/execution/adapters/FileFlowPersistenceAdapter.d.ts +4 -0
- package/dist/features/execution/adapters/FileFlowPersistenceAdapter.d.ts.map +1 -0
- package/dist/features/execution/adapters/FileFlowPersistenceAdapter.js +219 -0
- package/dist/features/execution/adapters/FileFlowPersistenceAdapter.js.map +1 -0
- package/dist/features/execution/application/cancelDurableRunAsync.d.ts +8 -0
- package/dist/features/execution/application/cancelDurableRunAsync.d.ts.map +1 -0
- package/dist/features/execution/application/cancelDurableRunAsync.js +22 -0
- package/dist/features/execution/application/cancelDurableRunAsync.js.map +1 -0
- package/dist/features/execution/application/createDurableRunAsync.d.ts +12 -0
- package/dist/features/execution/application/createDurableRunAsync.d.ts.map +1 -0
- package/dist/features/execution/application/createDurableRunAsync.js +8 -0
- package/dist/features/execution/application/createDurableRunAsync.js.map +1 -0
- package/dist/features/execution/application/runDurableWorkerAsync.d.ts +52 -0
- package/dist/features/execution/application/runDurableWorkerAsync.d.ts.map +1 -0
- package/dist/features/execution/application/runDurableWorkerAsync.js +195 -0
- package/dist/features/execution/application/runDurableWorkerAsync.js.map +1 -0
- package/dist/features/execution/domain/calculateRetryDueAt.d.ts +4 -0
- package/dist/features/execution/domain/calculateRetryDueAt.d.ts.map +1 -0
- package/dist/features/execution/domain/calculateRetryDueAt.js +14 -0
- package/dist/features/execution/domain/calculateRetryDueAt.js.map +1 -0
- package/dist/features/execution/domain/createDurableRun.d.ts +7 -0
- package/dist/features/execution/domain/createDurableRun.d.ts.map +1 -0
- package/dist/features/execution/domain/createDurableRun.js +10 -0
- package/dist/features/execution/domain/createDurableRun.js.map +1 -0
- package/dist/features/execution/domain/createRetryWork.d.ts +4 -0
- package/dist/features/execution/domain/createRetryWork.d.ts.map +1 -0
- package/dist/features/execution/domain/createRetryWork.js +17 -0
- package/dist/features/execution/domain/createRetryWork.js.map +1 -0
- package/dist/features/execution/domain/isCronDueAt.d.ts +7 -0
- package/dist/features/execution/domain/isCronDueAt.d.ts.map +1 -0
- package/dist/features/execution/domain/isCronDueAt.js +114 -0
- package/dist/features/execution/domain/isCronDueAt.js.map +1 -0
- package/dist/features/execution/ports/FlowPersistencePort.d.ts +34 -0
- package/dist/features/execution/ports/FlowPersistencePort.d.ts.map +1 -0
- package/dist/features/execution/ports/FlowPersistencePort.js +2 -0
- package/dist/features/execution/ports/FlowPersistencePort.js.map +1 -0
- package/dist/features/security/application/createFlowSecurityPort.js +2 -2
- package/dist/features/security/application/createFlowSecurityPort.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/types/durableExecution.d.ts +55 -0
- package/dist/types/durableExecution.d.ts.map +1 -0
- package/dist/types/durableExecution.js +2 -0
- package/dist/types/durableExecution.js.map +1 -0
- package/package.json +5 -1
- package/src/execution.ts +9 -0
- package/src/features/execution/adapters/FileFlowPersistenceAdapter.ts +333 -0
- package/src/features/execution/application/cancelDurableRunAsync.ts +28 -0
- package/src/features/execution/application/createDurableRunAsync.ts +14 -0
- package/src/features/execution/application/runDurableWorkerAsync.ts +337 -0
- package/src/features/execution/domain/calculateRetryDueAt.ts +21 -0
- package/src/features/execution/domain/createDurableRun.ts +14 -0
- package/src/features/execution/domain/createRetryWork.ts +22 -0
- package/src/features/execution/domain/isCronDueAt.ts +131 -0
- package/src/features/execution/ports/FlowPersistencePort.ts +38 -0
- package/src/features/security/application/createFlowSecurityPort.ts +2 -2
- package/src/index.ts +14 -0
- package/src/types/durableExecution.ts +61 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { FlowSecurityError } from '../../security/FlowSecurityError.js';
|
|
2
|
+
import { createRetryWork } from '../domain/createRetryWork.js';
|
|
3
|
+
/** Claim bounded durable work and execute it with fenced recovery, retry, and approval handling. */
|
|
4
|
+
export const runDurableWorkerAsync = async (input, dependencies) => {
|
|
5
|
+
assertWorkerInput(input);
|
|
6
|
+
const now = dependencies.clock.now();
|
|
7
|
+
const recoveredWorkIds = await dependencies.persistence.recoverExpiredClaimsAsync(now);
|
|
8
|
+
const claimed = await dependencies.persistence.claimDueWorkAsync({
|
|
9
|
+
leaseDurationMs: input.leaseDurationMs,
|
|
10
|
+
limit: Math.min(input.limit, input.concurrency),
|
|
11
|
+
now,
|
|
12
|
+
workerId: input.workerId,
|
|
13
|
+
});
|
|
14
|
+
const outcomes = await Promise.all(claimed.map((work) => processWorkAsync(work, input, dependencies)));
|
|
15
|
+
return { claimedWorkIds: claimed.map((work) => work.id), outcomes, recoveredWorkIds };
|
|
16
|
+
};
|
|
17
|
+
/** Executes one effect through the durable step and security boundaries. */
|
|
18
|
+
const processWorkAsync = async (work, input, dependencies) => {
|
|
19
|
+
const { lease } = work;
|
|
20
|
+
if (lease === undefined)
|
|
21
|
+
return { status: 'lost_lease', workId: work.id };
|
|
22
|
+
const run = await dependencies.persistence.readRunAsync(work.runId);
|
|
23
|
+
if (run === undefined || run.status === 'cancelled') {
|
|
24
|
+
await completeAsync(work, input.workerId, dependencies);
|
|
25
|
+
return { status: 'cancelled', workId: work.id };
|
|
26
|
+
}
|
|
27
|
+
if (isCompleted(run.checkpoint, work.stepId)) {
|
|
28
|
+
await completeAsync(work, input.workerId, dependencies);
|
|
29
|
+
return { status: 'already_completed', workId: work.id };
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const outbox = await prepareOutboxAsync(work, dependencies);
|
|
33
|
+
if (outbox.status === 'delivered') {
|
|
34
|
+
const transitioned = await transitionAsync(run, 'succeeded', completedCheckpoint(run.checkpoint, work.stepId), dependencies);
|
|
35
|
+
const completed = await completeAsync(work, input.workerId, dependencies);
|
|
36
|
+
return {
|
|
37
|
+
status: transitioned && completed ? 'already_completed' : 'lost_lease',
|
|
38
|
+
workId: work.id,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const effectStatus = await executeEffectAsync(work, run, dependencies);
|
|
42
|
+
await dependencies.persistence.markOutboxDeliveredAsync(work.id);
|
|
43
|
+
const transitioned = await transitionAsync(run, 'succeeded', completedCheckpoint(run.checkpoint, work.stepId), dependencies);
|
|
44
|
+
if (!transitioned || !(await completeAsync(work, input.workerId, dependencies)))
|
|
45
|
+
return { status: 'lost_lease', workId: work.id };
|
|
46
|
+
return { status: effectStatus, workId: work.id };
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (isApprovalPending(error))
|
|
50
|
+
return waitForApprovalAsync(work, run, input, dependencies);
|
|
51
|
+
return retryOrFailAsync(work, run, input, dependencies, error);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
/** Create an idempotency-keyed outbox record before delivery, or resume its pending record after a crash. */
|
|
55
|
+
const prepareOutboxAsync = async (work, dependencies) => {
|
|
56
|
+
const existing = await dependencies.persistence.readOutboxAsync(work.id);
|
|
57
|
+
if (existing !== undefined)
|
|
58
|
+
return existing;
|
|
59
|
+
const now = dependencies.clock.now().toISOString();
|
|
60
|
+
await dependencies.persistence.createOutboxAsync({
|
|
61
|
+
createdAt: now,
|
|
62
|
+
id: work.id,
|
|
63
|
+
idempotencyKey: work.idempotencyKey ?? work.id,
|
|
64
|
+
runId: work.runId,
|
|
65
|
+
status: 'pending',
|
|
66
|
+
workId: work.id,
|
|
67
|
+
});
|
|
68
|
+
const created = await dependencies.persistence.readOutboxAsync(work.id);
|
|
69
|
+
if (created === undefined)
|
|
70
|
+
throw new Error('FLOW_OUTBOX_CREATE_FAILED');
|
|
71
|
+
return created;
|
|
72
|
+
};
|
|
73
|
+
/** Runs internal work directly, while protected provider work requires reconciliation and a canonical permit. */
|
|
74
|
+
const executeEffectAsync = async (work, run, dependencies) => {
|
|
75
|
+
const execute = () => dependencies.effects.executeAsync({
|
|
76
|
+
idempotencyKey: work.idempotencyKey ?? work.id,
|
|
77
|
+
run,
|
|
78
|
+
work,
|
|
79
|
+
});
|
|
80
|
+
if (work.effectKind === 'internal') {
|
|
81
|
+
if (work.securityOperation !== undefined)
|
|
82
|
+
throw new Error('FLOW_INTERNAL_EFFECT_SECURITY_OPERATION');
|
|
83
|
+
await execute();
|
|
84
|
+
return 'succeeded';
|
|
85
|
+
}
|
|
86
|
+
if (work.securityOperation === undefined)
|
|
87
|
+
throw new Error('FLOW_PROTECTED_EFFECT_SECURITY_OPERATION');
|
|
88
|
+
if (dependencies.security === undefined)
|
|
89
|
+
throw new Error('FLOW_SECURITY_PORT_REQUIRED');
|
|
90
|
+
if (dependencies.effects.reconcileAsync === undefined)
|
|
91
|
+
throw new Error('FLOW_PROTECTED_EFFECT_RECONCILIATION_REQUIRED');
|
|
92
|
+
if ((await dependencies.effects.reconcileAsync({
|
|
93
|
+
idempotencyKey: work.idempotencyKey ?? work.id,
|
|
94
|
+
run,
|
|
95
|
+
work,
|
|
96
|
+
})) === 'delivered')
|
|
97
|
+
return 'succeeded';
|
|
98
|
+
const permit = await dependencies.security.authorizeAsync(work.securityOperation);
|
|
99
|
+
await permit.executeAsync(async () => execute());
|
|
100
|
+
return 'succeeded';
|
|
101
|
+
};
|
|
102
|
+
/** Persist a waiting approval and defer its at-least-once delivery without consuming the lease. */
|
|
103
|
+
const waitForApprovalAsync = async (work, run, input, dependencies) => {
|
|
104
|
+
const transitioned = await transitionAsync(run, 'waiting_approval', run.checkpoint, dependencies);
|
|
105
|
+
const released = await dependencies.persistence.releaseWorkAsync({
|
|
106
|
+
dueAt: new Date(dependencies.clock.now().getTime() + input.approvalPollIntervalMs).toISOString(),
|
|
107
|
+
id: work.id,
|
|
108
|
+
leaseFencingToken: claimFencingToken(work),
|
|
109
|
+
workerId: input.workerId,
|
|
110
|
+
});
|
|
111
|
+
return { status: transitioned && released ? 'waiting_approval' : 'lost_lease', workId: work.id };
|
|
112
|
+
};
|
|
113
|
+
/** Persist a bounded retry or terminal failure before releasing the claimed delivery. */
|
|
114
|
+
const retryOrFailAsync = async (work, run, input, dependencies, error) => {
|
|
115
|
+
if (work.attempt >= input.retry.maxAttempts) {
|
|
116
|
+
await dependencies.effects.compensateAsync?.({ run, work });
|
|
117
|
+
const transitioned = await transitionAsync(run, 'failed', failedCheckpoint(run.checkpoint, work.stepId), dependencies);
|
|
118
|
+
const completed = await completeAsync(work, input.workerId, dependencies);
|
|
119
|
+
return {
|
|
120
|
+
error: errorMessage(error),
|
|
121
|
+
status: transitioned && completed ? 'failed' : 'lost_lease',
|
|
122
|
+
workId: work.id,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
await dependencies.persistence.enqueueWorkAsync(createRetryWork(work, dependencies.clock.now(), input.retry));
|
|
126
|
+
const completed = await completeAsync(work, input.workerId, dependencies);
|
|
127
|
+
return {
|
|
128
|
+
error: errorMessage(error),
|
|
129
|
+
status: completed ? 'retry_scheduled' : 'lost_lease',
|
|
130
|
+
workId: work.id,
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
/** Compare-and-swap a run state while preserving its immutable flow revision. */
|
|
134
|
+
const transitionAsync = (run, status, checkpoint, dependencies) => dependencies.persistence.transitionRunAsync({
|
|
135
|
+
expectedRevision: run.revision,
|
|
136
|
+
run: {
|
|
137
|
+
...run,
|
|
138
|
+
...(checkpoint === undefined ? {} : { checkpoint }),
|
|
139
|
+
revision: run.revision + 1,
|
|
140
|
+
status,
|
|
141
|
+
updatedAt: dependencies.clock.now().toISOString(),
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
/** Complete a work lease only when its owner and fencing token still match. */
|
|
145
|
+
const completeAsync = (work, workerId, dependencies) => dependencies.persistence.completeWorkAsync({
|
|
146
|
+
id: work.id,
|
|
147
|
+
leaseFencingToken: claimFencingToken(work),
|
|
148
|
+
now: dependencies.clock.now(),
|
|
149
|
+
workerId,
|
|
150
|
+
});
|
|
151
|
+
/** Read the lease token only from a claimed work item. */
|
|
152
|
+
const claimFencingToken = (work) => {
|
|
153
|
+
if (work.lease === undefined)
|
|
154
|
+
throw new Error('FLOW_WORK_LEASE_MISSING');
|
|
155
|
+
return work.lease.fencingToken;
|
|
156
|
+
};
|
|
157
|
+
/** Add a successful step exactly once to an immutable checkpoint. */
|
|
158
|
+
const completedCheckpoint = (checkpoint, stepId) => ({
|
|
159
|
+
completedStepIds: unique([
|
|
160
|
+
...(checkpoint?.completedStepIds ?? []),
|
|
161
|
+
...(stepId === undefined ? [] : [stepId]),
|
|
162
|
+
]),
|
|
163
|
+
...(checkpoint?.data === undefined ? {} : { data: checkpoint.data }),
|
|
164
|
+
failedStepIds: checkpoint?.failedStepIds ?? [],
|
|
165
|
+
});
|
|
166
|
+
/** Add a terminally failed step to an immutable checkpoint. */
|
|
167
|
+
const failedCheckpoint = (checkpoint, stepId) => ({
|
|
168
|
+
completedStepIds: checkpoint?.completedStepIds ?? [],
|
|
169
|
+
...(checkpoint?.data === undefined ? {} : { data: checkpoint.data }),
|
|
170
|
+
failedStepIds: unique([
|
|
171
|
+
...(checkpoint?.failedStepIds ?? []),
|
|
172
|
+
...(stepId === undefined ? [] : [stepId]),
|
|
173
|
+
]),
|
|
174
|
+
});
|
|
175
|
+
/** Check whether a completed checkpoint makes a duplicate delivery a no-op. */
|
|
176
|
+
const isCompleted = (checkpoint, stepId) => stepId !== undefined && checkpoint?.completedStepIds.includes(stepId) === true;
|
|
177
|
+
/** Validate finite worker bounds before touching persistence. */
|
|
178
|
+
const assertWorkerInput = (input) => {
|
|
179
|
+
for (const value of [
|
|
180
|
+
input.approvalPollIntervalMs,
|
|
181
|
+
input.concurrency,
|
|
182
|
+
input.leaseDurationMs,
|
|
183
|
+
input.limit,
|
|
184
|
+
input.retry.maxAttempts,
|
|
185
|
+
])
|
|
186
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
187
|
+
throw new Error('FLOW_WORKER_INPUT_INVALID');
|
|
188
|
+
};
|
|
189
|
+
/** Identify the canonical pending-approval error without interpreting untrusted error shapes. */
|
|
190
|
+
const isApprovalPending = (error) => error instanceof FlowSecurityError && error.code === 'approval_pending';
|
|
191
|
+
/** Preserve a concise, provider-neutral failure signal in the worker result. */
|
|
192
|
+
const errorMessage = (error) => error instanceof Error ? error.message : 'FLOW_EFFECT_FAILED';
|
|
193
|
+
/** Keep persisted checkpoint arrays deterministic. */
|
|
194
|
+
const unique = (values) => [...new Set(values)];
|
|
195
|
+
//# sourceMappingURL=runDurableWorkerAsync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runDurableWorkerAsync.js","sourceRoot":"","sources":["../../../../src/features/execution/application/runDurableWorkerAsync.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n FlowDurableRun,\n FlowDurableWork,\n FlowRetryPolicy,\n FlowRunCheckpoint,\n} from '../../../types/durableExecution.js';\nimport type { FlowClock } from '../../../types/execution.js';\nimport type { FlowSecurityPort } from '../../../types/security.js';\nimport { FlowSecurityError } from '../../security/FlowSecurityError.js';\nimport { createRetryWork } from '../domain/createRetryWork.js';\nimport type { FlowPersistencePort } from '../ports/FlowPersistencePort.js';\n\n/** Claim bounded durable work and execute it with fenced recovery, retry, and approval handling. */\nexport const runDurableWorkerAsync = async (\n input: {\n readonly approvalPollIntervalMs: number;\n readonly concurrency: number;\n readonly leaseDurationMs: number;\n readonly limit: number;\n readonly retry: FlowRetryPolicy;\n readonly workerId: string;\n },\n dependencies: {\n readonly clock: FlowClock;\n readonly effects: FlowDurableEffectPort;\n readonly persistence: FlowPersistencePort;\n readonly security?: FlowSecurityPort;\n },\n): Promise<FlowWorkerResult> => {\n assertWorkerInput(input);\n const now = dependencies.clock.now();\n const recoveredWorkIds = await dependencies.persistence.recoverExpiredClaimsAsync(now);\n const claimed = await dependencies.persistence.claimDueWorkAsync({\n leaseDurationMs: input.leaseDurationMs,\n limit: Math.min(input.limit, input.concurrency),\n now,\n workerId: input.workerId,\n });\n const outcomes = await Promise.all(\n claimed.map((work) => processWorkAsync(work, input, dependencies)),\n );\n return { claimedWorkIds: claimed.map((work) => work.id), outcomes, recoveredWorkIds };\n};\n\n/** Executes one effect through the durable step and security boundaries. */\nconst processWorkAsync = async (\n work: FlowDurableWork,\n input: Parameters<typeof runDurableWorkerAsync>[0],\n dependencies: Parameters<typeof runDurableWorkerAsync>[1],\n): Promise<FlowWorkerOutcome> => {\n const { lease } = work;\n if (lease === undefined) return { status: 'lost_lease', workId: work.id };\n const run = await dependencies.persistence.readRunAsync(work.runId);\n if (run === undefined || run.status === 'cancelled') {\n await completeAsync(work, input.workerId, dependencies);\n return { status: 'cancelled', workId: work.id };\n }\n if (isCompleted(run.checkpoint, work.stepId)) {\n await completeAsync(work, input.workerId, dependencies);\n return { status: 'already_completed', workId: work.id };\n }\n try {\n const outbox = await prepareOutboxAsync(work, dependencies);\n if (outbox.status === 'delivered') {\n const transitioned = await transitionAsync(\n run,\n 'succeeded',\n completedCheckpoint(run.checkpoint, work.stepId),\n dependencies,\n );\n const completed = await completeAsync(work, input.workerId, dependencies);\n return {\n status: transitioned && completed ? 'already_completed' : 'lost_lease',\n workId: work.id,\n };\n }\n const effectStatus = await executeEffectAsync(work, run, dependencies);\n await dependencies.persistence.markOutboxDeliveredAsync(work.id);\n const transitioned = await transitionAsync(\n run,\n 'succeeded',\n completedCheckpoint(run.checkpoint, work.stepId),\n dependencies,\n );\n if (!transitioned || !(await completeAsync(work, input.workerId, dependencies)))\n return { status: 'lost_lease', workId: work.id };\n return { status: effectStatus, workId: work.id };\n } catch (error) {\n if (isApprovalPending(error)) return waitForApprovalAsync(work, run, input, dependencies);\n return retryOrFailAsync(work, run, input, dependencies, error);\n }\n};\n\n/** Create an idempotency-keyed outbox record before delivery, or resume its pending record after a crash. */\nconst prepareOutboxAsync = async (\n work: FlowDurableWork,\n dependencies: Parameters<typeof runDurableWorkerAsync>[1],\n) => {\n const existing = await dependencies.persistence.readOutboxAsync(work.id);\n if (existing !== undefined) return existing;\n const now = dependencies.clock.now().toISOString();\n await dependencies.persistence.createOutboxAsync({\n createdAt: now,\n id: work.id,\n idempotencyKey: work.idempotencyKey ?? work.id,\n runId: work.runId,\n status: 'pending',\n workId: work.id,\n });\n const created = await dependencies.persistence.readOutboxAsync(work.id);\n if (created === undefined) throw new Error('FLOW_OUTBOX_CREATE_FAILED');\n return created;\n};\n\n/** Runs internal work directly, while protected provider work requires reconciliation and a canonical permit. */\nconst executeEffectAsync = async (\n work: FlowDurableWork,\n run: FlowDurableRun,\n dependencies: Parameters<typeof runDurableWorkerAsync>[1],\n): Promise<'succeeded'> => {\n const execute = () =>\n dependencies.effects.executeAsync({\n idempotencyKey: work.idempotencyKey ?? work.id,\n run,\n work,\n });\n if (work.effectKind === 'internal') {\n if (work.securityOperation !== undefined)\n throw new Error('FLOW_INTERNAL_EFFECT_SECURITY_OPERATION');\n await execute();\n return 'succeeded';\n }\n if (work.securityOperation === undefined)\n throw new Error('FLOW_PROTECTED_EFFECT_SECURITY_OPERATION');\n if (dependencies.security === undefined) throw new Error('FLOW_SECURITY_PORT_REQUIRED');\n if (dependencies.effects.reconcileAsync === undefined)\n throw new Error('FLOW_PROTECTED_EFFECT_RECONCILIATION_REQUIRED');\n if (\n (await dependencies.effects.reconcileAsync({\n idempotencyKey: work.idempotencyKey ?? work.id,\n run,\n work,\n })) === 'delivered'\n )\n return 'succeeded';\n const permit = await dependencies.security.authorizeAsync(work.securityOperation);\n await permit.executeAsync(async () => execute());\n return 'succeeded';\n};\n\n/** Persist a waiting approval and defer its at-least-once delivery without consuming the lease. */\nconst waitForApprovalAsync = async (\n work: FlowDurableWork,\n run: FlowDurableRun,\n input: Parameters<typeof runDurableWorkerAsync>[0],\n dependencies: Parameters<typeof runDurableWorkerAsync>[1],\n): Promise<FlowWorkerOutcome> => {\n const transitioned = await transitionAsync(run, 'waiting_approval', run.checkpoint, dependencies);\n const released = await dependencies.persistence.releaseWorkAsync({\n dueAt: new Date(\n dependencies.clock.now().getTime() + input.approvalPollIntervalMs,\n ).toISOString(),\n id: work.id,\n leaseFencingToken: claimFencingToken(work),\n workerId: input.workerId,\n });\n return { status: transitioned && released ? 'waiting_approval' : 'lost_lease', workId: work.id };\n};\n\n/** Persist a bounded retry or terminal failure before releasing the claimed delivery. */\nconst retryOrFailAsync = async (\n work: FlowDurableWork,\n run: FlowDurableRun,\n input: Parameters<typeof runDurableWorkerAsync>[0],\n dependencies: Parameters<typeof runDurableWorkerAsync>[1],\n error: unknown,\n): Promise<FlowWorkerOutcome> => {\n if (work.attempt >= input.retry.maxAttempts) {\n await dependencies.effects.compensateAsync?.({ run, work });\n const transitioned = await transitionAsync(\n run,\n 'failed',\n failedCheckpoint(run.checkpoint, work.stepId),\n dependencies,\n );\n const completed = await completeAsync(work, input.workerId, dependencies);\n return {\n error: errorMessage(error),\n status: transitioned && completed ? 'failed' : 'lost_lease',\n workId: work.id,\n };\n }\n await dependencies.persistence.enqueueWorkAsync(\n createRetryWork(work, dependencies.clock.now(), input.retry),\n );\n const completed = await completeAsync(work, input.workerId, dependencies);\n return {\n error: errorMessage(error),\n status: completed ? 'retry_scheduled' : 'lost_lease',\n workId: work.id,\n };\n};\n\n/** Compare-and-swap a run state while preserving its immutable flow revision. */\nconst transitionAsync = (\n run: FlowDurableRun,\n status: FlowDurableRun['status'],\n checkpoint: FlowRunCheckpoint | undefined,\n dependencies: Parameters<typeof runDurableWorkerAsync>[1],\n): Promise<boolean> =>\n dependencies.persistence.transitionRunAsync({\n expectedRevision: run.revision,\n run: {\n ...run,\n ...(checkpoint === undefined ? {} : { checkpoint }),\n revision: run.revision + 1,\n status,\n updatedAt: dependencies.clock.now().toISOString(),\n },\n });\n\n/** Complete a work lease only when its owner and fencing token still match. */\nconst completeAsync = (\n work: FlowDurableWork,\n workerId: string,\n dependencies: Parameters<typeof runDurableWorkerAsync>[1],\n): Promise<boolean> =>\n dependencies.persistence.completeWorkAsync({\n id: work.id,\n leaseFencingToken: claimFencingToken(work),\n now: dependencies.clock.now(),\n workerId,\n });\n\n/** Read the lease token only from a claimed work item. */\nconst claimFencingToken = (work: FlowDurableWork): number => {\n if (work.lease === undefined) throw new Error('FLOW_WORK_LEASE_MISSING');\n return work.lease.fencingToken;\n};\n\n/** Add a successful step exactly once to an immutable checkpoint. */\nconst completedCheckpoint = (\n checkpoint: FlowRunCheckpoint | undefined,\n stepId: string | undefined,\n): FlowRunCheckpoint => ({\n completedStepIds: unique([\n ...(checkpoint?.completedStepIds ?? []),\n ...(stepId === undefined ? [] : [stepId]),\n ]),\n ...(checkpoint?.data === undefined ? {} : { data: checkpoint.data }),\n failedStepIds: checkpoint?.failedStepIds ?? [],\n});\n\n/** Add a terminally failed step to an immutable checkpoint. */\nconst failedCheckpoint = (\n checkpoint: FlowRunCheckpoint | undefined,\n stepId: string | undefined,\n): FlowRunCheckpoint => ({\n completedStepIds: checkpoint?.completedStepIds ?? [],\n ...(checkpoint?.data === undefined ? {} : { data: checkpoint.data }),\n failedStepIds: unique([\n ...(checkpoint?.failedStepIds ?? []),\n ...(stepId === undefined ? [] : [stepId]),\n ]),\n});\n\n/** Check whether a completed checkpoint makes a duplicate delivery a no-op. */\nconst isCompleted = (\n checkpoint: FlowRunCheckpoint | undefined,\n stepId: string | undefined,\n): boolean => stepId !== undefined && checkpoint?.completedStepIds.includes(stepId) === true;\n\n/** Validate finite worker bounds before touching persistence. */\nconst assertWorkerInput = (input: Parameters<typeof runDurableWorkerAsync>[0]): void => {\n for (const value of [\n input.approvalPollIntervalMs,\n input.concurrency,\n input.leaseDurationMs,\n input.limit,\n input.retry.maxAttempts,\n ])\n if (!Number.isSafeInteger(value) || value < 1) throw new Error('FLOW_WORKER_INPUT_INVALID');\n};\n\n/** Identify the canonical pending-approval error without interpreting untrusted error shapes. */\nconst isApprovalPending = (error: unknown): boolean =>\n error instanceof FlowSecurityError && error.code === 'approval_pending';\n\n/** Preserve a concise, provider-neutral failure signal in the worker result. */\nconst errorMessage = (error: unknown): string =>\n error instanceof Error ? error.message : 'FLOW_EFFECT_FAILED';\n\n/** Keep persisted checkpoint arrays deterministic. */\nconst unique = (values: readonly string[]): readonly string[] => [...new Set(values)];\n\n/** Application-owned effect boundary; adapters must apply the supplied idempotency key at their side-effect boundary. */\nexport interface FlowDurableEffectPort {\n readonly compensateAsync?: (input: {\n readonly run: FlowDurableRun;\n readonly work: FlowDurableWork;\n }) => Promise<void>;\n readonly executeAsync: (input: {\n readonly idempotencyKey: string;\n readonly run: FlowDurableRun;\n readonly work: FlowDurableWork;\n }) => Promise<void>;\n /**\n * Query the provider's idempotency boundary before a protected replay. A provider must return\n * `delivered` only when the stable key proves that the original effect completed.\n */\n readonly reconcileAsync?: (input: {\n readonly idempotencyKey: string;\n readonly run: FlowDurableRun;\n readonly work: FlowDurableWork;\n }) => Promise<'delivered' | 'unknown'>;\n}\n\n/** Observable outcome for every delivery claimed by one bounded worker tick. */\nexport interface FlowWorkerOutcome {\n readonly error?: string;\n readonly status:\n | 'already_completed'\n | 'cancelled'\n | 'failed'\n | 'lost_lease'\n | 'retry_scheduled'\n | 'succeeded'\n | 'waiting_approval';\n readonly workId: string;\n}\n\n/** Summary of one worker tick; delivery remains at-least-once until fenced completion. */\nexport interface FlowWorkerResult {\n readonly claimedWorkIds: readonly string[];\n readonly outcomes: readonly FlowWorkerOutcome[];\n readonly recoveredWorkIds: readonly string[];\n}\n"]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FlowRetryPolicy } from '../../../types/durableExecution.js';
|
|
2
|
+
/*** Calculate an exponential retry time from persisted attempt state without ambient time. */
|
|
3
|
+
export declare const calculateRetryDueAt: (now: Date, completedAttempt: number, policy: FlowRetryPolicy) => string;
|
|
4
|
+
//# sourceMappingURL=calculateRetryDueAt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calculateRetryDueAt.d.ts","sourceRoot":"","sources":["../../../../src/features/execution/domain/calculateRetryDueAt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAE1E,8FAA8F;AAC9F,eAAO,MAAM,mBAAmB,GAC9B,KAAK,IAAI,EACT,kBAAkB,MAAM,EACxB,QAAQ,eAAe,KACtB,MAaF,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*** Calculate an exponential retry time from persisted attempt state without ambient time. */
|
|
2
|
+
export const calculateRetryDueAt = (now, completedAttempt, policy) => {
|
|
3
|
+
if (!Number.isSafeInteger(completedAttempt) || completedAttempt < 1)
|
|
4
|
+
throw new Error('FLOW_RETRY_ATTEMPT_INVALID');
|
|
5
|
+
if (!Number.isSafeInteger(policy.baseDelayMs) || policy.baseDelayMs < 0)
|
|
6
|
+
throw new Error('FLOW_RETRY_BASE_DELAY_INVALID');
|
|
7
|
+
if (!Number.isSafeInteger(policy.maxDelayMs) ||
|
|
8
|
+
policy.maxDelayMs < policy.baseDelayMs ||
|
|
9
|
+
policy.maxDelayMs < 0)
|
|
10
|
+
throw new Error('FLOW_RETRY_MAX_DELAY_INVALID');
|
|
11
|
+
const delay = Math.min(policy.maxDelayMs, policy.baseDelayMs * 2 ** (completedAttempt - 1));
|
|
12
|
+
return new Date(now.getTime() + delay).toISOString();
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=calculateRetryDueAt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calculateRetryDueAt.js","sourceRoot":"","sources":["../../../../src/features/execution/domain/calculateRetryDueAt.ts"],"names":[],"mappings":"AAEA,8FAA8F;AAC9F,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,GAAS,EACT,gBAAwB,EACxB,MAAuB,EACf,EAAE;IACV,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,gBAAgB,CAAC,IAAI,gBAAgB,GAAG,CAAC;QACjE,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;IAChD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,WAAW,CAAC,IAAI,MAAM,CAAC,WAAW,GAAG,CAAC;QACrE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IACnD,IACE,CAAC,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,UAAU,CAAC;QACxC,MAAM,CAAC,UAAU,GAAG,MAAM,CAAC,WAAW;QACtC,MAAM,CAAC,UAAU,GAAG,CAAC;QAErB,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,WAAW,GAAG,CAAC,IAAI,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC;IAC5F,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,KAAK,CAAC,CAAC,WAAW,EAAE,CAAC;AACvD,CAAC,CAAC","sourcesContent":["import type { FlowRetryPolicy } from '../../../types/durableExecution.js';\n\n/*** Calculate an exponential retry time from persisted attempt state without ambient time. */\nexport const calculateRetryDueAt = (\n now: Date,\n completedAttempt: number,\n policy: FlowRetryPolicy,\n): string => {\n if (!Number.isSafeInteger(completedAttempt) || completedAttempt < 1)\n throw new Error('FLOW_RETRY_ATTEMPT_INVALID');\n if (!Number.isSafeInteger(policy.baseDelayMs) || policy.baseDelayMs < 0)\n throw new Error('FLOW_RETRY_BASE_DELAY_INVALID');\n if (\n !Number.isSafeInteger(policy.maxDelayMs) ||\n policy.maxDelayMs < policy.baseDelayMs ||\n policy.maxDelayMs < 0\n )\n throw new Error('FLOW_RETRY_MAX_DELAY_INVALID');\n const delay = Math.min(policy.maxDelayMs, policy.baseDelayMs * 2 ** (completedAttempt - 1));\n return new Date(now.getTime() + delay).toISOString();\n};\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FlowDefinitionRef, FlowDurableRun } from '../../../types/durableExecution.js';
|
|
2
|
+
/*** Create a revision-zero run pinned permanently to one published flow definition. */
|
|
3
|
+
export declare const createDurableRun: (input: {
|
|
4
|
+
readonly flow: FlowDefinitionRef;
|
|
5
|
+
readonly id: string;
|
|
6
|
+
}, now: Date) => FlowDurableRun;
|
|
7
|
+
//# sourceMappingURL=createDurableRun.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createDurableRun.d.ts","sourceRoot":"","sources":["../../../../src/features/execution/domain/createDurableRun.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAE5F,uFAAuF;AACvF,eAAO,MAAM,gBAAgB,GAC3B,OAAO;IAAE,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,EAChE,KAAK,IAAI,KACR,cAOD,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/*** Create a revision-zero run pinned permanently to one published flow definition. */
|
|
2
|
+
export const createDurableRun = (input, now) => ({
|
|
3
|
+
createdAt: now.toISOString(),
|
|
4
|
+
flow: { ...input.flow },
|
|
5
|
+
id: input.id,
|
|
6
|
+
revision: 0,
|
|
7
|
+
status: 'queued',
|
|
8
|
+
updatedAt: now.toISOString(),
|
|
9
|
+
});
|
|
10
|
+
//# sourceMappingURL=createDurableRun.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createDurableRun.js","sourceRoot":"","sources":["../../../../src/features/execution/domain/createDurableRun.ts"],"names":[],"mappings":"AAEA,uFAAuF;AACvF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAC9B,KAAgE,EAChE,GAAS,EACO,EAAE,CAAC,CAAC;IACpB,SAAS,EAAE,GAAG,CAAC,WAAW,EAAE;IAC5B,IAAI,EAAE,EAAE,GAAG,KAAK,CAAC,IAAI,EAAE;IACvB,EAAE,EAAE,KAAK,CAAC,EAAE;IACZ,QAAQ,EAAE,CAAC;IACX,MAAM,EAAE,QAAQ;IAChB,SAAS,EAAE,GAAG,CAAC,WAAW,EAAE;CAC7B,CAAC,CAAC","sourcesContent":["import type { FlowDefinitionRef, FlowDurableRun } from '../../../types/durableExecution.js';\n\n/*** Create a revision-zero run pinned permanently to one published flow definition. */\nexport const createDurableRun = (\n input: { readonly flow: FlowDefinitionRef; readonly id: string },\n now: Date,\n): FlowDurableRun => ({\n createdAt: now.toISOString(),\n flow: { ...input.flow },\n id: input.id,\n revision: 0,\n status: 'queued',\n updatedAt: now.toISOString(),\n});\n"]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FlowDurableWork, FlowRetryPolicy } from '../../../types/durableExecution.js';
|
|
2
|
+
/*** Create the next immutable work attempt from a completed attempt and controlled clock. */
|
|
3
|
+
export declare const createRetryWork: (completed: FlowDurableWork, now: Date, policy: FlowRetryPolicy) => FlowDurableWork;
|
|
4
|
+
//# sourceMappingURL=createRetryWork.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createRetryWork.d.ts","sourceRoot":"","sources":["../../../../src/features/execution/domain/createRetryWork.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAG3F,6FAA6F;AAC7F,eAAO,MAAM,eAAe,GAC1B,WAAW,eAAe,EAC1B,KAAK,IAAI,EACT,QAAQ,eAAe,KACtB,eAaD,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { calculateRetryDueAt } from './calculateRetryDueAt.js';
|
|
2
|
+
/*** Create the next immutable work attempt from a completed attempt and controlled clock. */
|
|
3
|
+
export const createRetryWork = (completed, now, policy) => ({
|
|
4
|
+
...(completed.idempotencyKey === undefined ? {} : { idempotencyKey: completed.idempotencyKey }),
|
|
5
|
+
attempt: completed.attempt + 1,
|
|
6
|
+
dueAt: calculateRetryDueAt(now, completed.attempt, policy),
|
|
7
|
+
effectKind: completed.effectKind,
|
|
8
|
+
flow: { ...completed.flow },
|
|
9
|
+
id: `${completed.runId}:attempt:${completed.attempt + 1}`,
|
|
10
|
+
runId: completed.runId,
|
|
11
|
+
...(completed.securityOperation === undefined
|
|
12
|
+
? {}
|
|
13
|
+
: { securityOperation: completed.securityOperation }),
|
|
14
|
+
...(completed.stepId === undefined ? {} : { stepId: completed.stepId }),
|
|
15
|
+
status: 'ready',
|
|
16
|
+
});
|
|
17
|
+
//# sourceMappingURL=createRetryWork.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createRetryWork.js","sourceRoot":"","sources":["../../../../src/features/execution/domain/createRetryWork.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAE/D,6FAA6F;AAC7F,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,SAA0B,EAC1B,GAAS,EACT,MAAuB,EACN,EAAE,CAAC,CAAC;IACrB,GAAG,CAAC,SAAS,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,SAAS,CAAC,cAAc,EAAE,CAAC;IAC/F,OAAO,EAAE,SAAS,CAAC,OAAO,GAAG,CAAC;IAC9B,KAAK,EAAE,mBAAmB,CAAC,GAAG,EAAE,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC;IAC1D,UAAU,EAAE,SAAS,CAAC,UAAU;IAChC,IAAI,EAAE,EAAE,GAAG,SAAS,CAAC,IAAI,EAAE;IAC3B,EAAE,EAAE,GAAG,SAAS,CAAC,KAAK,YAAY,SAAS,CAAC,OAAO,GAAG,CAAC,EAAE;IACzD,KAAK,EAAE,SAAS,CAAC,KAAK;IACtB,GAAG,CAAC,SAAS,CAAC,iBAAiB,KAAK,SAAS;QAC3C,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,EAAE,iBAAiB,EAAE,SAAS,CAAC,iBAAiB,EAAE,CAAC;IACvD,GAAG,CAAC,SAAS,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,CAAC;IACvE,MAAM,EAAE,OAAO;CAChB,CAAC,CAAC","sourcesContent":["import type { FlowDurableWork, FlowRetryPolicy } from '../../../types/durableExecution.js';\nimport { calculateRetryDueAt } from './calculateRetryDueAt.js';\n\n/*** Create the next immutable work attempt from a completed attempt and controlled clock. */\nexport const createRetryWork = (\n completed: FlowDurableWork,\n now: Date,\n policy: FlowRetryPolicy,\n): FlowDurableWork => ({\n ...(completed.idempotencyKey === undefined ? {} : { idempotencyKey: completed.idempotencyKey }),\n attempt: completed.attempt + 1,\n dueAt: calculateRetryDueAt(now, completed.attempt, policy),\n effectKind: completed.effectKind,\n flow: { ...completed.flow },\n id: `${completed.runId}:attempt:${completed.attempt + 1}`,\n runId: completed.runId,\n ...(completed.securityOperation === undefined\n ? {}\n : { securityOperation: completed.securityOperation }),\n ...(completed.stepId === undefined ? {} : { stepId: completed.stepId }),\n status: 'ready',\n});\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Determine whether one UTC instant matches a five-field POSIX cron expression in an IANA zone. */
|
|
2
|
+
export declare const isCronDueAt: (input: {
|
|
3
|
+
readonly cron: string;
|
|
4
|
+
readonly now: Date;
|
|
5
|
+
readonly timezone?: string;
|
|
6
|
+
}) => boolean;
|
|
7
|
+
//# sourceMappingURL=isCronDueAt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isCronDueAt.d.ts","sourceRoot":"","sources":["../../../../src/features/execution/domain/isCronDueAt.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,eAAO,MAAM,WAAW,GAAI,OAAO;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;IACnB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B,KAAG,OAuBH,CAAC"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/** Determine whether one UTC instant matches a five-field POSIX cron expression in an IANA zone. */
|
|
2
|
+
export const isCronDueAt = (input) => {
|
|
3
|
+
const fields = input.cron.trim().split(/\s+/);
|
|
4
|
+
if (fields.length !== 5)
|
|
5
|
+
throw new Error('FLOW_CRON_INVALID');
|
|
6
|
+
const local = localParts(input.now, input.timezone ?? 'Etc/UTC');
|
|
7
|
+
const [minute, hour, dayOfMonth, month, dayOfWeek] = fields;
|
|
8
|
+
if (minute === undefined ||
|
|
9
|
+
hour === undefined ||
|
|
10
|
+
dayOfMonth === undefined ||
|
|
11
|
+
month === undefined ||
|
|
12
|
+
dayOfWeek === undefined)
|
|
13
|
+
throw new Error('FLOW_CRON_INVALID');
|
|
14
|
+
const minuteMatches = matchesField(minute, local.minute, 0, 59);
|
|
15
|
+
const hourMatches = matchesField(hour, local.hour, 0, 23);
|
|
16
|
+
const monthMatches = matchesField(month, local.month, 1, 12);
|
|
17
|
+
const dayOfMonthMatches = matchesField(dayOfMonth, local.day, 1, 31);
|
|
18
|
+
const dayOfWeekMatches = matchesField(dayOfWeek, local.dayOfWeek, 0, 7);
|
|
19
|
+
const dayMatches = dayOfMonth === '*' || dayOfWeek === '*'
|
|
20
|
+
? dayOfMonthMatches && dayOfWeekMatches
|
|
21
|
+
: dayOfMonthMatches || dayOfWeekMatches;
|
|
22
|
+
return minuteMatches && hourMatches && monthMatches && dayMatches;
|
|
23
|
+
};
|
|
24
|
+
/** Format a UTC instant into its real local wall-clock fields; duplicate fall-back instants remain distinct instants. */
|
|
25
|
+
const localParts = (now, timezone) => {
|
|
26
|
+
try {
|
|
27
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
28
|
+
day: 'numeric',
|
|
29
|
+
hour: 'numeric',
|
|
30
|
+
hourCycle: 'h23',
|
|
31
|
+
minute: 'numeric',
|
|
32
|
+
month: 'numeric',
|
|
33
|
+
timeZone: timezone,
|
|
34
|
+
weekday: 'short',
|
|
35
|
+
}).formatToParts(now);
|
|
36
|
+
const read = (type) => parts.find((part) => part.type === type)?.value ?? invalidTimezone();
|
|
37
|
+
return {
|
|
38
|
+
day: Number(read('day')),
|
|
39
|
+
dayOfWeek: weekday(read('weekday')),
|
|
40
|
+
hour: Number(read('hour')),
|
|
41
|
+
minute: Number(read('minute')),
|
|
42
|
+
month: Number(read('month')),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new Error('FLOW_CRON_TIMEZONE_INVALID');
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
/** Match one POSIX cron field including lists, ranges, and positive step values. */
|
|
50
|
+
const matchesField = (field, value, minimum, maximum) => field.split(',').some((part) => matchesPart(part, value, minimum, maximum));
|
|
51
|
+
/** Match one validated cron list member. */
|
|
52
|
+
const matchesPart = (part, value, minimum, maximum) => {
|
|
53
|
+
const [rangePart, stepPart] = part.split('/');
|
|
54
|
+
if (part.split('/').length > 2 || rangePart === undefined)
|
|
55
|
+
throw new Error('FLOW_CRON_INVALID');
|
|
56
|
+
const step = stepPart === undefined ? 1 : parseValue(stepPart, 1, maximum - minimum + 1);
|
|
57
|
+
const [start, end] = parseRange(rangePart, minimum, maximum);
|
|
58
|
+
return value >= start && value <= end && (value - start) % step === 0;
|
|
59
|
+
};
|
|
60
|
+
/** Parse a wildcard, number, or inclusive numeric range. */
|
|
61
|
+
const parseRange = (range, minimum, maximum) => {
|
|
62
|
+
if (range === '*')
|
|
63
|
+
return [minimum, maximum];
|
|
64
|
+
const values = range.split('-');
|
|
65
|
+
if (values.length === 1) {
|
|
66
|
+
const [single] = values;
|
|
67
|
+
if (single === undefined)
|
|
68
|
+
throw new Error('FLOW_CRON_INVALID');
|
|
69
|
+
const value = parseValue(single, minimum, maximum);
|
|
70
|
+
return [value, value];
|
|
71
|
+
}
|
|
72
|
+
if (values.length !== 2 || values[0] === undefined || values[1] === undefined)
|
|
73
|
+
throw new Error('FLOW_CRON_INVALID');
|
|
74
|
+
const start = parseValue(values[0], minimum, maximum);
|
|
75
|
+
const end = parseValue(values[1], minimum, maximum);
|
|
76
|
+
if (end < start)
|
|
77
|
+
throw new Error('FLOW_CRON_INVALID');
|
|
78
|
+
return [start, end];
|
|
79
|
+
};
|
|
80
|
+
/** Parse one strict POSIX cron integer. */
|
|
81
|
+
const parseValue = (value, minimum, maximum) => {
|
|
82
|
+
if (!/^\d+$/.test(value))
|
|
83
|
+
throw new Error('FLOW_CRON_INVALID');
|
|
84
|
+
const parsed = Number(value);
|
|
85
|
+
if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum)
|
|
86
|
+
throw new Error('FLOW_CRON_INVALID');
|
|
87
|
+
return parsed;
|
|
88
|
+
};
|
|
89
|
+
/** Map English short weekday names produced by the fixed formatter to POSIX weekday values. */
|
|
90
|
+
const weekday = (value) => {
|
|
91
|
+
switch (value) {
|
|
92
|
+
case 'Sun':
|
|
93
|
+
return 0;
|
|
94
|
+
case 'Mon':
|
|
95
|
+
return 1;
|
|
96
|
+
case 'Tue':
|
|
97
|
+
return 2;
|
|
98
|
+
case 'Wed':
|
|
99
|
+
return 3;
|
|
100
|
+
case 'Thu':
|
|
101
|
+
return 4;
|
|
102
|
+
case 'Fri':
|
|
103
|
+
return 5;
|
|
104
|
+
case 'Sat':
|
|
105
|
+
return 6;
|
|
106
|
+
default:
|
|
107
|
+
return invalidTimezone();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
/** Keep an impossible formatter shape distinct from a cron syntax error. */
|
|
111
|
+
const invalidTimezone = () => {
|
|
112
|
+
throw new Error('FLOW_CRON_TIMEZONE_INVALID');
|
|
113
|
+
};
|
|
114
|
+
//# sourceMappingURL=isCronDueAt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"isCronDueAt.js","sourceRoot":"","sources":["../../../../src/features/execution/domain/isCronDueAt.ts"],"names":[],"mappings":"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","sourcesContent":["/** Determine whether one UTC instant matches a five-field POSIX cron expression in an IANA zone. */\nexport const isCronDueAt = (input: {\n readonly cron: string;\n readonly now: Date;\n readonly timezone?: string;\n}): boolean => {\n const fields = input.cron.trim().split(/\\s+/);\n if (fields.length !== 5) throw new Error('FLOW_CRON_INVALID');\n const local = localParts(input.now, input.timezone ?? 'Etc/UTC');\n const [minute, hour, dayOfMonth, month, dayOfWeek] = fields;\n if (\n minute === undefined ||\n hour === undefined ||\n dayOfMonth === undefined ||\n month === undefined ||\n dayOfWeek === undefined\n )\n throw new Error('FLOW_CRON_INVALID');\n const minuteMatches = matchesField(minute, local.minute, 0, 59);\n const hourMatches = matchesField(hour, local.hour, 0, 23);\n const monthMatches = matchesField(month, local.month, 1, 12);\n const dayOfMonthMatches = matchesField(dayOfMonth, local.day, 1, 31);\n const dayOfWeekMatches = matchesField(dayOfWeek, local.dayOfWeek, 0, 7);\n const dayMatches =\n dayOfMonth === '*' || dayOfWeek === '*'\n ? dayOfMonthMatches && dayOfWeekMatches\n : dayOfMonthMatches || dayOfWeekMatches;\n return minuteMatches && hourMatches && monthMatches && dayMatches;\n};\n\n/** Format a UTC instant into its real local wall-clock fields; duplicate fall-back instants remain distinct instants. */\nconst localParts = (\n now: Date,\n timezone: string,\n): {\n readonly day: number;\n readonly dayOfWeek: number;\n readonly hour: number;\n readonly minute: number;\n readonly month: number;\n} => {\n try {\n const parts = new Intl.DateTimeFormat('en-US', {\n day: 'numeric',\n hour: 'numeric',\n hourCycle: 'h23',\n minute: 'numeric',\n month: 'numeric',\n timeZone: timezone,\n weekday: 'short',\n }).formatToParts(now);\n const read = (type: Intl.DateTimeFormatPartTypes): string =>\n parts.find((part) => part.type === type)?.value ?? invalidTimezone();\n return {\n day: Number(read('day')),\n dayOfWeek: weekday(read('weekday')),\n hour: Number(read('hour')),\n minute: Number(read('minute')),\n month: Number(read('month')),\n };\n } catch {\n throw new Error('FLOW_CRON_TIMEZONE_INVALID');\n }\n};\n\n/** Match one POSIX cron field including lists, ranges, and positive step values. */\nconst matchesField = (field: string, value: number, minimum: number, maximum: number): boolean =>\n field.split(',').some((part) => matchesPart(part, value, minimum, maximum));\n\n/** Match one validated cron list member. */\nconst matchesPart = (part: string, value: number, minimum: number, maximum: number): boolean => {\n const [rangePart, stepPart] = part.split('/');\n if (part.split('/').length > 2 || rangePart === undefined) throw new Error('FLOW_CRON_INVALID');\n const step = stepPart === undefined ? 1 : parseValue(stepPart, 1, maximum - minimum + 1);\n const [start, end] = parseRange(rangePart, minimum, maximum);\n return value >= start && value <= end && (value - start) % step === 0;\n};\n\n/** Parse a wildcard, number, or inclusive numeric range. */\nconst parseRange = (range: string, minimum: number, maximum: number): readonly [number, number] => {\n if (range === '*') return [minimum, maximum];\n const values = range.split('-');\n if (values.length === 1) {\n const [single] = values;\n if (single === undefined) throw new Error('FLOW_CRON_INVALID');\n const value = parseValue(single, minimum, maximum);\n return [value, value];\n }\n if (values.length !== 2 || values[0] === undefined || values[1] === undefined)\n throw new Error('FLOW_CRON_INVALID');\n const start = parseValue(values[0], minimum, maximum);\n const end = parseValue(values[1], minimum, maximum);\n if (end < start) throw new Error('FLOW_CRON_INVALID');\n return [start, end];\n};\n\n/** Parse one strict POSIX cron integer. */\nconst parseValue = (value: string, minimum: number, maximum: number): number => {\n if (!/^\\d+$/.test(value)) throw new Error('FLOW_CRON_INVALID');\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < minimum || parsed > maximum)\n throw new Error('FLOW_CRON_INVALID');\n return parsed;\n};\n\n/** Map English short weekday names produced by the fixed formatter to POSIX weekday values. */\nconst weekday = (value: string): number => {\n switch (value) {\n case 'Sun':\n return 0;\n case 'Mon':\n return 1;\n case 'Tue':\n return 2;\n case 'Wed':\n return 3;\n case 'Thu':\n return 4;\n case 'Fri':\n return 5;\n case 'Sat':\n return 6;\n default:\n return invalidTimezone();\n }\n};\n\n/** Keep an impossible formatter shape distinct from a cron syntax error. */\nconst invalidTimezone = (): never => {\n throw new Error('FLOW_CRON_TIMEZONE_INVALID');\n};\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FlowDurableRun, FlowDurableWork, FlowOutboxRecord } from '../../../types/durableExecution.js';
|
|
2
|
+
export interface FlowPersistencePort {
|
|
3
|
+
readonly cancelRunWorkAsync: (runId: string) => Promise<readonly string[]>;
|
|
4
|
+
readonly claimDueWorkAsync: (input: {
|
|
5
|
+
readonly leaseDurationMs: number;
|
|
6
|
+
readonly limit: number;
|
|
7
|
+
readonly now: Date;
|
|
8
|
+
readonly workerId: string;
|
|
9
|
+
}) => Promise<readonly FlowDurableWork[]>;
|
|
10
|
+
readonly completeWorkAsync: (input: {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly leaseFencingToken: number;
|
|
13
|
+
readonly now: Date;
|
|
14
|
+
readonly workerId: string;
|
|
15
|
+
}) => Promise<boolean>;
|
|
16
|
+
readonly createOutboxAsync: (record: FlowOutboxRecord) => Promise<void>;
|
|
17
|
+
readonly createRunAsync: (run: FlowDurableRun) => Promise<void>;
|
|
18
|
+
readonly enqueueWorkAsync: (work: FlowDurableWork) => Promise<void>;
|
|
19
|
+
readonly markOutboxDeliveredAsync: (id: string) => Promise<boolean>;
|
|
20
|
+
readonly readOutboxAsync: (id: string) => Promise<FlowOutboxRecord | undefined>;
|
|
21
|
+
readonly readRunAsync: (id: string) => Promise<FlowDurableRun | undefined>;
|
|
22
|
+
readonly recoverExpiredClaimsAsync: (now: Date) => Promise<readonly string[]>;
|
|
23
|
+
readonly releaseWorkAsync: (input: {
|
|
24
|
+
readonly dueAt: string;
|
|
25
|
+
readonly id: string;
|
|
26
|
+
readonly leaseFencingToken: number;
|
|
27
|
+
readonly workerId: string;
|
|
28
|
+
}) => Promise<boolean>;
|
|
29
|
+
readonly transitionRunAsync: (input: {
|
|
30
|
+
readonly expectedRevision: number;
|
|
31
|
+
readonly run: FlowDurableRun;
|
|
32
|
+
}) => Promise<boolean>;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=FlowPersistencePort.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FlowPersistencePort.d.ts","sourceRoot":"","sources":["../../../../src/features/execution/ports/FlowPersistencePort.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,gBAAgB,EACjB,MAAM,oCAAoC,CAAC;AAE5C,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,kBAAkB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IAC3E,QAAQ,CAAC,iBAAiB,EAAE,CAAC,KAAK,EAAE;QAClC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;QACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;QACnB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;KAC3B,KAAK,OAAO,CAAC,SAAS,eAAe,EAAE,CAAC,CAAC;IAC1C,QAAQ,CAAC,iBAAiB,EAAE,CAAC,KAAK,EAAE;QAClC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;QACnC,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;QACnB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;KAC3B,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACvB,QAAQ,CAAC,iBAAiB,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,QAAQ,CAAC,cAAc,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE,QAAQ,CAAC,gBAAgB,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,QAAQ,CAAC,wBAAwB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACpE,QAAQ,CAAC,eAAe,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAAC;IAChF,QAAQ,CAAC,YAAY,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IAC3E,QAAQ,CAAC,yBAAyB,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,OAAO,CAAC,SAAS,MAAM,EAAE,CAAC,CAAC;IAC9E,QAAQ,CAAC,gBAAgB,EAAE,CAAC,KAAK,EAAE;QACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;QACpB,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;QACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;KAC3B,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACvB,QAAQ,CAAC,kBAAkB,EAAE,CAAC,KAAK,EAAE;QACnC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;QAClC,QAAQ,CAAC,GAAG,EAAE,cAAc,CAAC;KAC9B,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACxB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FlowPersistencePort.js","sourceRoot":"","sources":["../../../../src/features/execution/ports/FlowPersistencePort.ts"],"names":[],"mappings":"","sourcesContent":["import type {\n FlowDurableRun,\n FlowDurableWork,\n FlowOutboxRecord,\n} from '../../../types/durableExecution.js';\n\nexport interface FlowPersistencePort {\n readonly cancelRunWorkAsync: (runId: string) => Promise<readonly string[]>;\n readonly claimDueWorkAsync: (input: {\n readonly leaseDurationMs: number;\n readonly limit: number;\n readonly now: Date;\n readonly workerId: string;\n }) => Promise<readonly FlowDurableWork[]>;\n readonly completeWorkAsync: (input: {\n readonly id: string;\n readonly leaseFencingToken: number;\n readonly now: Date;\n readonly workerId: string;\n }) => Promise<boolean>;\n readonly createOutboxAsync: (record: FlowOutboxRecord) => Promise<void>;\n readonly createRunAsync: (run: FlowDurableRun) => Promise<void>;\n readonly enqueueWorkAsync: (work: FlowDurableWork) => Promise<void>;\n readonly markOutboxDeliveredAsync: (id: string) => Promise<boolean>;\n readonly readOutboxAsync: (id: string) => Promise<FlowOutboxRecord | undefined>;\n readonly readRunAsync: (id: string) => Promise<FlowDurableRun | undefined>;\n readonly recoverExpiredClaimsAsync: (now: Date) => Promise<readonly string[]>;\n readonly releaseWorkAsync: (input: {\n readonly dueAt: string;\n readonly id: string;\n readonly leaseFencingToken: number;\n readonly workerId: string;\n }) => Promise<boolean>;\n readonly transitionRunAsync: (input: {\n readonly expectedRevision: number;\n readonly run: FlowDurableRun;\n }) => Promise<boolean>;\n}\n"]}
|
|
@@ -17,9 +17,9 @@ const authorizeAsync = async (input, operation) => {
|
|
|
17
17
|
if (!isGranted(input.grants, identity.actor, identity.capability))
|
|
18
18
|
return denyAsync(input.audit, identity, targetHost, 'permission_denied');
|
|
19
19
|
const approval = await input.approvalStore.getAsync(identity);
|
|
20
|
-
if (
|
|
20
|
+
if (approval?.status === 'pending')
|
|
21
21
|
return denyAsync(input.audit, identity, targetHost, 'approval_pending');
|
|
22
|
-
if (approval
|
|
22
|
+
if (approval?.status !== 'approved' || !sameIdentity(approval.operation, identity))
|
|
23
23
|
return denyAsync(input.audit, identity, targetHost, 'approval_rejected');
|
|
24
24
|
if (!(await input.operationStore.claimAsync(identity)))
|
|
25
25
|
return denyAsync(input.audit, identity, targetHost, 'idempotency_conflict');
|