@fabricorg/platform-host 0.2.0 → 0.3.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 +8 -0
- package/README.md +6 -0
- package/dist/index.cjs +386 -204
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +61 -2
- package/dist/index.d.ts +61 -2
- package/dist/index.js +384 -205
- package/dist/index.js.map +1 -1
- package/package.json +62 -66
package/dist/index.js
CHANGED
|
@@ -22,9 +22,8 @@ function createGovernedActionHost(options) {
|
|
|
22
22
|
}
|
|
23
23
|
const actionInvocationId = createFabricId("act");
|
|
24
24
|
const correlationId = input.correlationId ?? createFabricId("corr");
|
|
25
|
-
const workflowId = `action-invocation-${actionInvocationId}`;
|
|
26
25
|
const durableParameters = options.redactActionParameters ? options.redactActionParameters(input.actionId, input.parameters) : input.parameters;
|
|
27
|
-
await options.store.createActionInvocation({
|
|
26
|
+
const durableInvocation = await options.store.createActionInvocation({
|
|
28
27
|
id: actionInvocationId,
|
|
29
28
|
tenantId: input.tenantId,
|
|
30
29
|
spaceId: input.spaceId,
|
|
@@ -36,26 +35,37 @@ function createGovernedActionHost(options) {
|
|
|
36
35
|
parameters: durableParameters,
|
|
37
36
|
result: {},
|
|
38
37
|
correlationId,
|
|
39
|
-
...input.causationId ? { causationId: input.causationId } : {}
|
|
38
|
+
...input.causationId ? { causationId: input.causationId } : {},
|
|
39
|
+
...input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}
|
|
40
40
|
});
|
|
41
|
+
const durableWorkflowId = `action-invocation-${durableInvocation.id}`;
|
|
42
|
+
if (durableInvocation.id !== actionInvocationId && isTerminal(durableInvocation.status)) {
|
|
43
|
+
return {
|
|
44
|
+
actionInvocationId: durableInvocation.id,
|
|
45
|
+
status: durableInvocation.status,
|
|
46
|
+
workflowId: durableWorkflowId,
|
|
47
|
+
result: durableInvocation.result,
|
|
48
|
+
...durableInvocation.error ? { error: durableInvocation.error } : {}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
41
51
|
if (options.dispatcher) {
|
|
42
52
|
try {
|
|
43
53
|
const dispatched = await options.dispatcher.dispatch({
|
|
44
|
-
actionInvocationId,
|
|
54
|
+
actionInvocationId: durableInvocation.id,
|
|
45
55
|
tenantId: input.tenantId,
|
|
46
56
|
spaceId: input.spaceId,
|
|
47
|
-
workflowId
|
|
57
|
+
workflowId: durableWorkflowId
|
|
48
58
|
});
|
|
49
59
|
return {
|
|
50
|
-
actionInvocationId,
|
|
51
|
-
status:
|
|
60
|
+
actionInvocationId: durableInvocation.id,
|
|
61
|
+
status: durableInvocation.status,
|
|
52
62
|
workflowId: dispatched.workflowId,
|
|
53
63
|
...dispatched.runId ? { runId: dispatched.runId } : {}
|
|
54
64
|
};
|
|
55
65
|
} catch (error) {
|
|
56
66
|
const message = errorMessage(error);
|
|
57
67
|
await options.store.updateActionInvocation(
|
|
58
|
-
|
|
68
|
+
durableInvocation.id,
|
|
59
69
|
input.tenantId,
|
|
60
70
|
input.spaceId,
|
|
61
71
|
{ status: "failed", error: message }
|
|
@@ -64,11 +74,11 @@ function createGovernedActionHost(options) {
|
|
|
64
74
|
}
|
|
65
75
|
}
|
|
66
76
|
const executed = await executeInvocation(
|
|
67
|
-
|
|
77
|
+
durableInvocation.id,
|
|
68
78
|
input.tenantId,
|
|
69
79
|
input.spaceId
|
|
70
80
|
);
|
|
71
|
-
return { ...executed, workflowId };
|
|
81
|
+
return { ...executed, workflowId: durableWorkflowId };
|
|
72
82
|
}
|
|
73
83
|
async function executeInvocation(actionInvocationId, tenantId, spaceId) {
|
|
74
84
|
const invocation = await options.store.getActionInvocation(
|
|
@@ -92,215 +102,219 @@ function createGovernedActionHost(options) {
|
|
|
92
102
|
await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
|
|
93
103
|
status: "running"
|
|
94
104
|
});
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
spaceId,
|
|
108
|
-
actionInvocationId,
|
|
109
|
-
actionId: action.actionId,
|
|
110
|
-
actorId: invocation.actorId,
|
|
111
|
-
actorType: invocation.actorType,
|
|
112
|
-
parameters: parsed.data,
|
|
113
|
-
db: options.store.db,
|
|
114
|
-
services: options.services,
|
|
115
|
-
mode: "execute"
|
|
116
|
-
});
|
|
117
|
-
for (const outcome of outcomes) {
|
|
118
|
-
await options.store.appendPolicyEvaluation({
|
|
119
|
-
id: createFabricId("pol"),
|
|
120
|
-
actionInvocationId,
|
|
105
|
+
try {
|
|
106
|
+
const parsed = action.schema.safeParse(invocation.parameters);
|
|
107
|
+
if (!parsed.success) {
|
|
108
|
+
const message = parsed.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ");
|
|
109
|
+
return fail(invocation, "validation_failed", message);
|
|
110
|
+
}
|
|
111
|
+
const authorizationInput = toAuthorizationInput(action, invocation);
|
|
112
|
+
const definitions = options.resolvePolicies ? await options.resolvePolicies({
|
|
113
|
+
...authorizationInput,
|
|
114
|
+
declaredPolicyIds: action.policies ?? []
|
|
115
|
+
}) : declaredPolicies(action.policies ?? []);
|
|
116
|
+
const outcomes = await evaluatePolicyDefinitions(definitions, {
|
|
121
117
|
tenantId,
|
|
122
118
|
spaceId,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
eventType: "ComplianceBlocked",
|
|
132
|
-
subjectType: "ActionInvocation",
|
|
133
|
-
subjectId: actionInvocationId,
|
|
134
|
-
payload: { actionId: action.actionId, policyId: aggregate.policyId, reason }
|
|
119
|
+
actionInvocationId,
|
|
120
|
+
actionId: action.actionId,
|
|
121
|
+
actorId: invocation.actorId,
|
|
122
|
+
actorType: invocation.actorType,
|
|
123
|
+
parameters: parsed.data,
|
|
124
|
+
db: options.store.db,
|
|
125
|
+
services: options.services,
|
|
126
|
+
mode: "execute"
|
|
135
127
|
});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
entityId
|
|
146
|
-
) ?? initialState(binding.entityType) : initialState(binding.entityType);
|
|
147
|
-
const targetState = typeof binding.targetState === "function" ? binding.targetState(parsed.data) : binding.targetState;
|
|
148
|
-
const transition = validateTransition(
|
|
149
|
-
binding.entityType,
|
|
150
|
-
currentState,
|
|
151
|
-
targetState,
|
|
152
|
-
action.actionId
|
|
153
|
-
);
|
|
154
|
-
if (!transition.valid) {
|
|
155
|
-
return fail(invocation, "failed", transition.error ?? "Invalid state transition");
|
|
128
|
+
for (const outcome of outcomes) {
|
|
129
|
+
await options.store.appendPolicyEvaluation({
|
|
130
|
+
id: createFabricId("pol"),
|
|
131
|
+
actionInvocationId,
|
|
132
|
+
tenantId,
|
|
133
|
+
spaceId,
|
|
134
|
+
outcome,
|
|
135
|
+
createdAt: now()
|
|
136
|
+
});
|
|
156
137
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
actorId: invocation.actorId,
|
|
168
|
-
actorType: invocation.actorType,
|
|
169
|
-
correlationId: invocation.correlationId,
|
|
170
|
-
...invocation.causationId ? { causationId: invocation.causationId } : {},
|
|
171
|
-
db,
|
|
172
|
-
services: options.services
|
|
173
|
-
},
|
|
174
|
-
parsed.data
|
|
175
|
-
) : Promise.resolve({ success: true, data: {} })
|
|
176
|
-
);
|
|
177
|
-
if (!handlerResult.success) {
|
|
178
|
-
return fail(invocation, "failed", handlerResult.error ?? "Action handler failed");
|
|
138
|
+
const aggregate = aggregatePolicyOutcomes(outcomes);
|
|
139
|
+
if (aggregate?.result === "block") {
|
|
140
|
+
const reason = aggregate.reason ?? `Blocked by policy ${aggregate.policyId}`;
|
|
141
|
+
await appendEvent(invocation, {
|
|
142
|
+
eventType: "ComplianceBlocked",
|
|
143
|
+
subjectType: "ActionInvocation",
|
|
144
|
+
subjectId: actionInvocationId,
|
|
145
|
+
payload: { actionId: action.actionId, policyId: aggregate.policyId, reason }
|
|
146
|
+
});
|
|
147
|
+
return fail(invocation, "blocked_by_policy", reason);
|
|
179
148
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
149
|
+
const binding = action.stateMachine;
|
|
150
|
+
if (binding) {
|
|
151
|
+
const entityId = binding.getEntityId(parsed.data);
|
|
152
|
+
const currentState = entityId ? await options.store.getEntityState(
|
|
153
|
+
tenantId,
|
|
154
|
+
spaceId,
|
|
155
|
+
binding.entityType,
|
|
156
|
+
entityId
|
|
157
|
+
) ?? initialState(binding.entityType) : initialState(binding.entityType);
|
|
158
|
+
const targetState = typeof binding.targetState === "function" ? binding.targetState(parsed.data) : binding.targetState;
|
|
159
|
+
const transition = validateTransition(
|
|
160
|
+
binding.entityType,
|
|
161
|
+
currentState,
|
|
162
|
+
targetState,
|
|
163
|
+
action.actionId
|
|
164
|
+
);
|
|
165
|
+
if (!transition.valid) {
|
|
166
|
+
return fail(invocation, "failed", transition.error ?? "Invalid state transition");
|
|
167
|
+
}
|
|
184
168
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
for (const step of action.adapterSteps ?? []) {
|
|
189
|
-
const input = step.getInput(parsed.data, data);
|
|
190
|
-
if (!input) continue;
|
|
191
|
-
const adapter = adapters.require(step.adapterType, step.operation);
|
|
192
|
-
const adapterInvocationId = createFabricId("adp");
|
|
193
|
-
const adapterEventSubject = options.adapterEventSubject?.(
|
|
194
|
-
action.actionId,
|
|
195
|
-
parsed.data,
|
|
196
|
-
data
|
|
197
|
-
) ?? { subjectType: "AdapterInvocation", subjectId: adapterInvocationId };
|
|
198
|
-
const recordedInput = options.redactAdapterInput ? options.redactAdapterInput(
|
|
199
|
-
action.actionId,
|
|
200
|
-
step.adapterType,
|
|
201
|
-
step.operation,
|
|
202
|
-
input
|
|
203
|
-
) : input;
|
|
204
|
-
const startedAt = now();
|
|
205
|
-
const record = {
|
|
206
|
-
id: adapterInvocationId,
|
|
207
|
-
actionInvocationId,
|
|
208
|
-
tenantId,
|
|
209
|
-
spaceId,
|
|
210
|
-
adapterType: step.adapterType,
|
|
211
|
-
operation: step.operation,
|
|
212
|
-
vendor: adapter.vendor,
|
|
213
|
-
status: "running",
|
|
214
|
-
input: recordedInput,
|
|
215
|
-
attempt: 1,
|
|
216
|
-
createdAt: startedAt,
|
|
217
|
-
updatedAt: startedAt
|
|
218
|
-
};
|
|
219
|
-
await options.store.createAdapterInvocation(record);
|
|
220
|
-
await appendEvent(invocation, {
|
|
221
|
-
eventType: "AdapterInvocationStarted",
|
|
222
|
-
subjectType: adapterEventSubject.subjectType,
|
|
223
|
-
subjectId: adapterEventSubject.subjectId,
|
|
224
|
-
payload: { adapterType: step.adapterType, operation: step.operation }
|
|
225
|
-
});
|
|
169
|
+
let data;
|
|
170
|
+
let domainEvents = [];
|
|
226
171
|
try {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
await options.store.updateAdapterInvocation(adapterInvocationId, {
|
|
232
|
-
attempt,
|
|
233
|
-
updatedAt: now()
|
|
234
|
-
});
|
|
235
|
-
return adapter.execute(input, {
|
|
172
|
+
const handlerResult = await options.store.transaction(
|
|
173
|
+
(db) => action.handler ? action.handler(
|
|
174
|
+
{
|
|
175
|
+
actionInvocationId,
|
|
236
176
|
tenantId,
|
|
237
177
|
spaceId,
|
|
238
|
-
|
|
239
|
-
|
|
178
|
+
actorId: invocation.actorId,
|
|
179
|
+
actorType: invocation.actorType,
|
|
240
180
|
correlationId: invocation.correlationId,
|
|
241
181
|
...invocation.causationId ? { causationId: invocation.causationId } : {},
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
182
|
+
db,
|
|
183
|
+
services: options.services
|
|
184
|
+
},
|
|
185
|
+
parsed.data
|
|
186
|
+
) : Promise.resolve({ success: true, data: {} })
|
|
187
|
+
);
|
|
188
|
+
if (!handlerResult.success) {
|
|
189
|
+
return fail(invocation, "failed", handlerResult.error ?? "Action handler failed");
|
|
190
|
+
}
|
|
191
|
+
data = handlerResult.data ?? {};
|
|
192
|
+
domainEvents = extractEvents(data);
|
|
193
|
+
if (action.eventPhase !== "after_adapters") {
|
|
194
|
+
for (const event of domainEvents) await appendEvent(invocation, event);
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
return fail(invocation, "failed", errorMessage(error));
|
|
198
|
+
}
|
|
199
|
+
for (const step of action.adapterSteps ?? []) {
|
|
200
|
+
const input = step.getInput(parsed.data, data);
|
|
201
|
+
if (!input) continue;
|
|
202
|
+
const adapter = adapters.require(step.adapterType, step.operation);
|
|
203
|
+
const adapterInvocationId = createFabricId("adp");
|
|
204
|
+
const adapterEventSubject = options.adapterEventSubject?.(
|
|
205
|
+
action.actionId,
|
|
206
|
+
parsed.data,
|
|
207
|
+
data
|
|
208
|
+
) ?? { subjectType: "AdapterInvocation", subjectId: adapterInvocationId };
|
|
209
|
+
const recordedInput = options.redactAdapterInput ? options.redactAdapterInput(
|
|
210
|
+
action.actionId,
|
|
211
|
+
step.adapterType,
|
|
212
|
+
step.operation,
|
|
213
|
+
input
|
|
214
|
+
) : input;
|
|
215
|
+
const startedAt = now();
|
|
216
|
+
const record = {
|
|
217
|
+
id: adapterInvocationId,
|
|
218
|
+
actionInvocationId,
|
|
219
|
+
tenantId,
|
|
220
|
+
spaceId,
|
|
221
|
+
adapterType: step.adapterType,
|
|
222
|
+
operation: step.operation,
|
|
223
|
+
vendor: adapter.vendor,
|
|
224
|
+
status: "running",
|
|
225
|
+
input: recordedInput,
|
|
226
|
+
attempt: 1,
|
|
227
|
+
createdAt: startedAt,
|
|
228
|
+
updatedAt: startedAt
|
|
229
|
+
};
|
|
230
|
+
await options.store.createAdapterInvocation(record);
|
|
231
|
+
await appendEvent(invocation, {
|
|
232
|
+
eventType: "AdapterInvocationStarted",
|
|
233
|
+
subjectType: adapterEventSubject.subjectType,
|
|
234
|
+
subjectId: adapterEventSubject.subjectId,
|
|
235
|
+
payload: { adapterType: step.adapterType, operation: step.operation }
|
|
248
236
|
});
|
|
249
|
-
|
|
237
|
+
try {
|
|
238
|
+
const result2 = await executeWithAdapterRetry({
|
|
239
|
+
policy: step.retryPolicy ?? adapter.retryPolicy,
|
|
240
|
+
defaultIdempotent: adapter.idempotent,
|
|
241
|
+
execute: async (attempt, maxAttempts) => {
|
|
242
|
+
await options.store.updateAdapterInvocation(adapterInvocationId, {
|
|
243
|
+
attempt,
|
|
244
|
+
updatedAt: now()
|
|
245
|
+
});
|
|
246
|
+
return adapter.execute(input, {
|
|
247
|
+
tenantId,
|
|
248
|
+
spaceId,
|
|
249
|
+
actionInvocationId,
|
|
250
|
+
adapterInvocationId,
|
|
251
|
+
correlationId: invocation.correlationId,
|
|
252
|
+
...invocation.causationId ? { causationId: invocation.causationId } : {},
|
|
253
|
+
attempt,
|
|
254
|
+
maxAttempts
|
|
255
|
+
});
|
|
256
|
+
},
|
|
257
|
+
isSuccessful: (result3) => result3.success,
|
|
258
|
+
getError: (result3) => result3.error
|
|
259
|
+
});
|
|
260
|
+
if (!result2.success) {
|
|
261
|
+
await options.store.updateAdapterInvocation(adapterInvocationId, {
|
|
262
|
+
status: "failed",
|
|
263
|
+
error: result2.error ?? "Adapter failed",
|
|
264
|
+
updatedAt: now()
|
|
265
|
+
});
|
|
266
|
+
await appendEvent(invocation, {
|
|
267
|
+
eventType: "AdapterInvocationFailed",
|
|
268
|
+
subjectType: adapterEventSubject.subjectType,
|
|
269
|
+
subjectId: adapterEventSubject.subjectId,
|
|
270
|
+
payload: { adapterType: step.adapterType, operation: step.operation }
|
|
271
|
+
});
|
|
272
|
+
return fail(invocation, "failed", result2.error ?? "Adapter failed");
|
|
273
|
+
}
|
|
274
|
+
const resultRecord = result2;
|
|
275
|
+
const output = isRecord(resultRecord.output) ? resultRecord.output : Object.fromEntries(
|
|
276
|
+
Object.entries(resultRecord).filter(
|
|
277
|
+
([key]) => key !== "success" && key !== "error"
|
|
278
|
+
)
|
|
279
|
+
);
|
|
250
280
|
await options.store.updateAdapterInvocation(adapterInvocationId, {
|
|
251
|
-
status: "
|
|
252
|
-
|
|
281
|
+
status: "succeeded",
|
|
282
|
+
output,
|
|
253
283
|
updatedAt: now()
|
|
254
284
|
});
|
|
255
285
|
await appendEvent(invocation, {
|
|
256
|
-
eventType: "
|
|
286
|
+
eventType: "AdapterInvocationSucceeded",
|
|
257
287
|
subjectType: adapterEventSubject.subjectType,
|
|
258
288
|
subjectId: adapterEventSubject.subjectId,
|
|
259
|
-
payload: {
|
|
289
|
+
payload: {
|
|
290
|
+
adapterType: step.adapterType,
|
|
291
|
+
operation: step.operation,
|
|
292
|
+
input: recordedInput,
|
|
293
|
+
output
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
} catch (error) {
|
|
297
|
+
const message = errorMessage(error);
|
|
298
|
+
await options.store.updateAdapterInvocation(adapterInvocationId, {
|
|
299
|
+
status: "failed",
|
|
300
|
+
error: message,
|
|
301
|
+
updatedAt: now()
|
|
260
302
|
});
|
|
261
|
-
return fail(invocation, "failed",
|
|
303
|
+
return fail(invocation, "failed", message);
|
|
262
304
|
}
|
|
263
|
-
const resultRecord = result2;
|
|
264
|
-
const output = isRecord(resultRecord.output) ? resultRecord.output : Object.fromEntries(
|
|
265
|
-
Object.entries(resultRecord).filter(
|
|
266
|
-
([key]) => key !== "success" && key !== "error"
|
|
267
|
-
)
|
|
268
|
-
);
|
|
269
|
-
await options.store.updateAdapterInvocation(adapterInvocationId, {
|
|
270
|
-
status: "succeeded",
|
|
271
|
-
output,
|
|
272
|
-
updatedAt: now()
|
|
273
|
-
});
|
|
274
|
-
await appendEvent(invocation, {
|
|
275
|
-
eventType: "AdapterInvocationSucceeded",
|
|
276
|
-
subjectType: adapterEventSubject.subjectType,
|
|
277
|
-
subjectId: adapterEventSubject.subjectId,
|
|
278
|
-
payload: {
|
|
279
|
-
adapterType: step.adapterType,
|
|
280
|
-
operation: step.operation,
|
|
281
|
-
input: recordedInput,
|
|
282
|
-
output
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
} catch (error) {
|
|
286
|
-
const message = errorMessage(error);
|
|
287
|
-
await options.store.updateAdapterInvocation(adapterInvocationId, {
|
|
288
|
-
status: "failed",
|
|
289
|
-
error: message,
|
|
290
|
-
updatedAt: now()
|
|
291
|
-
});
|
|
292
|
-
return fail(invocation, "failed", message);
|
|
293
305
|
}
|
|
306
|
+
if (action.eventPhase === "after_adapters") {
|
|
307
|
+
for (const event of domainEvents) await appendEvent(invocation, event);
|
|
308
|
+
}
|
|
309
|
+
const result = withoutPrivateHostFields(data);
|
|
310
|
+
await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
|
|
311
|
+
status: "completed",
|
|
312
|
+
result
|
|
313
|
+
});
|
|
314
|
+
return { actionInvocationId, status: "completed", result };
|
|
315
|
+
} catch (error) {
|
|
316
|
+
return fail(invocation, "failed", errorMessage(error));
|
|
294
317
|
}
|
|
295
|
-
if (action.eventPhase === "after_adapters") {
|
|
296
|
-
for (const event of domainEvents) await appendEvent(invocation, event);
|
|
297
|
-
}
|
|
298
|
-
const result = withoutPrivateHostFields(data);
|
|
299
|
-
await options.store.updateActionInvocation(actionInvocationId, tenantId, spaceId, {
|
|
300
|
-
status: "completed",
|
|
301
|
-
result
|
|
302
|
-
});
|
|
303
|
-
return { actionInvocationId, status: "completed", result };
|
|
304
318
|
}
|
|
305
319
|
async function appendEvent(invocation, event) {
|
|
306
320
|
const timestamp = now();
|
|
@@ -390,8 +404,14 @@ var MemoryPlatformHostStore = class {
|
|
|
390
404
|
return run(this.db);
|
|
391
405
|
}
|
|
392
406
|
async createActionInvocation(input) {
|
|
407
|
+
if (input.idempotencyKey) {
|
|
408
|
+
const existing = this.invocations.find(
|
|
409
|
+
(record2) => record2.tenantId === input.tenantId && record2.spaceId === input.spaceId && record2.actionId === input.actionId && record2.idempotencyKey === input.idempotencyKey
|
|
410
|
+
);
|
|
411
|
+
if (existing) return existing;
|
|
412
|
+
}
|
|
393
413
|
const now = /* @__PURE__ */ new Date();
|
|
394
|
-
const record = { ...input, createdAt: now, updatedAt: now };
|
|
414
|
+
const record = { ...input, attemptCount: 0, createdAt: now, updatedAt: now };
|
|
395
415
|
this.invocations.push(record);
|
|
396
416
|
return record;
|
|
397
417
|
}
|
|
@@ -433,6 +453,28 @@ var MemoryPlatformHostStore = class {
|
|
|
433
453
|
}
|
|
434
454
|
return state;
|
|
435
455
|
}
|
|
456
|
+
async listActionInvocations(input = {}) {
|
|
457
|
+
return this.invocations.filter(
|
|
458
|
+
(record) => (!input.tenantId || record.tenantId === input.tenantId) && (!input.spaceId || record.spaceId === input.spaceId) && (!input.statuses?.length || input.statuses.includes(record.status)) && (!input.updatedBefore || record.updatedAt < input.updatedBefore)
|
|
459
|
+
).sort((left, right) => left.createdAt.getTime() - right.createdAt.getTime()).slice(0, input.limit ?? 100);
|
|
460
|
+
}
|
|
461
|
+
async claimActionInvocations(input) {
|
|
462
|
+
const current = input.now ?? /* @__PURE__ */ new Date();
|
|
463
|
+
const eligible = this.invocations.filter(
|
|
464
|
+
(record) => (!input.tenantId || record.tenantId === input.tenantId) && (!input.spaceId || record.spaceId === input.spaceId) && (record.status === "pending" || record.status === "running" && (!record.leaseExpiresAt || record.leaseExpiresAt <= current))
|
|
465
|
+
).sort((left, right) => left.createdAt.getTime() - right.createdAt.getTime()).slice(0, input.limit ?? 10);
|
|
466
|
+
for (const record of eligible) {
|
|
467
|
+
record.status = "running";
|
|
468
|
+
record.leaseOwner = input.workerId;
|
|
469
|
+
record.leaseExpiresAt = new Date(current.getTime() + input.leaseDurationMs);
|
|
470
|
+
record.attemptCount += 1;
|
|
471
|
+
record.updatedAt = current;
|
|
472
|
+
}
|
|
473
|
+
return eligible;
|
|
474
|
+
}
|
|
475
|
+
async listEvents(tenantId, spaceId) {
|
|
476
|
+
return this.events.filter((event) => event.tenantId === tenantId && event.spaceId === spaceId).sort((left, right) => left.sequence - right.sequence);
|
|
477
|
+
}
|
|
436
478
|
};
|
|
437
479
|
|
|
438
480
|
// src/postgres-store.ts
|
|
@@ -451,9 +493,22 @@ var PostgresPlatformHostStore = class {
|
|
|
451
493
|
action_id text NOT NULL, action_version integer NOT NULL,
|
|
452
494
|
actor_id text NOT NULL, actor_type text NOT NULL, status text NOT NULL,
|
|
453
495
|
parameters jsonb NOT NULL, result jsonb NOT NULL DEFAULT '{}'::jsonb,
|
|
454
|
-
correlation_id text NOT NULL, causation_id text, error text,
|
|
496
|
+
correlation_id text NOT NULL, causation_id text, idempotency_key text, error text,
|
|
497
|
+
attempt_count integer NOT NULL DEFAULT 0, lease_owner text,
|
|
498
|
+
lease_expires_at timestamptz,
|
|
455
499
|
created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL
|
|
456
500
|
);
|
|
501
|
+
ALTER TABLE fabric_platform.action_invocations
|
|
502
|
+
ADD COLUMN IF NOT EXISTS idempotency_key text,
|
|
503
|
+
ADD COLUMN IF NOT EXISTS attempt_count integer NOT NULL DEFAULT 0,
|
|
504
|
+
ADD COLUMN IF NOT EXISTS lease_owner text,
|
|
505
|
+
ADD COLUMN IF NOT EXISTS lease_expires_at timestamptz;
|
|
506
|
+
CREATE UNIQUE INDEX IF NOT EXISTS action_invocations_idempotency_idx
|
|
507
|
+
ON fabric_platform.action_invocations
|
|
508
|
+
(tenant_id, space_id, action_id, idempotency_key)
|
|
509
|
+
WHERE idempotency_key IS NOT NULL;
|
|
510
|
+
CREATE INDEX IF NOT EXISTS action_invocations_worker_idx
|
|
511
|
+
ON fabric_platform.action_invocations (status, lease_expires_at, created_at);
|
|
457
512
|
CREATE TABLE IF NOT EXISTS fabric_platform.policy_evaluations (
|
|
458
513
|
id text PRIMARY KEY, action_invocation_id text NOT NULL,
|
|
459
514
|
tenant_id text NOT NULL, space_id text NOT NULL, outcome jsonb NOT NULL,
|
|
@@ -490,11 +545,14 @@ var PostgresPlatformHostStore = class {
|
|
|
490
545
|
}
|
|
491
546
|
async createActionInvocation(input) {
|
|
492
547
|
const now = /* @__PURE__ */ new Date();
|
|
493
|
-
await this.sql.query(
|
|
548
|
+
const result = await this.sql.query(
|
|
494
549
|
`INSERT INTO fabric_platform.action_invocations
|
|
495
550
|
(id,tenant_id,space_id,action_id,action_version,actor_id,actor_type,status,
|
|
496
|
-
parameters,result,correlation_id,causation_id,error,created_at,updated_at)
|
|
497
|
-
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$
|
|
551
|
+
parameters,result,correlation_id,causation_id,idempotency_key,error,created_at,updated_at)
|
|
552
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9::jsonb,$10::jsonb,$11,$12,$13,$14,$15,$15)
|
|
553
|
+
ON CONFLICT (tenant_id,space_id,action_id,idempotency_key)
|
|
554
|
+
WHERE idempotency_key IS NOT NULL DO UPDATE SET id=fabric_platform.action_invocations.id
|
|
555
|
+
RETURNING *`,
|
|
498
556
|
[
|
|
499
557
|
input.id,
|
|
500
558
|
input.tenantId,
|
|
@@ -508,11 +566,12 @@ var PostgresPlatformHostStore = class {
|
|
|
508
566
|
JSON.stringify(input.result),
|
|
509
567
|
input.correlationId,
|
|
510
568
|
input.causationId ?? null,
|
|
569
|
+
input.idempotencyKey ?? null,
|
|
511
570
|
input.error ?? null,
|
|
512
571
|
now
|
|
513
572
|
]
|
|
514
573
|
);
|
|
515
|
-
return
|
|
574
|
+
return toActionRecord(result.rows[0]);
|
|
516
575
|
}
|
|
517
576
|
async getActionInvocation(id, tenantId, spaceId) {
|
|
518
577
|
const result = await this.sql.query(
|
|
@@ -526,7 +585,12 @@ var PostgresPlatformHostStore = class {
|
|
|
526
585
|
await this.sql.query(
|
|
527
586
|
`UPDATE fabric_platform.action_invocations SET
|
|
528
587
|
status=COALESCE($4,status), result=COALESCE($5::jsonb,result),
|
|
529
|
-
error=CASE WHEN $6::boolean THEN $7 ELSE error END,
|
|
588
|
+
error=CASE WHEN $6::boolean THEN $7 ELSE error END,
|
|
589
|
+
lease_owner=CASE WHEN $4 IN ('completed','failed','blocked_by_policy','validation_failed')
|
|
590
|
+
THEN NULL ELSE lease_owner END,
|
|
591
|
+
lease_expires_at=CASE WHEN $4 IN ('completed','failed','blocked_by_policy','validation_failed')
|
|
592
|
+
THEN NULL ELSE lease_expires_at END,
|
|
593
|
+
updated_at=now()
|
|
530
594
|
WHERE id=$1 AND tenant_id=$2 AND space_id=$3`,
|
|
531
595
|
[
|
|
532
596
|
id,
|
|
@@ -641,6 +705,57 @@ var PostgresPlatformHostStore = class {
|
|
|
641
705
|
);
|
|
642
706
|
return result.rows[0]?.state;
|
|
643
707
|
}
|
|
708
|
+
async listActionInvocations(input = {}) {
|
|
709
|
+
const conditions = [];
|
|
710
|
+
const values = [];
|
|
711
|
+
const add = (condition, value) => {
|
|
712
|
+
values.push(value);
|
|
713
|
+
conditions.push(condition.replace("?", `$${values.length}`));
|
|
714
|
+
};
|
|
715
|
+
if (input.tenantId) add("tenant_id=?", input.tenantId);
|
|
716
|
+
if (input.spaceId) add("space_id=?", input.spaceId);
|
|
717
|
+
if (input.statuses?.length) add("status = ANY(?::text[])", [...input.statuses]);
|
|
718
|
+
if (input.updatedBefore) add("updated_at<?", input.updatedBefore);
|
|
719
|
+
values.push(Math.max(1, Math.min(input.limit ?? 100, 1e3)));
|
|
720
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
721
|
+
const result = await this.sql.query(
|
|
722
|
+
`SELECT * FROM fabric_platform.action_invocations ${where}
|
|
723
|
+
ORDER BY created_at LIMIT $${values.length}`,
|
|
724
|
+
values
|
|
725
|
+
);
|
|
726
|
+
return result.rows.map(toActionRecord);
|
|
727
|
+
}
|
|
728
|
+
async claimActionInvocations(input) {
|
|
729
|
+
const current = input.now ?? /* @__PURE__ */ new Date();
|
|
730
|
+
const limit = Math.max(1, Math.min(input.limit ?? 10, 100));
|
|
731
|
+
const leaseExpiresAt = new Date(current.getTime() + input.leaseDurationMs);
|
|
732
|
+
const result = await this.sql.query(
|
|
733
|
+
`WITH claimable AS (
|
|
734
|
+
SELECT id FROM fabric_platform.action_invocations
|
|
735
|
+
WHERE (status='pending' OR
|
|
736
|
+
(status='running' AND (lease_expires_at IS NULL OR lease_expires_at <= $1)))
|
|
737
|
+
AND ($2::text IS NULL OR tenant_id=$2)
|
|
738
|
+
AND ($3::text IS NULL OR space_id=$3)
|
|
739
|
+
ORDER BY created_at
|
|
740
|
+
FOR UPDATE SKIP LOCKED
|
|
741
|
+
LIMIT $4
|
|
742
|
+
)
|
|
743
|
+
UPDATE fabric_platform.action_invocations AS invocation
|
|
744
|
+
SET status='running', lease_owner=$5, lease_expires_at=$6,
|
|
745
|
+
attempt_count=attempt_count+1, updated_at=$1
|
|
746
|
+
FROM claimable WHERE invocation.id=claimable.id
|
|
747
|
+
RETURNING invocation.*`,
|
|
748
|
+
[
|
|
749
|
+
current,
|
|
750
|
+
input.tenantId ?? null,
|
|
751
|
+
input.spaceId ?? null,
|
|
752
|
+
limit,
|
|
753
|
+
input.workerId,
|
|
754
|
+
leaseExpiresAt
|
|
755
|
+
]
|
|
756
|
+
);
|
|
757
|
+
return result.rows.map(toActionRecord);
|
|
758
|
+
}
|
|
644
759
|
async listEvents(tenantId, spaceId) {
|
|
645
760
|
const result = await this.sql.query(
|
|
646
761
|
`SELECT * FROM fabric_platform.asset_events
|
|
@@ -664,6 +779,10 @@ function toActionRecord(row) {
|
|
|
664
779
|
result: row.result,
|
|
665
780
|
correlationId: String(row.correlation_id),
|
|
666
781
|
...row.causation_id ? { causationId: String(row.causation_id) } : {},
|
|
782
|
+
...row.idempotency_key ? { idempotencyKey: String(row.idempotency_key) } : {},
|
|
783
|
+
attemptCount: Number(row.attempt_count ?? 0),
|
|
784
|
+
...row.lease_owner ? { leaseOwner: String(row.lease_owner) } : {},
|
|
785
|
+
...row.lease_expires_at ? { leaseExpiresAt: new Date(row.lease_expires_at) } : {},
|
|
667
786
|
...row.error ? { error: String(row.error) } : {},
|
|
668
787
|
createdAt: new Date(row.created_at),
|
|
669
788
|
updatedAt: new Date(row.updated_at)
|
|
@@ -690,6 +809,66 @@ function toEventRecord(row) {
|
|
|
690
809
|
};
|
|
691
810
|
}
|
|
692
811
|
|
|
693
|
-
|
|
812
|
+
// src/worker.ts
|
|
813
|
+
var DEFAULT_BATCH_SIZE = 10;
|
|
814
|
+
var DEFAULT_LEASE_DURATION_MS = 5 * 6e4;
|
|
815
|
+
var DEFAULT_POLL_INTERVAL_MS = 1e3;
|
|
816
|
+
function createStoreBackedActionDispatcher() {
|
|
817
|
+
return {
|
|
818
|
+
dispatch: async (input) => ({ workflowId: input.workflowId })
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
async function runPlatformActionWorkerCycle(options) {
|
|
822
|
+
const claimed = await options.store.claimActionInvocations({
|
|
823
|
+
workerId: options.workerId,
|
|
824
|
+
limit: options.batchSize ?? DEFAULT_BATCH_SIZE,
|
|
825
|
+
leaseDurationMs: options.leaseDurationMs ?? DEFAULT_LEASE_DURATION_MS,
|
|
826
|
+
...options.tenantId ? { tenantId: options.tenantId } : {},
|
|
827
|
+
...options.spaceId ? { spaceId: options.spaceId } : {}
|
|
828
|
+
});
|
|
829
|
+
let completed = 0;
|
|
830
|
+
let failed = 0;
|
|
831
|
+
for (const invocation of claimed) {
|
|
832
|
+
try {
|
|
833
|
+
const result = await options.host.executeInvocation(
|
|
834
|
+
invocation.id,
|
|
835
|
+
invocation.tenantId,
|
|
836
|
+
invocation.spaceId
|
|
837
|
+
);
|
|
838
|
+
if (result.status === "completed") completed += 1;
|
|
839
|
+
else failed += 1;
|
|
840
|
+
} catch (error) {
|
|
841
|
+
failed += 1;
|
|
842
|
+
options.onError?.(error, invocation);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return { claimed: claimed.length, completed, failed };
|
|
846
|
+
}
|
|
847
|
+
async function runPlatformActionWorker(options) {
|
|
848
|
+
while (!options.signal?.aborted) {
|
|
849
|
+
try {
|
|
850
|
+
await runPlatformActionWorkerCycle(options);
|
|
851
|
+
} catch (error) {
|
|
852
|
+
options.onError?.(error);
|
|
853
|
+
}
|
|
854
|
+
await abortableDelay(options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS, options.signal);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
async function abortableDelay(milliseconds, signal) {
|
|
858
|
+
if (signal?.aborted) return;
|
|
859
|
+
await new Promise((resolve) => {
|
|
860
|
+
const timeout = setTimeout(resolve, milliseconds);
|
|
861
|
+
signal?.addEventListener(
|
|
862
|
+
"abort",
|
|
863
|
+
() => {
|
|
864
|
+
clearTimeout(timeout);
|
|
865
|
+
resolve();
|
|
866
|
+
},
|
|
867
|
+
{ once: true }
|
|
868
|
+
);
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
export { MemoryPlatformHostStore, PostgresPlatformHostStore, createGovernedActionHost, createStoreBackedActionDispatcher, runPlatformActionWorker, runPlatformActionWorkerCycle };
|
|
694
873
|
//# sourceMappingURL=index.js.map
|
|
695
874
|
//# sourceMappingURL=index.js.map
|