@fabricorg/platform-host 1.0.0 → 2.0.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 +33 -0
- package/README.md +27 -3
- package/dist/index.cjs +338 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +144 -4
- package/dist/index.d.ts +144 -4
- package/dist/index.js +336 -3
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# @fabricorg/platform-host
|
|
2
2
|
|
|
3
|
+
## 2.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Close final uplift audit gaps: align Host 2.x documentation and migration evidence, surface every
|
|
8
|
+
compiler/generator API to LLM consumers, make memory outbox egress directly usable, sanitize relay
|
|
9
|
+
failures, validate complete archetype state maps, support recursive JSON Schema generation, reject
|
|
10
|
+
cross-platform path traversal, synchronize artifact version stamps, and include package licenses.
|
|
11
|
+
- Require explicit domain snapshot/restore support before memory stores advertise atomic outbox
|
|
12
|
+
transactions, buffer Host lifecycle commits until success, fail missing transaction capabilities
|
|
13
|
+
before handlers run, sanitize relay failures, and exclude sensitive Host lifecycle events from bus
|
|
14
|
+
egress by default.
|
|
15
|
+
- Prevalidate buffered memory transactions so failed commits cannot append partial event or outbox rows.
|
|
16
|
+
- Qualify result-validation rollback documentation by the configured domain transaction provider.
|
|
17
|
+
- Updated dependencies
|
|
18
|
+
- @fabricorg/platform@0.11.1
|
|
19
|
+
|
|
20
|
+
## 2.0.0
|
|
21
|
+
|
|
22
|
+
### Major Changes
|
|
23
|
+
|
|
24
|
+
- Require `@fabricorg/platform@^0.11.0` and its manifest v2/result-schema contract. Applications
|
|
25
|
+
upgrading from Host 1.x must upgrade Platform in the same deployment and regenerate manifest consumers.
|
|
26
|
+
- Add the optional durable event outbox, PostgreSQL and memory adapters, and at-least-once relay.
|
|
27
|
+
- Add manifest v2 typed entity contracts, v1 reader normalization, optional action result schemas, and
|
|
28
|
+
Host enforcement of result validation before events and adapters.
|
|
29
|
+
|
|
30
|
+
### Patch Changes
|
|
31
|
+
|
|
32
|
+
- Updated dependencies
|
|
33
|
+
- Updated dependencies
|
|
34
|
+
- @fabricorg/platform@0.11.0
|
|
35
|
+
|
|
3
36
|
## 1.0.0
|
|
4
37
|
|
|
5
38
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The canonical host for the `@fabricorg/platform` mutation pipeline.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
pnpm add @fabricorg/platform@^0.
|
|
6
|
+
pnpm add @fabricorg/platform@^0.11.0 @fabricorg/platform-host@^2.0.0
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
## AI agent integration boundary
|
|
@@ -15,7 +15,7 @@ registration, and submit stable idempotent commands. The gateway derives tenant
|
|
|
15
15
|
calls `submitAction()`; an agent must never call handlers, adapters, workflow internals, or database
|
|
16
16
|
writes directly.
|
|
17
17
|
|
|
18
|
-
See the [Platform Host
|
|
18
|
+
See the [Platform Host 2.x agent integration
|
|
19
19
|
guide](https://platform.fabric.pro/docs/platform/reference/platform-host) for application wiring,
|
|
20
20
|
execution-time authorization, the PostgreSQL transaction binder, recovery semantics, and the external
|
|
21
21
|
gateway contract.
|
|
@@ -100,6 +100,13 @@ application knows how its `TDb` is rebound to the transaction's SQL client.
|
|
|
100
100
|
Stores without this additive capability retain the legacy boundary for compatibility and must not
|
|
101
101
|
claim atomic domain-write/event persistence.
|
|
102
102
|
|
|
103
|
+
Actions can optionally declare `resultSchema`. The Host validates public handler result data after
|
|
104
|
+
the handler and before canonical event append or adapter execution. Invalid results fail the invocation,
|
|
105
|
+
emit no domain/completion event, invoke no adapter, and persist only a sanitized validation failure.
|
|
106
|
+
Domain writes roll back only when the configured store transaction provider includes those writes.
|
|
107
|
+
This validates the handler result contract; it does not observe arbitrary writes through an
|
|
108
|
+
application-owned `TDb`.
|
|
109
|
+
|
|
103
110
|
## Agent HITL
|
|
104
111
|
|
|
105
112
|
Hosts can inject a vertical-owned `hitlEvaluator`. It runs only for `actorType: "agent"`, after schema
|
|
@@ -171,7 +178,7 @@ explain which contract and provider adapter governed a mutation after dependenci
|
|
|
171
178
|
const host = createGovernedActionHost({
|
|
172
179
|
// ...
|
|
173
180
|
runtimeEvidence: {
|
|
174
|
-
hostPackageVersion: "0.
|
|
181
|
+
hostPackageVersion: "2.0.1",
|
|
175
182
|
policyRulesetVersion: "gtm-rules.v8",
|
|
176
183
|
providerBridge: { name: "@fabric-harness/databricks", version: "1" },
|
|
177
184
|
},
|
|
@@ -181,3 +188,20 @@ const host = createGovernedActionHost({
|
|
|
181
188
|
`MemoryPlatformHostStore` is for tests and local demos. Production control planes use
|
|
182
189
|
`PostgresPlatformHostStore` with Databricks Lakebase (or standard Postgres), call
|
|
183
190
|
`ensureSchema()` at startup, and hydrate projections from `listEvents()`.
|
|
191
|
+
|
|
192
|
+
## Durable outbox egress
|
|
193
|
+
|
|
194
|
+
Configure `outbox` on `createGovernedActionHost()` only with an `OutboxPlatformHostStore`.
|
|
195
|
+
The Host then uses `appendEventWithOutbox` so the canonical event and delivery record share
|
|
196
|
+
the proven event transaction. `runOutboxRelayCycle()` leases and publishes records with retry
|
|
197
|
+
and dead-letter handling. Delivery is at least once: consumers deduplicate on immutable
|
|
198
|
+
`eventId`. Payload classification is metadata, not a redaction mechanism; event payloads must
|
|
199
|
+
already be audit-safe.
|
|
200
|
+
|
|
201
|
+
Adapter and compliance lifecycle events remain canonical but are excluded from bus egress by default.
|
|
202
|
+
Use `shouldPublish` to opt them in only when the application has made their payloads safe for that bus.
|
|
203
|
+
|
|
204
|
+
For deterministic local tests, `MemoryPlatformHostStore` enables governed Host outbox egress only
|
|
205
|
+
when its constructor receives a `MemoryPlatformHostTransactionProvider` with domain `snapshot` and
|
|
206
|
+
`restore` functions. Without that explicit rollback seam, `transactionalOutbox` is false and Host
|
|
207
|
+
construction rejects outbox configuration. PostgreSQL remains the production implementation.
|
package/dist/index.cjs
CHANGED
|
@@ -16,6 +16,12 @@ var RecoverableFinalizationError = class extends Error {
|
|
|
16
16
|
name = "RecoverableFinalizationError";
|
|
17
17
|
};
|
|
18
18
|
function createGovernedActionHost(options) {
|
|
19
|
+
if (options.outbox) {
|
|
20
|
+
const outboxStore = asOutboxStore(options.store);
|
|
21
|
+
if (!outboxStore || !asAtomicMutationStore(options.store) || outboxStore.transactionalOutbox !== true) {
|
|
22
|
+
throw new Error("Outbox egress requires a store with transactionalOutbox and transactionWithEvents capabilities.");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
19
25
|
const adapters = new platform.AdapterRegistry();
|
|
20
26
|
for (const adapter of options.adapters ?? []) adapters.register(adapter);
|
|
21
27
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
@@ -322,6 +328,9 @@ function createGovernedActionHost(options) {
|
|
|
322
328
|
let domainEvents = [];
|
|
323
329
|
try {
|
|
324
330
|
const runHandler = async (db, transaction) => {
|
|
331
|
+
if (options.outbox && !transaction?.appendEventWithOutbox) {
|
|
332
|
+
throw new Error("Outbox egress requires transactionWithEvents to provide appendEventWithOutbox before handler execution.");
|
|
333
|
+
}
|
|
325
334
|
const handlerResult = action.handler ? await action.handler(
|
|
326
335
|
{
|
|
327
336
|
actionInvocationId,
|
|
@@ -340,6 +349,13 @@ function createGovernedActionHost(options) {
|
|
|
340
349
|
throw new Error(handlerResult.error ?? "Action handler failed");
|
|
341
350
|
}
|
|
342
351
|
const handlerData = handlerResult.data ?? {};
|
|
352
|
+
if (action.resultSchema) {
|
|
353
|
+
const publicResult = withoutPrivateHostFields(handlerData, eventResultFields);
|
|
354
|
+
const parsedResult = action.resultSchema.safeParse(publicResult);
|
|
355
|
+
if (!parsedResult.success) {
|
|
356
|
+
throw new Error(`Action result validation failed at ${parsedResult.error.issues.map((issue) => issue.path.map(String).join(".") || "result").join(", ")}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
343
359
|
const events = extractEvents(handlerData);
|
|
344
360
|
const undeclaredEvent = events.find(
|
|
345
361
|
(event) => !action.emitsEvents.includes(event.eventType)
|
|
@@ -680,7 +696,30 @@ function createGovernedActionHost(options) {
|
|
|
680
696
|
correlationId: invocation.correlationId,
|
|
681
697
|
...invocation.causationId ? { causationId: invocation.causationId } : {}
|
|
682
698
|
};
|
|
683
|
-
|
|
699
|
+
if (options.outbox) {
|
|
700
|
+
const hostLifecycleEvent = (/* @__PURE__ */ new Set(["AdapterInvocationStarted", "AdapterInvocationSucceeded", "AdapterInvocationFailed", "ComplianceBlocked"])).has(envelope.eventType);
|
|
701
|
+
const shouldPublish = options.outbox.shouldPublish?.(envelope) ?? !hostLifecycleEvent;
|
|
702
|
+
if (!shouldPublish) {
|
|
703
|
+
await (transaction ?? options.store).appendEvent(envelope);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
const traceContext = options.outbox.traceContext?.(envelope);
|
|
707
|
+
const metadata = {
|
|
708
|
+
producerModuleVersion: options.outbox.producerModuleVersion(invocation.actionId, invocation.actionVersion),
|
|
709
|
+
payloadClassification: options.outbox.classifyPayload(envelope),
|
|
710
|
+
...traceContext ? { traceContext } : {}
|
|
711
|
+
};
|
|
712
|
+
if (transaction) {
|
|
713
|
+
if (!transaction.appendEventWithOutbox) throw new Error("Outbox egress requires an atomic transaction with appendEventWithOutbox.");
|
|
714
|
+
await transaction.appendEventWithOutbox(envelope, metadata);
|
|
715
|
+
} else {
|
|
716
|
+
const outboxStore = asOutboxStore(options.store);
|
|
717
|
+
if (!outboxStore) throw new Error("Outbox egress requires an outbox-capable Platform Host store.");
|
|
718
|
+
await outboxStore.appendEventWithOutbox(envelope, metadata);
|
|
719
|
+
}
|
|
720
|
+
} else {
|
|
721
|
+
await (transaction ?? options.store).appendEvent(envelope);
|
|
722
|
+
}
|
|
684
723
|
}
|
|
685
724
|
async function fail(invocation, status, error) {
|
|
686
725
|
await options.store.updateActionInvocation(
|
|
@@ -717,6 +756,10 @@ function asAtomicMutationStore(store) {
|
|
|
717
756
|
const candidate = store;
|
|
718
757
|
return typeof candidate.transactionWithEvents === "function" ? store : void 0;
|
|
719
758
|
}
|
|
759
|
+
function asOutboxStore(store) {
|
|
760
|
+
const candidate = store;
|
|
761
|
+
return typeof candidate.appendEventWithOutbox === "function" && typeof candidate.claimOutbox === "function" ? store : void 0;
|
|
762
|
+
}
|
|
720
763
|
function asGovernanceStore(store) {
|
|
721
764
|
const candidate = store;
|
|
722
765
|
return typeof candidate.recordMutationGovernance === "function" && typeof candidate.appendPolicyObligations === "function" && typeof candidate.appendExecutionAttestation === "function" ? store : void 0;
|
|
@@ -773,12 +816,83 @@ function lifecycleId(prefix, invocationId, key) {
|
|
|
773
816
|
return `${prefix}_${invocationId}_${safeKey}`;
|
|
774
817
|
}
|
|
775
818
|
|
|
819
|
+
// src/outbox.ts
|
|
820
|
+
function toEnterpriseEventEnvelope(event, metadata) {
|
|
821
|
+
return {
|
|
822
|
+
eventId: event.id,
|
|
823
|
+
eventType: event.eventType,
|
|
824
|
+
eventSchemaVersion: event.eventSchemaVersion,
|
|
825
|
+
tenantId: event.tenantId,
|
|
826
|
+
spaceId: event.spaceId,
|
|
827
|
+
subjectType: event.subjectType,
|
|
828
|
+
subjectId: event.subjectId,
|
|
829
|
+
sequence: event.sequence,
|
|
830
|
+
...event.actionInvocationId ? { actionInvocationId: event.actionInvocationId } : {},
|
|
831
|
+
correlationId: event.correlationId,
|
|
832
|
+
...event.causationId ? { causationId: event.causationId } : {},
|
|
833
|
+
occurredAt: event.occurredAt,
|
|
834
|
+
recordedAt: event.recordedAt,
|
|
835
|
+
producerModuleVersion: metadata.producerModuleVersion,
|
|
836
|
+
payloadClassification: metadata.payloadClassification,
|
|
837
|
+
...metadata.traceContext ? { traceContext: { ...metadata.traceContext } } : {},
|
|
838
|
+
payload: event.payload
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
async function runOutboxRelayCycle(options) {
|
|
842
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
843
|
+
const maxAttempts = options.maxAttempts ?? 10;
|
|
844
|
+
const records = await options.store.claimOutbox({
|
|
845
|
+
workerId: options.workerId,
|
|
846
|
+
leaseDurationMs: options.leaseDurationMs ?? 3e4,
|
|
847
|
+
limit: options.batchSize ?? 100,
|
|
848
|
+
now: now()
|
|
849
|
+
});
|
|
850
|
+
const result = { claimed: records.length, published: 0, failed: 0, deadLettered: 0 };
|
|
851
|
+
for (const record of records) {
|
|
852
|
+
try {
|
|
853
|
+
await options.publisher.publish(record.event);
|
|
854
|
+
try {
|
|
855
|
+
await options.store.markOutboxPublished(record.id, options.workerId, now());
|
|
856
|
+
} catch {
|
|
857
|
+
result.failed += 1;
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
result.published += 1;
|
|
861
|
+
} catch {
|
|
862
|
+
const deadLetter = record.attemptCount >= maxAttempts;
|
|
863
|
+
try {
|
|
864
|
+
await options.store.markOutboxFailed({
|
|
865
|
+
id: record.id,
|
|
866
|
+
workerId: options.workerId,
|
|
867
|
+
error: "Event publisher failed",
|
|
868
|
+
availableAt: new Date(now().getTime() + (options.retryDelayMs?.(record.attemptCount) ?? 1e3)),
|
|
869
|
+
deadLetter
|
|
870
|
+
});
|
|
871
|
+
} catch {
|
|
872
|
+
}
|
|
873
|
+
result.failed += 1;
|
|
874
|
+
if (deadLetter) result.deadLettered += 1;
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
return result;
|
|
878
|
+
}
|
|
879
|
+
function cloneOutboxRecord(record) {
|
|
880
|
+
return {
|
|
881
|
+
...record,
|
|
882
|
+
event: { ...record.event, ...record.event.traceContext ? { traceContext: { ...record.event.traceContext } } : {} }
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
776
886
|
// src/memory-store.ts
|
|
777
887
|
var MemoryPlatformHostStore = class {
|
|
778
|
-
constructor(db) {
|
|
888
|
+
constructor(db, transactionProvider) {
|
|
779
889
|
this.db = db;
|
|
890
|
+
this.transactionalOutbox = transactionProvider !== void 0;
|
|
891
|
+
if (transactionProvider) this.transactionWithEvents = (run) => this.runTransactionWithEvents(run, transactionProvider);
|
|
780
892
|
}
|
|
781
893
|
db;
|
|
894
|
+
transactionalOutbox;
|
|
895
|
+
transactionTail = Promise.resolve();
|
|
782
896
|
invocations = [];
|
|
783
897
|
policyEvaluations = [];
|
|
784
898
|
adapterInvocations = [];
|
|
@@ -786,9 +900,54 @@ var MemoryPlatformHostStore = class {
|
|
|
786
900
|
policyObligations = [];
|
|
787
901
|
executionAttestations = [];
|
|
788
902
|
externalReconciliations = [];
|
|
903
|
+
outbox = [];
|
|
789
904
|
async transaction(run) {
|
|
790
905
|
return run(this.db);
|
|
791
906
|
}
|
|
907
|
+
async runTransactionWithEvents(run, transactionProvider) {
|
|
908
|
+
let release;
|
|
909
|
+
const previous = this.transactionTail;
|
|
910
|
+
this.transactionTail = new Promise((resolve) => {
|
|
911
|
+
release = resolve;
|
|
912
|
+
});
|
|
913
|
+
await previous;
|
|
914
|
+
let domainSnapshot;
|
|
915
|
+
let snapshotCreated = false;
|
|
916
|
+
try {
|
|
917
|
+
domainSnapshot = transactionProvider.snapshot(this.db);
|
|
918
|
+
snapshotCreated = true;
|
|
919
|
+
const pendingEvents = [];
|
|
920
|
+
const pendingUpdates = [];
|
|
921
|
+
const appendPending = async (event, metadata) => {
|
|
922
|
+
if (this.events.some((candidate) => candidate.id === event.id) || pendingEvents.some((candidate) => candidate.event.id === event.id)) return;
|
|
923
|
+
pendingEvents.push({ event, ...metadata ? { metadata } : {} });
|
|
924
|
+
};
|
|
925
|
+
const result = await run({
|
|
926
|
+
db: this.db,
|
|
927
|
+
appendEvent: (event) => appendPending(event),
|
|
928
|
+
appendEventWithOutbox: (event, metadata) => appendPending(event, metadata),
|
|
929
|
+
nextEventSequence: async (tenantId, spaceId) => (await this.listEvents(tenantId, spaceId)).length + pendingEvents.filter((candidate) => candidate.event.tenantId === tenantId && candidate.event.spaceId === spaceId).length + 1,
|
|
930
|
+
listEvents: async (tenantId, spaceId) => [...await this.listEvents(tenantId, spaceId), ...pendingEvents.map((candidate) => candidate.event).filter((event) => event.tenantId === tenantId && event.spaceId === spaceId)],
|
|
931
|
+
updateActionInvocation: async (id, tenantId, spaceId, patch) => {
|
|
932
|
+
pendingUpdates.push({ id, tenantId, spaceId, patch });
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
for (const update of pendingUpdates) {
|
|
936
|
+
if (!await this.getActionInvocation(update.id, update.tenantId, update.spaceId)) throw new Error(`ActionInvocation not found: ${update.id}`);
|
|
937
|
+
}
|
|
938
|
+
for (const pending of pendingEvents) {
|
|
939
|
+
if (pending.metadata) await this.appendEventWithOutbox(pending.event, pending.metadata);
|
|
940
|
+
else await this.appendEvent(pending.event);
|
|
941
|
+
}
|
|
942
|
+
for (const update of pendingUpdates) await this.updateActionInvocation(update.id, update.tenantId, update.spaceId, update.patch);
|
|
943
|
+
return result;
|
|
944
|
+
} catch (error) {
|
|
945
|
+
if (snapshotCreated) transactionProvider.restore(this.db, domainSnapshot);
|
|
946
|
+
throw error;
|
|
947
|
+
} finally {
|
|
948
|
+
release();
|
|
949
|
+
}
|
|
950
|
+
}
|
|
792
951
|
async createActionInvocation(input) {
|
|
793
952
|
if (input.idempotencyKey) {
|
|
794
953
|
const existing = this.invocations.find(
|
|
@@ -904,6 +1063,53 @@ var MemoryPlatformHostStore = class {
|
|
|
904
1063
|
if (this.events.some((candidate) => candidate.id === event.id)) return;
|
|
905
1064
|
this.events.push(event);
|
|
906
1065
|
}
|
|
1066
|
+
async appendEventWithOutbox(event, metadata) {
|
|
1067
|
+
if (this.events.some((candidate) => candidate.id === event.id)) return;
|
|
1068
|
+
const createdAt = new Date(event.recordedAt);
|
|
1069
|
+
this.events.push(event);
|
|
1070
|
+
this.outbox.push({
|
|
1071
|
+
id: event.id,
|
|
1072
|
+
tenantId: event.tenantId,
|
|
1073
|
+
spaceId: event.spaceId,
|
|
1074
|
+
event: toEnterpriseEventEnvelope(event, metadata),
|
|
1075
|
+
status: "pending",
|
|
1076
|
+
attemptCount: 0,
|
|
1077
|
+
availableAt: createdAt,
|
|
1078
|
+
createdAt
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
async claimOutbox(input) {
|
|
1082
|
+
const current = input.now ?? /* @__PURE__ */ new Date();
|
|
1083
|
+
return this.outbox.filter((record) => record.status === "pending" && record.availableAt <= current && (!record.leaseExpiresAt || record.leaseExpiresAt <= current) && (!input.tenantId || record.tenantId === input.tenantId) && (!input.spaceId || record.spaceId === input.spaceId)).sort((left, right) => left.createdAt.getTime() - right.createdAt.getTime() || left.id.localeCompare(right.id)).slice(0, Math.max(1, Math.min(input.limit ?? 100, 1e3))).map((record) => {
|
|
1084
|
+
record.leaseOwner = input.workerId;
|
|
1085
|
+
record.leaseExpiresAt = new Date(current.getTime() + input.leaseDurationMs);
|
|
1086
|
+
record.attemptCount += 1;
|
|
1087
|
+
return cloneOutboxRecord(record);
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
async markOutboxPublished(id, workerId, publishedAt) {
|
|
1091
|
+
const record = this.requireLeasedOutbox(id, workerId);
|
|
1092
|
+
record.status = "published";
|
|
1093
|
+
record.publishedAt = publishedAt;
|
|
1094
|
+
delete record.leaseOwner;
|
|
1095
|
+
delete record.leaseExpiresAt;
|
|
1096
|
+
}
|
|
1097
|
+
async markOutboxFailed(input) {
|
|
1098
|
+
const record = this.requireLeasedOutbox(input.id, input.workerId);
|
|
1099
|
+
record.status = input.deadLetter ? "dead_letter" : "pending";
|
|
1100
|
+
record.lastError = input.error;
|
|
1101
|
+
record.availableAt = input.availableAt;
|
|
1102
|
+
delete record.leaseOwner;
|
|
1103
|
+
delete record.leaseExpiresAt;
|
|
1104
|
+
}
|
|
1105
|
+
async listOutbox(input = {}) {
|
|
1106
|
+
return this.outbox.filter((record) => (!input.tenantId || record.tenantId === input.tenantId) && (!input.spaceId || record.spaceId === input.spaceId) && (!input.statuses?.length || input.statuses.includes(record.status))).map(cloneOutboxRecord);
|
|
1107
|
+
}
|
|
1108
|
+
requireLeasedOutbox(id, workerId) {
|
|
1109
|
+
const record = this.outbox.find((candidate) => candidate.id === id);
|
|
1110
|
+
if (!record || record.leaseOwner !== workerId) throw new Error(`Outbox record ${id} is not leased by ${workerId}.`);
|
|
1111
|
+
return record;
|
|
1112
|
+
}
|
|
907
1113
|
async nextEventSequence(tenantId, spaceId) {
|
|
908
1114
|
return this.events.filter((event) => event.tenantId === tenantId && event.spaceId === spaceId).length + 1;
|
|
909
1115
|
}
|
|
@@ -947,12 +1153,14 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
947
1153
|
constructor(db, sql, transactionProvider) {
|
|
948
1154
|
this.db = db;
|
|
949
1155
|
this.sql = sql;
|
|
1156
|
+
this.transactionalOutbox = transactionProvider !== void 0;
|
|
950
1157
|
if (transactionProvider) {
|
|
951
1158
|
this.transactionWithEvents = async (run) => transactionProvider.run(async ({ db: db2, sql: sql2 }) => {
|
|
952
1159
|
const scoped = new _PostgresPlatformHostStore(db2, sql2);
|
|
953
1160
|
return run({
|
|
954
1161
|
db: db2,
|
|
955
1162
|
appendEvent: (event) => scoped.appendEvent(event),
|
|
1163
|
+
appendEventWithOutbox: (event, metadata) => scoped.appendEventWithOutbox(event, metadata),
|
|
956
1164
|
nextEventSequence: (tenantId, spaceId) => scoped.nextEventSequence(tenantId, spaceId),
|
|
957
1165
|
listEvents: (tenantId, spaceId) => scoped.listEvents(tenantId, spaceId),
|
|
958
1166
|
updateActionInvocation: (id, tenantId, spaceId, patch) => scoped.updateActionInvocation(id, tenantId, spaceId, patch)
|
|
@@ -963,6 +1171,7 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
963
1171
|
db;
|
|
964
1172
|
sql;
|
|
965
1173
|
transactionWithEvents;
|
|
1174
|
+
transactionalOutbox;
|
|
966
1175
|
async ensureSchema() {
|
|
967
1176
|
await this.sql.query(`
|
|
968
1177
|
CREATE SCHEMA IF NOT EXISTS fabric_platform;
|
|
@@ -1046,6 +1255,15 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1046
1255
|
CREATE INDEX IF NOT EXISTS asset_events_subject_idx
|
|
1047
1256
|
ON fabric_platform.asset_events
|
|
1048
1257
|
(tenant_id, space_id, subject_type, subject_id, sequence);
|
|
1258
|
+
CREATE TABLE IF NOT EXISTS fabric_platform.event_outbox (
|
|
1259
|
+
id text PRIMARY KEY, tenant_id text NOT NULL, space_id text NOT NULL,
|
|
1260
|
+
event jsonb NOT NULL, status text NOT NULL DEFAULT 'pending',
|
|
1261
|
+
attempt_count integer NOT NULL DEFAULT 0, available_at timestamptz NOT NULL,
|
|
1262
|
+
lease_owner text, lease_expires_at timestamptz, last_error text,
|
|
1263
|
+
created_at timestamptz NOT NULL, published_at timestamptz
|
|
1264
|
+
);
|
|
1265
|
+
CREATE INDEX IF NOT EXISTS event_outbox_claim_idx
|
|
1266
|
+
ON fabric_platform.event_outbox (status, available_at, lease_expires_at, created_at);
|
|
1049
1267
|
`);
|
|
1050
1268
|
}
|
|
1051
1269
|
async transaction(run) {
|
|
@@ -1328,6 +1546,86 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1328
1546
|
]
|
|
1329
1547
|
);
|
|
1330
1548
|
}
|
|
1549
|
+
async appendEventWithOutbox(event, metadata) {
|
|
1550
|
+
const envelope = toEnterpriseEventEnvelope(event, metadata);
|
|
1551
|
+
await this.sql.query(
|
|
1552
|
+
`WITH inserted_event AS (
|
|
1553
|
+
INSERT INTO fabric_platform.asset_events
|
|
1554
|
+
(id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
|
|
1555
|
+
actor_id,actor_type,action_invocation_id,payload,sequence,occurred_at,recorded_at,
|
|
1556
|
+
correlation_id,causation_id)
|
|
1557
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16)
|
|
1558
|
+
ON CONFLICT (id) DO NOTHING RETURNING id
|
|
1559
|
+
)
|
|
1560
|
+
INSERT INTO fabric_platform.event_outbox
|
|
1561
|
+
(id,tenant_id,space_id,event,status,attempt_count,available_at,created_at)
|
|
1562
|
+
SELECT $1,$2,$3,$17::jsonb,'pending',0,$14,$14 FROM inserted_event
|
|
1563
|
+
ON CONFLICT (id) DO NOTHING`,
|
|
1564
|
+
[
|
|
1565
|
+
event.id,
|
|
1566
|
+
event.tenantId,
|
|
1567
|
+
event.spaceId,
|
|
1568
|
+
event.eventType,
|
|
1569
|
+
event.eventSchemaVersion,
|
|
1570
|
+
event.subjectType,
|
|
1571
|
+
event.subjectId,
|
|
1572
|
+
event.actorId,
|
|
1573
|
+
event.actorType,
|
|
1574
|
+
event.actionInvocationId ?? null,
|
|
1575
|
+
JSON.stringify(event.payload),
|
|
1576
|
+
event.sequence,
|
|
1577
|
+
event.occurredAt,
|
|
1578
|
+
event.recordedAt,
|
|
1579
|
+
event.correlationId,
|
|
1580
|
+
event.causationId ?? null,
|
|
1581
|
+
JSON.stringify(envelope)
|
|
1582
|
+
]
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
async claimOutbox(input) {
|
|
1586
|
+
const current = input.now ?? /* @__PURE__ */ new Date();
|
|
1587
|
+
const leaseExpiresAt = new Date(current.getTime() + input.leaseDurationMs);
|
|
1588
|
+
const result = await this.sql.query(
|
|
1589
|
+
`WITH claimable AS (
|
|
1590
|
+
SELECT id FROM fabric_platform.event_outbox
|
|
1591
|
+
WHERE status='pending' AND available_at <= $1
|
|
1592
|
+
AND (lease_expires_at IS NULL OR lease_expires_at <= $1)
|
|
1593
|
+
AND ($2::text IS NULL OR tenant_id=$2)
|
|
1594
|
+
AND ($3::text IS NULL OR space_id=$3)
|
|
1595
|
+
ORDER BY created_at,id FOR UPDATE SKIP LOCKED LIMIT $4
|
|
1596
|
+
)
|
|
1597
|
+
UPDATE fabric_platform.event_outbox AS item
|
|
1598
|
+
SET lease_owner=$5, lease_expires_at=$6, attempt_count=attempt_count+1
|
|
1599
|
+
FROM claimable WHERE item.id=claimable.id RETURNING item.*`,
|
|
1600
|
+
[current, input.tenantId ?? null, input.spaceId ?? null, Math.max(1, Math.min(input.limit ?? 100, 1e3)), input.workerId, leaseExpiresAt]
|
|
1601
|
+
);
|
|
1602
|
+
return result.rows.map(toOutboxRecord);
|
|
1603
|
+
}
|
|
1604
|
+
async markOutboxPublished(id, workerId, publishedAt) {
|
|
1605
|
+
const result = await this.sql.query(
|
|
1606
|
+
`UPDATE fabric_platform.event_outbox SET status='published',published_at=$3,
|
|
1607
|
+
lease_owner=NULL,lease_expires_at=NULL WHERE id=$1 AND lease_owner=$2 RETURNING id`,
|
|
1608
|
+
[id, workerId, publishedAt]
|
|
1609
|
+
);
|
|
1610
|
+
if (result.rows.length === 0) throw new Error(`Outbox record ${id} is not leased by ${workerId}.`);
|
|
1611
|
+
}
|
|
1612
|
+
async markOutboxFailed(input) {
|
|
1613
|
+
const result = await this.sql.query(
|
|
1614
|
+
`UPDATE fabric_platform.event_outbox SET status=$3,last_error=$4,available_at=$5,
|
|
1615
|
+
lease_owner=NULL,lease_expires_at=NULL WHERE id=$1 AND lease_owner=$2 RETURNING id`,
|
|
1616
|
+
[input.id, input.workerId, input.deadLetter ? "dead_letter" : "pending", input.error, input.availableAt]
|
|
1617
|
+
);
|
|
1618
|
+
if (result.rows.length === 0) throw new Error(`Outbox record ${input.id} is not leased by ${input.workerId}.`);
|
|
1619
|
+
}
|
|
1620
|
+
async listOutbox(input = {}) {
|
|
1621
|
+
const result = await this.sql.query(
|
|
1622
|
+
`SELECT * FROM fabric_platform.event_outbox
|
|
1623
|
+
WHERE ($1::text IS NULL OR tenant_id=$1) AND ($2::text IS NULL OR space_id=$2)
|
|
1624
|
+
AND ($3::text[] IS NULL OR status=ANY($3::text[])) ORDER BY created_at,id`,
|
|
1625
|
+
[input.tenantId ?? null, input.spaceId ?? null, input.statuses?.length ? input.statuses : null]
|
|
1626
|
+
);
|
|
1627
|
+
return result.rows.map(toOutboxRecord);
|
|
1628
|
+
}
|
|
1331
1629
|
async nextEventSequence(tenantId, spaceId) {
|
|
1332
1630
|
const result = await this.sql.query(
|
|
1333
1631
|
`INSERT INTO fabric_platform.event_sequences (tenant_id,space_id,next_sequence)
|
|
@@ -1516,6 +1814,41 @@ function toEventRecord(row) {
|
|
|
1516
1814
|
...row.causation_id ? { causationId: String(row.causation_id) } : {}
|
|
1517
1815
|
};
|
|
1518
1816
|
}
|
|
1817
|
+
function toOutboxRecord(row) {
|
|
1818
|
+
const event = row.event;
|
|
1819
|
+
return {
|
|
1820
|
+
id: String(row.id),
|
|
1821
|
+
tenantId: String(row.tenant_id),
|
|
1822
|
+
spaceId: String(row.space_id),
|
|
1823
|
+
event: {
|
|
1824
|
+
eventId: String(event.eventId),
|
|
1825
|
+
eventType: String(event.eventType),
|
|
1826
|
+
eventSchemaVersion: Number(event.eventSchemaVersion),
|
|
1827
|
+
tenantId: String(event.tenantId),
|
|
1828
|
+
spaceId: String(event.spaceId),
|
|
1829
|
+
subjectType: String(event.subjectType),
|
|
1830
|
+
subjectId: String(event.subjectId),
|
|
1831
|
+
sequence: Number(event.sequence),
|
|
1832
|
+
...event.actionInvocationId ? { actionInvocationId: String(event.actionInvocationId) } : {},
|
|
1833
|
+
correlationId: String(event.correlationId),
|
|
1834
|
+
...event.causationId ? { causationId: String(event.causationId) } : {},
|
|
1835
|
+
occurredAt: new Date(event.occurredAt),
|
|
1836
|
+
recordedAt: new Date(event.recordedAt),
|
|
1837
|
+
producerModuleVersion: String(event.producerModuleVersion),
|
|
1838
|
+
payloadClassification: String(event.payloadClassification),
|
|
1839
|
+
...event.traceContext ? { traceContext: event.traceContext } : {},
|
|
1840
|
+
payload: event.payload
|
|
1841
|
+
},
|
|
1842
|
+
status: String(row.status),
|
|
1843
|
+
attemptCount: Number(row.attempt_count),
|
|
1844
|
+
availableAt: new Date(row.available_at),
|
|
1845
|
+
...row.lease_owner ? { leaseOwner: String(row.lease_owner) } : {},
|
|
1846
|
+
...row.lease_expires_at ? { leaseExpiresAt: new Date(row.lease_expires_at) } : {},
|
|
1847
|
+
...row.last_error ? { lastError: String(row.last_error) } : {},
|
|
1848
|
+
createdAt: new Date(row.created_at),
|
|
1849
|
+
...row.published_at ? { publishedAt: new Date(row.published_at) } : {}
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1519
1852
|
|
|
1520
1853
|
// src/worker.ts
|
|
1521
1854
|
var DEFAULT_BATCH_SIZE = 10;
|
|
@@ -1588,9 +1921,12 @@ async function abortableDelay(milliseconds, signal) {
|
|
|
1588
1921
|
exports.MemoryPlatformHostStore = MemoryPlatformHostStore;
|
|
1589
1922
|
exports.PLATFORM_HOST_CONTRACT_VERSION = PLATFORM_HOST_CONTRACT_VERSION;
|
|
1590
1923
|
exports.PostgresPlatformHostStore = PostgresPlatformHostStore;
|
|
1924
|
+
exports.cloneOutboxRecord = cloneOutboxRecord;
|
|
1591
1925
|
exports.createGovernedActionHost = createGovernedActionHost;
|
|
1592
1926
|
exports.createStoreBackedActionDispatcher = createStoreBackedActionDispatcher;
|
|
1927
|
+
exports.runOutboxRelayCycle = runOutboxRelayCycle;
|
|
1593
1928
|
exports.runPlatformActionWorker = runPlatformActionWorker;
|
|
1594
1929
|
exports.runPlatformActionWorkerCycle = runPlatformActionWorkerCycle;
|
|
1930
|
+
exports.toEnterpriseEventEnvelope = toEnterpriseEventEnvelope;
|
|
1595
1931
|
//# sourceMappingURL=index.cjs.map
|
|
1596
1932
|
//# sourceMappingURL=index.cjs.map
|