@rotorsoft/act 1.20.0 → 1.21.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/dist/.tsbuildinfo +1 -1
- package/dist/@types/act.d.ts +26 -1
- package/dist/@types/act.d.ts.map +1 -1
- package/dist/@types/adapters/in-memory-store.d.ts +13 -4
- package/dist/@types/adapters/in-memory-store.d.ts.map +1 -1
- package/dist/@types/builders/state-builder.d.ts +35 -9
- package/dist/@types/builders/state-builder.d.ts.map +1 -1
- package/dist/@types/internal/autoclose-policy.d.ts +39 -13
- package/dist/@types/internal/autoclose-policy.d.ts.map +1 -1
- package/dist/@types/internal/autoclose-reaction.d.ts.map +1 -1
- package/dist/@types/internal/close-cycle.d.ts +5 -0
- package/dist/@types/internal/close-cycle.d.ts.map +1 -1
- package/dist/@types/internal/close-signal.d.ts +8 -0
- package/dist/@types/internal/close-signal.d.ts.map +1 -1
- package/dist/@types/internal/index.d.ts +1 -1
- package/dist/@types/internal/index.d.ts.map +1 -1
- package/dist/@types/internal/reactions.d.ts.map +1 -1
- package/dist/@types/types/action.d.ts +46 -13
- package/dist/@types/types/action.d.ts.map +1 -1
- package/dist/@types/types/ports.d.ts +29 -3
- package/dist/@types/types/ports.d.ts.map +1 -1
- package/dist/{chunk-TODGOWEZ.js → chunk-KBTC3NGQ.js} +64 -14
- package/dist/chunk-KBTC3NGQ.js.map +1 -0
- package/dist/index.cjs +228 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +166 -25
- package/dist/index.js.map +1 -1
- package/dist/test/index.cjs +63 -13
- package/dist/test/index.cjs.map +1 -1
- package/dist/test/index.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-TODGOWEZ.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -803,6 +803,10 @@ var InMemoryStream = class {
|
|
|
803
803
|
var InMemoryStore = class {
|
|
804
804
|
// stored events
|
|
805
805
|
_events = [];
|
|
806
|
+
// next event id — monotonic, never reused. Deletions (truncate, windowed
|
|
807
|
+
// prune) punch holes in the id sequence, so ids are NOT array indexes and
|
|
808
|
+
// NOT `_events.length`; they only stay sorted ascending in `_events`.
|
|
809
|
+
_next_id = 0;
|
|
806
810
|
// stored stream positions and other metadata
|
|
807
811
|
_streams = /* @__PURE__ */ new Map();
|
|
808
812
|
// last committed version per stream — O(1) replacement for filter-on-commit
|
|
@@ -820,11 +824,26 @@ var InMemoryStore = class {
|
|
|
820
824
|
_pii = /* @__PURE__ */ new Map();
|
|
821
825
|
_reset_indexes() {
|
|
822
826
|
this._events.length = 0;
|
|
827
|
+
this._next_id = 0;
|
|
823
828
|
this._stream_versions.clear();
|
|
824
829
|
this._max_event_id_by_stream.clear();
|
|
825
830
|
this._max_non_snap_event_id = -1;
|
|
826
831
|
this._pii.clear();
|
|
827
832
|
}
|
|
833
|
+
// First index whose event id is greater than `after`. `_events` stays
|
|
834
|
+
// sorted ascending by id (append-only commits; deletions preserve
|
|
835
|
+
// order), so id-bounded scans binary-search their start instead of
|
|
836
|
+
// assuming id === index — an invariant that truncation breaks.
|
|
837
|
+
_first_index_after(after) {
|
|
838
|
+
let lo = 0;
|
|
839
|
+
let hi = this._events.length;
|
|
840
|
+
while (lo < hi) {
|
|
841
|
+
const mid = lo + hi >>> 1;
|
|
842
|
+
if (this._events[mid].id > after) hi = mid;
|
|
843
|
+
else lo = mid + 1;
|
|
844
|
+
}
|
|
845
|
+
return lo;
|
|
846
|
+
}
|
|
828
847
|
// Attach the isolated PII payload (or null) to an event before handing it to
|
|
829
848
|
// a caller. Allocation-free for events without PII — by far the common case.
|
|
830
849
|
_with_pii(e) {
|
|
@@ -877,7 +896,7 @@ var InMemoryStore = class {
|
|
|
877
896
|
await sleep();
|
|
878
897
|
let count = 0;
|
|
879
898
|
if (query?.backward) {
|
|
880
|
-
let i = (query?.before
|
|
899
|
+
let i = (query?.before !== void 0 ? this._first_index_after(query.before - 1) : this._events.length) - 1;
|
|
881
900
|
while (i >= 0) {
|
|
882
901
|
const e = this._events[i--];
|
|
883
902
|
if (query && !this.in_query(query, e)) continue;
|
|
@@ -892,7 +911,7 @@ var InMemoryStore = class {
|
|
|
892
911
|
if (query?.limit && count >= query.limit) break;
|
|
893
912
|
}
|
|
894
913
|
} else {
|
|
895
|
-
let i = (query?.after ?? -1)
|
|
914
|
+
let i = this._first_index_after(query?.after ?? -1);
|
|
896
915
|
if (query?.with_snaps && query.stream_exact && query.stream !== void 0 && query.after === void 0) {
|
|
897
916
|
for (let j = this._events.length - 1; j >= 0; j--) {
|
|
898
917
|
const e = this._events[j];
|
|
@@ -941,7 +960,7 @@ var InMemoryStore = class {
|
|
|
941
960
|
let last_non_snap_id = -1;
|
|
942
961
|
const committed = msgs.map(({ name, data, pii }) => {
|
|
943
962
|
const c = {
|
|
944
|
-
id: this.
|
|
963
|
+
id: this._next_id++,
|
|
945
964
|
stream,
|
|
946
965
|
version,
|
|
947
966
|
created: /* @__PURE__ */ new Date(),
|
|
@@ -1282,7 +1301,7 @@ var InMemoryStore = class {
|
|
|
1282
1301
|
count++;
|
|
1283
1302
|
if (count >= limit) break;
|
|
1284
1303
|
}
|
|
1285
|
-
return { maxEventId: this._events.
|
|
1304
|
+
return { maxEventId: this._events.at(-1)?.id ?? -1, count };
|
|
1286
1305
|
}
|
|
1287
1306
|
/**
|
|
1288
1307
|
* Per-stream aggregated stats — see {@link Store.query_stats}.
|
|
@@ -1363,13 +1382,41 @@ var InMemoryStore = class {
|
|
|
1363
1382
|
}
|
|
1364
1383
|
/**
|
|
1365
1384
|
* Atomically truncates streams and seeds each with a snapshot or tombstone.
|
|
1366
|
-
*
|
|
1385
|
+
* Windowed targets (`before` set) prune the prefix below the closest safe
|
|
1386
|
+
* `__snapshot__` instead — no seed, subscriptions untouched, no-op when no
|
|
1387
|
+
* snapshot qualifies.
|
|
1388
|
+
* @param targets - Streams to truncate with optional snapshot state and meta,
|
|
1389
|
+
* or a `before`/`max_id` boundary for a windowed prefix delete.
|
|
1367
1390
|
* @returns Map keyed by stream name, each entry with `deleted` count and `committed` event.
|
|
1368
1391
|
*/
|
|
1369
1392
|
async truncate(targets) {
|
|
1370
1393
|
await sleep();
|
|
1394
|
+
const result = /* @__PURE__ */ new Map();
|
|
1395
|
+
const windowed = targets.filter((t) => t.before !== void 0);
|
|
1396
|
+
if (windowed.length) {
|
|
1397
|
+
const drop = /* @__PURE__ */ new Set();
|
|
1398
|
+
for (const { stream, before, max_id } of windowed) {
|
|
1399
|
+
let boundary;
|
|
1400
|
+
for (const e of this._events) {
|
|
1401
|
+
if (e.stream === stream && e.name === SNAP_EVENT && e.created < before && (max_id === void 0 || e.id <= max_id) && (!boundary || e.id > boundary.id))
|
|
1402
|
+
boundary = e;
|
|
1403
|
+
}
|
|
1404
|
+
if (!boundary) continue;
|
|
1405
|
+
let deleted = 0;
|
|
1406
|
+
for (const e of this._events) {
|
|
1407
|
+
if (e.stream === stream && e.id < boundary.id) {
|
|
1408
|
+
drop.add(e.id);
|
|
1409
|
+
this._pii.get(stream)?.delete(e.id);
|
|
1410
|
+
deleted++;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
result.set(stream, { deleted, committed: boundary, before });
|
|
1414
|
+
}
|
|
1415
|
+
if (drop.size) this._events = this._events.filter((e) => !drop.has(e.id));
|
|
1416
|
+
}
|
|
1417
|
+
const full = targets.filter((t) => t.before === void 0);
|
|
1371
1418
|
const deleted_counts = /* @__PURE__ */ new Map();
|
|
1372
|
-
const stream_set = new Set(
|
|
1419
|
+
const stream_set = new Set(full.map((t) => t.stream));
|
|
1373
1420
|
for (const e of this._events) {
|
|
1374
1421
|
if (stream_set.has(e.stream)) {
|
|
1375
1422
|
deleted_counts.set(e.stream, (deleted_counts.get(e.stream) ?? 0) + 1);
|
|
@@ -1380,11 +1427,11 @@ var InMemoryStore = class {
|
|
|
1380
1427
|
this._streams.delete(stream);
|
|
1381
1428
|
this._stream_versions.delete(stream);
|
|
1382
1429
|
this._max_event_id_by_stream.delete(stream);
|
|
1430
|
+
this._pii.delete(stream);
|
|
1383
1431
|
}
|
|
1384
|
-
const
|
|
1385
|
-
for (const { stream, snapshot, meta } of targets) {
|
|
1432
|
+
for (const { stream, snapshot, meta } of full) {
|
|
1386
1433
|
const event = {
|
|
1387
|
-
id: this.
|
|
1434
|
+
id: this._next_id++,
|
|
1388
1435
|
stream,
|
|
1389
1436
|
version: 0,
|
|
1390
1437
|
created: /* @__PURE__ */ new Date(),
|
|
@@ -1416,25 +1463,27 @@ var InMemoryStore = class {
|
|
|
1416
1463
|
* throw inside the driver restores the snapshot, leaving the store
|
|
1417
1464
|
* byte-for-byte unchanged from the operator's perspective.
|
|
1418
1465
|
*
|
|
1419
|
-
* `id`s are reassigned `0..N-1` as events arrive (
|
|
1420
|
-
*
|
|
1421
|
-
*
|
|
1466
|
+
* `id`s are reassigned `0..N-1` as events arrive (dense — the
|
|
1467
|
+
* monotonic id counter restarts at 0 for the rebuild). `created` is
|
|
1468
|
+
* preserved verbatim from the source.
|
|
1422
1469
|
*/
|
|
1423
1470
|
async restore(driver) {
|
|
1424
1471
|
await sleep();
|
|
1425
1472
|
const prev_events = this._events;
|
|
1473
|
+
const prev_next_id = this._next_id;
|
|
1426
1474
|
const prev_streams = this._streams;
|
|
1427
1475
|
const prev_stream_versions = this._stream_versions;
|
|
1428
1476
|
const prev_max_event_id_by_stream = this._max_event_id_by_stream;
|
|
1429
1477
|
const prev_max_non_snap_event_id = this._max_non_snap_event_id;
|
|
1430
1478
|
this._events = [];
|
|
1479
|
+
this._next_id = 0;
|
|
1431
1480
|
this._streams = /* @__PURE__ */ new Map();
|
|
1432
1481
|
this._stream_versions = /* @__PURE__ */ new Map();
|
|
1433
1482
|
this._max_event_id_by_stream = /* @__PURE__ */ new Map();
|
|
1434
1483
|
this._max_non_snap_event_id = -1;
|
|
1435
1484
|
try {
|
|
1436
1485
|
await driver(async (event) => {
|
|
1437
|
-
const id = this.
|
|
1486
|
+
const id = this._next_id++;
|
|
1438
1487
|
const committed = { ...event, id };
|
|
1439
1488
|
this._events.push(committed);
|
|
1440
1489
|
this._stream_versions.set(event.stream, event.version);
|
|
@@ -1446,6 +1495,7 @@ var InMemoryStore = class {
|
|
|
1446
1495
|
});
|
|
1447
1496
|
} catch (err) {
|
|
1448
1497
|
this._events = prev_events;
|
|
1498
|
+
this._next_id = prev_next_id;
|
|
1449
1499
|
this._streams = prev_streams;
|
|
1450
1500
|
this._stream_versions = prev_stream_versions;
|
|
1451
1501
|
this._max_event_id_by_stream = prev_max_event_id_by_stream;
|
|
@@ -2009,12 +2059,22 @@ function in_autoclose_window(window, now) {
|
|
|
2009
2059
|
|
|
2010
2060
|
// src/internal/autoclose-policy.ts
|
|
2011
2061
|
var import_zod6 = require("zod");
|
|
2012
|
-
var
|
|
2013
|
-
var
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2062
|
+
var DAY_MS = 864e5;
|
|
2063
|
+
var MIN_AFTER_DAYS = 1 / 1440;
|
|
2064
|
+
var MIN_KEEP_DAYS = 1;
|
|
2065
|
+
function days_after(date, days) {
|
|
2066
|
+
return new Date(date.getTime() + days * DAY_MS);
|
|
2067
|
+
}
|
|
2068
|
+
function days_before_now(days) {
|
|
2069
|
+
return new Date(Date.now() - days * DAY_MS);
|
|
2070
|
+
}
|
|
2071
|
+
var window_schema = (field, min_days, floor) => import_zod6.z.object({
|
|
2072
|
+
days: import_zod6.z.number({ message: `autocloses: ${field}.days must be a number` }).positive(`autocloses: ${field}.days must be > 0`)
|
|
2073
|
+
}).refine((o) => o.days >= min_days, {
|
|
2074
|
+
message: `autocloses: ${field} resolves to < ${floor}, which is too short to be a meaningful retention window`
|
|
2017
2075
|
});
|
|
2076
|
+
var AfterSchema = window_schema("after", MIN_AFTER_DAYS, "one minute");
|
|
2077
|
+
var KeepSchema = window_schema("keep", MIN_KEEP_DAYS, "one day");
|
|
2018
2078
|
var IsSchema = import_zod6.z.union([
|
|
2019
2079
|
import_zod6.z.string().min(1, "autocloses: is must be a non-empty event name"),
|
|
2020
2080
|
import_zod6.z.array(
|
|
@@ -2036,16 +2096,16 @@ var AutoclosePolicySchema = import_zod6.z.object({
|
|
|
2036
2096
|
after: AfterSchema.optional(),
|
|
2037
2097
|
is: IsSchema.optional(),
|
|
2038
2098
|
reaches: ReachesSchema.optional(),
|
|
2039
|
-
or: OrBlockSchema.optional()
|
|
2099
|
+
or: OrBlockSchema.optional(),
|
|
2100
|
+
keep: KeepSchema.optional()
|
|
2040
2101
|
}).strict().refine(
|
|
2041
|
-
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0 || o.or !== void 0,
|
|
2102
|
+
(o) => o.after !== void 0 || o.is !== void 0 || o.reaches !== void 0 || o.or !== void 0 || o.keep !== void 0,
|
|
2042
2103
|
{
|
|
2043
|
-
message: "autocloses: at least one of after / is / reaches / or must be specified \u2014 empty `{}` is a misconfiguration"
|
|
2104
|
+
message: "autocloses: at least one of after / is / reaches / or / keep must be specified \u2014 empty `{}` is a misconfiguration"
|
|
2044
2105
|
}
|
|
2045
2106
|
);
|
|
2046
2107
|
function compile_after(after) {
|
|
2047
|
-
|
|
2048
|
-
return (_stream, head) => Date.now() - head.created.getTime() >= after_ms;
|
|
2108
|
+
return (_stream, head) => days_after(head.created, after.days) <= /* @__PURE__ */ new Date();
|
|
2049
2109
|
}
|
|
2050
2110
|
function compile_is(is) {
|
|
2051
2111
|
const set = new Set(typeof is === "string" ? [is] : is);
|
|
@@ -2054,13 +2114,17 @@ function compile_is(is) {
|
|
|
2054
2114
|
function compile_reaches(reaches) {
|
|
2055
2115
|
return (_stream, _head, count) => count >= reaches;
|
|
2056
2116
|
}
|
|
2057
|
-
function
|
|
2117
|
+
function policy_min_after_days(options) {
|
|
2058
2118
|
const parsed = AutoclosePolicySchema.parse(options);
|
|
2059
2119
|
const windows = [];
|
|
2060
|
-
if (parsed.after) windows.push(parsed.after.days
|
|
2061
|
-
if (parsed.or?.after) windows.push(parsed.or.after.days
|
|
2120
|
+
if (parsed.after) windows.push(parsed.after.days);
|
|
2121
|
+
if (parsed.or?.after) windows.push(parsed.or.after.days);
|
|
2062
2122
|
return windows.length ? Math.min(...windows) : void 0;
|
|
2063
2123
|
}
|
|
2124
|
+
function policy_keep_days(options) {
|
|
2125
|
+
const parsed = AutoclosePolicySchema.parse(options);
|
|
2126
|
+
return parsed.keep?.days;
|
|
2127
|
+
}
|
|
2064
2128
|
function compile_autoclose_policy(options) {
|
|
2065
2129
|
const parsed = AutoclosePolicySchema.parse(options);
|
|
2066
2130
|
const and_preds = [];
|
|
@@ -2103,12 +2167,20 @@ var CloseSignal = class extends Error {
|
|
|
2103
2167
|
* event id when omitted.
|
|
2104
2168
|
*/
|
|
2105
2169
|
at;
|
|
2170
|
+
/**
|
|
2171
|
+
* Windowed close (#1011): prune events older than this cutoff behind
|
|
2172
|
+
* the closest safe snapshot instead of retiring the stream. Thrown by
|
|
2173
|
+
* the autoclose reaction of a `.autocloses({ keep })` state; flows
|
|
2174
|
+
* into {@link CloseTarget.before}. Omitted → a full close.
|
|
2175
|
+
*/
|
|
2176
|
+
before;
|
|
2106
2177
|
constructor(opts) {
|
|
2107
2178
|
super("reaction requested close");
|
|
2108
2179
|
this.name = "CloseSignal";
|
|
2109
2180
|
this.stream = opts?.stream;
|
|
2110
2181
|
this.archive = opts?.archive;
|
|
2111
2182
|
this.at = opts?.at;
|
|
2183
|
+
this.before = opts?.before;
|
|
2112
2184
|
}
|
|
2113
2185
|
};
|
|
2114
2186
|
|
|
@@ -2132,7 +2204,8 @@ function synthesize_autoclose_reactions(registry, states, config2) {
|
|
|
2132
2204
|
for (const st of states.values()) {
|
|
2133
2205
|
const predicate = st.autoclose;
|
|
2134
2206
|
if (!predicate) continue;
|
|
2135
|
-
const
|
|
2207
|
+
const after_days = st.autoclose_after_days;
|
|
2208
|
+
const keep_days = st.autoclose_keep_days;
|
|
2136
2209
|
const archiver = st.archive;
|
|
2137
2210
|
const reaction = {
|
|
2138
2211
|
// Run on a SYNTHETIC stream — `source` is the aggregate, `target` is a
|
|
@@ -2156,7 +2229,8 @@ function synthesize_autoclose_reactions(registry, states, config2) {
|
|
|
2156
2229
|
});
|
|
2157
2230
|
const stats = await store2().query_stats([aggregate], {
|
|
2158
2231
|
count: true,
|
|
2159
|
-
|
|
2232
|
+
tail: keep_days !== void 0 ? true : void 0,
|
|
2233
|
+
exclude: keep_days !== void 0 ? [TOMBSTONE_EVENT, SNAP_EVENT] : [TOMBSTONE_EVENT]
|
|
2160
2234
|
});
|
|
2161
2235
|
const entry = stats.get(aggregate);
|
|
2162
2236
|
if (!entry) return;
|
|
@@ -2170,9 +2244,24 @@ function synthesize_autoclose_reactions(registry, states, config2) {
|
|
|
2170
2244
|
// aggregate) sees it caught up instead of blocking its own close.
|
|
2171
2245
|
at: head.id
|
|
2172
2246
|
});
|
|
2173
|
-
if (
|
|
2247
|
+
if (keep_days !== void 0) {
|
|
2248
|
+
const cutoff = days_before_now(keep_days);
|
|
2249
|
+
if (entry.tail.created < cutoff)
|
|
2250
|
+
throw new CloseSignal({
|
|
2251
|
+
stream: aggregate,
|
|
2252
|
+
before: cutoff,
|
|
2253
|
+
archive: archiver ? () => archiver(aggregate, head, cutoff) : void 0,
|
|
2254
|
+
at: head.id
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
const dues = [];
|
|
2258
|
+
if (after_days !== void 0)
|
|
2259
|
+
dues.push(days_after(head.created, after_days));
|
|
2260
|
+
if (keep_days !== void 0)
|
|
2261
|
+
dues.push(days_after(entry.tail.created, keep_days));
|
|
2262
|
+
if (dues.length)
|
|
2174
2263
|
throw new DeferSignal({
|
|
2175
|
-
at: new Date(
|
|
2264
|
+
at: new Date(Math.min(...dues.map((d) => d.getTime())))
|
|
2176
2265
|
});
|
|
2177
2266
|
}
|
|
2178
2267
|
};
|
|
@@ -2559,8 +2648,20 @@ var CircuitBreaker = class {
|
|
|
2559
2648
|
var SAFETY_PROBE_PAGE_SIZE = 1e3;
|
|
2560
2649
|
async function run_close_cycle(targets, deps) {
|
|
2561
2650
|
const target_map = new Map(targets.map((t) => [t.stream, t]));
|
|
2562
|
-
const
|
|
2651
|
+
for (const t of target_map.values()) {
|
|
2652
|
+
if (t.before !== void 0 && t.restart)
|
|
2653
|
+
throw new Error(
|
|
2654
|
+
`close: \`before\` and \`restart\` are mutually exclusive (stream "${t.stream}") \u2014 a windowed close keeps the stream live behind a real snapshot; restart reseeds it`
|
|
2655
|
+
);
|
|
2656
|
+
}
|
|
2657
|
+
const windowed = [...target_map.values()].filter(
|
|
2658
|
+
(t) => t.before !== void 0
|
|
2659
|
+
);
|
|
2660
|
+
const full = [...target_map.values()].filter((t) => t.before === void 0);
|
|
2563
2661
|
const skipped = [];
|
|
2662
|
+
const windowed_result = windowed.length ? await run_windowed_closes(windowed, deps, skipped) : /* @__PURE__ */ new Map();
|
|
2663
|
+
if (!full.length) return { truncated: windowed_result, skipped };
|
|
2664
|
+
const streams = full.map((t) => t.stream);
|
|
2564
2665
|
const stream_info = await scan_stream_heads(streams);
|
|
2565
2666
|
const safe = await partition_by_safety(
|
|
2566
2667
|
stream_info,
|
|
@@ -2568,7 +2669,7 @@ async function run_close_cycle(targets, deps) {
|
|
|
2568
2669
|
skipped,
|
|
2569
2670
|
deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE
|
|
2570
2671
|
);
|
|
2571
|
-
if (!safe.length) return { truncated:
|
|
2672
|
+
if (!safe.length) return { truncated: windowed_result, skipped };
|
|
2572
2673
|
const { guarded, guard_events } = await guard_with_tombstones(
|
|
2573
2674
|
safe,
|
|
2574
2675
|
stream_info,
|
|
@@ -2576,7 +2677,7 @@ async function run_close_cycle(targets, deps) {
|
|
|
2576
2677
|
deps.tombstone,
|
|
2577
2678
|
skipped
|
|
2578
2679
|
);
|
|
2579
|
-
if (!guarded.length) return { truncated:
|
|
2680
|
+
if (!guarded.length) return { truncated: windowed_result, skipped };
|
|
2580
2681
|
const seed_states = await load_restart_seeds(
|
|
2581
2682
|
guarded,
|
|
2582
2683
|
target_map,
|
|
@@ -2592,8 +2693,62 @@ async function run_close_cycle(targets, deps) {
|
|
|
2592
2693
|
guard_events,
|
|
2593
2694
|
deps.correlation
|
|
2594
2695
|
);
|
|
2696
|
+
for (const [stream, entry] of windowed_result) truncated.set(stream, entry);
|
|
2595
2697
|
return { truncated, skipped };
|
|
2596
2698
|
}
|
|
2699
|
+
async function run_windowed_closes(windowed, deps, skipped) {
|
|
2700
|
+
const min_at = deps.reactive_events_size > 0 ? await probe_min_watermarks(
|
|
2701
|
+
windowed.map((t) => t.stream),
|
|
2702
|
+
deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE
|
|
2703
|
+
) : /* @__PURE__ */ new Map();
|
|
2704
|
+
for (const t of windowed) {
|
|
2705
|
+
if (t.archive) await t.archive();
|
|
2706
|
+
}
|
|
2707
|
+
const truncated = await store2().truncate(
|
|
2708
|
+
windowed.map((t) => ({
|
|
2709
|
+
stream: t.stream,
|
|
2710
|
+
before: t.before,
|
|
2711
|
+
max_id: min_at.get(t.stream)
|
|
2712
|
+
}))
|
|
2713
|
+
);
|
|
2714
|
+
for (const t of windowed) {
|
|
2715
|
+
if (!truncated.has(t.stream)) skipped.push(t.stream);
|
|
2716
|
+
}
|
|
2717
|
+
return truncated;
|
|
2718
|
+
}
|
|
2719
|
+
async function probe_min_watermarks(streams, page_size) {
|
|
2720
|
+
const min_at = /* @__PURE__ */ new Map();
|
|
2721
|
+
const source_regex = /* @__PURE__ */ new Map();
|
|
2722
|
+
const get_regex = (source) => {
|
|
2723
|
+
let re = source_regex.get(source);
|
|
2724
|
+
if (!re) {
|
|
2725
|
+
re = new RegExp(source);
|
|
2726
|
+
source_regex.set(source, re);
|
|
2727
|
+
}
|
|
2728
|
+
return re;
|
|
2729
|
+
};
|
|
2730
|
+
let after;
|
|
2731
|
+
for (; ; ) {
|
|
2732
|
+
let last;
|
|
2733
|
+
const { count } = await store2().query_streams(
|
|
2734
|
+
(position) => {
|
|
2735
|
+
last = position.stream;
|
|
2736
|
+
const source_re = position.source ? get_regex(position.source) : void 0;
|
|
2737
|
+
for (const stream of streams) {
|
|
2738
|
+
if (!source_re || source_re.test(stream)) {
|
|
2739
|
+
const prev = min_at.get(stream);
|
|
2740
|
+
if (prev === void 0 || position.at < prev)
|
|
2741
|
+
min_at.set(stream, position.at);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
},
|
|
2745
|
+
{ after, limit: page_size, source_matches: streams }
|
|
2746
|
+
);
|
|
2747
|
+
if (count < page_size) break;
|
|
2748
|
+
after = last;
|
|
2749
|
+
}
|
|
2750
|
+
return min_at;
|
|
2751
|
+
}
|
|
2597
2752
|
async function scan_stream_heads(streams) {
|
|
2598
2753
|
const stats = await store2().query_stats(streams, {
|
|
2599
2754
|
exclude: [SNAP_EVENT]
|
|
@@ -3794,8 +3949,13 @@ function build_handle(deps) {
|
|
|
3794
3949
|
acked_at: error.at ?? event.id,
|
|
3795
3950
|
// Close the signalled stream (the autoclose reaction's aggregate,
|
|
3796
3951
|
// which differs from its synthetic lease stream); a self-closing
|
|
3797
|
-
// user reaction omits it and closes its own lease stream.
|
|
3798
|
-
|
|
3952
|
+
// user reaction omits it and closes its own lease stream. A
|
|
3953
|
+
// carried `before` makes it a windowed close (prune, not retire).
|
|
3954
|
+
close: {
|
|
3955
|
+
stream: error.stream ?? stream,
|
|
3956
|
+
archive: error.archive,
|
|
3957
|
+
before: error.before
|
|
3958
|
+
}
|
|
3799
3959
|
};
|
|
3800
3960
|
return finalize(
|
|
3801
3961
|
lease,
|
|
@@ -5098,7 +5258,19 @@ var Act = class {
|
|
|
5098
5258
|
* 7. **Cache** — invalidate (tombstoned) or warm (restarted)
|
|
5099
5259
|
* 8. **Emit "closed"** — lifecycle event with results
|
|
5100
5260
|
*
|
|
5101
|
-
*
|
|
5261
|
+
* Targets carrying `before` take the **windowed** branch instead —
|
|
5262
|
+
* close the books on a rolling window: probe the min consumer
|
|
5263
|
+
* watermark (so the boundary never rises past a lagging reaction),
|
|
5264
|
+
* run the archive callback against the cutoff, then prune the prefix
|
|
5265
|
+
* below the closest safe `__snapshot__`. No tombstone guard (the
|
|
5266
|
+
* pre-cutoff prefix is immutable), no seed, cache untouched — the
|
|
5267
|
+
* stream stays live and keeps accepting actions. Requires the state
|
|
5268
|
+
* to snapshot via `.snap(...)`; no qualifying snapshot ⇒ the stream
|
|
5269
|
+
* lands in `skipped` (retry after the next snapshot). Windowed
|
|
5270
|
+
* entries in the result echo `before` and carry the surviving
|
|
5271
|
+
* boundary snapshot as `committed`.
|
|
5272
|
+
*
|
|
5273
|
+
* @param targets - Per-stream close options (stream, restart?, archive?, before?)
|
|
5102
5274
|
* @returns `{ truncated: TruncateResult, skipped: string[] }`
|
|
5103
5275
|
*
|
|
5104
5276
|
* @example Archive and close
|
|
@@ -5116,6 +5288,19 @@ var Act = class {
|
|
|
5116
5288
|
* { stream: "counter-2" }, // tombstoned
|
|
5117
5289
|
* ]);
|
|
5118
5290
|
* ```
|
|
5291
|
+
*
|
|
5292
|
+
* @example Windowed close — keep the last 180 days of real events
|
|
5293
|
+
* ```typescript
|
|
5294
|
+
* const cutoff = new Date();
|
|
5295
|
+
* cutoff.setDate(cutoff.getDate() - 180);
|
|
5296
|
+
* await app.close([
|
|
5297
|
+
* {
|
|
5298
|
+
* stream: "ledger-acme",
|
|
5299
|
+
* before: cutoff,
|
|
5300
|
+
* archive: async () => { await archiveToS3("ledger-acme", cutoff); },
|
|
5301
|
+
* },
|
|
5302
|
+
* ]);
|
|
5303
|
+
* ```
|
|
5119
5304
|
*/
|
|
5120
5305
|
async close(targets) {
|
|
5121
5306
|
if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
|
|
@@ -5648,10 +5833,16 @@ function action_builder(state2) {
|
|
|
5648
5833
|
".autocloses(...) requires a policy object; got " + typeof policy
|
|
5649
5834
|
);
|
|
5650
5835
|
}
|
|
5836
|
+
if (policy.keep && !internal.snap) {
|
|
5837
|
+
throw new Error(
|
|
5838
|
+
".autocloses({ keep }) requires .snap(...) earlier in the chain \u2014 a rolling window prunes behind a real snapshot, so a state that never snapshots has nothing to prune behind."
|
|
5839
|
+
);
|
|
5840
|
+
}
|
|
5651
5841
|
internal.autoclose = compile_autoclose_policy(
|
|
5652
5842
|
policy
|
|
5653
5843
|
);
|
|
5654
|
-
internal.
|
|
5844
|
+
internal.autoclose_after_days = policy_min_after_days(policy);
|
|
5845
|
+
internal.autoclose_keep_days = policy_keep_days(policy);
|
|
5655
5846
|
return builder;
|
|
5656
5847
|
},
|
|
5657
5848
|
archives(archive) {
|