@rotorsoft/act 1.32.5 → 1.32.6

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.
@@ -394,9 +394,9 @@ export declare class Act<TSchemaReg extends SchemaRegister<TActions>, TEvents ex
394
394
  */
395
395
  private readonly _close_locks;
396
396
  /**
397
- * Run `work` under the per-stream close lock (#1222). Serializes
398
- * windowed-close critical sections for the same stream while letting
399
- * different streams proceed in parallel.
397
+ * Run `work` under the per-stream close lock (#1222). Serializes close
398
+ * critical sections for the same stream (windowed prunes, and full closes
399
+ * since #1738) while letting different streams proceed in parallel.
400
400
  */
401
401
  private _with_close_lock;
402
402
  /** Drain lanes declared via `.withLane(...)`. Implicit default not included. */
@@ -70,9 +70,13 @@ export type CloseCycleDeps = {
70
70
  * same prefix — a double S3 upload / double JSONL append. This runs the
71
71
  * given work under a process-local per-stream lock so the two closers
72
72
  * serialize; the second sees the already-pruned prefix and skips its
73
- * archive. Provided by the Act orchestrator (shared across `app.close`
74
- * and the drain's `on_close`); defaults to identity (no serialization)
75
- * when the cycle is exercised in isolation.
73
+ * archive. The full (tombstone/restart) path takes the same lock for
74
+ * every stream it closes: without it, a second full closer that arrives
75
+ * while the first is archiving mistakes the first closer's tombstone for
76
+ * an interrupted close, resumes it, and both archive and truncate (#1738).
77
+ * Provided by the Act orchestrator (shared across `app.close` and the
78
+ * drain's `on_close`); defaults to identity (no serialization) when the
79
+ * cycle is exercised in isolation.
76
80
  */
77
81
  readonly with_stream_lock?: <T>(stream: string, work: () => Promise<T>) => Promise<T>;
78
82
  };
@@ -1 +1 @@
1
- {"version":3,"file":"close-cycle.d.ts","sourceRoot":"","sources":["../../../src/internal/close-cycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,MAAM,EAEN,KAAK,EACN,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IACnE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,oBAAoB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAClE;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,EAC5B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KACnB,OAAO,CAAC,CAAC,CAAC,CAAC;CACjB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAuB3C;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,WAAW,EAAE,EACtB,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,WAAW,CAAC,CA+FtB"}
1
+ {"version":3,"file":"close-cycle.d.ts","sourceRoot":"","sources":["../../../src/internal/close-cycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,MAAM,EAEN,KAAK,EACN,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,cAAc,EAAE,WAAW,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IACnE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,oBAAoB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAClE;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC,EAC5B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KACnB,OAAO,CAAC,CAAC,CAAC,CAAC;CACjB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAuB3C;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,WAAW,EAAE,EACtB,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,WAAW,CAAC,CA0BtB"}
package/dist/index.cjs CHANGED
@@ -2720,6 +2720,23 @@ async function run_close_cycle(targets, deps) {
2720
2720
  const skipped = [];
2721
2721
  const windowed_result = windowed.length ? await run_windowed_closes(windowed, deps, skipped) : /* @__PURE__ */ new Map();
2722
2722
  if (!full.length) return { truncated: windowed_result, skipped };
2723
+ const truncated = await with_stream_locks(
2724
+ full.map((t) => t.stream),
2725
+ deps,
2726
+ () => run_full_closes(full, target_map, deps, skipped)
2727
+ );
2728
+ for (const [stream, entry] of windowed_result) truncated.set(stream, entry);
2729
+ return { truncated, skipped };
2730
+ }
2731
+ function with_stream_locks(streams, deps, work) {
2732
+ const with_lock = deps.with_stream_lock ?? ((_stream, w) => w());
2733
+ return [...streams].sort().reduceRight(
2734
+ (inner, stream) => () => with_lock(stream, inner),
2735
+ work
2736
+ )();
2737
+ }
2738
+ async function run_full_closes(full, target_map, deps, skipped) {
2739
+ const none = /* @__PURE__ */ new Map();
2723
2740
  const streams = full.map((t) => t.stream);
2724
2741
  const stream_info = await scan_stream_heads(streams);
2725
2742
  for (const target of full) {
@@ -2742,7 +2759,7 @@ async function run_close_cycle(targets, deps) {
2742
2759
  deps.probe_page_size ?? SAFETY_PROBE_PAGE_SIZE,
2743
2760
  deps.catch_up_correlation
2744
2761
  );
2745
- if (!safe.length) return { truncated: windowed_result, skipped };
2762
+ if (!safe.length) return none;
2746
2763
  const { guarded, guard_events } = await guard_with_tombstones(
2747
2764
  safe,
2748
2765
  stream_info,
@@ -2750,7 +2767,7 @@ async function run_close_cycle(targets, deps) {
2750
2767
  deps.tombstone,
2751
2768
  skipped
2752
2769
  );
2753
- if (!guarded.length) return { truncated: windowed_result, skipped };
2770
+ if (!guarded.length) return none;
2754
2771
  const seed_states = await load_restart_seeds(
2755
2772
  guarded,
2756
2773
  target_map,
@@ -2760,14 +2777,18 @@ async function run_close_cycle(targets, deps) {
2760
2777
  deps.logger
2761
2778
  );
2762
2779
  await run_archive_callbacks(guarded, target_map);
2763
- const truncated = await truncate_and_warm_cache(
2780
+ const still_guarded = await heads_still_guarded(
2764
2781
  guarded,
2782
+ guard_events,
2783
+ skipped
2784
+ );
2785
+ if (!still_guarded.length) return none;
2786
+ return truncate_and_warm_cache(
2787
+ still_guarded,
2765
2788
  seed_states,
2766
2789
  guard_events,
2767
2790
  deps.correlation
2768
2791
  );
2769
- for (const [stream, entry] of windowed_result) truncated.set(stream, entry);
2770
- return { truncated, skipped };
2771
2792
  }
2772
2793
  async function run_windowed_closes(windowed, deps, skipped) {
2773
2794
  const checkpoint = deps.reactive_events_size > 0 ? await deps.catch_up_correlation(Number.MAX_SAFE_INTEGER) : -1;
@@ -2957,6 +2978,15 @@ async function run_archive_callbacks(guarded, target_map) {
2957
2978
  if (archive_fn) await archive_fn();
2958
2979
  }
2959
2980
  }
2981
+ async function heads_still_guarded(guarded, guard_events, skipped) {
2982
+ const heads = await store2().query_stats(guarded, {});
2983
+ return guarded.filter((stream) => {
2984
+ if (heads.get(stream)?.head.id === guard_events.get(stream).id)
2985
+ return true;
2986
+ skipped.push(stream);
2987
+ return false;
2988
+ });
2989
+ }
2960
2990
  async function truncate_and_warm_cache(guarded, seed_states, guard_events, correlation) {
2961
2991
  const trunc_targets = guarded.map((stream) => {
2962
2992
  const snapshot = seed_states.get(stream);
@@ -5231,9 +5261,9 @@ var Act = class {
5231
5261
  */
5232
5262
  _close_locks = /* @__PURE__ */ new Map();
5233
5263
  /**
5234
- * Run `work` under the per-stream close lock (#1222). Serializes
5235
- * windowed-close critical sections for the same stream while letting
5236
- * different streams proceed in parallel.
5264
+ * Run `work` under the per-stream close lock (#1222). Serializes close
5265
+ * critical sections for the same stream (windowed prunes, and full closes
5266
+ * since #1738) while letting different streams proceed in parallel.
5237
5267
  */
5238
5268
  _with_close_lock(stream, work) {
5239
5269
  const prev = this._close_locks.get(stream) ?? Promise.resolve();