@rotorsoft/act 1.23.32 → 1.23.34

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.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  sleep,
22
22
  store,
23
23
  validate
24
- } from "./chunk-5B45U2IS.js";
24
+ } from "./chunk-TTOT5CRM.js";
25
25
  import {
26
26
  ActorSchema,
27
27
  CausationEventSchema,
@@ -1430,14 +1430,19 @@ async function scan_stream_heads(streams) {
1430
1430
  const stats = await store().query_stats(streams, {
1431
1431
  exclude: [SNAP_EVENT]
1432
1432
  });
1433
+ const domain_heads = await store().query_stats(streams, {
1434
+ exclude: [SNAP_EVENT, TOMBSTONE_EVENT]
1435
+ });
1433
1436
  const true_heads = await store().query_stats(streams, {});
1434
1437
  const out = /* @__PURE__ */ new Map();
1435
- for (const [stream, { head }] of stats) {
1436
- if (head.name === TOMBSTONE_EVENT) continue;
1438
+ for (const [stream, { head }] of domain_heads) {
1439
+ const marker_head = stats.get(stream)?.head;
1440
+ const interrupted = marker_head?.name === TOMBSTONE_EVENT;
1437
1441
  out.set(stream, {
1438
1442
  max_id: head.id,
1439
1443
  version: true_heads.get(stream).head.version,
1440
- last_event_name: head.name
1444
+ last_event_name: head.name,
1445
+ ...interrupted ? { resumed_guard: { id: marker_head.id } } : {}
1441
1446
  });
1442
1447
  }
1443
1448
  return out;
@@ -1486,6 +1491,11 @@ async function guard_with_tombstones(safe, stream_info, correlation, tombstone2,
1486
1491
  await Promise.all(
1487
1492
  safe.map(async (stream) => {
1488
1493
  const info = stream_info.get(stream);
1494
+ if (info.resumed_guard) {
1495
+ guarded.push(stream);
1496
+ guard_events.set(stream, { id: info.resumed_guard.id, stream });
1497
+ return;
1498
+ }
1489
1499
  const committed = await tombstone2(stream, info.version, correlation);
1490
1500
  if (committed) {
1491
1501
  guarded.push(stream);
@@ -1563,6 +1573,8 @@ var DEFAULT_COLD_START_BACK_SCAN = 1e4;
1563
1573
  var CorrelateCycle = class {
1564
1574
  _checkpoint = -1;
1565
1575
  _initialized = false;
1576
+ /** In-flight init, memoized for single-flight and cleared on failure. */
1577
+ _init_promise;
1566
1578
  _timer = void 0;
1567
1579
  // target → the priority it was last subscribed at. A dynamic target is
1568
1580
  // re-subscribed (so the store's `GREATEST` upsert runs) only when a later
@@ -1645,7 +1657,16 @@ var CorrelateCycle = class {
1645
1657
  */
1646
1658
  async init() {
1647
1659
  if (this._initialized) return;
1660
+ if (!this._init_promise) {
1661
+ this._init_promise = this._run_init().catch((error) => {
1662
+ this._init_promise = void 0;
1663
+ throw error;
1664
+ });
1665
+ }
1666
+ await this._init_promise;
1648
1667
  this._initialized = true;
1668
+ }
1669
+ async _run_init() {
1649
1670
  const { watermark } = await store().subscribe([...this._static_targets]);
1650
1671
  this._checkpoint = this._has_dynamic_resolvers ? Math.max(-1, watermark - this._cold_start_back_scan) : watermark;
1651
1672
  this._on_init?.();
@@ -1654,6 +1675,26 @@ var CorrelateCycle = class {
1654
1675
  }
1655
1676
  await this._on_init_async?.();
1656
1677
  }
1678
+ /**
1679
+ * Forget targets whose subscription rows no longer exist, so a later
1680
+ * scan can re-subscribe them.
1681
+ *
1682
+ * A full close deletes the closed stream's subscription row. The
1683
+ * in-process dedup would otherwise still believe the target is
1684
+ * subscribed and never re-issue `subscribe()`, silently stopping
1685
+ * delivery for any reaction whose target is named after the stream —
1686
+ * the documented per-aggregate shape `.to(e => ({target: e.stream}))`
1687
+ * makes those two namespaces collide by construction (#1398).
1688
+ *
1689
+ * Static targets are left alone: they are subscribed once at init and
1690
+ * recorded at +Infinity so the dynamic path never re-opens them.
1691
+ */
1692
+ forget_subscribed(streams) {
1693
+ const statics = new Set(this._static_targets.map((t) => t.stream));
1694
+ for (const stream of streams) {
1695
+ if (!statics.has(stream)) this._subscribed.delete(stream);
1696
+ }
1697
+ }
1657
1698
  /**
1658
1699
  * Discover dynamic-resolver targets in the events past the checkpoint
1659
1700
  * and register any new streams via `cd.subscribe`. Static targets are
@@ -3276,6 +3317,7 @@ var Act = class {
3276
3317
  correlation: close_correlation(this._correlator, close_actor),
3277
3318
  with_stream_lock: (stream, work) => this._with_close_lock(stream, work)
3278
3319
  });
3320
+ this._forget_closed_subscriptions(result);
3279
3321
  try {
3280
3322
  this.emit("closed", result);
3281
3323
  } catch (err) {
@@ -4270,6 +4312,15 @@ var Act = class {
4270
4312
  * ]);
4271
4313
  * ```
4272
4314
  */
4315
+ /**
4316
+ * After a close, forget any target whose subscription row the truncate
4317
+ * removed, so a later correlate can re-subscribe it (#1398). Restart
4318
+ * targets keep their row, so only fully-retired streams are forgotten.
4319
+ */
4320
+ _forget_closed_subscriptions(result) {
4321
+ const retired = [...result.truncated.entries()].filter(([, r]) => r.committed.name === TOMBSTONE_EVENT).map(([stream]) => stream);
4322
+ if (retired.length) this._correlate.forget_subscribed(retired);
4323
+ }
4273
4324
  async close(targets) {
4274
4325
  if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
4275
4326
  return this._scoped(async () => {
@@ -4284,6 +4335,7 @@ var Act = class {
4284
4335
  correlation: close_correlation(this._correlator, close_actor),
4285
4336
  with_stream_lock: (stream, work) => this._with_close_lock(stream, work)
4286
4337
  });
4338
+ this._forget_closed_subscriptions(result);
4287
4339
  this.emit("closed", result);
4288
4340
  return result;
4289
4341
  });