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