@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.cjs CHANGED
@@ -1418,8 +1418,11 @@ var InMemoryStore = class {
1418
1418
  }
1419
1419
  }
1420
1420
  this._events = this._events.filter((e) => !stream_set.has(e.stream));
1421
+ const restarted = new Set(
1422
+ full.filter((t) => t.snapshot !== void 0).map((t) => t.stream)
1423
+ );
1421
1424
  for (const stream of stream_set) {
1422
- this._streams.delete(stream);
1425
+ if (!restarted.has(stream)) this._streams.delete(stream);
1423
1426
  this._stream_versions.delete(stream);
1424
1427
  this._max_event_id_by_stream.delete(stream);
1425
1428
  this._pii.delete(stream);
@@ -2952,14 +2955,19 @@ async function scan_stream_heads(streams) {
2952
2955
  const stats = await store2().query_stats(streams, {
2953
2956
  exclude: [SNAP_EVENT]
2954
2957
  });
2958
+ const domain_heads = await store2().query_stats(streams, {
2959
+ exclude: [SNAP_EVENT, TOMBSTONE_EVENT]
2960
+ });
2955
2961
  const true_heads = await store2().query_stats(streams, {});
2956
2962
  const out = /* @__PURE__ */ new Map();
2957
- for (const [stream, { head }] of stats) {
2958
- if (head.name === TOMBSTONE_EVENT) continue;
2963
+ for (const [stream, { head }] of domain_heads) {
2964
+ const marker_head = stats.get(stream)?.head;
2965
+ const interrupted = marker_head?.name === TOMBSTONE_EVENT;
2959
2966
  out.set(stream, {
2960
2967
  max_id: head.id,
2961
2968
  version: true_heads.get(stream).head.version,
2962
- last_event_name: head.name
2969
+ last_event_name: head.name,
2970
+ ...interrupted ? { resumed_guard: { id: marker_head.id } } : {}
2963
2971
  });
2964
2972
  }
2965
2973
  return out;
@@ -3008,6 +3016,11 @@ async function guard_with_tombstones(safe, stream_info, correlation, tombstone2,
3008
3016
  await Promise.all(
3009
3017
  safe.map(async (stream) => {
3010
3018
  const info = stream_info.get(stream);
3019
+ if (info.resumed_guard) {
3020
+ guarded.push(stream);
3021
+ guard_events.set(stream, { id: info.resumed_guard.id, stream });
3022
+ return;
3023
+ }
3011
3024
  const committed = await tombstone2(stream, info.version, correlation);
3012
3025
  if (committed) {
3013
3026
  guarded.push(stream);
@@ -3085,6 +3098,8 @@ var DEFAULT_COLD_START_BACK_SCAN = 1e4;
3085
3098
  var CorrelateCycle = class {
3086
3099
  _checkpoint = -1;
3087
3100
  _initialized = false;
3101
+ /** In-flight init, memoized for single-flight and cleared on failure. */
3102
+ _init_promise;
3088
3103
  _timer = void 0;
3089
3104
  // target → the priority it was last subscribed at. A dynamic target is
3090
3105
  // re-subscribed (so the store's `GREATEST` upsert runs) only when a later
@@ -3167,7 +3182,16 @@ var CorrelateCycle = class {
3167
3182
  */
3168
3183
  async init() {
3169
3184
  if (this._initialized) return;
3185
+ if (!this._init_promise) {
3186
+ this._init_promise = this._run_init().catch((error) => {
3187
+ this._init_promise = void 0;
3188
+ throw error;
3189
+ });
3190
+ }
3191
+ await this._init_promise;
3170
3192
  this._initialized = true;
3193
+ }
3194
+ async _run_init() {
3171
3195
  const { watermark } = await store2().subscribe([...this._static_targets]);
3172
3196
  this._checkpoint = this._has_dynamic_resolvers ? Math.max(-1, watermark - this._cold_start_back_scan) : watermark;
3173
3197
  this._on_init?.();
@@ -3176,6 +3200,26 @@ var CorrelateCycle = class {
3176
3200
  }
3177
3201
  await this._on_init_async?.();
3178
3202
  }
3203
+ /**
3204
+ * Forget targets whose subscription rows no longer exist, so a later
3205
+ * scan can re-subscribe them.
3206
+ *
3207
+ * A full close deletes the closed stream's subscription row. The
3208
+ * in-process dedup would otherwise still believe the target is
3209
+ * subscribed and never re-issue `subscribe()`, silently stopping
3210
+ * delivery for any reaction whose target is named after the stream —
3211
+ * the documented per-aggregate shape `.to(e => ({target: e.stream}))`
3212
+ * makes those two namespaces collide by construction (#1398).
3213
+ *
3214
+ * Static targets are left alone: they are subscribed once at init and
3215
+ * recorded at +Infinity so the dynamic path never re-opens them.
3216
+ */
3217
+ forget_subscribed(streams) {
3218
+ const statics = new Set(this._static_targets.map((t) => t.stream));
3219
+ for (const stream of streams) {
3220
+ if (!statics.has(stream)) this._subscribed.delete(stream);
3221
+ }
3222
+ }
3179
3223
  /**
3180
3224
  * Discover dynamic-resolver targets in the events past the checkpoint
3181
3225
  * and register any new streams via `cd.subscribe`. Static targets are
@@ -4798,6 +4842,7 @@ var Act = class {
4798
4842
  correlation: close_correlation(this._correlator, close_actor),
4799
4843
  with_stream_lock: (stream, work) => this._with_close_lock(stream, work)
4800
4844
  });
4845
+ this._forget_closed_subscriptions(result);
4801
4846
  try {
4802
4847
  this.emit("closed", result);
4803
4848
  } catch (err) {
@@ -5792,6 +5837,15 @@ var Act = class {
5792
5837
  * ]);
5793
5838
  * ```
5794
5839
  */
5840
+ /**
5841
+ * After a close, forget any target whose subscription row the truncate
5842
+ * removed, so a later correlate can re-subscribe it (#1398). Restart
5843
+ * targets keep their row, so only fully-retired streams are forgotten.
5844
+ */
5845
+ _forget_closed_subscriptions(result) {
5846
+ const retired = [...result.truncated.entries()].filter(([, r]) => r.committed.name === TOMBSTONE_EVENT).map(([stream]) => stream);
5847
+ if (retired.length) this._correlate.forget_subscribed(retired);
5848
+ }
5795
5849
  async close(targets) {
5796
5850
  if (!targets.length) return { truncated: /* @__PURE__ */ new Map(), skipped: [] };
5797
5851
  return this._scoped(async () => {
@@ -5806,6 +5860,7 @@ var Act = class {
5806
5860
  correlation: close_correlation(this._correlator, close_actor),
5807
5861
  with_stream_lock: (stream, work) => this._with_close_lock(stream, work)
5808
5862
  });
5863
+ this._forget_closed_subscriptions(result);
5809
5864
  this.emit("closed", result);
5810
5865
  return result;
5811
5866
  });