@atolis-hq/wake 0.4.44 → 0.4.46
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/dist/src/bootstrap/persistence-composition.js +1 -5
- package/dist/src/bootstrap/version.js +1 -1
- package/dist/src/control-plane/application/activation-scheduler-ports.d.ts +2 -0
- package/dist/src/control-plane/application/activation-scheduler.js +59 -34
- package/dist/src/control-plane/application/advance-once-dispatch.d.ts +3 -0
- package/dist/src/control-plane/application/advance-once-dispatch.js +69 -39
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/README.md +11 -15
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/file-processor-state-store.d.ts +2 -2
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/file-processor-state-store.d.ts.map +1 -1
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/file-processor-state-store.js +15 -58
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/file-projection-store.d.ts +1 -6
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/file-projection-store.d.ts.map +1 -1
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/file-projection-store.js +20 -68
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/index.d.ts +1 -1
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/index.d.ts.map +1 -1
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/processor-state-paths.d.ts +0 -3
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/processor-state-paths.d.ts.map +1 -1
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/processor-state-paths.js +5 -20
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/storage-name.d.ts +1 -1
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/storage-name.d.ts.map +1 -1
- package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/storage-name.js +5 -7
- package/dist/src/surfaces/api/contracts/transport-values.d.ts +2 -7
- package/dist/src/surfaces/api/contracts/transport-values.js +9 -2
- package/dist/src/surfaces/web-assets/assets/index-D1NjO4qa.css +1 -0
- package/dist/src/surfaces/web-assets/assets/index-aTndh5BE.js +1118 -0
- package/dist/src/surfaces/web-assets/index.html +2 -2
- package/package.json +1 -1
- package/dist/src/surfaces/web-assets/assets/index-D9xGa5tD.css +0 -1
- package/dist/src/surfaces/web-assets/assets/index-DkmZZW60.js +0 -1179
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { FileCheckpointStore, FileEventJournal, FileProcessorStateStore, FileProjectionStore, createFileProcessorRunSerialiser, } from '@atolis-hq/eventing-filesystem';
|
|
2
|
-
import { DeliveryOutcomeProcessorConsumer } from '../integrations/index.js';
|
|
3
2
|
function identity(value) {
|
|
4
3
|
return value;
|
|
5
4
|
}
|
|
@@ -27,10 +26,7 @@ export function composePersistence(paths, clock, options) {
|
|
|
27
26
|
const subscriptionRunSerialiser = options.subscriptionRunSerialiser ?? createFileProcessorRunSerialiser(paths.dataRoot);
|
|
28
27
|
return {
|
|
29
28
|
journal: serializeJournalAppends((options.decorateJournal ?? identity)(options.journal ?? new FileEventJournal(paths.dataRoot, clock))),
|
|
30
|
-
projections: (options.decorateProjections ?? identity)(options.projections ??
|
|
31
|
-
new FileProjectionStore(paths.dataRoot, {
|
|
32
|
-
protectedProcessorStateConsumers: [DeliveryOutcomeProcessorConsumer],
|
|
33
|
-
})),
|
|
29
|
+
projections: (options.decorateProjections ?? identity)(options.projections ?? new FileProjectionStore(paths.dataRoot)),
|
|
34
30
|
checkpoints: (options.decorateCheckpoints ?? identity)(options.checkpoints ?? new FileCheckpointStore(paths.dataRoot)),
|
|
35
31
|
processorState: options.processorState ?? new FileProcessorStateStore(paths.dataRoot),
|
|
36
32
|
subscriptionRunSerialiser,
|
|
@@ -124,6 +124,6 @@ export function resolveWakeVersion(options = {}) {
|
|
|
124
124
|
return `g${headHash.slice(0, 7)}`;
|
|
125
125
|
return '0.1.0-dev';
|
|
126
126
|
}
|
|
127
|
-
export const wakeVersion = "v0.4.
|
|
127
|
+
export const wakeVersion = "v0.4.46";
|
|
128
128
|
/** The registry-installable version used by packaged sandbox images. */
|
|
129
129
|
export const wakePackageVersion = packageVersion(defaultRepoRoot) ?? wakeVersion;
|
|
@@ -12,6 +12,7 @@ export interface OrchestrationPort {
|
|
|
12
12
|
activation: ActivityActivationView;
|
|
13
13
|
}[]>;
|
|
14
14
|
listWaiting(): Promise<readonly (WorkflowInstanceView | null)[]>;
|
|
15
|
+
get?(workflowInstanceId: string): Promise<WorkflowInstanceView | null>;
|
|
15
16
|
expireTimedOutWaits?(context: CommandContext): Promise<readonly WorkflowInstanceView[]>;
|
|
16
17
|
listAll?(): Promise<readonly WorkflowInstanceView[]>;
|
|
17
18
|
validateActivationDispatch?(workflowInstanceId: string, context: CommandContext): Promise<boolean>;
|
|
@@ -32,6 +33,7 @@ export interface OrchestrationPort {
|
|
|
32
33
|
readonly message: string;
|
|
33
34
|
}, context: CommandContext): Promise<WorkflowInstanceView | null>;
|
|
34
35
|
markActivationStarted(workflowInstanceId: string, activationId: string, context: CommandContext): Promise<WorkflowInstanceView | null>;
|
|
36
|
+
block?(workflowInstanceId: string, reason: string, context: CommandContext): Promise<WorkflowInstanceView | null>;
|
|
35
37
|
}
|
|
36
38
|
export interface ExecutionPort {
|
|
37
39
|
recoverActive?(owner: string): Promise<readonly RunView[]>;
|
|
@@ -5,7 +5,7 @@ import { isAmbiguityResolutionBlock, WorkflowStatus, } from '../../orchestration
|
|
|
5
5
|
import { WorkStatus } from '../../work/index.js';
|
|
6
6
|
import { ControlStreamKind } from '../contracts/streams.js';
|
|
7
7
|
import { DispatchPolicy } from '../domain/dispatch-policy.js';
|
|
8
|
-
import { isRunnerQuotaOutcome, reportUnappliedRunnerQuotaRetry, runDispatchLoop, runnerQuotaMessage, } from './advance-once-dispatch.js';
|
|
8
|
+
import { advancementWasDurablyApplied, blockIsolatedWorkflow, isRunnerQuotaOutcome, reportUnappliedRunnerQuotaRetry, runDispatchLoop, runnerQuotaMessage, } from './advance-once-dispatch.js';
|
|
9
9
|
import { findUnresolvedSucceededTerminal, findUnresolvedTerminal, } from './execution-reconciliation.js';
|
|
10
10
|
export function createActivationScheduler(orchestration, execution, resources, clock, dependencies) {
|
|
11
11
|
const runnerIneligibility = dependencies.runnerIneligibility ?? (async () => new Set());
|
|
@@ -106,43 +106,68 @@ export function createActivationScheduler(orchestration, execution, resources, c
|
|
|
106
106
|
if (recovery !== undefined) {
|
|
107
107
|
if (await isDispatchPaused())
|
|
108
108
|
return { kind: 'paused' };
|
|
109
|
-
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
109
|
+
try {
|
|
110
|
+
if (recovery.run.status === RunStatus.Succeeded) {
|
|
111
|
+
if (isRunnerQuotaOutcome(recovery.run.outcome)) {
|
|
112
|
+
const retried = await orchestration.retryRunnerQuotaFailure?.(recovery.item.workflow.workflowInstanceId, {
|
|
113
|
+
activationId: recovery.item.activation.activationId,
|
|
114
|
+
runId: recovery.run.runId,
|
|
115
|
+
runnerName: recovery.run.runner?.name ?? 'unknown-runner',
|
|
116
|
+
message: runnerQuotaMessage(recovery.run.outcome),
|
|
117
|
+
}, context(recovery.run.runId));
|
|
118
|
+
reportUnappliedRunnerQuotaRetry(retried, {
|
|
119
|
+
activationId: recovery.item.activation.activationId,
|
|
120
|
+
runId: recovery.run.runId,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
await orchestration.acceptOutcome({
|
|
125
|
+
workflowInstanceId: recovery.item.workflow.workflowInstanceId,
|
|
126
|
+
activationId: recovery.item.activation.activationId,
|
|
127
|
+
outcome: recovery.run.outcome,
|
|
128
|
+
}, context(recovery.run.runId));
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
kind: 'progressed',
|
|
132
|
+
dispatched: [
|
|
133
|
+
{ activationId: recovery.item.activation.activationId, runId: recovery.run.runId },
|
|
134
|
+
],
|
|
135
|
+
};
|
|
128
136
|
}
|
|
137
|
+
await orchestration.resolveExecutionFailure?.(recovery.item.workflow.workflowInstanceId, {
|
|
138
|
+
activationId: recovery.item.activation.activationId,
|
|
139
|
+
runId: recovery.run.runId,
|
|
140
|
+
reason: recovery.run.failure?.message ?? 'execution failed',
|
|
141
|
+
}, context(recovery.run.runId));
|
|
129
142
|
return {
|
|
130
|
-
kind:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
],
|
|
143
|
+
kind: WorkflowStatus.Blocked,
|
|
144
|
+
workflowInstanceId: recovery.item.workflow.workflowInstanceId,
|
|
145
|
+
reason: recovery.run.failure?.message ?? 'execution failed',
|
|
134
146
|
};
|
|
135
147
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
reason
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (await advancementWasDurablyApplied(orchestration, recovery.item.workflow.workflowInstanceId, recovery.item.activation.activationId))
|
|
150
|
+
throw error;
|
|
151
|
+
const reason = await blockIsolatedWorkflow(orchestration, recovery.item.workflow.workflowInstanceId, error, context(recovery.run.runId));
|
|
152
|
+
const result = await runDispatchLoop(pending.filter((item) => item.workflow.workflowInstanceId !== recovery.item.workflow.workflowInstanceId), {
|
|
153
|
+
orchestration,
|
|
154
|
+
execution,
|
|
155
|
+
resources,
|
|
156
|
+
dispatchPolicy,
|
|
157
|
+
maxConcurrentRuns,
|
|
158
|
+
maxDispatches,
|
|
159
|
+
runnerIneligibility,
|
|
160
|
+
isDispatchPaused,
|
|
161
|
+
commandContext: context,
|
|
162
|
+
});
|
|
163
|
+
return result.kind === 'progressed'
|
|
164
|
+
? result
|
|
165
|
+
: {
|
|
166
|
+
kind: WorkflowStatus.Blocked,
|
|
167
|
+
workflowInstanceId: recovery.item.workflow.workflowInstanceId,
|
|
168
|
+
reason,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
146
171
|
}
|
|
147
172
|
const result = await runDispatchLoop(pending, {
|
|
148
173
|
orchestration,
|
|
@@ -20,6 +20,9 @@ interface PendingActivation {
|
|
|
20
20
|
readonly workflow: WorkflowInstanceView;
|
|
21
21
|
readonly activation: ActivityActivationView;
|
|
22
22
|
}
|
|
23
|
+
export declare function advancementWasDurablyApplied(orchestration: OrchestrationPort, workflowInstanceId: WorkflowInstanceView['workflowInstanceId'], activationId: ActivityActivationView['activationId']): Promise<boolean>;
|
|
24
|
+
export declare function isolatedWorkflowReason(error: unknown): string;
|
|
25
|
+
export declare function blockIsolatedWorkflow(orchestration: OrchestrationPort, workflowInstanceId: string, error: unknown, context: CommandContext): Promise<string>;
|
|
23
26
|
export declare function isRunnerQuotaOutcome(outcome: ActivityOutcome): boolean;
|
|
24
27
|
export declare function runnerQuotaMessage(outcome: ActivityOutcome): string;
|
|
25
28
|
/**
|
|
@@ -2,6 +2,24 @@ import { ActivityFailureCode, ActivityOutcomeKind, } from '../../activities/inde
|
|
|
2
2
|
import { ActivationClaimConflictError, isActiveRunStatus, NoEligibleRunnerError, RunStatus, WorkspaceMode, } from '../../execution/index.js';
|
|
3
3
|
import { WorkflowStatus } from '../../orchestration/index.js';
|
|
4
4
|
import { isExecutionFailureTerminal } from './execution-reconciliation.js';
|
|
5
|
+
export async function advancementWasDurablyApplied(orchestration, workflowInstanceId, activationId) {
|
|
6
|
+
const current = await orchestration.get?.(workflowInstanceId);
|
|
7
|
+
return (current !== undefined && current !== null && current.acceptedOutcomes.includes(activationId));
|
|
8
|
+
}
|
|
9
|
+
const maximumIsolationReasonLength = 2_000;
|
|
10
|
+
export function isolatedWorkflowReason(error) {
|
|
11
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
12
|
+
return `workflow advancement failed: ${message}`.slice(0, maximumIsolationReasonLength);
|
|
13
|
+
}
|
|
14
|
+
export async function blockIsolatedWorkflow(orchestration, workflowInstanceId, error, context) {
|
|
15
|
+
const reason = isolatedWorkflowReason(error);
|
|
16
|
+
if (orchestration.block === undefined)
|
|
17
|
+
throw new Error('Orchestration port cannot durably block an isolated workflow', {
|
|
18
|
+
cause: error,
|
|
19
|
+
});
|
|
20
|
+
await orchestration.block(workflowInstanceId, reason, context);
|
|
21
|
+
return reason;
|
|
22
|
+
}
|
|
5
23
|
export function isRunnerQuotaOutcome(outcome) {
|
|
6
24
|
return (outcome.kind === ActivityOutcomeKind.Failed &&
|
|
7
25
|
typeof outcome.data === 'object' &&
|
|
@@ -37,6 +55,8 @@ export function reportUnappliedRunnerQuotaRetry(result, input) {
|
|
|
37
55
|
export async function runDispatchLoop(pending, ctx) {
|
|
38
56
|
const dispatched = [];
|
|
39
57
|
const dispatchedIds = new Set();
|
|
58
|
+
const isolatedIds = new Set();
|
|
59
|
+
let isolated;
|
|
40
60
|
let stopReason;
|
|
41
61
|
while (dispatched.length < ctx.maxDispatches) {
|
|
42
62
|
const allRuns = await ctx.execution.list();
|
|
@@ -48,7 +68,9 @@ export async function runDispatchLoop(pending, ctx) {
|
|
|
48
68
|
stopReason = { kind: 'no-work' };
|
|
49
69
|
break;
|
|
50
70
|
}
|
|
51
|
-
const selectedCandidate = ctx.dispatchPolicy.select(
|
|
71
|
+
const selectedCandidate = ctx.dispatchPolicy.select(pending
|
|
72
|
+
.filter((item) => !isolatedIds.has(item.workflow.workflowInstanceId))
|
|
73
|
+
.map((item, requestedPosition) => ({
|
|
52
74
|
workItemId: item.workflow.workItemId,
|
|
53
75
|
activationId: item.activation.activationId,
|
|
54
76
|
requestedPosition,
|
|
@@ -56,7 +78,7 @@ export async function runDispatchLoop(pending, ctx) {
|
|
|
56
78
|
activeActivationIds.has(item.activation.activationId) ||
|
|
57
79
|
branchActiveWorkflowIds.has(item.workflow.workflowInstanceId),
|
|
58
80
|
cancelled: false,
|
|
59
|
-
})))
|
|
81
|
+
})))[0];
|
|
60
82
|
const selected = selectedCandidate === undefined
|
|
61
83
|
? undefined
|
|
62
84
|
: pending.find((item) => item.activation.activationId === selectedCandidate.activationId);
|
|
@@ -73,17 +95,16 @@ export async function runDispatchLoop(pending, ctx) {
|
|
|
73
95
|
stopReason = { kind: 'paused' };
|
|
74
96
|
break;
|
|
75
97
|
}
|
|
76
|
-
if ((await ctx.orchestration.validateActivationDispatch?.(selected.workflow.workflowInstanceId, ctx.commandContext(selected.activation.activationId))) === false) {
|
|
77
|
-
stopReason = { kind: 'no-work' };
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
await ctx.orchestration.markActivationStarted(selected.workflow.workflowInstanceId, selected.activation.activationId, ctx.commandContext(selected.activation.activationId));
|
|
81
|
-
const correlated = await ctx.resources.correlationsForWork(selected.workflow.workItemId);
|
|
82
|
-
const resourceViews = (await Promise.all(correlated.map((entry) => ctx.resources.get(entry.resourceId)))).filter((resource) => resource !== null);
|
|
83
98
|
const ineligible = await ctx.runnerIneligibility();
|
|
84
|
-
let run;
|
|
85
99
|
try {
|
|
86
|
-
|
|
100
|
+
if ((await ctx.orchestration.validateActivationDispatch?.(selected.workflow.workflowInstanceId, ctx.commandContext(selected.activation.activationId))) === false) {
|
|
101
|
+
stopReason = { kind: 'no-work' };
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
await ctx.orchestration.markActivationStarted(selected.workflow.workflowInstanceId, selected.activation.activationId, ctx.commandContext(selected.activation.activationId));
|
|
105
|
+
const correlated = await ctx.resources.correlationsForWork(selected.workflow.workItemId);
|
|
106
|
+
const resourceViews = (await Promise.all(correlated.map((entry) => ctx.resources.get(entry.resourceId)))).filter((resource) => resource !== null);
|
|
107
|
+
const run = await ctx.execution.attempt(selected.activation, {
|
|
87
108
|
workItemId: selected.workflow.workItemId,
|
|
88
109
|
workflowInstanceId: selected.workflow.workflowInstanceId,
|
|
89
110
|
orchestrationGroupId: selected.workflow.orchestrationGroupId,
|
|
@@ -95,20 +116,9 @@ export async function runDispatchLoop(pending, ctx) {
|
|
|
95
116
|
awaitImmediateCompletion: true,
|
|
96
117
|
...(ineligible.size === 0 ? {} : { ineligibleRunners: ineligible }),
|
|
97
118
|
});
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
stopReason = { kind: 'no-work' };
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
if (error instanceof NoEligibleRunnerError) {
|
|
105
|
-
stopReason = { kind: 'no-work' };
|
|
106
|
-
break;
|
|
107
|
-
}
|
|
108
|
-
throw error;
|
|
109
|
-
}
|
|
110
|
-
if (run.status === RunStatus.Succeeded && run.outcome !== undefined) {
|
|
111
|
-
if (isRunnerQuotaOutcome(run.outcome)) {
|
|
119
|
+
if (run.status === RunStatus.Succeeded &&
|
|
120
|
+
run.outcome !== undefined &&
|
|
121
|
+
isRunnerQuotaOutcome(run.outcome)) {
|
|
112
122
|
// No isDispatchPaused check here: a quota retry re-requests the same
|
|
113
123
|
// stage's activity without publishing an outcome or consuming retry
|
|
114
124
|
// budget, so it must not be held behind maintenance pause the way an
|
|
@@ -124,7 +134,7 @@ export async function runDispatchLoop(pending, ctx) {
|
|
|
124
134
|
runId: run.runId,
|
|
125
135
|
});
|
|
126
136
|
}
|
|
127
|
-
else {
|
|
137
|
+
else if (run.status === RunStatus.Succeeded && run.outcome !== undefined) {
|
|
128
138
|
if (await ctx.isDispatchPaused()) {
|
|
129
139
|
stopReason = { kind: 'paused' };
|
|
130
140
|
break;
|
|
@@ -135,25 +145,45 @@ export async function runDispatchLoop(pending, ctx) {
|
|
|
135
145
|
outcome: run.outcome,
|
|
136
146
|
}, ctx.commandContext(run.runId));
|
|
137
147
|
}
|
|
148
|
+
if (isExecutionFailureTerminal(run.status))
|
|
149
|
+
await ctx.orchestration.resolveExecutionFailure?.(selected.workflow.workflowInstanceId, {
|
|
150
|
+
activationId: selected.activation.activationId,
|
|
151
|
+
runId: run.runId,
|
|
152
|
+
reason: run.failure?.message ?? 'execution failed',
|
|
153
|
+
}, ctx.commandContext(run.runId));
|
|
154
|
+
if (run.status !== RunStatus.Succeeded && !isActiveRunStatus(run.status)) {
|
|
155
|
+
stopReason = {
|
|
156
|
+
kind: WorkflowStatus.Blocked,
|
|
157
|
+
workflowInstanceId: selected.workflow.workflowInstanceId,
|
|
158
|
+
reason: run.failure?.message ?? 'execution failed',
|
|
159
|
+
};
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
dispatched.push({ activationId: selected.activation.activationId, runId: run.runId });
|
|
163
|
+
dispatchedIds.add(selected.activation.activationId);
|
|
138
164
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (error instanceof ActivationClaimConflictError) {
|
|
167
|
+
stopReason = { kind: 'no-work' };
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
if (error instanceof NoEligibleRunnerError) {
|
|
171
|
+
stopReason = { kind: 'no-work' };
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
if (await advancementWasDurablyApplied(ctx.orchestration, selected.workflow.workflowInstanceId, selected.activation.activationId))
|
|
175
|
+
throw error;
|
|
176
|
+
const reason = await blockIsolatedWorkflow(ctx.orchestration, selected.workflow.workflowInstanceId, error, ctx.commandContext(selected.activation.activationId));
|
|
177
|
+
isolatedIds.add(selected.workflow.workflowInstanceId);
|
|
178
|
+
isolated = {
|
|
147
179
|
kind: WorkflowStatus.Blocked,
|
|
148
180
|
workflowInstanceId: selected.workflow.workflowInstanceId,
|
|
149
|
-
reason
|
|
181
|
+
reason,
|
|
150
182
|
};
|
|
151
|
-
|
|
183
|
+
continue;
|
|
152
184
|
}
|
|
153
|
-
dispatched.push({ activationId: selected.activation.activationId, runId: run.runId });
|
|
154
|
-
dispatchedIds.add(selected.activation.activationId);
|
|
155
185
|
}
|
|
156
186
|
return dispatched.length > 0
|
|
157
187
|
? { kind: 'progressed', dispatched }
|
|
158
|
-
: (stopReason ?? { kind: 'no-work' });
|
|
188
|
+
: (stopReason ?? isolated ?? { kind: 'no-work' });
|
|
159
189
|
}
|
|
@@ -18,23 +18,19 @@ durable flat JSONL segment format, while checkpoints, processor state,
|
|
|
18
18
|
projections, locks, and processor serialisation use compatible atomic
|
|
19
19
|
filesystem operations.
|
|
20
20
|
|
|
21
|
-
`FileProjectionStore` is exclusively for rebuildable Eventing read models
|
|
22
|
-
`FileProcessorStateStore` persists processor-owned recovery state separately
|
|
23
|
-
When processor state uses established compatible paths beneath `projections/`,
|
|
24
|
-
pass each state consumer to `FileProjectionStore` so a projection rebuild
|
|
25
|
-
reserves its current, legacy, and collision-isolated directories:
|
|
21
|
+
`FileProjectionStore` is exclusively for rebuildable Eventing read models and
|
|
22
|
+
`FileProcessorStateStore` persists processor-owned recovery state separately:
|
|
26
23
|
|
|
27
24
|
```ts
|
|
28
|
-
const projections = new FileProjectionStore(dataRoot
|
|
29
|
-
protectedProcessorStateConsumers: ['reactor:delivery-outcomes'],
|
|
30
|
-
});
|
|
25
|
+
const projections = new FileProjectionStore(dataRoot);
|
|
31
26
|
const processorState = new FileProcessorStateStore(dataRoot);
|
|
32
27
|
```
|
|
33
28
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
29
|
+
Projection records are stored at
|
|
30
|
+
`projections/projection/<namespace-sha256>/<key-sha256>.json`; processor state
|
|
31
|
+
is stored at `projections/processor-state/<consumer-sha256>/<key-sha256>.json`.
|
|
32
|
+
`FileProjectionStore.clear()` removes only the rebuildable `projection` subtree
|
|
33
|
+
and never infers processor state from record contents. This is a fresh-storage
|
|
34
|
+
format: the adapter does not read, migrate, or write prior projection paths.
|
|
35
|
+
All supported imports come from `@atolis-hq/eventing-filesystem`; internal
|
|
36
|
+
codecs and storage helpers are not public API.
|
package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/file-processor-state-store.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare class FileProcessorStateStore implements ProcessorStateStore {
|
|
|
6
6
|
write<Value>(state: StoredProcessorState<Value>): Promise<void>;
|
|
7
7
|
delete(consumer: string, key: string): Promise<void>;
|
|
8
8
|
private paths;
|
|
9
|
-
private
|
|
10
|
-
private
|
|
9
|
+
private withStateLock;
|
|
10
|
+
private readState;
|
|
11
11
|
}
|
|
12
12
|
//# sourceMappingURL=file-processor-state-store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-processor-state-store.d.ts","sourceRoot":"","sources":["../src/file-processor-state-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"file-processor-state-store.d.ts","sourceRoot":"","sources":["../src/file-processor-state-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAarF,qBAAa,uBAAwB,YAAW,mBAAmB;IACrD,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,MAAM;IAEnC,IAAI,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAQvF,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,oBAAoB,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAY/D,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS1D,OAAO,CAAC,KAAK;YAIC,aAAa;YAIb,SAAS;CAQxB"}
|
package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/file-processor-state-store.js
CHANGED
|
@@ -5,7 +5,6 @@ import { withFileLock } from './file-lock.js';
|
|
|
5
5
|
import { atomicJson } from './file-projection-store.js';
|
|
6
6
|
import { processorStatePaths } from './processor-state-paths.js';
|
|
7
7
|
import { isCompatibleProcessorStateRecord, } from './processor-state-record.js';
|
|
8
|
-
import { encodeLegacyStorageName } from './storage-name.js';
|
|
9
8
|
export class FileProcessorStateStore {
|
|
10
9
|
root;
|
|
11
10
|
constructor(root) {
|
|
@@ -13,69 +12,46 @@ export class FileProcessorStateStore {
|
|
|
13
12
|
}
|
|
14
13
|
async read(consumer, key) {
|
|
15
14
|
const paths = this.paths(consumer, key);
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
if (stored !== undefined && stored !== null)
|
|
15
|
+
const stored = await this.readState(paths);
|
|
16
|
+
if (stored !== null && matchesStateIdentity(stored, paths.namespace, key))
|
|
19
17
|
return { consumer, key, value: stored.value };
|
|
20
18
|
return null;
|
|
21
19
|
}
|
|
22
20
|
async write(state) {
|
|
23
21
|
const paths = this.paths(state.consumer, state.key);
|
|
24
|
-
await this.
|
|
25
|
-
const
|
|
26
|
-
const current = candidateState(candidates, paths.current);
|
|
22
|
+
await this.withStateLock(paths.current, async () => {
|
|
23
|
+
const current = await this.readState(paths);
|
|
27
24
|
if (matchesStateIdentity(current, paths.namespace, state.key))
|
|
28
25
|
return atomicJson(paths.current, compatibleRecord(paths.namespace, state));
|
|
29
|
-
const isolated = candidateState(candidates, paths.isolated);
|
|
30
|
-
if (matchesStateIdentity(isolated, paths.namespace, state.key))
|
|
31
|
-
return atomicJson(paths.isolated, compatibleRecord(paths.namespace, state));
|
|
32
26
|
if (current === null)
|
|
33
27
|
return atomicJson(paths.current, compatibleRecord(paths.namespace, state));
|
|
34
|
-
if (isolated === null)
|
|
35
|
-
return atomicJson(paths.isolated, compatibleRecord(paths.namespace, state));
|
|
36
28
|
throw new Error(`Processor state paths are occupied for ${state.consumer}:${state.key}`);
|
|
37
29
|
});
|
|
38
30
|
}
|
|
39
31
|
async delete(consumer, key) {
|
|
40
32
|
const paths = this.paths(consumer, key);
|
|
41
|
-
await this.
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
await rm(path, { force: true });
|
|
46
|
-
}
|
|
33
|
+
await this.withStateLock(paths.current, async () => {
|
|
34
|
+
const state = await this.readState(paths);
|
|
35
|
+
if (matchesStateIdentity(state, paths.namespace, key))
|
|
36
|
+
await rm(paths.current, { force: true });
|
|
47
37
|
});
|
|
48
38
|
}
|
|
49
39
|
paths(consumer, key) {
|
|
50
40
|
return processorStatePaths(this.root, consumer, key);
|
|
51
41
|
}
|
|
52
|
-
async
|
|
53
|
-
|
|
54
|
-
.map((path) => processorStateLockPath(this.root, path))
|
|
55
|
-
.sort();
|
|
56
|
-
return withLocks(locks, operation);
|
|
42
|
+
async withStateLock(path, operation) {
|
|
43
|
+
return withFileLock(processorStateLockPath(this.root, path), operation, { waitMs: 5_000 });
|
|
57
44
|
}
|
|
58
|
-
async
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
return candidates;
|
|
45
|
+
async readState(paths) {
|
|
46
|
+
const state = await readCompatibleState(paths.current);
|
|
47
|
+
if (state !== null && !matchesStateIdentity(state, paths.namespace, paths.key))
|
|
48
|
+
throw invalidProcessorStateRecord(paths.current);
|
|
49
|
+
return state;
|
|
65
50
|
}
|
|
66
51
|
}
|
|
67
|
-
function legacyProcessorStatePath(root, namespace, key) {
|
|
68
|
-
return join(root, 'projections', encodeLegacyStorageName(namespace), `${encodeLegacyStorageName(key)}.json`);
|
|
69
|
-
}
|
|
70
52
|
function processorStateLockPath(root, path) {
|
|
71
53
|
return join(root, 'locks', 'processor-state', `${createHash('sha256').update(path).digest('hex')}.lock`);
|
|
72
54
|
}
|
|
73
|
-
async function withLocks(paths, operation) {
|
|
74
|
-
const [path, ...rest] = paths;
|
|
75
|
-
if (path === undefined)
|
|
76
|
-
return operation();
|
|
77
|
-
return withFileLock(path, () => withLocks(rest, operation), { waitMs: 5_000 });
|
|
78
|
-
}
|
|
79
55
|
async function readCompatibleState(path) {
|
|
80
56
|
let raw;
|
|
81
57
|
try {
|
|
@@ -103,25 +79,6 @@ function invalidProcessorStateRecord(path, cause) {
|
|
|
103
79
|
function matchesStateIdentity(stored, namespace, key) {
|
|
104
80
|
return stored?.namespace === namespace && stored.key === key;
|
|
105
81
|
}
|
|
106
|
-
function assertCandidateProvenance(root, path, stored, namespace, key) {
|
|
107
|
-
if (matchesStateIdentity(stored, namespace, key))
|
|
108
|
-
return;
|
|
109
|
-
if (legacyProcessorStatePath(root, stored.namespace, stored.key) === path)
|
|
110
|
-
return;
|
|
111
|
-
throw invalidProcessorStateRecord(path);
|
|
112
|
-
}
|
|
113
82
|
function compatibleRecord(namespace, state) {
|
|
114
83
|
return { namespace, key: state.key, lastGlobalPosition: 0, value: state.value };
|
|
115
84
|
}
|
|
116
|
-
function uniquePaths(paths) {
|
|
117
|
-
return [...new Set(paths)];
|
|
118
|
-
}
|
|
119
|
-
function candidatePaths(paths) {
|
|
120
|
-
return uniquePaths([paths.current, paths.isolated, paths.legacy]);
|
|
121
|
-
}
|
|
122
|
-
function candidateState(candidates, path) {
|
|
123
|
-
const candidate = candidates.find((value) => value.path === path);
|
|
124
|
-
if (candidate === undefined)
|
|
125
|
-
throw new Error(`Missing processor state candidate for ${path}`);
|
|
126
|
-
return candidate.state;
|
|
127
|
-
}
|
package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/file-projection-store.d.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import type { ProjectionStore, StoredProjection } from '@atolis-hq/eventing';
|
|
2
|
-
export interface FileProjectionStoreOptions {
|
|
3
|
-
readonly protectedProcessorStateConsumers?: readonly string[];
|
|
4
|
-
}
|
|
5
2
|
export declare class FileProjectionStore implements ProjectionStore {
|
|
6
3
|
private readonly root;
|
|
7
|
-
|
|
8
|
-
constructor(root: string, options?: FileProjectionStoreOptions);
|
|
4
|
+
constructor(root: string);
|
|
9
5
|
private readonly listCache;
|
|
10
6
|
read<Value>(namespace: string, key: string): Promise<StoredProjection<Value> | null>;
|
|
11
7
|
write<Value>(projection: StoredProjection<Value>): Promise<void>;
|
|
@@ -14,6 +10,5 @@ export declare class FileProjectionStore implements ProjectionStore {
|
|
|
14
10
|
private clearProjectionNamespace;
|
|
15
11
|
private clearAllProjectionDirectories;
|
|
16
12
|
}
|
|
17
|
-
export declare function encode(value: string): string;
|
|
18
13
|
export declare function atomicJson(path: string, value: unknown): Promise<void>;
|
|
19
14
|
//# sourceMappingURL=file-projection-store.d.ts.map
|
package/dist/src/node_modules/@atolis-hq/eventing-filesystem/dist/file-projection-store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-projection-store.d.ts","sourceRoot":"","sources":["../src/file-projection-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"file-projection-store.d.ts","sourceRoot":"","sources":["../src/file-projection-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAO7E,qBAAa,mBAAoB,YAAW,eAAe;IAC7C,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,MAAM;IAEzC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgD;IAEpE,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;IAUpF,KAAK,CAAC,KAAK,EAAE,UAAU,EAAE,gBAAgB,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IA2ChE,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;IAoB3E,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAOhC,wBAAwB;YAIxB,6BAA6B;CAG5C;AAgID,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5E"}
|