@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.js
CHANGED
|
@@ -140,7 +140,8 @@ var InMemoryCache = class {
|
|
|
140
140
|
var Errors = {
|
|
141
141
|
ValidationError: "ERR_VALIDATION",
|
|
142
142
|
InvariantError: "ERR_INVARIANT",
|
|
143
|
-
ConcurrencyError: "ERR_CONCURRENCY"
|
|
143
|
+
ConcurrencyError: "ERR_CONCURRENCY",
|
|
144
|
+
StreamClosedError: "ERR_STREAM_CLOSED"
|
|
144
145
|
};
|
|
145
146
|
var ValidationError = class extends Error {
|
|
146
147
|
constructor(target, payload, details) {
|
|
@@ -162,6 +163,13 @@ var InvariantError = class extends Error {
|
|
|
162
163
|
this.name = Errors.InvariantError;
|
|
163
164
|
}
|
|
164
165
|
};
|
|
166
|
+
var StreamClosedError = class extends Error {
|
|
167
|
+
constructor(stream) {
|
|
168
|
+
super(`Stream "${stream}" is closed (tombstoned)`);
|
|
169
|
+
this.stream = stream;
|
|
170
|
+
this.name = Errors.StreamClosedError;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
165
173
|
var ConcurrencyError = class extends Error {
|
|
166
174
|
constructor(stream, lastVersion, events, expectedVersion) {
|
|
167
175
|
super(
|
|
@@ -595,6 +603,41 @@ var InMemoryStore = class {
|
|
|
595
603
|
}
|
|
596
604
|
return count;
|
|
597
605
|
}
|
|
606
|
+
/**
|
|
607
|
+
* Atomically truncates streams and seeds each with a snapshot or tombstone.
|
|
608
|
+
* @param targets - Streams to truncate with optional snapshot state and meta.
|
|
609
|
+
* @returns Map keyed by stream name, each entry with `deleted` count and `committed` event.
|
|
610
|
+
*/
|
|
611
|
+
async truncate(targets) {
|
|
612
|
+
await sleep();
|
|
613
|
+
const deletedCounts = /* @__PURE__ */ new Map();
|
|
614
|
+
const streamSet = new Set(targets.map((t) => t.stream));
|
|
615
|
+
for (const e of this._events) {
|
|
616
|
+
if (streamSet.has(e.stream)) {
|
|
617
|
+
deletedCounts.set(e.stream, (deletedCounts.get(e.stream) ?? 0) + 1);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
this._events = this._events.filter((e) => !streamSet.has(e.stream));
|
|
621
|
+
const result = /* @__PURE__ */ new Map();
|
|
622
|
+
for (const { stream, snapshot, meta } of targets) {
|
|
623
|
+
this._streams.delete(stream);
|
|
624
|
+
const event = {
|
|
625
|
+
id: this._events.length,
|
|
626
|
+
stream,
|
|
627
|
+
version: 0,
|
|
628
|
+
created: /* @__PURE__ */ new Date(),
|
|
629
|
+
name: snapshot !== void 0 ? SNAP_EVENT : TOMBSTONE_EVENT,
|
|
630
|
+
data: snapshot ?? {},
|
|
631
|
+
meta: meta ?? { correlation: "", causation: {} }
|
|
632
|
+
};
|
|
633
|
+
this._events.push(event);
|
|
634
|
+
result.set(stream, {
|
|
635
|
+
deleted: deletedCounts.get(stream) ?? 0,
|
|
636
|
+
committed: event
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
return result;
|
|
640
|
+
}
|
|
598
641
|
};
|
|
599
642
|
|
|
600
643
|
// src/ports.ts
|
|
@@ -641,6 +684,7 @@ function dispose(disposer) {
|
|
|
641
684
|
return disposeAndExit;
|
|
642
685
|
}
|
|
643
686
|
var SNAP_EVENT = "__snapshot__";
|
|
687
|
+
var TOMBSTONE_EVENT = "__tombstone__";
|
|
644
688
|
function build_tracer(logLevel2) {
|
|
645
689
|
if (logLevel2 === "trace") {
|
|
646
690
|
const logger4 = log();
|
|
@@ -781,6 +825,8 @@ async function action(me, action2, target, payload, reactingTo, skipValidation =
|
|
|
781
825
|
if (!stream) throw new Error("Missing target stream");
|
|
782
826
|
payload = skipValidation ? payload : validate(action2, payload, me.actions[action2]);
|
|
783
827
|
const snapshot = await load(me, stream);
|
|
828
|
+
if (snapshot.event?.name === TOMBSTONE_EVENT)
|
|
829
|
+
throw new StreamClosedError(stream);
|
|
784
830
|
const expected = expectedVersion ?? snapshot.event?.version;
|
|
785
831
|
logger2.trace(
|
|
786
832
|
payload,
|
|
@@ -1559,6 +1605,170 @@ var Act = class {
|
|
|
1559
1605
|
this._settle_timer = void 0;
|
|
1560
1606
|
}
|
|
1561
1607
|
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Close the books — guard, archive, truncate, and optionally restart streams.
|
|
1610
|
+
*
|
|
1611
|
+
* Safely removes historical events from the operational store:
|
|
1612
|
+
*
|
|
1613
|
+
* 1. **Correlate** — discover pending reaction targets
|
|
1614
|
+
* 2. **Safety check** — skip streams with pending reactions (skipped when no reactive events)
|
|
1615
|
+
* 3. **Guard** — commit `__tombstone__` with `expectedVersion` to block concurrent writes
|
|
1616
|
+
* 4. **Load state** — for streams in `snapshots`, load final state while guarded (no races)
|
|
1617
|
+
* 5. **Archive** — user callback per stream (abort-all on failure, streams are guarded)
|
|
1618
|
+
* 6. **Truncate + seed** — atomic: delete all events, insert `__snapshot__` or `__tombstone__`
|
|
1619
|
+
* 7. **Cache** — invalidate (tombstoned) or warm (restarted)
|
|
1620
|
+
* 8. **Emit "closed"** — lifecycle event with results
|
|
1621
|
+
*
|
|
1622
|
+
* @param targets - Per-stream close options (stream, restart?, archive?)
|
|
1623
|
+
* @returns `{ truncated: TruncateResult, skipped: string[] }`
|
|
1624
|
+
*
|
|
1625
|
+
* @example Archive and close
|
|
1626
|
+
* ```typescript
|
|
1627
|
+
* await app.close([
|
|
1628
|
+
* { stream: "order-123", archive: async () => { await archiveToS3("order-123"); } },
|
|
1629
|
+
* { stream: "order-456" },
|
|
1630
|
+
* ]);
|
|
1631
|
+
* ```
|
|
1632
|
+
*
|
|
1633
|
+
* @example Close with restart (state loaded automatically after guard)
|
|
1634
|
+
* ```typescript
|
|
1635
|
+
* await app.close([
|
|
1636
|
+
* { stream: "counter-1", restart: true },
|
|
1637
|
+
* { stream: "counter-2" }, // tombstoned
|
|
1638
|
+
* ]);
|
|
1639
|
+
* ```
|
|
1640
|
+
*/
|
|
1641
|
+
async close(targets) {
|
|
1642
|
+
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
1643
|
+
const targetMap = new Map(targets.map((t) => [t.stream, t]));
|
|
1644
|
+
const streams = [...targetMap.keys()];
|
|
1645
|
+
await this.correlate({ limit: 1e3 });
|
|
1646
|
+
const streamInfo = /* @__PURE__ */ new Map();
|
|
1647
|
+
await Promise.all(
|
|
1648
|
+
streams.map(async (s) => {
|
|
1649
|
+
let maxId = -1;
|
|
1650
|
+
let version = -1;
|
|
1651
|
+
await store().query(
|
|
1652
|
+
(e) => {
|
|
1653
|
+
if (e.name !== TOMBSTONE_EVENT) {
|
|
1654
|
+
maxId = e.id;
|
|
1655
|
+
version = e.version;
|
|
1656
|
+
}
|
|
1657
|
+
},
|
|
1658
|
+
{ stream: s, stream_exact: true, backward: true, limit: 1 }
|
|
1659
|
+
);
|
|
1660
|
+
if (maxId >= 0) streamInfo.set(s, { maxId, version });
|
|
1661
|
+
})
|
|
1662
|
+
);
|
|
1663
|
+
const skipped = [];
|
|
1664
|
+
let safe;
|
|
1665
|
+
if (this._reactive_events.size === 0) {
|
|
1666
|
+
safe = [...streamInfo.keys()];
|
|
1667
|
+
} else {
|
|
1668
|
+
const pendingSet = /* @__PURE__ */ new Set();
|
|
1669
|
+
const leases = await store().claim(1e3, 1e3, randomUUID2(), 1);
|
|
1670
|
+
if (leases.length) await store().ack(leases);
|
|
1671
|
+
for (const lease of leases) {
|
|
1672
|
+
const sourceRe = lease.source ? RegExp(lease.source) : void 0;
|
|
1673
|
+
for (const [stream, info] of streamInfo) {
|
|
1674
|
+
if ((!sourceRe || sourceRe.test(stream)) && lease.at < info.maxId) {
|
|
1675
|
+
pendingSet.add(stream);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
safe = [];
|
|
1680
|
+
for (const [stream] of streamInfo) {
|
|
1681
|
+
if (pendingSet.has(stream)) {
|
|
1682
|
+
skipped.push(stream);
|
|
1683
|
+
} else {
|
|
1684
|
+
safe.push(stream);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
if (!safe.length) {
|
|
1689
|
+
const result2 = { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
1690
|
+
this.emit("closed", result2);
|
|
1691
|
+
return result2;
|
|
1692
|
+
}
|
|
1693
|
+
const correlation = randomUUID2();
|
|
1694
|
+
const guarded = [];
|
|
1695
|
+
const guardEvents = /* @__PURE__ */ new Map();
|
|
1696
|
+
await Promise.all(
|
|
1697
|
+
safe.map(async (stream) => {
|
|
1698
|
+
try {
|
|
1699
|
+
const info = streamInfo.get(stream);
|
|
1700
|
+
const [committed] = await store().commit(
|
|
1701
|
+
stream,
|
|
1702
|
+
[{ name: TOMBSTONE_EVENT, data: {} }],
|
|
1703
|
+
{ correlation, causation: {} },
|
|
1704
|
+
info.version
|
|
1705
|
+
);
|
|
1706
|
+
guarded.push(stream);
|
|
1707
|
+
guardEvents.set(stream, { id: committed.id, stream });
|
|
1708
|
+
} catch {
|
|
1709
|
+
skipped.push(stream);
|
|
1710
|
+
}
|
|
1711
|
+
})
|
|
1712
|
+
);
|
|
1713
|
+
if (!guarded.length) {
|
|
1714
|
+
const result2 = { truncated: /* @__PURE__ */ new Map(), skipped };
|
|
1715
|
+
this.emit("closed", result2);
|
|
1716
|
+
return result2;
|
|
1717
|
+
}
|
|
1718
|
+
const mergedState = [...this._states.values()][0];
|
|
1719
|
+
const seedStates = /* @__PURE__ */ new Map();
|
|
1720
|
+
if (mergedState) {
|
|
1721
|
+
await Promise.all(
|
|
1722
|
+
guarded.filter((s) => targetMap.get(s)?.restart).map(async (stream) => {
|
|
1723
|
+
const snap2 = await load(mergedState, stream);
|
|
1724
|
+
seedStates.set(stream, snap2.state);
|
|
1725
|
+
})
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1728
|
+
for (const stream of guarded) {
|
|
1729
|
+
const archiveFn = targetMap.get(stream)?.archive;
|
|
1730
|
+
if (archiveFn) await archiveFn();
|
|
1731
|
+
}
|
|
1732
|
+
const truncTargets = guarded.map((stream) => {
|
|
1733
|
+
const snapshot = seedStates.get(stream);
|
|
1734
|
+
const guard = guardEvents.get(stream);
|
|
1735
|
+
return {
|
|
1736
|
+
stream,
|
|
1737
|
+
snapshot,
|
|
1738
|
+
meta: {
|
|
1739
|
+
correlation,
|
|
1740
|
+
causation: {
|
|
1741
|
+
event: {
|
|
1742
|
+
id: guard.id,
|
|
1743
|
+
name: TOMBSTONE_EVENT,
|
|
1744
|
+
stream: guard.stream
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
});
|
|
1750
|
+
const truncated = await store().truncate(truncTargets);
|
|
1751
|
+
await Promise.all(
|
|
1752
|
+
guarded.map(async (stream) => {
|
|
1753
|
+
const entry = truncated.get(stream);
|
|
1754
|
+
const state2 = seedStates.get(stream);
|
|
1755
|
+
if (state2 && entry) {
|
|
1756
|
+
await cache().set(stream, {
|
|
1757
|
+
state: state2,
|
|
1758
|
+
version: entry.committed.version,
|
|
1759
|
+
event_id: entry.committed.id,
|
|
1760
|
+
patches: 0,
|
|
1761
|
+
snaps: 1
|
|
1762
|
+
});
|
|
1763
|
+
} else {
|
|
1764
|
+
await cache().invalidate(stream);
|
|
1765
|
+
}
|
|
1766
|
+
})
|
|
1767
|
+
);
|
|
1768
|
+
const result = { truncated, skipped };
|
|
1769
|
+
this.emit("closed", result);
|
|
1770
|
+
return result;
|
|
1771
|
+
}
|
|
1562
1772
|
/**
|
|
1563
1773
|
* Debounced, non-blocking correlate→drain cycle.
|
|
1564
1774
|
*
|
|
@@ -1745,7 +1955,6 @@ var _this_ = ({ stream }) => ({
|
|
|
1745
1955
|
source: stream,
|
|
1746
1956
|
target: stream
|
|
1747
1957
|
});
|
|
1748
|
-
var _void_ = () => void 0;
|
|
1749
1958
|
|
|
1750
1959
|
// src/act-builder.ts
|
|
1751
1960
|
function act(states = /* @__PURE__ */ new Map(), registry = {
|
|
@@ -1823,13 +2032,6 @@ function act(states = /* @__PURE__ */ new Map(), registry = {
|
|
|
1823
2032
|
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
1824
2033
|
});
|
|
1825
2034
|
return builder;
|
|
1826
|
-
},
|
|
1827
|
-
void() {
|
|
1828
|
-
registry.events[event].reactions.set(handler.name, {
|
|
1829
|
-
...reaction,
|
|
1830
|
-
resolver: _void_
|
|
1831
|
-
});
|
|
1832
|
-
return builder;
|
|
1833
2035
|
}
|
|
1834
2036
|
};
|
|
1835
2037
|
}
|
|
@@ -1892,13 +2094,6 @@ function _projection(target, events) {
|
|
|
1892
2094
|
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
1893
2095
|
});
|
|
1894
2096
|
return nextBuilder;
|
|
1895
|
-
},
|
|
1896
|
-
void() {
|
|
1897
|
-
register.reactions.set(handler.name, {
|
|
1898
|
-
...reaction,
|
|
1899
|
-
resolver: _void_
|
|
1900
|
-
});
|
|
1901
|
-
return nextBuilder;
|
|
1902
2097
|
}
|
|
1903
2098
|
};
|
|
1904
2099
|
}
|
|
@@ -1974,13 +2169,6 @@ function slice(states = /* @__PURE__ */ new Map(), actions = {}, events = {}, pr
|
|
|
1974
2169
|
resolver: typeof resolver === "string" ? { target: resolver } : resolver
|
|
1975
2170
|
});
|
|
1976
2171
|
return builder;
|
|
1977
|
-
},
|
|
1978
|
-
void() {
|
|
1979
|
-
events[event].reactions.set(handler.name, {
|
|
1980
|
-
...reaction,
|
|
1981
|
-
resolver: _void_
|
|
1982
|
-
});
|
|
1983
|
-
return builder;
|
|
1984
2172
|
}
|
|
1985
2173
|
};
|
|
1986
2174
|
}
|
|
@@ -2104,6 +2292,8 @@ export {
|
|
|
2104
2292
|
PackageSchema,
|
|
2105
2293
|
QuerySchema,
|
|
2106
2294
|
SNAP_EVENT,
|
|
2295
|
+
StreamClosedError,
|
|
2296
|
+
TOMBSTONE_EVENT,
|
|
2107
2297
|
TargetSchema,
|
|
2108
2298
|
ValidationError,
|
|
2109
2299
|
ZodEmpty,
|