@fabricorg/platform-host 8.0.0 → 8.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/dist/index.d.cts CHANGED
@@ -554,6 +554,13 @@ export interface PlatformHostStore<TDb> {
554
554
  * optional only for compatibility with existing stores and direct callers.
555
555
  */
556
556
  updateAdapterInvocation(id: string, patch: Partial<Pick<AdapterInvocationRecord, "status" | "output" | "error" | "attempt" | "updatedAt" | "reconciliation">>, scope?: AdapterInvocationScope): Promise<void>;
557
+ /**
558
+ * First-write-wins: an event ID the store already holds, in any tenant or space, is a no-op
559
+ * that keeps the first event. Do not throw, replace or merge it, regardless of
560
+ * the later payload, sequence or timestamps. Host re-appends lifecycle events
561
+ * under stable IDs on retry/recovery with fresh sequences and timestamps and
562
+ * potentially different payloads.
563
+ */
557
564
  appendEvent(event: AssetEventEnvelope): Promise<void>;
558
565
  nextEventSequence(tenantId: string, spaceId: string): Promise<number>;
559
566
  /**
@@ -588,7 +595,22 @@ export interface PlatformHostMutationTransaction<TDb> {
588
595
  * Required by serialized saga parent lifecycle transitions.
589
596
  */
590
597
  getEvent?(id: string, tenantId: string, spaceId: string): Promise<AssetEventEnvelope | undefined>;
598
+ /**
599
+ * First-write-wins: an event ID the store already holds, in any tenant or space, is a no-op
600
+ * that keeps the first event. Do not throw, replace or merge it, regardless of
601
+ * the later payload, sequence or timestamps. Host re-appends lifecycle events
602
+ * under stable IDs on retry/recovery with fresh sequences and timestamps and
603
+ * potentially different payloads.
604
+ */
591
605
  appendEvent(event: AssetEventEnvelope): Promise<void>;
606
+ /**
607
+ * First-write-wins: an event ID the store already holds, in any tenant or space, is a no-op
608
+ * that keeps the first event and adds no outbox row, even when the
609
+ * first append had none. Do not throw,
610
+ * replace or merge it, regardless of the later payload, sequence or timestamps.
611
+ * Host re-appends lifecycle events under stable IDs on retry/recovery with fresh
612
+ * sequences and timestamps and potentially different payloads.
613
+ */
592
614
  appendEventWithOutbox?(event: AssetEventEnvelope, metadata: OutboxEventMetadata): Promise<void>;
593
615
  nextEventSequence(tenantId: string, spaceId: string): Promise<number>;
594
616
  listEvents(tenantId: string, spaceId: string): Promise<AssetEventEnvelope[]>;
@@ -706,6 +728,14 @@ export interface ClaimOutboxInput {
706
728
  export interface OutboxPlatformHostStore<TDb> extends PlatformHostStore<TDb> {
707
729
  /** True only when transactionWithEvents supplies appendEventWithOutbox in the same unit of work. */
708
730
  readonly transactionalOutbox?: boolean;
731
+ /**
732
+ * First-write-wins: an event ID the store already holds, in any tenant or space, is a no-op
733
+ * that keeps the first event and adds no outbox row, even when the
734
+ * first append had none. Do not throw,
735
+ * replace or merge it, regardless of the later payload, sequence or timestamps.
736
+ * Host re-appends lifecycle events under stable IDs on retry/recovery with fresh
737
+ * sequences and timestamps and potentially different payloads.
738
+ */
709
739
  appendEventWithOutbox(event: AssetEventEnvelope, metadata: OutboxEventMetadata): Promise<void>;
710
740
  claimOutbox(input: ClaimOutboxInput): Promise<OutboxRecord[]>;
711
741
  markOutboxPublished(id: string, workerId: string, publishedAt: Date, leaseToken?: number): Promise<void>;
@@ -1766,4 +1796,80 @@ export declare function tenantIsolationChecks(): readonly TenantIsolationCheck[]
1766
1796
  /** Run every check, returning each result rather than throwing on the first. */
1767
1797
  export declare function runTenantIsolationChecks(subject: TenantIsolationSubject): Promise<TenantIsolationResult>;
1768
1798
  //#endregion
1799
+ //#region src/event-append-conformance.d.ts
1800
+ export interface EventAppendSubject<TDb = unknown> {
1801
+ store: Pick<PlatformHostStore<TDb>, "appendEvent"> & Pick<RecoverablePlatformHostStore<TDb>, "listEvents"> & Partial<Pick<OutboxPlatformHostStore<TDb>, "appendEventWithOutbox" | "listOutbox" | "claimOutbox" | "markOutboxPublished" | "markOutboxFailed">> & Partial<Pick<AtomicMutationPlatformHostStore<TDb>, "transactionWithEvents">>;
1802
+ tenantId: string;
1803
+ /** Optional: restore a known state before a check; probes tolerate accumulated state. */
1804
+ reset?(): Promise<void> | void;
1805
+ }
1806
+ export interface EventAppendCheck {
1807
+ id: "fabric.store.event-append-first-write-wins.v1" | "fabric.store.event-append-outbox-first-write-wins.v1" | "fabric.store.event-append-outbox-after-plain.v1" | "fabric.store.event-append-transaction-first-write-wins.v1" | "fabric.store.event-append-transaction-outbox-first-write-wins.v1" | "fabric.store.event-append-transaction-outbox-after-plain.v1";
1808
+ run<TDb>(subject: EventAppendSubject<TDb>): Promise<string[]>;
1809
+ }
1810
+ export interface EventAppendResult {
1811
+ passed: boolean;
1812
+ checks: Array<{
1813
+ id: EventAppendCheck["id"];
1814
+ status: "passed" | "failed" | "skipped";
1815
+ evidence: string[];
1816
+ error?: string;
1817
+ }>;
1818
+ }
1819
+ /**
1820
+ * Six checks detecting common first-write-wins violations through plain,
1821
+ * outbox and transaction appends, including outbox-after-plain and committed
1822
+ * duplicates: throwing, replacing, merging, keeping both events, selecting the
1823
+ * earlier-ordered or later-ordered duplicate, adding outbox rows, and resetting
1824
+ * or rewriting existing outbox rows in the probed states. They are not exhaustive.
1825
+ * Duplicates have both earlier and later ordering fields and vary the remaining
1826
+ * envelope and outbox metadata fields. Whole-record snapshots compare duplicates
1827
+ * while pending, leased by the probe, retry-pending, and settled as published.
1828
+ * Retry-pending accepts a cleared or expired lease, the probe owner retained
1829
+ * without an expiry, or an expiry within five seconds of the probe's clock,
1830
+ * which is waited out before the row is claimed again; attempt counts and
1831
+ * error formatting are not prescribed.
1832
+ * Initial comparisons accept store-specific fields and fields the store omits
1833
+ * or returns as null; transaction verification runs after commit without requiring
1834
+ * read-your-writes. Each committed duplicate transaction also appends a fresh plain
1835
+ * control event whose presence is verified after commit.
1836
+ * Successful outbox-creating checks leave the probe row
1837
+ * published although nothing was sent; its event-append-probe space identifies
1838
+ * it. Absence checks write a control row that cleanup dead-letters. These rows can
1839
+ * be requeued by operator tooling; exclude probe spaces from real publication.
1840
+ *
1841
+ * Intended for test or staging stores with outbox relays paused for the probe
1842
+ * tenant. Event and availability inputs use whole seconds a few seconds in the
1843
+ * past; claims and publication markers use real current time. Checks leave
1844
+ * append-only events in fresh event-append-probe spaces. Final cleanup
1845
+ * attempts to dead-letter visible probe rows still pending with
1846
+ * "event append probe; never published".
1847
+ * Claims use real current time, a pending-probe-row limit, and a 30-second lease.
1848
+ * A filter-ignoring claim can lease foreign rows and make whatever writes the
1849
+ * claim itself makes (such as incrementing attempts). Checks write nothing to
1850
+ * foreign rows and stop claiming for that run (or that standalone check).
1851
+ * After a filter violation, cleanup can dead-letter only probe rows returned by
1852
+ * that claim, using the probe worker and claim tokens without further claims.
1853
+ * Failures list visible probe rows still pending and instruct manual dead-lettering.
1854
+ * Later outbox checks in the run fail as skipped because of the earlier filter
1855
+ * violation. Leases lapse within 30 seconds
1856
+ * of the claim if the store honours the lease duration.
1857
+ *
1858
+ * Missing append capabilities skip; missing listOutbox, claimOutbox,
1859
+ * markOutboxPublished or markOutboxFailed fails verification/cleanup.
1860
+ * Known limits: rules comparing ordering fields in mixed directions, store-wide
1861
+ * uniqueness across spaces or tenants, the dead-letter state, claims that
1862
+ * lease foreign rows without returning them, buffered merges into fields the
1863
+ * store never returns, and duplicate handling that only holds within one
1864
+ * process. Stores requiring gapless sequences, or unique keys over fields the
1865
+ * control events share with the probe event, can fail falsely.
1866
+ */
1867
+ export declare function eventAppendChecks(): readonly EventAppendCheck[];
1868
+ /**
1869
+ * Run eventAppendChecks and collect results instead of stopping at the first
1870
+ * failure. Use test or staging stores with relays paused for the probe tenant;
1871
+ * see eventAppendChecks for probe publication markers and cleanup side effects.
1872
+ */
1873
+ export declare function runEventAppendChecks<TDb>(subject: EventAppendSubject<TDb>): Promise<EventAppendResult>;
1874
+ //#endregion
1769
1875
  export { canonicalJson };
package/dist/index.d.ts CHANGED
@@ -554,6 +554,13 @@ export interface PlatformHostStore<TDb> {
554
554
  * optional only for compatibility with existing stores and direct callers.
555
555
  */
556
556
  updateAdapterInvocation(id: string, patch: Partial<Pick<AdapterInvocationRecord, "status" | "output" | "error" | "attempt" | "updatedAt" | "reconciliation">>, scope?: AdapterInvocationScope): Promise<void>;
557
+ /**
558
+ * First-write-wins: an event ID the store already holds, in any tenant or space, is a no-op
559
+ * that keeps the first event. Do not throw, replace or merge it, regardless of
560
+ * the later payload, sequence or timestamps. Host re-appends lifecycle events
561
+ * under stable IDs on retry/recovery with fresh sequences and timestamps and
562
+ * potentially different payloads.
563
+ */
557
564
  appendEvent(event: AssetEventEnvelope): Promise<void>;
558
565
  nextEventSequence(tenantId: string, spaceId: string): Promise<number>;
559
566
  /**
@@ -588,7 +595,22 @@ export interface PlatformHostMutationTransaction<TDb> {
588
595
  * Required by serialized saga parent lifecycle transitions.
589
596
  */
590
597
  getEvent?(id: string, tenantId: string, spaceId: string): Promise<AssetEventEnvelope | undefined>;
598
+ /**
599
+ * First-write-wins: an event ID the store already holds, in any tenant or space, is a no-op
600
+ * that keeps the first event. Do not throw, replace or merge it, regardless of
601
+ * the later payload, sequence or timestamps. Host re-appends lifecycle events
602
+ * under stable IDs on retry/recovery with fresh sequences and timestamps and
603
+ * potentially different payloads.
604
+ */
591
605
  appendEvent(event: AssetEventEnvelope): Promise<void>;
606
+ /**
607
+ * First-write-wins: an event ID the store already holds, in any tenant or space, is a no-op
608
+ * that keeps the first event and adds no outbox row, even when the
609
+ * first append had none. Do not throw,
610
+ * replace or merge it, regardless of the later payload, sequence or timestamps.
611
+ * Host re-appends lifecycle events under stable IDs on retry/recovery with fresh
612
+ * sequences and timestamps and potentially different payloads.
613
+ */
592
614
  appendEventWithOutbox?(event: AssetEventEnvelope, metadata: OutboxEventMetadata): Promise<void>;
593
615
  nextEventSequence(tenantId: string, spaceId: string): Promise<number>;
594
616
  listEvents(tenantId: string, spaceId: string): Promise<AssetEventEnvelope[]>;
@@ -706,6 +728,14 @@ export interface ClaimOutboxInput {
706
728
  export interface OutboxPlatformHostStore<TDb> extends PlatformHostStore<TDb> {
707
729
  /** True only when transactionWithEvents supplies appendEventWithOutbox in the same unit of work. */
708
730
  readonly transactionalOutbox?: boolean;
731
+ /**
732
+ * First-write-wins: an event ID the store already holds, in any tenant or space, is a no-op
733
+ * that keeps the first event and adds no outbox row, even when the
734
+ * first append had none. Do not throw,
735
+ * replace or merge it, regardless of the later payload, sequence or timestamps.
736
+ * Host re-appends lifecycle events under stable IDs on retry/recovery with fresh
737
+ * sequences and timestamps and potentially different payloads.
738
+ */
709
739
  appendEventWithOutbox(event: AssetEventEnvelope, metadata: OutboxEventMetadata): Promise<void>;
710
740
  claimOutbox(input: ClaimOutboxInput): Promise<OutboxRecord[]>;
711
741
  markOutboxPublished(id: string, workerId: string, publishedAt: Date, leaseToken?: number): Promise<void>;
@@ -1766,4 +1796,80 @@ export declare function tenantIsolationChecks(): readonly TenantIsolationCheck[]
1766
1796
  /** Run every check, returning each result rather than throwing on the first. */
1767
1797
  export declare function runTenantIsolationChecks(subject: TenantIsolationSubject): Promise<TenantIsolationResult>;
1768
1798
  //#endregion
1799
+ //#region src/event-append-conformance.d.ts
1800
+ export interface EventAppendSubject<TDb = unknown> {
1801
+ store: Pick<PlatformHostStore<TDb>, "appendEvent"> & Pick<RecoverablePlatformHostStore<TDb>, "listEvents"> & Partial<Pick<OutboxPlatformHostStore<TDb>, "appendEventWithOutbox" | "listOutbox" | "claimOutbox" | "markOutboxPublished" | "markOutboxFailed">> & Partial<Pick<AtomicMutationPlatformHostStore<TDb>, "transactionWithEvents">>;
1802
+ tenantId: string;
1803
+ /** Optional: restore a known state before a check; probes tolerate accumulated state. */
1804
+ reset?(): Promise<void> | void;
1805
+ }
1806
+ export interface EventAppendCheck {
1807
+ id: "fabric.store.event-append-first-write-wins.v1" | "fabric.store.event-append-outbox-first-write-wins.v1" | "fabric.store.event-append-outbox-after-plain.v1" | "fabric.store.event-append-transaction-first-write-wins.v1" | "fabric.store.event-append-transaction-outbox-first-write-wins.v1" | "fabric.store.event-append-transaction-outbox-after-plain.v1";
1808
+ run<TDb>(subject: EventAppendSubject<TDb>): Promise<string[]>;
1809
+ }
1810
+ export interface EventAppendResult {
1811
+ passed: boolean;
1812
+ checks: Array<{
1813
+ id: EventAppendCheck["id"];
1814
+ status: "passed" | "failed" | "skipped";
1815
+ evidence: string[];
1816
+ error?: string;
1817
+ }>;
1818
+ }
1819
+ /**
1820
+ * Six checks detecting common first-write-wins violations through plain,
1821
+ * outbox and transaction appends, including outbox-after-plain and committed
1822
+ * duplicates: throwing, replacing, merging, keeping both events, selecting the
1823
+ * earlier-ordered or later-ordered duplicate, adding outbox rows, and resetting
1824
+ * or rewriting existing outbox rows in the probed states. They are not exhaustive.
1825
+ * Duplicates have both earlier and later ordering fields and vary the remaining
1826
+ * envelope and outbox metadata fields. Whole-record snapshots compare duplicates
1827
+ * while pending, leased by the probe, retry-pending, and settled as published.
1828
+ * Retry-pending accepts a cleared or expired lease, the probe owner retained
1829
+ * without an expiry, or an expiry within five seconds of the probe's clock,
1830
+ * which is waited out before the row is claimed again; attempt counts and
1831
+ * error formatting are not prescribed.
1832
+ * Initial comparisons accept store-specific fields and fields the store omits
1833
+ * or returns as null; transaction verification runs after commit without requiring
1834
+ * read-your-writes. Each committed duplicate transaction also appends a fresh plain
1835
+ * control event whose presence is verified after commit.
1836
+ * Successful outbox-creating checks leave the probe row
1837
+ * published although nothing was sent; its event-append-probe space identifies
1838
+ * it. Absence checks write a control row that cleanup dead-letters. These rows can
1839
+ * be requeued by operator tooling; exclude probe spaces from real publication.
1840
+ *
1841
+ * Intended for test or staging stores with outbox relays paused for the probe
1842
+ * tenant. Event and availability inputs use whole seconds a few seconds in the
1843
+ * past; claims and publication markers use real current time. Checks leave
1844
+ * append-only events in fresh event-append-probe spaces. Final cleanup
1845
+ * attempts to dead-letter visible probe rows still pending with
1846
+ * "event append probe; never published".
1847
+ * Claims use real current time, a pending-probe-row limit, and a 30-second lease.
1848
+ * A filter-ignoring claim can lease foreign rows and make whatever writes the
1849
+ * claim itself makes (such as incrementing attempts). Checks write nothing to
1850
+ * foreign rows and stop claiming for that run (or that standalone check).
1851
+ * After a filter violation, cleanup can dead-letter only probe rows returned by
1852
+ * that claim, using the probe worker and claim tokens without further claims.
1853
+ * Failures list visible probe rows still pending and instruct manual dead-lettering.
1854
+ * Later outbox checks in the run fail as skipped because of the earlier filter
1855
+ * violation. Leases lapse within 30 seconds
1856
+ * of the claim if the store honours the lease duration.
1857
+ *
1858
+ * Missing append capabilities skip; missing listOutbox, claimOutbox,
1859
+ * markOutboxPublished or markOutboxFailed fails verification/cleanup.
1860
+ * Known limits: rules comparing ordering fields in mixed directions, store-wide
1861
+ * uniqueness across spaces or tenants, the dead-letter state, claims that
1862
+ * lease foreign rows without returning them, buffered merges into fields the
1863
+ * store never returns, and duplicate handling that only holds within one
1864
+ * process. Stores requiring gapless sequences, or unique keys over fields the
1865
+ * control events share with the probe event, can fail falsely.
1866
+ */
1867
+ export declare function eventAppendChecks(): readonly EventAppendCheck[];
1868
+ /**
1869
+ * Run eventAppendChecks and collect results instead of stopping at the first
1870
+ * failure. Use test or staging stores with relays paused for the probe tenant;
1871
+ * see eventAppendChecks for probe publication markers and cleanup side effects.
1872
+ */
1873
+ export declare function runEventAppendChecks<TDb>(subject: EventAppendSubject<TDb>): Promise<EventAppendResult>;
1874
+ //#endregion
1769
1875
  export { canonicalJson };