@fabricorg/platform-host 7.1.0 → 7.1.1
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 +6 -0
- package/README.md +3 -1
- package/dist/index.cjs +16 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +16 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @fabricorg/platform-host
|
|
2
2
|
|
|
3
|
+
## 7.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 91f7d7e: A repeated completion was judged identical by outcome alone, so a second `completed` callback carrying a different result was absorbed as a replay and its evidence discarded. A recorded completion now carries a canonical digest of provider, reference, outcome, result, error, and evidence, with observation time left out because a redelivery carries a new one. A repeat with the same digest is absorbed; the same outcome with a different digest is a contradiction and is handled as one: reconciliation, the first outcome kept, the second logged, `ExternalOperationContradicted` emitted.
|
|
8
|
+
|
|
3
9
|
## 7.1.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -265,7 +265,9 @@ await host.completeExternalInvocation(actionInvocationId, tenantId, spaceId, {
|
|
|
265
265
|
```
|
|
266
266
|
|
|
267
267
|
Matching is by reference: a completion naming a reference the invocation is not waiting on is
|
|
268
|
-
refused. A repeated identical completion is absorbed
|
|
268
|
+
refused. A repeated identical completion is absorbed, identity being a digest of provider,
|
|
269
|
+
reference, outcome, result, error, and evidence. A contradictory one, a flipped outcome or the same
|
|
270
|
+
outcome with different evidence, moves the invocation to
|
|
269
271
|
`reconciliation_required`, keeps the first outcome on record, logs the second, and emits
|
|
270
272
|
`ExternalOperationContradicted`. Under an atomic store the read and write happen under one lock. The
|
|
271
273
|
callback's result is validated against the action's result schema and stripped of private fields.
|
package/dist/index.cjs
CHANGED
|
@@ -1352,11 +1352,12 @@ function createGovernedActionHost(options) {
|
|
|
1352
1352
|
if (completion.provider !== void 0 && recorded.provider !== void 0 && completion.provider !== recorded.provider) {
|
|
1353
1353
|
throw new ExternalCompletionError("provider_mismatch", `ActionInvocation ${actionInvocationId} was completed by "${recorded.provider}", not "${completion.provider}".`);
|
|
1354
1354
|
}
|
|
1355
|
-
|
|
1355
|
+
const incomingDigest = completionDigest(completion, recorded.provider ?? completion.provider);
|
|
1356
|
+
if (recorded.digest === incomingDigest) {
|
|
1356
1357
|
return withConsistency(actionResult(invocation), invocation.actionId);
|
|
1357
1358
|
}
|
|
1358
|
-
const message = `External operation "${completion.externalReference}" reported ${completion.outcome} after reporting ${recorded.outcome}.`;
|
|
1359
|
-
await recordCompletionReconciliation(invocation, { ...completion, kind: `contradicted:${
|
|
1359
|
+
const message = recorded.outcome === completion.outcome ? `External operation "${completion.externalReference}" reported ${completion.outcome} again with different evidence.` : `External operation "${completion.externalReference}" reported ${completion.outcome} after reporting ${recorded.outcome}.`;
|
|
1360
|
+
await recordCompletionReconciliation(invocation, { ...completion, kind: `contradicted:${incomingDigest.slice(0, 16)}` }, message);
|
|
1360
1361
|
await appendEvent(invocation, {
|
|
1361
1362
|
eventType: "ExternalOperationContradicted",
|
|
1362
1363
|
subjectType: "AdapterInvocation",
|
|
@@ -1386,7 +1387,8 @@ function createGovernedActionHost(options) {
|
|
|
1386
1387
|
...completion,
|
|
1387
1388
|
provider: completion.provider ?? pending.provider,
|
|
1388
1389
|
observedAt: completion.observedAt instanceof Date ? completion.observedAt.toISOString() : completion.observedAt,
|
|
1389
|
-
recordedAt
|
|
1390
|
+
recordedAt,
|
|
1391
|
+
digest: completionDigest(completion, completion.provider ?? pending.provider)
|
|
1390
1392
|
};
|
|
1391
1393
|
const subject = { subjectType: "AdapterInvocation", subjectId: pending.adapterInvocationId };
|
|
1392
1394
|
const settles = invocation.status === "running" && pending.parkedAt !== void 0 || invocation.status === "reconciliation_required" && invocation.error?.includes("did not complete by") === true;
|
|
@@ -1425,6 +1427,16 @@ function createGovernedActionHost(options) {
|
|
|
1425
1427
|
if (settles) emitInvocationStatusTelemetry(invocation, "failed", options.telemetry, now(), invocation.status);
|
|
1426
1428
|
return withConsistency(actionResult({ ...invocation, ...patch }), invocation.actionId);
|
|
1427
1429
|
}
|
|
1430
|
+
function completionDigest(completion, provider) {
|
|
1431
|
+
return crypto.createHash("sha256").update(canonicalJson({
|
|
1432
|
+
provider: provider ?? null,
|
|
1433
|
+
externalReference: completion.externalReference,
|
|
1434
|
+
outcome: completion.outcome,
|
|
1435
|
+
result: completion.result ?? null,
|
|
1436
|
+
error: completion.error ?? null,
|
|
1437
|
+
evidenceReferences: completion.evidenceReferences ?? null
|
|
1438
|
+
})).digest("hex");
|
|
1439
|
+
}
|
|
1428
1440
|
async function recordCompletionReconciliation(invocation, completion, reason) {
|
|
1429
1441
|
const governanceStore = asGovernanceStore(options.store);
|
|
1430
1442
|
if (!governanceStore) throw new Error("External completion reconciliation requires a governance-capable store.");
|