@rotorsoft/act 0.28.0 → 0.29.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/README.md +1 -1
- package/dist/.tsbuildinfo +1 -1
- package/dist/@types/act-builder.d.ts +3 -3
- package/dist/@types/act-builder.d.ts.map +1 -1
- package/dist/@types/act.d.ts +38 -1
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/InMemoryStore.d.ts +14 -1
- package/dist/@types/adapters/InMemoryStore.d.ts.map +1 -1
- package/dist/@types/event-sourcing.d.ts.map +1 -1
- package/dist/@types/merge.d.ts +0 -1
- package/dist/@types/merge.d.ts.map +1 -1
- package/dist/@types/ports.d.ts +8 -0
- package/dist/@types/ports.d.ts.map +1 -1
- package/dist/@types/projection-builder.d.ts +1 -4
- package/dist/@types/projection-builder.d.ts.map +1 -1
- package/dist/@types/slice-builder.d.ts +1 -2
- package/dist/@types/slice-builder.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +31 -0
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/errors.d.ts +31 -0
- package/dist/@types/types/errors.d.ts.map +1 -1
- package/dist/@types/types/ports.d.ts +28 -0
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/index.cjs +215 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +213 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -47,6 +47,8 @@ __export(index_exports, {
|
|
|
47
47
|
PackageSchema: () => PackageSchema,
|
|
48
48
|
QuerySchema: () => QuerySchema,
|
|
49
49
|
SNAP_EVENT: () => SNAP_EVENT,
|
|
50
|
+
StreamClosedError: () => StreamClosedError,
|
|
51
|
+
TOMBSTONE_EVENT: () => TOMBSTONE_EVENT,
|
|
50
52
|
TargetSchema: () => TargetSchema,
|
|
51
53
|
ValidationError: () => ValidationError,
|
|
52
54
|
ZodEmpty: () => ZodEmpty,
|
|
@@ -210,7 +212,8 @@ var InMemoryCache = class {
|
|
|
210
212
|
var Errors = {
|
|
211
213
|
ValidationError: "ERR_VALIDATION",
|
|
212
214
|
InvariantError: "ERR_INVARIANT",
|
|
213
|
-
ConcurrencyError: "ERR_CONCURRENCY"
|
|
215
|
+
ConcurrencyError: "ERR_CONCURRENCY",
|
|
216
|
+
StreamClosedError: "ERR_STREAM_CLOSED"
|
|
214
217
|
};
|
|
215
218
|
var ValidationError = class extends Error {
|
|
216
219
|
constructor(target, payload, details) {
|
|
@@ -232,6 +235,13 @@ var InvariantError = class extends Error {
|
|
|
232
235
|
this.name = Errors.InvariantError;
|
|
233
236
|
}
|
|
234
237
|
};
|
|
238
|
+
var StreamClosedError = class extends Error {
|
|
239
|
+
constructor(stream) {
|
|
240
|
+
super(`Stream "${stream}" is closed (tombstoned)`);
|
|
241
|
+
this.stream = stream;
|
|
242
|
+
this.name = Errors.StreamClosedError;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
235
245
|
var ConcurrencyError = class extends Error {
|
|
236
246
|
constructor(stream, lastVersion, events, expectedVersion) {
|
|
237
247
|
super(
|
|
@@ -665,6 +675,41 @@ var InMemoryStore = class {
|
|
|
665
675
|
}
|
|
666
676
|
return count;
|
|
667
677
|
}
|
|
678
|
+
/**
|
|
679
|
+
* Atomically truncates streams and seeds each with a snapshot or tombstone.
|
|
680
|
+
* @param targets - Streams to truncate with optional snapshot state and meta.
|
|
681
|
+
* @returns Map keyed by stream name, each entry with `deleted` count and `committed` event.
|
|
682
|
+
*/
|
|
683
|
+
async truncate(targets) {
|
|
684
|
+
await sleep();
|
|
685
|
+
const deletedCounts = /* @__PURE__ */ new Map();
|
|
686
|
+
const streamSet = new Set(targets.map((t) => t.stream));
|
|
687
|
+
for (const e of this._events) {
|
|
688
|
+
if (streamSet.has(e.stream)) {
|
|
689
|
+
deletedCounts.set(e.stream, (deletedCounts.get(e.stream) ?? 0) + 1);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
this._events = this._events.filter((e) => !streamSet.has(e.stream));
|
|
693
|
+
const result = /* @__PURE__ */ new Map();
|
|
694
|
+
for (const { stream, snapshot, meta } of targets) {
|
|
695
|
+
this._streams.delete(stream);
|
|
696
|
+
const event = {
|
|
697
|
+
id: this._events.length,
|
|
698
|
+
stream,
|
|
699
|
+
version: 0,
|
|
700
|
+
created: /* @__PURE__ */ new Date(),
|
|
701
|
+
name: snapshot !== void 0 ? SNAP_EVENT : TOMBSTONE_EVENT,
|
|
702
|
+
data: snapshot ?? {},
|
|
703
|
+
meta: meta ?? { correlation: "", causation: {} }
|
|
704
|
+
};
|
|
705
|
+
this._events.push(event);
|
|
706
|
+
result.set(stream, {
|
|
707
|
+
deleted: deletedCounts.get(stream) ?? 0,
|
|
708
|
+
committed: event
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
return result;
|
|
712
|
+
}
|
|
668
713
|
};
|
|
669
714
|
|
|
670
715
|
// src/ports.ts
|
|
@@ -711,6 +756,7 @@ function dispose(disposer) {
|
|
|
711
756
|
return disposeAndExit;
|
|
712
757
|
}
|
|
713
758
|
var SNAP_EVENT = "__snapshot__";
|
|
759
|
+
var TOMBSTONE_EVENT = "__tombstone__";
|
|
714
760
|
function build_tracer(logLevel2) {
|
|
715
761
|
if (logLevel2 === "trace") {
|
|
716
762
|
const logger4 = log();
|
|
@@ -851,6 +897,8 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
851
897
|
if (!stream) throw new Error("Missing target stream");
|
|
852
898
|
payload = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
|
|
853
899
|
const snapshot = await load(me, stream);
|
|
900
|
+
if (snapshot.event?.name === TOMBSTONE_EVENT)
|
|
901
|
+
throw new StreamClosedError(stream);
|
|
854
902
|
const expected = expectedVersion ?? snapshot.event?.version;
|
|
855
903
|
logger2.trace(
|
|
856
904
|
payload,
|
|
@@ -1629,6 +1677,170 @@ var Act = class {
|
|
|
1629
1677
|
this._settle_timer = void 0;
|
|
1630
1678
|
}
|
|
1631
1679
|
}
|
|
1680
|
+
/**
|
|
1681
|
+
* Close the books — guard, archive, truncate, and optionally restart streams.
|
|
1682
|
+
*
|
|
1683
|
+
* Safely removes historical events from the operational store:
|
|
1684
|
+
*
|
|
1685
|
+
* 1. **Correlate** — discover pending reaction targets
|
|
1686
|
+
* 2. **Safety check** — skip streams with pending reactions (skipped when no reactive events)
|
|
1687
|
+
* 3. **Guard** — commit `__tombstone__` with `expectedVersion` to block concurrent writes
|
|
1688
|
+
* 4. **Load state** — for streams in `snapshots`, load final state while guarded (no races)
|
|
1689
|
+
* 5. **Archive** — user callback per stream (abort-all on failure, streams are guarded)
|
|
1690
|
+
* 6. **Truncate + seed** — atomic: delete all events, insert `__snapshot__` or `__tombstone__`
|
|
1691
|
+
* 7. **Cache** — invalidate (tombstoned) or warm (restarted)
|
|
1692
|
+
* 8. **Emit "closed"** — lifecycle event with results
|
|
1693
|
+
*
|
|
1694
|
+
* @param targets - Per-stream close options (stream, restart?, archive?)
|
|
1695
|
+
* @returns `{ truncated: TruncateResult, skipped: string[] }`
|
|
1696
|
+
*
|
|
1697
|
+
* @example Archive and close
|
|
1698
|
+
* ```typescript
|
|
1699
|
+
* await app.close([
|
|
1700
|
+
* { stream: "order-123", archive: async () => { await archiveToS3("order-123"); } },
|
|
1701
|
+
* { stream: "order-456" },
|
|
1702
|
+
* ]);
|
|
1703
|
+
* ```
|
|
1704
|
+
*
|
|
1705
|
+
* @example Close with restart (state loaded automatically after guard)
|
|
1706
|
+
* ```typescript
|
|
1707
|
+
* await app.close([
|
|
1708
|
+
* { stream: "counter-1", restart: true },
|
|
1709
|
+
* { stream: "counter-2" }, // tombstoned
|
|
1710
|
+
* ]);
|
|
1711
|
+
* ```
|
|
1712
|
+
*/
|
|
1713
|
+
async close(targets) {
|
|
1714
|
+
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
1715
|
+
const targetMap = new Map(targets.map((t) => [t.stream, t]));
|
|
1716
|
+
const streams = [...targetMap.keys()];
|
|
1717
|
+
await this.correlate({ limit: 1e3 });
|
|
1718
|
+
const streamInfo = /* @__PURE__ */ new Map();
|
|
1719
|
+
await Promise.all(
|
|
1720
|
+
streams.map(async (s) => {
|
|
1721
|
+
let maxId = -1;
|
|
1722
|
+
let version = -1;
|
|
1723
|
+
await store().query(
|
|
1724
|
+
(e) => {
|
|
1725
|
+
if (e.name !== TOMBSTONE_EVENT) {
|
|
1726
|
+
maxId = e.id;
|
|
1727
|
+
version = e.version;
|
|
1728
|
+
}
|
|
1729
|
+
},
|
|
1730
|
+
{ stream: s, stream_exact: true, backward: true, limit: 1 }
|
|
1731
|
+
);
|
|
1732
|
+
if (maxId >= 0) streamInfo.set(s, { maxId, version });
|
|
1733
|
+
})
|
|
1734
|
+
);
|
|
1735
|
+
const skipped = [];
|
|
1736
|
+
let safe;
|
|
1737
|
+
if (this._reactive_events.size === 0) {
|
|
1738
|
+
safe = [...streamInfo.keys()];
|
|
1739
|
+
} else {
|
|
1740
|
+
const pendingSet = /* @__PURE__ */ new Set();
|
|
1741
|
+
const leases = await store().claim(1e3, 1e3, (0, import_crypto2.randomUUID)(), 1);
|
|
1742
|
+
if (leases.length) await store().ack(leases);
|
|
1743
|
+
for (const lease of leases) {
|
|
1744
|
+
const sourceRe = lease.source ? RegExp(lease.source) : void 0;
|
|
1745
|
+
for (const [stream, info] of streamInfo) {
|
|
1746
|
+
if ((!sourceRe || sourceRe.test(stream)) && lease.at < info.maxId) {
|
|
1747
|
+
pendingSet.add(stream);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
safe = [];
|
|
1752
|
+
for (const [stream] of streamInfo) {
|
|
1753
|
+
if (pendingSet.has(stream)) {
|
|
1754
|
+
skipped.push(stream);
|
|
1755
|
+
} else {
|
|
1756
|
+
safe.push(stream);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
if (!safe.length) {
|
|
1761
|
+
const result2 = { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
1762
|
+
this.emit("closed", result2);
|
|
1763
|
+
return result2;
|
|
1764
|
+
}
|
|
1765
|
+
const correlation = (0, import_crypto2.randomUUID)();
|
|
1766
|
+
const guarded = [];
|
|
1767
|
+
const guardEvents = /* @__PURE__ */ new Map();
|
|
1768
|
+
await Promise.all(
|
|
1769
|
+
safe.map(async (stream) => {
|
|
1770
|
+
try {
|
|
1771
|
+
const info = streamInfo.get(stream);
|
|
1772
|
+
const [committed] = await store().commit(
|
|
1773
|
+
stream,
|
|
1774
|
+
[{ name: TOMBSTONE_EVENT, data: {} }],
|
|
1775
|
+
{ correlation, causation: {} },
|
|
1776
|
+
info.version
|
|
1777
|
+
);
|
|
1778
|
+
guarded.push(stream);
|
|
1779
|
+
guardEvents.set(stream, { id: committed.id, stream });
|
|
1780
|
+
} catch {
|
|
1781
|
+
skipped.push(stream);
|
|
1782
|
+
}
|
|
1783
|
+
})
|
|
1784
|
+
);
|
|
1785
|
+
if (!guarded.length) {
|
|
1786
|
+
const result2 = { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
1787
|
+
this.emit("closed", result2);
|
|
1788
|
+
return result2;
|
|
1789
|
+
}
|
|
1790
|
+
const mergedState = [...this._states.values()][0];
|
|
1791
|
+
const seedStates = /* @__PURE__ */ new Map();
|
|
1792
|
+
if (mergedState) {
|
|
1793
|
+
await Promise.all(
|
|
1794
|
+
guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
|
|
1795
|
+
const snap2 = await load(mergedState, stream);
|
|
1796
|
+
seedStates.set(stream, snap2.state);
|
|
1797
|
+
})
|
|
1798
|
+
);
|
|
1799
|
+
}
|
|
1800
|
+
for (const stream of guarded) {
|
|
1801
|
+
const archiveFn = targetMap.get(stream)?.archive;
|
|
1802
|
+
if (archiveFn) await archiveFn();
|
|
1803
|
+
}
|
|
1804
|
+
const truncTargets = guarded.map((stream) => {
|
|
1805
|
+
const snapshot = seedStates.get(stream);
|
|
1806
|
+
const guard = guardEvents.get(stream);
|
|
1807
|
+
return {
|
|
1808
|
+
stream,
|
|
1809
|
+
snapshot,
|
|
1810
|
+
meta: {
|
|
1811
|
+
correlation,
|
|
1812
|
+
causation: {
|
|
1813
|
+
event: {
|
|
1814
|
+
id: guard.id,
|
|
1815
|
+
name: TOMBSTONE_EVENT,
|
|
1816
|
+
stream: guard.stream
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
};
|
|
1821
|
+
});
|
|
1822
|
+
const truncated = await store().truncate(truncTargets);
|
|
1823
|
+
await Promise.all(
|
|
1824
|
+
guarded.map(async (stream) => {
|
|
1825
|
+
const entry = truncated.get(stream);
|
|
1826
|
+
const state2 = seedStates.get(stream);
|
|
1827
|
+
if (state2 && entry) {
|
|
1828
|
+
await cache().set(stream, {
|
|
1829
|
+
state: state2,
|
|
1830
|
+
version: entry.committed.version,
|
|
1831
|
+
event_id: entry.committed.id,
|
|
1832
|
+
patches: 0,
|
|
1833
|
+
snaps: 1
|
|
1834
|
+
});
|
|
1835
|
+
} else {
|
|
1836
|
+
await cache().invalidate(stream);
|
|
1837
|
+
}
|
|
1838
|
+
})
|
|
1839
|
+
);
|
|
1840
|
+
const result = { truncated, skipped };
|
|
1841
|
+
this.emit("closed", result);
|
|
1842
|
+
return result;
|
|
1843
|
+
}
|
|
1632
1844
|
/**
|
|
1633
1845
|
* Debounced, non-blocking correlate→drain cycle.
|
|
1634
1846
|
*
|
|
@@ -1815,7 +2027,6 @@ var _this_ = ({ stream }) => ({
|
|
|
1815
2027
|
source: stream,
|
|
1816
2028
|
target: stream
|
|
1817
2029
|
});
|
|
1818
|
-
var _void_ = () => void 0;
|
|
1819
2030
|
|
|
1820
2031
|
// src/act-builder.ts
|
|
1821
2032
|
function act(states = /* @__PURE__ */ new Map(), registry = {
|
|
@@ -1893,13 +2104,6 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
|
|
|
1893
2104
|
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
1894
2105
|
});
|
|
1895
2106
|
return builder;
|
|
1896
|
-
},
|
|
1897
|
-
void() {
|
|
1898
|
-
registry.events[event].reactions.set(handler.name, {
|
|
1899
|
-
...reaction,
|
|
1900
|
-
resolver: _void_
|
|
1901
|
-
});
|
|
1902
|
-
return builder;
|
|
1903
2107
|
}
|
|
1904
2108
|
};
|
|
1905
2109
|
}
|
|
@@ -1962,13 +2166,6 @@ function _projection(target, events) {
|
|
|
1962
2166
|
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
1963
2167
|
});
|
|
1964
2168
|
return nextBuilder;
|
|
1965
|
-
},
|
|
1966
|
-
void() {
|
|
1967
|
-
register.reactions.set(handler.name, {
|
|
1968
|
-
...reaction,
|
|
1969
|
-
resolver: _void_
|
|
1970
|
-
});
|
|
1971
|
-
return nextBuilder;
|
|
1972
2169
|
}
|
|
1973
2170
|
};
|
|
1974
2171
|
}
|
|
@@ -2044,13 +2241,6 @@ function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, pr
|
|
|
2044
2241
|
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
2045
2242
|
});
|
|
2046
2243
|
return builder;
|
|
2047
|
-
},
|
|
2048
|
-
void() {
|
|
2049
|
-
events[event].reactions.set(handler.name, {
|
|
2050
|
-
...reaction,
|
|
2051
|
-
resolver: _void_
|
|
2052
|
-
});
|
|
2053
|
-
return builder;
|
|
2054
2244
|
}
|
|
2055
2245
|
};
|
|
2056
2246
|
}
|
|
@@ -2175,6 +2365,8 @@ function action_builder(state2) {
|
|
|
2175
2365
|
PackageSchema,
|
|
2176
2366
|
QuerySchema,
|
|
2177
2367
|
SNAP_EVENT,
|
|
2368
|
+
StreamClosedError,
|
|
2369
|
+
TOMBSTONE_EVENT,
|
|
2178
2370
|
TargetSchema,
|
|
2179
2371
|
ValidationError,
|
|
2180
2372
|
ZodEmpty,
|