@fabricorg/platform-host 8.1.2 → 8.2.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 +46 -0
- package/README.md +154 -14
- package/dist/index.cjs +450 -154
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -7
- package/dist/index.d.ts +39 -7
- package/dist/index.js +451 -155
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -96,6 +96,78 @@ function normalizeProvenance(input, options) {
|
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
//#endregion
|
|
99
|
+
//#region src/action-finalization.ts
|
|
100
|
+
/** Strict JSON evidence: reject lossy serialization rather than silently dropping values. */
|
|
101
|
+
function finalizationJson(value) {
|
|
102
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
103
|
+
function check(item) {
|
|
104
|
+
if (item === null || typeof item === "string" || typeof item === "boolean") return;
|
|
105
|
+
if (typeof item === "number" && Number.isFinite(item)) return;
|
|
106
|
+
if (typeof item !== "object" || !item || ancestors.has(item)) throw new Error("Finalization requires JSON-compatible data.");
|
|
107
|
+
if (!Array.isArray(item) && Object.getPrototypeOf(item) !== Object.prototype && Object.getPrototypeOf(item) !== null) throw new Error("Finalization requires plain JSON objects.");
|
|
108
|
+
if (Object.getOwnPropertySymbols(item).length > 0) throw new Error("Finalization requires JSON string keys.");
|
|
109
|
+
if (Array.isArray(item)) {
|
|
110
|
+
const keys = Object.keys(item);
|
|
111
|
+
if (keys.length !== item.length || keys.some((key, index) => key !== String(index))) throw new Error("Finalization requires dense JSON arrays.");
|
|
112
|
+
}
|
|
113
|
+
ancestors.add(item);
|
|
114
|
+
for (const child of Object.values(item)) check(child);
|
|
115
|
+
ancestors.delete(item);
|
|
116
|
+
}
|
|
117
|
+
check(value);
|
|
118
|
+
return JSON.parse(JSON.stringify(value));
|
|
119
|
+
}
|
|
120
|
+
/** Match the persisted JSON representation before validating finalization execution input. */
|
|
121
|
+
function normalizeFinalizationJson(value) {
|
|
122
|
+
return finalizationJson(JSON.parse(JSON.stringify(value)));
|
|
123
|
+
}
|
|
124
|
+
/** Omit only public optional fields; payloads and patch values retain strict JSON validation. */
|
|
125
|
+
function omitUndefinedOptionalFields(value, keys) {
|
|
126
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return value;
|
|
127
|
+
const copy = { ...value };
|
|
128
|
+
for (const key of keys) if (Object.hasOwn(copy, key) && copy[key] === void 0) delete copy[key];
|
|
129
|
+
return copy;
|
|
130
|
+
}
|
|
131
|
+
function validateFinalization(value, action, privateFields, contracts) {
|
|
132
|
+
const normalized = omitUndefinedOptionalFields(value, ["events", "resultPatch"]);
|
|
133
|
+
if (normalized !== value && normalized && typeof normalized === "object" && !Array.isArray(normalized)) {
|
|
134
|
+
const record = normalized;
|
|
135
|
+
if (Array.isArray(record.events)) {
|
|
136
|
+
const events = Object.assign(record.events.slice(), record.events);
|
|
137
|
+
for (let index = 0; index < events.length; index++) if (Object.hasOwn(events, index)) events[index] = omitUndefinedOptionalFields(events[index], ["eventSchemaVersion"]);
|
|
138
|
+
record.events = events;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const result = finalizationJson(normalized);
|
|
142
|
+
if (!result || typeof result !== "object" || Array.isArray(result) || Object.keys(result).some((key) => key !== "events" && key !== "resultPatch")) throw new Error("Finalization must return only events and resultPatch.");
|
|
143
|
+
if (result.resultPatch !== void 0) {
|
|
144
|
+
if (!result.resultPatch || typeof result.resultPatch !== "object" || Array.isArray(result.resultPatch)) throw new Error("Finalization resultPatch must be an object.");
|
|
145
|
+
if (Object.keys(result.resultPatch).some((key) => privateFields.includes(key))) throw new Error("Finalization resultPatch contains a private result key.");
|
|
146
|
+
}
|
|
147
|
+
if (result.events !== void 0) {
|
|
148
|
+
if (!Array.isArray(result.events)) throw new Error("Finalization events must be an array.");
|
|
149
|
+
for (const event of result.events) {
|
|
150
|
+
if (!event || typeof event !== "object" || Array.isArray(event) || Object.keys(event).some((key) => ![
|
|
151
|
+
"eventType",
|
|
152
|
+
"subjectType",
|
|
153
|
+
"subjectId",
|
|
154
|
+
"payload",
|
|
155
|
+
"eventSchemaVersion"
|
|
156
|
+
].includes(key))) throw new Error("Invalid finalization event fields.");
|
|
157
|
+
for (const key of [
|
|
158
|
+
"eventType",
|
|
159
|
+
"subjectType",
|
|
160
|
+
"subjectId"
|
|
161
|
+
]) if (typeof event[key] !== "string" || !event[key].trim()) throw new Error("Finalization event identifiers must be non-empty.");
|
|
162
|
+
if (!Object.hasOwn(event, "payload")) throw new Error("Finalization event requires a JSON payload.");
|
|
163
|
+
if (event.eventSchemaVersion !== void 0 && (!Number.isInteger(event.eventSchemaVersion) || event.eventSchemaVersion <= 0)) throw new Error("Finalization eventSchemaVersion must be a positive integer.");
|
|
164
|
+
if (!action.emitsEvents.includes(event.eventType)) throw new Error("Finalization emitted undeclared event type.");
|
|
165
|
+
if (contracts && (!contracts.hasEventType(event.eventType) || !contracts.hasSubjectType(event.subjectType))) throw new Error("Finalization emitted an unregistered event or subject type.");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
99
171
|
//#region src/observability.ts
|
|
100
172
|
/** Version of the vendor-neutral health/readiness contract. */
|
|
101
173
|
const PLATFORM_HOST_HEALTH_CONTRACT_VERSION = 1;
|
|
@@ -1463,6 +1535,7 @@ function createGovernedActionHost(options) {
|
|
|
1463
1535
|
await attemptEvidenceWrites(evidenceWrites);
|
|
1464
1536
|
}
|
|
1465
1537
|
async function failExecution(record, status, error) {
|
|
1538
|
+
if (record.finalizationCheckpoint && hasDurableProviderEffect) return settleAppliedEffect(error);
|
|
1466
1539
|
await persistExecutionInvocation(record, {
|
|
1467
1540
|
status,
|
|
1468
1541
|
error
|
|
@@ -1755,14 +1828,34 @@ function createGovernedActionHost(options) {
|
|
|
1755
1828
|
};
|
|
1756
1829
|
}
|
|
1757
1830
|
const action = actionResolver(invocation.actionId);
|
|
1758
|
-
|
|
1759
|
-
|
|
1831
|
+
let hasNonSucceededAdapterRow = false;
|
|
1832
|
+
if ((action || invocation.finalizationCheckpoint) && (resumingRunningInvocation || invocation.attemptCount > 1)) try {
|
|
1833
|
+
let runningCheckpoint;
|
|
1834
|
+
for (const stepIndex of Array.from({ length: Math.max((action?.adapterSteps ?? []).length, invocation.finalizationCheckpoint?.steps.length ?? 0) }, (_, index) => index)) {
|
|
1760
1835
|
const record = await options.store.getAdapterInvocation(lifecycleId("adp", actionInvocationId, String(stepIndex)), adapterInvocationScope);
|
|
1761
|
-
if (record)
|
|
1836
|
+
if (record) {
|
|
1837
|
+
adapterStatuses.set(record.id, record.status);
|
|
1838
|
+
hasNonSucceededAdapterRow ||= record.status !== "succeeded";
|
|
1839
|
+
}
|
|
1762
1840
|
if (record?.status === "succeeded") hasDurableProviderEffect = true;
|
|
1841
|
+
if (invocation.finalizationCheckpoint && record?.status === "running") runningCheckpoint ??= {
|
|
1842
|
+
stepIndex,
|
|
1843
|
+
record
|
|
1844
|
+
};
|
|
1763
1845
|
if (record?.status !== "ambiguous") continue;
|
|
1764
1846
|
return await settleUnknownOutcome(stepIndex, recordedAdapterReconciliation(record), record.reconciliation?.adapterEventSubject);
|
|
1765
1847
|
}
|
|
1848
|
+
if (runningCheckpoint) {
|
|
1849
|
+
const { stepIndex, record } = runningCheckpoint;
|
|
1850
|
+
return await settleUnknownOutcome(stepIndex, {
|
|
1851
|
+
kind: "adapter_outcome_ambiguous",
|
|
1852
|
+
adapterInvocationId: record.id,
|
|
1853
|
+
adapterType: record.adapterType,
|
|
1854
|
+
operation: record.operation,
|
|
1855
|
+
vendor: record.vendor,
|
|
1856
|
+
message: "Recorded adapter outcome is unknown."
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1766
1859
|
} catch (error) {
|
|
1767
1860
|
if (error instanceof RecoverableFinalizationError) throw error;
|
|
1768
1861
|
throw new RecoverableFinalizationError(caughtError({
|
|
@@ -1772,6 +1865,30 @@ function createGovernedActionHost(options) {
|
|
|
1772
1865
|
}
|
|
1773
1866
|
if (invocation.status === "running" && invocation.pendingCompletion && !invocation.leaseOwner) return withConsistency(actionResult(invocation), invocation.actionId);
|
|
1774
1867
|
if (!action) return hostOperation(() => failExecution(invocation, "failed", `Unknown action: ${invocation.actionId}`));
|
|
1868
|
+
if (invocation.finalizationCheckpoint && !action.finalize) return interruptedNonIdempotent("Action no longer declares its checkpointed finalizer.");
|
|
1869
|
+
let hasFencedAdapterWrites = false;
|
|
1870
|
+
try {
|
|
1871
|
+
(0, _fabricorg_platform.assertAdapterStepDeadlines)(action);
|
|
1872
|
+
(0, _fabricorg_platform.assertActionFinalization)(action);
|
|
1873
|
+
if (action.finalize) {
|
|
1874
|
+
const store = asAtomicMutationStore(options.store);
|
|
1875
|
+
if (!store) throw new Error("Action finalization requires atomic mutation transactions.");
|
|
1876
|
+
await store.transactionWithEvents(async (transaction) => {
|
|
1877
|
+
if (typeof transaction.getActionInvocationForUpdate !== "function" || typeof transaction.updateLeasedActionInvocation !== "function" || options.outbox && typeof transaction.appendEventWithOutbox !== "function") throw new Error("Action finalization requires atomic invocation locking and transaction-scoped lease fencing.");
|
|
1878
|
+
hasFencedAdapterWrites = typeof options.store.writeAdapterInvocation === "function" && typeof transaction.writeAdapterInvocation === "function";
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
} catch (error) {
|
|
1882
|
+
return interruptedNonIdempotent(caughtError({
|
|
1883
|
+
actionId: action.actionId,
|
|
1884
|
+
source: "execution"
|
|
1885
|
+
}, error));
|
|
1886
|
+
}
|
|
1887
|
+
const replayCheckpoint = action.finalize && (resumingRunningInvocation || invocation.attemptCount > 1) ? invocation.finalizationCheckpoint : void 0;
|
|
1888
|
+
if (replayCheckpoint) {
|
|
1889
|
+
if (replayCheckpoint.protocol !== "atomic-after-adapters-v1" || replayCheckpoint.actionVersion !== action.version || replayCheckpoint.steps.length !== (action.adapterSteps ?? []).length || replayCheckpoint.steps.some((step, index) => step.adapterType !== action.adapterSteps[index]?.adapterType || step.operation !== action.adapterSteps[index]?.operation) || invocation.pendingCompletion) return interruptedNonIdempotent("Finalization checkpoint protocol, action version, step identity or handoff is incompatible with replay.");
|
|
1890
|
+
}
|
|
1891
|
+
if (action.finalize && !replayCheckpoint && (resumingRunningInvocation || invocation.attemptCount > 1) && hasDurableProviderEffect) return interruptedNonIdempotent("Finalizing action recovery requires a preparation checkpoint; reconciliation is required.");
|
|
1775
1892
|
if (action.admitSubmission !== void 0 && !isAdmissionTime(invocation.admittedAt)) return hostOperation(() => failExecution(invocation, "failed", `Invocation of ${action.actionId} was not admitted at submission; an action that declares admitSubmission runs only on invocations its check admitted.`));
|
|
1776
1893
|
if (action.kind === "saga") return hostOperation(() => failExecution(invocation, "failed", `Saga action ${invocation.actionId} requires a durable PlatformActionDispatcher and cannot execute as an atomic Host action.`));
|
|
1777
1894
|
async function interruptedNonIdempotent(message) {
|
|
@@ -1801,8 +1918,9 @@ function createGovernedActionHost(options) {
|
|
|
1801
1918
|
}
|
|
1802
1919
|
return hostOperation(() => failExecution(invocation, "failed", message));
|
|
1803
1920
|
}
|
|
1804
|
-
|
|
1805
|
-
if (invocation.attemptCount
|
|
1921
|
+
const canReplayNonIdempotent = !!replayCheckpoint && !hasNonSucceededAdapterRow && (!executionLease || hasFencedAdapterWrites);
|
|
1922
|
+
if (resumingRunningInvocation && invocation.attemptCount === 0 && !invocation.leaseOwner && !action.idempotent && !canReplayNonIdempotent) return interruptedNonIdempotent(`Interrupted inline action ${invocation.actionId} is not declared idempotent; manual reconciliation is required.`);
|
|
1923
|
+
if (invocation.attemptCount > 1 && !action.idempotent && !canReplayNonIdempotent && invocation.deferredBeforeEffectAttempt !== invocation.attemptCount - 1) return interruptedNonIdempotent(`Interrupted action ${invocation.actionId} is not declared idempotent; manual reconciliation is required.`);
|
|
1806
1924
|
if (invocation.status !== "running") await hostOperation(() => options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { status: "running" }));
|
|
1807
1925
|
emitPlatformHostTelemetry(options.telemetry, {
|
|
1808
1926
|
kind: "event",
|
|
@@ -2003,144 +2121,209 @@ function createGovernedActionHost(options) {
|
|
|
2003
2121
|
}))));
|
|
2004
2122
|
}
|
|
2005
2123
|
}
|
|
2124
|
+
const preparedInputs = /* @__PURE__ */ new Map();
|
|
2125
|
+
let preparation = replayCheckpoint;
|
|
2006
2126
|
let data;
|
|
2127
|
+
let finalizationInput;
|
|
2007
2128
|
let domainEvents = [];
|
|
2008
2129
|
let pendingTransitionEvent;
|
|
2009
2130
|
try {
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
targetState,
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2131
|
+
if (replayCheckpoint) {
|
|
2132
|
+
data = structuredClone(replayCheckpoint.result);
|
|
2133
|
+
finalizationInput = structuredClone({
|
|
2134
|
+
params: replayCheckpoint.params,
|
|
2135
|
+
handlerData: replayCheckpoint.handlerData
|
|
2136
|
+
});
|
|
2137
|
+
domainEvents = structuredClone(replayCheckpoint.handlerEvents);
|
|
2138
|
+
pendingTransitionEvent = structuredClone(replayCheckpoint.stateTransition);
|
|
2139
|
+
} else {
|
|
2140
|
+
const runHandler = async (db, transaction) => {
|
|
2141
|
+
if (options.outbox && !transaction?.appendEventWithOutbox) throw new HostError("Outbox egress requires transactionWithEvents to provide appendEventWithOutbox before handler execution.");
|
|
2142
|
+
if (action.finalize) {
|
|
2143
|
+
const current = await transaction.getActionInvocationForUpdate(actionInvocationId, tenantId, spaceId);
|
|
2144
|
+
if (!current || current.status !== "running" || current.finalizationCheckpoint || (executionLease ? current.leaseOwner !== executionLease.leaseOwner || current.leaseToken !== executionLease.leaseToken : !!current.leaseOwner)) throw new RecoverableFinalizationError(`Invocation preparation ownership changed: ${actionInvocationId}`);
|
|
2145
|
+
}
|
|
2146
|
+
const binding = action.stateMachine;
|
|
2147
|
+
let replayedTransition;
|
|
2148
|
+
let pendingStateTransition;
|
|
2149
|
+
let validatedTransition;
|
|
2150
|
+
if (binding) {
|
|
2151
|
+
const machine = stateMachineResolver(binding.entityType);
|
|
2152
|
+
const entityId = binding.getEntityId(parsed.data);
|
|
2153
|
+
const getEntityState = transaction?.getEntityState ? transaction.getEntityState.bind(transaction) : options.store.getEntityState.bind(options.store);
|
|
2154
|
+
const currentState = entityId ? await hostOperation(() => getEntityState(tenantId, spaceId, binding.entityType, entityId)) ?? initialState(machine) : initialState(machine);
|
|
2155
|
+
const targetState = typeof binding.targetState === "function" ? binding.targetState(parsed.data) : binding.targetState;
|
|
2156
|
+
if (targetState !== "") {
|
|
2157
|
+
const transition = await (0, _fabricorg_platform.evaluateStateMachineTransition)(machine, binding.entityType, currentState, targetState, action.actionId, {
|
|
2158
|
+
entity: {
|
|
2159
|
+
...entityId ? { id: entityId } : {},
|
|
2160
|
+
state: currentState
|
|
2161
|
+
},
|
|
2162
|
+
actionInvocationId,
|
|
2163
|
+
actorId: invocation.actorId,
|
|
2164
|
+
parameters: parsed.data,
|
|
2165
|
+
db
|
|
2166
|
+
});
|
|
2167
|
+
validatedTransition = {
|
|
2168
|
+
entityId,
|
|
2169
|
+
currentState,
|
|
2170
|
+
targetState,
|
|
2171
|
+
valid: transition.valid
|
|
2172
|
+
};
|
|
2173
|
+
let replayingAppliedTransition = false;
|
|
2174
|
+
if (action.idempotent && currentState === targetState && !transition.valid && entityId) {
|
|
2175
|
+
let source;
|
|
2176
|
+
if (transaction?.getEntityStateSource) source = await hostOperation(() => transaction.getEntityStateSource(tenantId, spaceId, binding.entityType, entityId));
|
|
2177
|
+
else if (options.store.getEntityStateSource) source = await hostOperation(() => options.store.getEntityStateSource(tenantId, spaceId, binding.entityType, entityId));
|
|
2178
|
+
else {
|
|
2179
|
+
const eventStore = options.store;
|
|
2180
|
+
const listEvents = transaction?.listEvents?.bind(transaction) ?? eventStore.listEvents?.bind(eventStore);
|
|
2181
|
+
const events = listEvents ? await hostOperation(() => listEvents(tenantId, spaceId)) : [];
|
|
2182
|
+
let latest;
|
|
2183
|
+
for (const event of events) if (event.tenantId === tenantId && event.spaceId === spaceId && event.subjectType === binding.entityType && event.subjectId === entityId && typeof event.payload?.toState === "string" && (!latest || event.sequence > latest.sequence)) latest = event;
|
|
2184
|
+
if (latest && typeof latest.actionInvocationId === "string") source = {
|
|
2185
|
+
state: latest.payload.toState,
|
|
2186
|
+
actionInvocationId: latest.actionInvocationId
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
replayingAppliedTransition = source?.actionInvocationId === actionInvocationId && source.state === currentState;
|
|
2190
|
+
}
|
|
2191
|
+
const pending = invocation.pendingStateTransition;
|
|
2192
|
+
if (replayingAppliedTransition && pending && pending.entityType === binding.entityType && pending.entityId === entityId && pending.toState === currentState) replayedTransition = pending;
|
|
2193
|
+
if (action.idempotent && transition.valid && entityId && action.eventPhase === "after_adapters") {
|
|
2194
|
+
pendingStateTransition = {
|
|
2195
|
+
entityType: binding.entityType,
|
|
2196
|
+
entityId,
|
|
2197
|
+
fromState: currentState,
|
|
2198
|
+
toState: targetState
|
|
2053
2199
|
};
|
|
2200
|
+
if (!transaction) await hostOperation(() => options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, { pendingStateTransition }));
|
|
2054
2201
|
}
|
|
2055
|
-
replayingAppliedTransition
|
|
2202
|
+
if (!transition.valid && !replayingAppliedTransition) throw transition.error != null ? new Error(transition.error) : new HostError("Invalid state transition");
|
|
2056
2203
|
}
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2204
|
+
}
|
|
2205
|
+
const handlerResult = action.handler ? await action.handler({
|
|
2206
|
+
actionInvocationId,
|
|
2207
|
+
tenantId,
|
|
2208
|
+
spaceId,
|
|
2209
|
+
actorId: invocation.actorId,
|
|
2210
|
+
actorType: invocation.actorType,
|
|
2211
|
+
correlationId: invocation.correlationId,
|
|
2212
|
+
...invocation.causationId ? { causationId: invocation.causationId } : {},
|
|
2213
|
+
...invocation.provenance ? { provenance: invocation.provenance } : {},
|
|
2214
|
+
...isAdmissionTime(invocation.admittedAt) ? { admittedAt: invocation.admittedAt } : {},
|
|
2215
|
+
db,
|
|
2216
|
+
services: options.services,
|
|
2217
|
+
...action.finalize ? { finalizationProtocol: "atomic-after-adapters-v1" } : {}
|
|
2218
|
+
}, parsed.data) : {
|
|
2219
|
+
success: true,
|
|
2220
|
+
data: {}
|
|
2221
|
+
};
|
|
2222
|
+
if (!handlerResult.success) throw handlerResult.error != null ? new Error(handlerResult.error) : new HostError("Action handler failed");
|
|
2223
|
+
const handlerData = handlerResult.data ?? {};
|
|
2224
|
+
if (action.resultSchema && !action.finalize) {
|
|
2225
|
+
const publicResult = withoutPrivateHostFields(handlerData, eventResultFields);
|
|
2226
|
+
const parsedResult = action.resultSchema.safeParse(publicResult);
|
|
2227
|
+
if (!parsedResult.success) throw new HostError(`Action result validation failed at ${parsedResult.error.issues.map((issue) => issue.path.map(String).join(".") || "result").join(", ")}`);
|
|
2228
|
+
}
|
|
2229
|
+
const preparedFinalizationInput = action.finalize ? {
|
|
2230
|
+
params: normalizeFinalizationJson(parsed.data),
|
|
2231
|
+
handlerData: normalizeFinalizationJson(withoutPrivateHostFields(handlerData, eventResultFields))
|
|
2232
|
+
} : void 0;
|
|
2233
|
+
const extractedEvents = extractEvents(handlerData);
|
|
2234
|
+
const events = action.finalize ? normalizeFinalizationJson(extractedEvents) : extractedEvents;
|
|
2235
|
+
const undeclaredEvent = events.find((event) => !action.emitsEvents.includes(event.eventType));
|
|
2236
|
+
if (undeclaredEvent) throw new HostError(`${action.actionId} emitted undeclared event type ${undeclaredEvent.eventType}`);
|
|
2237
|
+
if (action.eventPhase !== "after_adapters") for (const [index, event] of events.entries()) await hostOperation(() => appendEvent(invocation, event, `domain:${index}`, action.version, transaction));
|
|
2238
|
+
let transitionEvent;
|
|
2239
|
+
if (binding && validatedTransition?.entityId && (validatedTransition.valid || replayedTransition)) {
|
|
2240
|
+
const { entityId, currentState, targetState } = validatedTransition;
|
|
2241
|
+
if (!events.some((event) => event.eventType === "StateTransitioned" && event.subjectType === binding.entityType && event.subjectId === entityId)) transitionEvent = {
|
|
2242
|
+
eventType: "StateTransitioned",
|
|
2243
|
+
subjectType: binding.entityType,
|
|
2244
|
+
subjectId: entityId,
|
|
2245
|
+
payload: {
|
|
2061
2246
|
entityType: binding.entityType,
|
|
2062
2247
|
entityId,
|
|
2063
|
-
fromState: currentState,
|
|
2064
|
-
toState: targetState
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
}
|
|
2068
|
-
if (!transition.valid && !replayingAppliedTransition) throw transition.error != null ? new Error(transition.error) : new HostError("Invalid state transition");
|
|
2248
|
+
fromState: replayedTransition?.fromState ?? currentState,
|
|
2249
|
+
toState: targetState,
|
|
2250
|
+
causedByAction: action.actionId
|
|
2251
|
+
}
|
|
2252
|
+
};
|
|
2069
2253
|
}
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
...invocation.causationId ? { causationId: invocation.causationId } : {},
|
|
2079
|
-
...invocation.provenance ? { provenance: invocation.provenance } : {},
|
|
2080
|
-
...isAdmissionTime(invocation.admittedAt) ? { admittedAt: invocation.admittedAt } : {},
|
|
2081
|
-
db,
|
|
2082
|
-
services: options.services
|
|
2083
|
-
}, parsed.data) : {
|
|
2084
|
-
success: true,
|
|
2085
|
-
data: {}
|
|
2086
|
-
};
|
|
2087
|
-
if (!handlerResult.success) throw handlerResult.error != null ? new Error(handlerResult.error) : new HostError("Action handler failed");
|
|
2088
|
-
const handlerData = handlerResult.data ?? {};
|
|
2089
|
-
if (action.resultSchema) {
|
|
2090
|
-
const publicResult = withoutPrivateHostFields(handlerData, eventResultFields);
|
|
2091
|
-
const parsedResult = action.resultSchema.safeParse(publicResult);
|
|
2092
|
-
if (!parsedResult.success) throw new HostError(`Action result validation failed at ${parsedResult.error.issues.map((issue) => issue.path.map(String).join(".") || "result").join(", ")}`);
|
|
2093
|
-
}
|
|
2094
|
-
const events = extractEvents(handlerData);
|
|
2095
|
-
const undeclaredEvent = events.find((event) => !action.emitsEvents.includes(event.eventType));
|
|
2096
|
-
if (undeclaredEvent) throw new HostError(`${action.actionId} emitted undeclared event type ${undeclaredEvent.eventType}`);
|
|
2097
|
-
if (action.eventPhase !== "after_adapters") for (const [index, event] of events.entries()) await hostOperation(() => appendEvent(invocation, event, `domain:${index}`, action.version, transaction));
|
|
2098
|
-
let transitionEvent;
|
|
2099
|
-
if (binding && validatedTransition?.entityId && (validatedTransition.valid || replayedTransition)) {
|
|
2100
|
-
const { entityId, currentState, targetState } = validatedTransition;
|
|
2101
|
-
if (!events.some((event) => event.eventType === "StateTransitioned" && event.subjectType === binding.entityType && event.subjectId === entityId)) transitionEvent = {
|
|
2102
|
-
eventType: "StateTransitioned",
|
|
2103
|
-
subjectType: binding.entityType,
|
|
2104
|
-
subjectId: entityId,
|
|
2105
|
-
payload: {
|
|
2106
|
-
entityType: binding.entityType,
|
|
2107
|
-
entityId,
|
|
2108
|
-
fromState: replayedTransition?.fromState ?? currentState,
|
|
2109
|
-
toState: targetState,
|
|
2110
|
-
causedByAction: action.actionId
|
|
2254
|
+
if (transitionEvent && action.eventPhase !== "after_adapters") await hostOperation(() => appendEvent(invocation, transitionEvent, "state:transition", 1, transaction));
|
|
2255
|
+
if (transaction && pendingStateTransition) await hostOperation(() => transaction.updateActionInvocation(actionInvocationId, tenantId, spaceId, { pendingStateTransition }));
|
|
2256
|
+
if (preparedFinalizationInput) {
|
|
2257
|
+
const skipped = [];
|
|
2258
|
+
for (const [index, step] of (action.adapterSteps ?? []).entries()) {
|
|
2259
|
+
const input = step.getInput(structuredClone(preparedFinalizationInput.params), structuredClone(preparedFinalizationInput.handlerData));
|
|
2260
|
+
preparedInputs.set(index, input);
|
|
2261
|
+
if (!input) skipped.push(index);
|
|
2111
2262
|
}
|
|
2263
|
+
preparation = normalizeFinalizationJson({
|
|
2264
|
+
protocol: "atomic-after-adapters-v1",
|
|
2265
|
+
actionVersion: action.version,
|
|
2266
|
+
steps: (action.adapterSteps ?? []).map(({ adapterType, operation }) => ({
|
|
2267
|
+
adapterType,
|
|
2268
|
+
operation
|
|
2269
|
+
})),
|
|
2270
|
+
...preparedFinalizationInput,
|
|
2271
|
+
handlerEvents: events,
|
|
2272
|
+
result: preparedFinalizationInput.handlerData,
|
|
2273
|
+
...transitionEvent ? { stateTransition: transitionEvent } : {},
|
|
2274
|
+
skipped
|
|
2275
|
+
});
|
|
2276
|
+
if (new TextEncoder().encode((0, _fabricorg_platform.canonicalJson)(preparation)).byteLength > 262144) throw new HostError("Finalization checkpoint exceeds 256 KiB.");
|
|
2277
|
+
if (executionLease) {
|
|
2278
|
+
if (!await transaction.updateLeasedActionInvocation({
|
|
2279
|
+
id: actionInvocationId,
|
|
2280
|
+
tenantId,
|
|
2281
|
+
spaceId,
|
|
2282
|
+
workerId: executionLease.leaseOwner,
|
|
2283
|
+
leaseToken: executionLease.leaseToken,
|
|
2284
|
+
patch: { finalizationCheckpoint: preparation }
|
|
2285
|
+
})) throw new RecoverableFinalizationError(`Invocation lease lost: ${actionInvocationId}`);
|
|
2286
|
+
} else await transaction.updateActionInvocation(actionInvocationId, tenantId, spaceId, { finalizationCheckpoint: preparation });
|
|
2287
|
+
}
|
|
2288
|
+
return {
|
|
2289
|
+
data: handlerData,
|
|
2290
|
+
domainEvents: events,
|
|
2291
|
+
pendingTransitionEvent: transitionEvent,
|
|
2292
|
+
finalizationInput: preparedFinalizationInput
|
|
2112
2293
|
};
|
|
2113
|
-
}
|
|
2114
|
-
if (transitionEvent && action.eventPhase !== "after_adapters") await hostOperation(() => appendEvent(invocation, transitionEvent, "state:transition", 1, transaction));
|
|
2115
|
-
if (transaction && pendingStateTransition) await hostOperation(() => transaction.updateActionInvocation(actionInvocationId, tenantId, spaceId, { pendingStateTransition }));
|
|
2116
|
-
return {
|
|
2117
|
-
data: handlerData,
|
|
2118
|
-
domainEvents: events,
|
|
2119
|
-
pendingTransitionEvent: transitionEvent
|
|
2120
2294
|
};
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2295
|
+
const atomicStore = asAtomicMutationStore(options.store);
|
|
2296
|
+
let callbackFailure;
|
|
2297
|
+
const run = async (db, transaction) => {
|
|
2298
|
+
callbackFailure = void 0;
|
|
2299
|
+
try {
|
|
2300
|
+
return await runHandler(db, transaction);
|
|
2301
|
+
} catch (error) {
|
|
2302
|
+
callbackFailure = { error };
|
|
2303
|
+
throw error;
|
|
2304
|
+
}
|
|
2305
|
+
};
|
|
2306
|
+
const executed = await (async () => {
|
|
2307
|
+
try {
|
|
2308
|
+
return atomicStore ? await atomicStore.transactionWithEvents((transaction) => run(transaction.db, transaction)) : await options.store.transaction((db) => run(db));
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
if (callbackFailure) throw callbackFailure.error;
|
|
2311
|
+
if (action.finalize) throw new RecoverableFinalizationError(caughtError({
|
|
2312
|
+
actionId: action.actionId,
|
|
2313
|
+
source: "execution"
|
|
2314
|
+
}, error));
|
|
2315
|
+
return hostOperation(() => Promise.reject(error));
|
|
2316
|
+
}
|
|
2317
|
+
})();
|
|
2318
|
+
data = executed.data;
|
|
2319
|
+
finalizationInput = executed.finalizationInput;
|
|
2320
|
+
domainEvents = executed.domainEvents;
|
|
2321
|
+
pendingTransitionEvent = executed.pendingTransitionEvent;
|
|
2322
|
+
if (preparation) invocation = {
|
|
2323
|
+
...invocation,
|
|
2324
|
+
finalizationCheckpoint: preparation
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2144
2327
|
} catch (error) {
|
|
2145
2328
|
if (error instanceof RecoverableFinalizationError) throw error;
|
|
2146
2329
|
if ((0, _fabricorg_platform.isReconciliationRequiredError)(error)) {
|
|
@@ -2165,6 +2348,7 @@ function createGovernedActionHost(options) {
|
|
|
2165
2348
|
source: "handler"
|
|
2166
2349
|
}, error)));
|
|
2167
2350
|
}
|
|
2351
|
+
const finalizationSteps = [];
|
|
2168
2352
|
let pendingHandoff = invocation.pendingCompletion;
|
|
2169
2353
|
for (const [stepIndex, step] of (action.adapterSteps ?? []).entries()) {
|
|
2170
2354
|
const adapterInvocationId = lifecycleId("adp", actionInvocationId, String(stepIndex));
|
|
@@ -2234,8 +2418,16 @@ function createGovernedActionHost(options) {
|
|
|
2234
2418
|
}, error));
|
|
2235
2419
|
}
|
|
2236
2420
|
if (previousAdapterInvocation?.status === "ambiguous") return await settleUnknownOutcome(stepIndex, recordedAdapterReconciliation(previousAdapterInvocation), previousAdapterInvocation.reconciliation?.adapterEventSubject);
|
|
2237
|
-
const input = step.getInput(parsed.data, data);
|
|
2238
|
-
if (!input)
|
|
2421
|
+
const input = action.finalize ? preparation.skipped.includes(stepIndex) ? void 0 : replayCheckpoint ? step.getInput(structuredClone(replayCheckpoint.params), structuredClone(replayCheckpoint.handlerData)) : preparedInputs.get(stepIndex) : step.getInput(parsed.data, data);
|
|
2422
|
+
if (!input) {
|
|
2423
|
+
if (action.finalize) finalizationSteps.push({
|
|
2424
|
+
stepIndex,
|
|
2425
|
+
adapterType: step.adapterType,
|
|
2426
|
+
operation: step.operation,
|
|
2427
|
+
status: "skipped"
|
|
2428
|
+
});
|
|
2429
|
+
continue;
|
|
2430
|
+
}
|
|
2239
2431
|
const adapter = adapters.require(step.adapterType, step.operation);
|
|
2240
2432
|
const adapterEventSubject = options.adapterEventSubject?.(action.actionId, parsed.data, data) ?? {
|
|
2241
2433
|
subjectType: "AdapterInvocation",
|
|
@@ -2391,8 +2583,9 @@ function createGovernedActionHost(options) {
|
|
|
2391
2583
|
let thrownOutcome;
|
|
2392
2584
|
let adapterDeadlineTimer;
|
|
2393
2585
|
try {
|
|
2394
|
-
const
|
|
2395
|
-
const
|
|
2586
|
+
const effectiveDeadlineMs = step.deadlineMs ?? options.adapterDeadlineMs;
|
|
2587
|
+
const adapterController = effectiveDeadlineMs ? new AbortController() : void 0;
|
|
2588
|
+
const deadlineMs = effectiveDeadlineMs ? now().getTime() + effectiveDeadlineMs : void 0;
|
|
2396
2589
|
if (adapterController && deadlineMs) {
|
|
2397
2590
|
const remaining = deadlineMs - now().getTime();
|
|
2398
2591
|
if (remaining <= 0) throw new HostError(`Adapter deadline already expired before execution: ${step.adapterType}:${step.operation}`);
|
|
@@ -2577,6 +2770,7 @@ function createGovernedActionHost(options) {
|
|
|
2577
2770
|
...pendingCompletion ? { pendingCompletion } : {}
|
|
2578
2771
|
}, action.actionId);
|
|
2579
2772
|
}
|
|
2773
|
+
if (action.finalize) output = normalizeFinalizationJson(output);
|
|
2580
2774
|
successOutput = output;
|
|
2581
2775
|
if (handoff) {
|
|
2582
2776
|
const reference = typeof handoff.externalReference === "string" ? handoff.externalReference.trim() : "";
|
|
@@ -2810,7 +3004,8 @@ function createGovernedActionHost(options) {
|
|
|
2810
3004
|
return hostOperation(() => failExecution(invocation, "failed", message));
|
|
2811
3005
|
}
|
|
2812
3006
|
}
|
|
2813
|
-
|
|
3007
|
+
if (action.finalize && pendingHandoff) return settleAppliedEffect("Finalizing actions cannot finalize accepted external operations.", pendingHandoff);
|
|
3008
|
+
let result = action.finalize ? { ...replayCheckpoint?.result ?? finalizationInput.handlerData } : withoutPrivateHostFields(data, eventResultFields);
|
|
2814
3009
|
const parked = pendingHandoff ? {
|
|
2815
3010
|
...pendingHandoff,
|
|
2816
3011
|
parkedAt: now()
|
|
@@ -2824,25 +3019,83 @@ function createGovernedActionHost(options) {
|
|
|
2824
3019
|
result
|
|
2825
3020
|
};
|
|
2826
3021
|
let verdictWritten = true;
|
|
3022
|
+
let finalizationCallbackFailure;
|
|
2827
3023
|
try {
|
|
2828
3024
|
const atomicStore = asAtomicMutationStore(options.store);
|
|
2829
3025
|
if (action.eventPhase === "after_adapters" && atomicStore) {
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
if (
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
3026
|
+
if (action.finalize) for (const [stepIndex, step] of (action.adapterSteps ?? []).entries()) {
|
|
3027
|
+
if (finalizationSteps.some((entry) => entry.stepIndex === stepIndex)) continue;
|
|
3028
|
+
const row = await options.store.getAdapterInvocation(lifecycleId("adp", actionInvocationId, String(stepIndex)), adapterInvocationScope);
|
|
3029
|
+
if (row?.status !== "succeeded" || row.output === void 0) {
|
|
3030
|
+
const error = /* @__PURE__ */ new Error("Finalization requires successful stored adapter output.");
|
|
3031
|
+
finalizationCallbackFailure = { error };
|
|
3032
|
+
throw error;
|
|
3033
|
+
}
|
|
3034
|
+
finalizationSteps.push({
|
|
3035
|
+
stepIndex,
|
|
3036
|
+
adapterType: step.adapterType,
|
|
3037
|
+
operation: step.operation,
|
|
3038
|
+
status: "succeeded",
|
|
3039
|
+
adapterInvocationId: row.id,
|
|
3040
|
+
output: finalizationJson(row.output)
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
3043
|
+
const finalizeSuccess = () => atomicStore.transactionWithEvents(async (transaction) => {
|
|
3044
|
+
try {
|
|
3045
|
+
let finalized;
|
|
3046
|
+
async function assertFinalizationOwner() {
|
|
3047
|
+
const current = await transaction.getActionInvocationForUpdate(actionInvocationId, tenantId, spaceId);
|
|
3048
|
+
if (!current || current.status !== "running" || (executionLease ? current.leaseOwner !== executionLease.leaseOwner || current.leaseToken !== executionLease.leaseToken : !!current.leaseOwner)) throw new RecoverableFinalizationError(`Invocation lease lost or no longer running: ${actionInvocationId}`);
|
|
3049
|
+
}
|
|
3050
|
+
if (action.finalize) {
|
|
3051
|
+
await assertFinalizationOwner();
|
|
3052
|
+
finalized = validateFinalization(await action.finalize({
|
|
3053
|
+
actionInvocationId,
|
|
3054
|
+
tenantId,
|
|
3055
|
+
spaceId,
|
|
3056
|
+
actorId: invocation.actorId,
|
|
3057
|
+
actorType: invocation.actorType,
|
|
3058
|
+
correlationId: invocation.correlationId,
|
|
3059
|
+
causationId: invocation.causationId,
|
|
3060
|
+
provenance: invocation.provenance ? structuredClone(invocation.provenance) : void 0,
|
|
3061
|
+
db: transaction.db
|
|
3062
|
+
}, {
|
|
3063
|
+
...structuredClone(finalizationInput),
|
|
3064
|
+
adapterOutputs: structuredClone(finalizationSteps.sort((a, b) => a.stepIndex - b.stepIndex))
|
|
3065
|
+
}), action, eventResultFields, options.eventContracts);
|
|
3066
|
+
result = withoutPrivateHostFields({
|
|
3067
|
+
...result,
|
|
3068
|
+
...finalized.resultPatch
|
|
3069
|
+
}, eventResultFields);
|
|
3070
|
+
if (action.resultSchema && !action.resultSchema.safeParse(result).success) throw new Error("Finalization merged result does not match resultSchema.");
|
|
3071
|
+
finalPatch.result = result;
|
|
3072
|
+
}
|
|
3073
|
+
for (const [index, event] of domainEvents.entries()) await appendEvent(invocation, event, `domain:${index}`, action.version, transaction);
|
|
3074
|
+
for (const [index, event] of (finalized?.events ?? []).entries()) await appendEvent(invocation, event, `finalize:${index}`, action.version, transaction);
|
|
3075
|
+
if (pendingTransitionEvent) await appendEvent(invocation, pendingTransitionEvent, "state:transition", 1, transaction);
|
|
3076
|
+
if (action.finalize) await assertFinalizationOwner();
|
|
3077
|
+
if (executionLease) {
|
|
3078
|
+
if (!transaction.updateLeasedActionInvocation) throw new HostError("Leased atomic finalization requires transaction-scoped fencing support.");
|
|
3079
|
+
if (!await transaction.updateLeasedActionInvocation({
|
|
3080
|
+
id: actionInvocationId,
|
|
3081
|
+
tenantId,
|
|
3082
|
+
spaceId,
|
|
3083
|
+
workerId: executionLease.leaseOwner,
|
|
3084
|
+
leaseToken: executionLease.leaseToken,
|
|
3085
|
+
patch: finalPatch
|
|
3086
|
+
})) throw new RecoverableFinalizationError(`Invocation lease lost: ${actionInvocationId}`);
|
|
3087
|
+
} else verdictWritten = await writeInlineInvocation(invocation, finalPatch, transaction, onPreservedReconciliation);
|
|
3088
|
+
} catch (error) {
|
|
3089
|
+
finalizationCallbackFailure = { error };
|
|
3090
|
+
throw error;
|
|
3091
|
+
}
|
|
3092
|
+
});
|
|
3093
|
+
if (action.finalize) await finalizeSuccess();
|
|
3094
|
+
else await hostOperation(finalizeSuccess);
|
|
3095
|
+
if (!pendingHandoff && verdictWritten) {
|
|
3096
|
+
if (action.finalize) rememberVerdict(finalPatch);
|
|
3097
|
+
emitInvocationStatusTelemetry(invocation, "completed", options.telemetry, now());
|
|
3098
|
+
}
|
|
2846
3099
|
} else {
|
|
2847
3100
|
if (action.eventPhase === "after_adapters") {
|
|
2848
3101
|
for (const [index, event] of domainEvents.entries()) await hostOperation(() => appendEvent(invocation, event, `domain:${index}`, action.version));
|
|
@@ -2851,6 +3104,18 @@ function createGovernedActionHost(options) {
|
|
|
2851
3104
|
await hostOperation(() => persistExecutionInvocation(invocation, finalPatch));
|
|
2852
3105
|
}
|
|
2853
3106
|
} catch (error) {
|
|
3107
|
+
if (action.finalize) {
|
|
3108
|
+
if (error instanceof RecoverableFinalizationError) throw error;
|
|
3109
|
+
if (!finalizationCallbackFailure || finalizationCallbackFailure.error !== error) throw new RecoverableFinalizationError(caughtError({
|
|
3110
|
+
actionId: action.actionId,
|
|
3111
|
+
source: "execution"
|
|
3112
|
+
}, error));
|
|
3113
|
+
const message = caughtError({
|
|
3114
|
+
actionId: action.actionId,
|
|
3115
|
+
source: "execution"
|
|
3116
|
+
}, error);
|
|
3117
|
+
return hasDurableProviderEffect ? settleAppliedEffect(message) : hostOperation(() => failExecution(invocation, "failed", message));
|
|
3118
|
+
}
|
|
2854
3119
|
if (action.eventPhase === "after_adapters") throw new RecoverableFinalizationError(errorMessage(error));
|
|
2855
3120
|
throw error;
|
|
2856
3121
|
}
|
|
@@ -2865,7 +3130,13 @@ function createGovernedActionHost(options) {
|
|
|
2865
3130
|
} catch (error) {
|
|
2866
3131
|
if (error instanceof RecoverableFinalizationError) throw error;
|
|
2867
3132
|
if (retryableGovernance && (0, _fabricorg_platform.isRetryableExecutionError)(error)) {
|
|
2868
|
-
|
|
3133
|
+
let reason = executionReasonOverride === "approval_resume" ? "approval resume" : !invocation.leaseOwner || invocation.leaseToken === void 0 ? "not leased" : invocation.attemptCount >= retryableExecutionMaxAttempts ? `attempts exhausted (${invocation.attemptCount}/${retryableExecutionMaxAttempts})` : void 0;
|
|
3134
|
+
if (!reason && !action.idempotent) try {
|
|
3135
|
+
await persistExecutionInvocation(invocation, { deferredBeforeEffectAttempt: invocation.attemptCount });
|
|
3136
|
+
} catch (markerError) {
|
|
3137
|
+
if (markerError instanceof RecoverableFinalizationError) throw markerError;
|
|
3138
|
+
reason = "pre-effect marker could not be recorded";
|
|
3139
|
+
}
|
|
2869
3140
|
if (!reason) throw error;
|
|
2870
3141
|
return hostOperation(() => failExecution(invocation, "failed", caughtError({
|
|
2871
3142
|
actionId: invocation.actionId,
|
|
@@ -3671,6 +3942,11 @@ var MemoryPlatformHostStore = class {
|
|
|
3671
3942
|
const record = this.invocations.find((row) => row.id === id && row.tenantId === tenantId && row.spaceId === spaceId);
|
|
3672
3943
|
if (!record) throw new Error(`ActionInvocation not found: ${id}`);
|
|
3673
3944
|
Object.assign(record, patch, { updatedAt: /* @__PURE__ */ new Date() });
|
|
3945
|
+
if (Object.hasOwn(patch, "finalizationCheckpoint")) {
|
|
3946
|
+
if (patch.finalizationCheckpoint) record.finalizationCheckpoint = structuredClone(patch.finalizationCheckpoint);
|
|
3947
|
+
else delete record.finalizationCheckpoint;
|
|
3948
|
+
}
|
|
3949
|
+
if (Object.hasOwn(patch, "deferredBeforeEffectAttempt") && patch.deferredBeforeEffectAttempt === void 0) delete record.deferredBeforeEffectAttempt;
|
|
3674
3950
|
if (Object.hasOwn(patch, "pendingStateTransition")) {
|
|
3675
3951
|
if (patch.pendingStateTransition) record.pendingStateTransition = { ...patch.pendingStateTransition };
|
|
3676
3952
|
else delete record.pendingStateTransition;
|
|
@@ -4307,6 +4583,16 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
4307
4583
|
sql: `
|
|
4308
4584
|
ALTER TABLE fabric_platform.adapter_invocations ADD COLUMN IF NOT EXISTS reconciliation jsonb;
|
|
4309
4585
|
`
|
|
4586
|
+
},
|
|
4587
|
+
{
|
|
4588
|
+
version: 9,
|
|
4589
|
+
name: "pre_effect_governance_deferral",
|
|
4590
|
+
sql: `ALTER TABLE fabric_platform.action_invocations ADD COLUMN IF NOT EXISTS deferred_before_effect_attempt integer;`
|
|
4591
|
+
},
|
|
4592
|
+
{
|
|
4593
|
+
version: 10,
|
|
4594
|
+
name: "finalization_preparation_checkpoint",
|
|
4595
|
+
sql: `ALTER TABLE fabric_platform.action_invocations ADD COLUMN IF NOT EXISTS finalization_checkpoint jsonb;`
|
|
4310
4596
|
}
|
|
4311
4597
|
]);
|
|
4312
4598
|
}
|
|
@@ -4380,6 +4666,8 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
4380
4666
|
pending_completion=CASE WHEN $12::boolean THEN $13::jsonb ELSE pending_completion END,
|
|
4381
4667
|
completion_due_at=CASE WHEN $12::boolean THEN ($13::jsonb->>'dueAt')::timestamptz ELSE completion_due_at END,
|
|
4382
4668
|
external_completion=CASE WHEN $14::boolean THEN $15::jsonb ELSE external_completion END,
|
|
4669
|
+
${Object.hasOwn(patch, "finalizationCheckpoint") ? `finalization_checkpoint=$${18 + (Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? 2 : 0)}::jsonb,` : ""}
|
|
4670
|
+
${Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? "deferred_before_effect_attempt=CASE WHEN $18::boolean THEN $19::integer ELSE deferred_before_effect_attempt END," : ""}
|
|
4383
4671
|
pending_state_transition=CASE WHEN $16::boolean THEN $17::jsonb ELSE pending_state_transition END,
|
|
4384
4672
|
lease_owner=CASE WHEN ($13::jsonb IS NOT NULL AND $4='running') OR $4 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
|
|
4385
4673
|
THEN NULL ELSE lease_owner END,
|
|
@@ -4403,7 +4691,9 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
4403
4691
|
Object.hasOwn(patch, "externalCompletion"),
|
|
4404
4692
|
patch.externalCompletion ? JSON.stringify(patch.externalCompletion) : null,
|
|
4405
4693
|
Object.hasOwn(patch, "pendingStateTransition"),
|
|
4406
|
-
patch.pendingStateTransition ? JSON.stringify(patch.pendingStateTransition) : null
|
|
4694
|
+
patch.pendingStateTransition ? JSON.stringify(patch.pendingStateTransition) : null,
|
|
4695
|
+
...Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? [true, patch.deferredBeforeEffectAttempt ?? null] : [],
|
|
4696
|
+
...Object.hasOwn(patch, "finalizationCheckpoint") ? [patch.finalizationCheckpoint ? JSON.stringify(patch.finalizationCheckpoint) : null] : []
|
|
4407
4697
|
]);
|
|
4408
4698
|
}
|
|
4409
4699
|
async recordHitlDecision(actionInvocationId, tenantId, spaceId, decision) {
|
|
@@ -5033,6 +5323,8 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
5033
5323
|
pending_completion=CASE WHEN $14::boolean THEN $15::jsonb ELSE pending_completion END,
|
|
5034
5324
|
completion_due_at=CASE WHEN $14::boolean THEN ($15::jsonb->>'dueAt')::timestamptz ELSE completion_due_at END,
|
|
5035
5325
|
external_completion=CASE WHEN $16::boolean THEN $17::jsonb ELSE external_completion END,
|
|
5326
|
+
${Object.hasOwn(patch, "finalizationCheckpoint") ? `finalization_checkpoint=$${20 + (Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? 2 : 0)}::jsonb,` : ""}
|
|
5327
|
+
${Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? "deferred_before_effect_attempt=CASE WHEN $20::boolean THEN $21::integer ELSE deferred_before_effect_attempt END," : ""}
|
|
5036
5328
|
pending_state_transition=CASE WHEN $18::boolean THEN $19::jsonb ELSE pending_state_transition END,
|
|
5037
5329
|
lease_owner=CASE WHEN ($15::jsonb IS NOT NULL AND $6='running') OR $6 IN ('waiting_for_approval','completed','failed','blocked_by_policy','reconciliation_required','validation_failed')
|
|
5038
5330
|
THEN NULL ELSE lease_owner END,
|
|
@@ -5060,7 +5352,9 @@ var PostgresPlatformHostStore = class PostgresPlatformHostStore {
|
|
|
5060
5352
|
Object.hasOwn(patch, "externalCompletion"),
|
|
5061
5353
|
patch.externalCompletion ? JSON.stringify(patch.externalCompletion) : null,
|
|
5062
5354
|
Object.hasOwn(patch, "pendingStateTransition"),
|
|
5063
|
-
patch.pendingStateTransition ? JSON.stringify(patch.pendingStateTransition) : null
|
|
5355
|
+
patch.pendingStateTransition ? JSON.stringify(patch.pendingStateTransition) : null,
|
|
5356
|
+
...Object.hasOwn(patch, "deferredBeforeEffectAttempt") ? [true, patch.deferredBeforeEffectAttempt ?? null] : [],
|
|
5357
|
+
...Object.hasOwn(patch, "finalizationCheckpoint") ? [patch.finalizationCheckpoint ? JSON.stringify(patch.finalizationCheckpoint) : null] : []
|
|
5064
5358
|
])).rows.length === 1;
|
|
5065
5359
|
}
|
|
5066
5360
|
async renewActionInvocationLease(input) {
|
|
@@ -5152,11 +5446,13 @@ function toActionRecord(row) {
|
|
|
5152
5446
|
...row.execution_reason ? { executionReason: String(row.execution_reason) } : {},
|
|
5153
5447
|
...row.authorization_reconciliation ? { authorizationReconciliation: row.authorization_reconciliation } : {},
|
|
5154
5448
|
...row.adapter_reconciliation ? { adapterReconciliation: row.adapter_reconciliation } : {},
|
|
5449
|
+
...row.finalization_checkpoint ? { finalizationCheckpoint: row.finalization_checkpoint } : {},
|
|
5155
5450
|
...row.pending_state_transition ? { pendingStateTransition: row.pending_state_transition } : {},
|
|
5156
5451
|
...row.pending_completion ? { pendingCompletion: toPendingCompletion(row.pending_completion) } : {},
|
|
5157
5452
|
...row.external_completion ? { externalCompletion: toExternalCompletion(row.external_completion) } : {},
|
|
5158
5453
|
...row.authorization_binding_id ? { authorizationBindingId: String(row.authorization_binding_id) } : {},
|
|
5159
5454
|
attemptCount: Number(row.attempt_count ?? 0),
|
|
5455
|
+
...row.deferred_before_effect_attempt == null ? {} : { deferredBeforeEffectAttempt: Number(row.deferred_before_effect_attempt) },
|
|
5160
5456
|
leaseToken: Number(row.lease_token ?? 0),
|
|
5161
5457
|
...row.lease_owner ? { leaseOwner: String(row.lease_owner) } : {},
|
|
5162
5458
|
...row.lease_expires_at ? { leaseExpiresAt: new Date(row.lease_expires_at) } : {},
|