@fabricorg/platform-host 1.0.0 → 2.0.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 +21 -0
- package/README.md +18 -3
- package/dist/index.cjs +280 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +133 -3
- package/dist/index.d.ts +133 -3
- package/dist/index.js +278 -2
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# @fabricorg/platform-host
|
|
2
2
|
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Add the optional durable event outbox, PostgreSQL and memory adapters, and at-least-once relay.
|
|
8
|
+
- Add manifest v2 typed entity contracts, v1 reader normalization, optional action result schemas, and
|
|
9
|
+
Host enforcement of result validation before events and adapters.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Updated dependencies
|
|
14
|
+
- Updated dependencies
|
|
15
|
+
- @fabricorg/platform@0.11.0
|
|
16
|
+
|
|
17
|
+
## Unreleased
|
|
18
|
+
|
|
19
|
+
- Validate optional action result schemas immediately after handler execution and before events or
|
|
20
|
+
adapters. Invalid results roll back the handler transaction and persist a sanitized failure.
|
|
21
|
+
- Add optional memory and PostgreSQL outbox capabilities with atomic canonical-event records,
|
|
22
|
+
lease-based at-least-once relay, retry, replay, and dead-letter state.
|
|
23
|
+
|
|
3
24
|
## 1.0.0
|
|
4
25
|
|
|
5
26
|
### 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.10.0 @fabricorg/platform-host@^1.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 0
|
|
18
|
+
See the [Platform Host 1.0 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,12 @@ 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 roll back the
|
|
105
|
+
handler transaction, emit no domain/completion event, invoke no adapter, and persist only a sanitized
|
|
106
|
+
validation failure. This validates the handler result contract; it does not observe arbitrary writes
|
|
107
|
+
through an application-owned `TDb`.
|
|
108
|
+
|
|
103
109
|
## Agent HITL
|
|
104
110
|
|
|
105
111
|
Hosts can inject a vertical-owned `hitlEvaluator`. It runs only for `actorType: "agent"`, after schema
|
|
@@ -171,7 +177,7 @@ explain which contract and provider adapter governed a mutation after dependenci
|
|
|
171
177
|
const host = createGovernedActionHost({
|
|
172
178
|
// ...
|
|
173
179
|
runtimeEvidence: {
|
|
174
|
-
hostPackageVersion: "0.
|
|
180
|
+
hostPackageVersion: "1.0.0",
|
|
175
181
|
policyRulesetVersion: "gtm-rules.v8",
|
|
176
182
|
providerBridge: { name: "@fabric-harness/databricks", version: "1" },
|
|
177
183
|
},
|
|
@@ -181,3 +187,12 @@ const host = createGovernedActionHost({
|
|
|
181
187
|
`MemoryPlatformHostStore` is for tests and local demos. Production control planes use
|
|
182
188
|
`PostgresPlatformHostStore` with Databricks Lakebase (or standard Postgres), call
|
|
183
189
|
`ensureSchema()` at startup, and hydrate projections from `listEvents()`.
|
|
190
|
+
|
|
191
|
+
## Durable outbox egress
|
|
192
|
+
|
|
193
|
+
Configure `outbox` on `createGovernedActionHost()` only with an `OutboxPlatformHostStore`.
|
|
194
|
+
The Host then uses `appendEventWithOutbox` so the canonical event and delivery record share
|
|
195
|
+
the proven event transaction. `runOutboxRelayCycle()` leases and publishes records with retry
|
|
196
|
+
and dead-letter handling. Delivery is at least once: consumers deduplicate on immutable
|
|
197
|
+
`eventId`. Payload classification is metadata, not a redaction mechanism; event payloads must
|
|
198
|
+
already be audit-safe.
|
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());
|
|
@@ -340,6 +346,13 @@ function createGovernedActionHost(options) {
|
|
|
340
346
|
throw new Error(handlerResult.error ?? "Action handler failed");
|
|
341
347
|
}
|
|
342
348
|
const handlerData = handlerResult.data ?? {};
|
|
349
|
+
if (action.resultSchema) {
|
|
350
|
+
const publicResult = withoutPrivateHostFields(handlerData, eventResultFields);
|
|
351
|
+
const parsedResult = action.resultSchema.safeParse(publicResult);
|
|
352
|
+
if (!parsedResult.success) {
|
|
353
|
+
throw new Error(`Action result validation failed at ${parsedResult.error.issues.map((issue) => issue.path.map(String).join(".") || "result").join(", ")}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
343
356
|
const events = extractEvents(handlerData);
|
|
344
357
|
const undeclaredEvent = events.find(
|
|
345
358
|
(event) => !action.emitsEvents.includes(event.eventType)
|
|
@@ -680,7 +693,24 @@ function createGovernedActionHost(options) {
|
|
|
680
693
|
correlationId: invocation.correlationId,
|
|
681
694
|
...invocation.causationId ? { causationId: invocation.causationId } : {}
|
|
682
695
|
};
|
|
683
|
-
|
|
696
|
+
if (options.outbox) {
|
|
697
|
+
const traceContext = options.outbox.traceContext?.(envelope);
|
|
698
|
+
const metadata = {
|
|
699
|
+
producerModuleVersion: options.outbox.producerModuleVersion(invocation.actionId, invocation.actionVersion),
|
|
700
|
+
payloadClassification: options.outbox.classifyPayload(envelope),
|
|
701
|
+
...traceContext ? { traceContext } : {}
|
|
702
|
+
};
|
|
703
|
+
if (transaction) {
|
|
704
|
+
if (!transaction.appendEventWithOutbox) throw new Error("Outbox egress requires an atomic transaction with appendEventWithOutbox.");
|
|
705
|
+
await transaction.appendEventWithOutbox(envelope, metadata);
|
|
706
|
+
} else {
|
|
707
|
+
const outboxStore = asOutboxStore(options.store);
|
|
708
|
+
if (!outboxStore) throw new Error("Outbox egress requires an outbox-capable Platform Host store.");
|
|
709
|
+
await outboxStore.appendEventWithOutbox(envelope, metadata);
|
|
710
|
+
}
|
|
711
|
+
} else {
|
|
712
|
+
await (transaction ?? options.store).appendEvent(envelope);
|
|
713
|
+
}
|
|
684
714
|
}
|
|
685
715
|
async function fail(invocation, status, error) {
|
|
686
716
|
await options.store.updateActionInvocation(
|
|
@@ -717,6 +747,10 @@ function asAtomicMutationStore(store) {
|
|
|
717
747
|
const candidate = store;
|
|
718
748
|
return typeof candidate.transactionWithEvents === "function" ? store : void 0;
|
|
719
749
|
}
|
|
750
|
+
function asOutboxStore(store) {
|
|
751
|
+
const candidate = store;
|
|
752
|
+
return typeof candidate.appendEventWithOutbox === "function" && typeof candidate.claimOutbox === "function" ? store : void 0;
|
|
753
|
+
}
|
|
720
754
|
function asGovernanceStore(store) {
|
|
721
755
|
const candidate = store;
|
|
722
756
|
return typeof candidate.recordMutationGovernance === "function" && typeof candidate.appendPolicyObligations === "function" && typeof candidate.appendExecutionAttestation === "function" ? store : void 0;
|
|
@@ -773,6 +807,73 @@ function lifecycleId(prefix, invocationId, key) {
|
|
|
773
807
|
return `${prefix}_${invocationId}_${safeKey}`;
|
|
774
808
|
}
|
|
775
809
|
|
|
810
|
+
// src/outbox.ts
|
|
811
|
+
function toEnterpriseEventEnvelope(event, metadata) {
|
|
812
|
+
return {
|
|
813
|
+
eventId: event.id,
|
|
814
|
+
eventType: event.eventType,
|
|
815
|
+
eventSchemaVersion: event.eventSchemaVersion,
|
|
816
|
+
tenantId: event.tenantId,
|
|
817
|
+
spaceId: event.spaceId,
|
|
818
|
+
subjectType: event.subjectType,
|
|
819
|
+
subjectId: event.subjectId,
|
|
820
|
+
sequence: event.sequence,
|
|
821
|
+
...event.actionInvocationId ? { actionInvocationId: event.actionInvocationId } : {},
|
|
822
|
+
correlationId: event.correlationId,
|
|
823
|
+
...event.causationId ? { causationId: event.causationId } : {},
|
|
824
|
+
occurredAt: event.occurredAt,
|
|
825
|
+
recordedAt: event.recordedAt,
|
|
826
|
+
producerModuleVersion: metadata.producerModuleVersion,
|
|
827
|
+
payloadClassification: metadata.payloadClassification,
|
|
828
|
+
...metadata.traceContext ? { traceContext: { ...metadata.traceContext } } : {},
|
|
829
|
+
payload: event.payload
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
async function runOutboxRelayCycle(options) {
|
|
833
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
834
|
+
const maxAttempts = options.maxAttempts ?? 10;
|
|
835
|
+
const records = await options.store.claimOutbox({
|
|
836
|
+
workerId: options.workerId,
|
|
837
|
+
leaseDurationMs: options.leaseDurationMs ?? 3e4,
|
|
838
|
+
limit: options.batchSize ?? 100,
|
|
839
|
+
now: now()
|
|
840
|
+
});
|
|
841
|
+
const result = { claimed: records.length, published: 0, failed: 0, deadLettered: 0 };
|
|
842
|
+
for (const record of records) {
|
|
843
|
+
try {
|
|
844
|
+
await options.publisher.publish(record.event);
|
|
845
|
+
try {
|
|
846
|
+
await options.store.markOutboxPublished(record.id, options.workerId, now());
|
|
847
|
+
} catch {
|
|
848
|
+
result.failed += 1;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
result.published += 1;
|
|
852
|
+
} catch (error) {
|
|
853
|
+
const deadLetter = record.attemptCount >= maxAttempts;
|
|
854
|
+
try {
|
|
855
|
+
await options.store.markOutboxFailed({
|
|
856
|
+
id: record.id,
|
|
857
|
+
workerId: options.workerId,
|
|
858
|
+
error: error instanceof Error ? error.message : String(error),
|
|
859
|
+
availableAt: new Date(now().getTime() + (options.retryDelayMs?.(record.attemptCount) ?? 1e3)),
|
|
860
|
+
deadLetter
|
|
861
|
+
});
|
|
862
|
+
} catch {
|
|
863
|
+
}
|
|
864
|
+
result.failed += 1;
|
|
865
|
+
if (deadLetter) result.deadLettered += 1;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return result;
|
|
869
|
+
}
|
|
870
|
+
function cloneOutboxRecord(record) {
|
|
871
|
+
return {
|
|
872
|
+
...record,
|
|
873
|
+
event: { ...record.event, ...record.event.traceContext ? { traceContext: { ...record.event.traceContext } } : {} }
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
|
|
776
877
|
// src/memory-store.ts
|
|
777
878
|
var MemoryPlatformHostStore = class {
|
|
778
879
|
constructor(db) {
|
|
@@ -786,6 +887,7 @@ var MemoryPlatformHostStore = class {
|
|
|
786
887
|
policyObligations = [];
|
|
787
888
|
executionAttestations = [];
|
|
788
889
|
externalReconciliations = [];
|
|
890
|
+
outbox = [];
|
|
789
891
|
async transaction(run) {
|
|
790
892
|
return run(this.db);
|
|
791
893
|
}
|
|
@@ -904,6 +1006,53 @@ var MemoryPlatformHostStore = class {
|
|
|
904
1006
|
if (this.events.some((candidate) => candidate.id === event.id)) return;
|
|
905
1007
|
this.events.push(event);
|
|
906
1008
|
}
|
|
1009
|
+
async appendEventWithOutbox(event, metadata) {
|
|
1010
|
+
if (this.events.some((candidate) => candidate.id === event.id)) return;
|
|
1011
|
+
const createdAt = new Date(event.recordedAt);
|
|
1012
|
+
this.events.push(event);
|
|
1013
|
+
this.outbox.push({
|
|
1014
|
+
id: event.id,
|
|
1015
|
+
tenantId: event.tenantId,
|
|
1016
|
+
spaceId: event.spaceId,
|
|
1017
|
+
event: toEnterpriseEventEnvelope(event, metadata),
|
|
1018
|
+
status: "pending",
|
|
1019
|
+
attemptCount: 0,
|
|
1020
|
+
availableAt: createdAt,
|
|
1021
|
+
createdAt
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
async claimOutbox(input) {
|
|
1025
|
+
const current = input.now ?? /* @__PURE__ */ new Date();
|
|
1026
|
+
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) => {
|
|
1027
|
+
record.leaseOwner = input.workerId;
|
|
1028
|
+
record.leaseExpiresAt = new Date(current.getTime() + input.leaseDurationMs);
|
|
1029
|
+
record.attemptCount += 1;
|
|
1030
|
+
return cloneOutboxRecord(record);
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
async markOutboxPublished(id, workerId, publishedAt) {
|
|
1034
|
+
const record = this.requireLeasedOutbox(id, workerId);
|
|
1035
|
+
record.status = "published";
|
|
1036
|
+
record.publishedAt = publishedAt;
|
|
1037
|
+
delete record.leaseOwner;
|
|
1038
|
+
delete record.leaseExpiresAt;
|
|
1039
|
+
}
|
|
1040
|
+
async markOutboxFailed(input) {
|
|
1041
|
+
const record = this.requireLeasedOutbox(input.id, input.workerId);
|
|
1042
|
+
record.status = input.deadLetter ? "dead_letter" : "pending";
|
|
1043
|
+
record.lastError = input.error;
|
|
1044
|
+
record.availableAt = input.availableAt;
|
|
1045
|
+
delete record.leaseOwner;
|
|
1046
|
+
delete record.leaseExpiresAt;
|
|
1047
|
+
}
|
|
1048
|
+
async listOutbox(input = {}) {
|
|
1049
|
+
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);
|
|
1050
|
+
}
|
|
1051
|
+
requireLeasedOutbox(id, workerId) {
|
|
1052
|
+
const record = this.outbox.find((candidate) => candidate.id === id);
|
|
1053
|
+
if (!record || record.leaseOwner !== workerId) throw new Error(`Outbox record ${id} is not leased by ${workerId}.`);
|
|
1054
|
+
return record;
|
|
1055
|
+
}
|
|
907
1056
|
async nextEventSequence(tenantId, spaceId) {
|
|
908
1057
|
return this.events.filter((event) => event.tenantId === tenantId && event.spaceId === spaceId).length + 1;
|
|
909
1058
|
}
|
|
@@ -947,12 +1096,14 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
947
1096
|
constructor(db, sql, transactionProvider) {
|
|
948
1097
|
this.db = db;
|
|
949
1098
|
this.sql = sql;
|
|
1099
|
+
this.transactionalOutbox = transactionProvider !== void 0;
|
|
950
1100
|
if (transactionProvider) {
|
|
951
1101
|
this.transactionWithEvents = async (run) => transactionProvider.run(async ({ db: db2, sql: sql2 }) => {
|
|
952
1102
|
const scoped = new _PostgresPlatformHostStore(db2, sql2);
|
|
953
1103
|
return run({
|
|
954
1104
|
db: db2,
|
|
955
1105
|
appendEvent: (event) => scoped.appendEvent(event),
|
|
1106
|
+
appendEventWithOutbox: (event, metadata) => scoped.appendEventWithOutbox(event, metadata),
|
|
956
1107
|
nextEventSequence: (tenantId, spaceId) => scoped.nextEventSequence(tenantId, spaceId),
|
|
957
1108
|
listEvents: (tenantId, spaceId) => scoped.listEvents(tenantId, spaceId),
|
|
958
1109
|
updateActionInvocation: (id, tenantId, spaceId, patch) => scoped.updateActionInvocation(id, tenantId, spaceId, patch)
|
|
@@ -963,6 +1114,7 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
963
1114
|
db;
|
|
964
1115
|
sql;
|
|
965
1116
|
transactionWithEvents;
|
|
1117
|
+
transactionalOutbox;
|
|
966
1118
|
async ensureSchema() {
|
|
967
1119
|
await this.sql.query(`
|
|
968
1120
|
CREATE SCHEMA IF NOT EXISTS fabric_platform;
|
|
@@ -1046,6 +1198,15 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1046
1198
|
CREATE INDEX IF NOT EXISTS asset_events_subject_idx
|
|
1047
1199
|
ON fabric_platform.asset_events
|
|
1048
1200
|
(tenant_id, space_id, subject_type, subject_id, sequence);
|
|
1201
|
+
CREATE TABLE IF NOT EXISTS fabric_platform.event_outbox (
|
|
1202
|
+
id text PRIMARY KEY, tenant_id text NOT NULL, space_id text NOT NULL,
|
|
1203
|
+
event jsonb NOT NULL, status text NOT NULL DEFAULT 'pending',
|
|
1204
|
+
attempt_count integer NOT NULL DEFAULT 0, available_at timestamptz NOT NULL,
|
|
1205
|
+
lease_owner text, lease_expires_at timestamptz, last_error text,
|
|
1206
|
+
created_at timestamptz NOT NULL, published_at timestamptz
|
|
1207
|
+
);
|
|
1208
|
+
CREATE INDEX IF NOT EXISTS event_outbox_claim_idx
|
|
1209
|
+
ON fabric_platform.event_outbox (status, available_at, lease_expires_at, created_at);
|
|
1049
1210
|
`);
|
|
1050
1211
|
}
|
|
1051
1212
|
async transaction(run) {
|
|
@@ -1328,6 +1489,86 @@ var PostgresPlatformHostStore = class _PostgresPlatformHostStore {
|
|
|
1328
1489
|
]
|
|
1329
1490
|
);
|
|
1330
1491
|
}
|
|
1492
|
+
async appendEventWithOutbox(event, metadata) {
|
|
1493
|
+
const envelope = toEnterpriseEventEnvelope(event, metadata);
|
|
1494
|
+
await this.sql.query(
|
|
1495
|
+
`WITH inserted_event AS (
|
|
1496
|
+
INSERT INTO fabric_platform.asset_events
|
|
1497
|
+
(id,tenant_id,space_id,event_type,event_schema_version,subject_type,subject_id,
|
|
1498
|
+
actor_id,actor_type,action_invocation_id,payload,sequence,occurred_at,recorded_at,
|
|
1499
|
+
correlation_id,causation_id)
|
|
1500
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11::jsonb,$12,$13,$14,$15,$16)
|
|
1501
|
+
ON CONFLICT (id) DO NOTHING RETURNING id
|
|
1502
|
+
)
|
|
1503
|
+
INSERT INTO fabric_platform.event_outbox
|
|
1504
|
+
(id,tenant_id,space_id,event,status,attempt_count,available_at,created_at)
|
|
1505
|
+
SELECT $1,$2,$3,$17::jsonb,'pending',0,$14,$14 FROM inserted_event
|
|
1506
|
+
ON CONFLICT (id) DO NOTHING`,
|
|
1507
|
+
[
|
|
1508
|
+
event.id,
|
|
1509
|
+
event.tenantId,
|
|
1510
|
+
event.spaceId,
|
|
1511
|
+
event.eventType,
|
|
1512
|
+
event.eventSchemaVersion,
|
|
1513
|
+
event.subjectType,
|
|
1514
|
+
event.subjectId,
|
|
1515
|
+
event.actorId,
|
|
1516
|
+
event.actorType,
|
|
1517
|
+
event.actionInvocationId ?? null,
|
|
1518
|
+
JSON.stringify(event.payload),
|
|
1519
|
+
event.sequence,
|
|
1520
|
+
event.occurredAt,
|
|
1521
|
+
event.recordedAt,
|
|
1522
|
+
event.correlationId,
|
|
1523
|
+
event.causationId ?? null,
|
|
1524
|
+
JSON.stringify(envelope)
|
|
1525
|
+
]
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
async claimOutbox(input) {
|
|
1529
|
+
const current = input.now ?? /* @__PURE__ */ new Date();
|
|
1530
|
+
const leaseExpiresAt = new Date(current.getTime() + input.leaseDurationMs);
|
|
1531
|
+
const result = await this.sql.query(
|
|
1532
|
+
`WITH claimable AS (
|
|
1533
|
+
SELECT id FROM fabric_platform.event_outbox
|
|
1534
|
+
WHERE status='pending' AND available_at <= $1
|
|
1535
|
+
AND (lease_expires_at IS NULL OR lease_expires_at <= $1)
|
|
1536
|
+
AND ($2::text IS NULL OR tenant_id=$2)
|
|
1537
|
+
AND ($3::text IS NULL OR space_id=$3)
|
|
1538
|
+
ORDER BY created_at,id FOR UPDATE SKIP LOCKED LIMIT $4
|
|
1539
|
+
)
|
|
1540
|
+
UPDATE fabric_platform.event_outbox AS item
|
|
1541
|
+
SET lease_owner=$5, lease_expires_at=$6, attempt_count=attempt_count+1
|
|
1542
|
+
FROM claimable WHERE item.id=claimable.id RETURNING item.*`,
|
|
1543
|
+
[current, input.tenantId ?? null, input.spaceId ?? null, Math.max(1, Math.min(input.limit ?? 100, 1e3)), input.workerId, leaseExpiresAt]
|
|
1544
|
+
);
|
|
1545
|
+
return result.rows.map(toOutboxRecord);
|
|
1546
|
+
}
|
|
1547
|
+
async markOutboxPublished(id, workerId, publishedAt) {
|
|
1548
|
+
const result = await this.sql.query(
|
|
1549
|
+
`UPDATE fabric_platform.event_outbox SET status='published',published_at=$3,
|
|
1550
|
+
lease_owner=NULL,lease_expires_at=NULL WHERE id=$1 AND lease_owner=$2 RETURNING id`,
|
|
1551
|
+
[id, workerId, publishedAt]
|
|
1552
|
+
);
|
|
1553
|
+
if (result.rows.length === 0) throw new Error(`Outbox record ${id} is not leased by ${workerId}.`);
|
|
1554
|
+
}
|
|
1555
|
+
async markOutboxFailed(input) {
|
|
1556
|
+
const result = await this.sql.query(
|
|
1557
|
+
`UPDATE fabric_platform.event_outbox SET status=$3,last_error=$4,available_at=$5,
|
|
1558
|
+
lease_owner=NULL,lease_expires_at=NULL WHERE id=$1 AND lease_owner=$2 RETURNING id`,
|
|
1559
|
+
[input.id, input.workerId, input.deadLetter ? "dead_letter" : "pending", input.error, input.availableAt]
|
|
1560
|
+
);
|
|
1561
|
+
if (result.rows.length === 0) throw new Error(`Outbox record ${input.id} is not leased by ${input.workerId}.`);
|
|
1562
|
+
}
|
|
1563
|
+
async listOutbox(input = {}) {
|
|
1564
|
+
const result = await this.sql.query(
|
|
1565
|
+
`SELECT * FROM fabric_platform.event_outbox
|
|
1566
|
+
WHERE ($1::text IS NULL OR tenant_id=$1) AND ($2::text IS NULL OR space_id=$2)
|
|
1567
|
+
AND ($3::text[] IS NULL OR status=ANY($3::text[])) ORDER BY created_at,id`,
|
|
1568
|
+
[input.tenantId ?? null, input.spaceId ?? null, input.statuses?.length ? input.statuses : null]
|
|
1569
|
+
);
|
|
1570
|
+
return result.rows.map(toOutboxRecord);
|
|
1571
|
+
}
|
|
1331
1572
|
async nextEventSequence(tenantId, spaceId) {
|
|
1332
1573
|
const result = await this.sql.query(
|
|
1333
1574
|
`INSERT INTO fabric_platform.event_sequences (tenant_id,space_id,next_sequence)
|
|
@@ -1516,6 +1757,41 @@ function toEventRecord(row) {
|
|
|
1516
1757
|
...row.causation_id ? { causationId: String(row.causation_id) } : {}
|
|
1517
1758
|
};
|
|
1518
1759
|
}
|
|
1760
|
+
function toOutboxRecord(row) {
|
|
1761
|
+
const event = row.event;
|
|
1762
|
+
return {
|
|
1763
|
+
id: String(row.id),
|
|
1764
|
+
tenantId: String(row.tenant_id),
|
|
1765
|
+
spaceId: String(row.space_id),
|
|
1766
|
+
event: {
|
|
1767
|
+
eventId: String(event.eventId),
|
|
1768
|
+
eventType: String(event.eventType),
|
|
1769
|
+
eventSchemaVersion: Number(event.eventSchemaVersion),
|
|
1770
|
+
tenantId: String(event.tenantId),
|
|
1771
|
+
spaceId: String(event.spaceId),
|
|
1772
|
+
subjectType: String(event.subjectType),
|
|
1773
|
+
subjectId: String(event.subjectId),
|
|
1774
|
+
sequence: Number(event.sequence),
|
|
1775
|
+
...event.actionInvocationId ? { actionInvocationId: String(event.actionInvocationId) } : {},
|
|
1776
|
+
correlationId: String(event.correlationId),
|
|
1777
|
+
...event.causationId ? { causationId: String(event.causationId) } : {},
|
|
1778
|
+
occurredAt: new Date(event.occurredAt),
|
|
1779
|
+
recordedAt: new Date(event.recordedAt),
|
|
1780
|
+
producerModuleVersion: String(event.producerModuleVersion),
|
|
1781
|
+
payloadClassification: String(event.payloadClassification),
|
|
1782
|
+
...event.traceContext ? { traceContext: event.traceContext } : {},
|
|
1783
|
+
payload: event.payload
|
|
1784
|
+
},
|
|
1785
|
+
status: String(row.status),
|
|
1786
|
+
attemptCount: Number(row.attempt_count),
|
|
1787
|
+
availableAt: new Date(row.available_at),
|
|
1788
|
+
...row.lease_owner ? { leaseOwner: String(row.lease_owner) } : {},
|
|
1789
|
+
...row.lease_expires_at ? { leaseExpiresAt: new Date(row.lease_expires_at) } : {},
|
|
1790
|
+
...row.last_error ? { lastError: String(row.last_error) } : {},
|
|
1791
|
+
createdAt: new Date(row.created_at),
|
|
1792
|
+
...row.published_at ? { publishedAt: new Date(row.published_at) } : {}
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1519
1795
|
|
|
1520
1796
|
// src/worker.ts
|
|
1521
1797
|
var DEFAULT_BATCH_SIZE = 10;
|
|
@@ -1588,9 +1864,12 @@ async function abortableDelay(milliseconds, signal) {
|
|
|
1588
1864
|
exports.MemoryPlatformHostStore = MemoryPlatformHostStore;
|
|
1589
1865
|
exports.PLATFORM_HOST_CONTRACT_VERSION = PLATFORM_HOST_CONTRACT_VERSION;
|
|
1590
1866
|
exports.PostgresPlatformHostStore = PostgresPlatformHostStore;
|
|
1867
|
+
exports.cloneOutboxRecord = cloneOutboxRecord;
|
|
1591
1868
|
exports.createGovernedActionHost = createGovernedActionHost;
|
|
1592
1869
|
exports.createStoreBackedActionDispatcher = createStoreBackedActionDispatcher;
|
|
1870
|
+
exports.runOutboxRelayCycle = runOutboxRelayCycle;
|
|
1593
1871
|
exports.runPlatformActionWorker = runPlatformActionWorker;
|
|
1594
1872
|
exports.runPlatformActionWorkerCycle = runPlatformActionWorkerCycle;
|
|
1873
|
+
exports.toEnterpriseEventEnvelope = toEnterpriseEventEnvelope;
|
|
1595
1874
|
//# sourceMappingURL=index.cjs.map
|
|
1596
1875
|
//# sourceMappingURL=index.cjs.map
|