@rotorsoft/act 1.32.19 → 1.32.21

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
@@ -1665,13 +1665,14 @@ var CorrelateCycle = class {
1665
1665
  cd,
1666
1666
  max_subscribed_streams,
1667
1667
  declared_lanes,
1668
+ shard,
1668
1669
  on_init,
1669
1670
  on_init_async,
1670
1671
  cold_start_back_scan = DEFAULT_COLD_START_BACK_SCAN,
1671
1672
  lease_millis = DEFAULT_CORRELATION_LEASE_MS
1672
1673
  }) {
1673
1674
  this._lease_millis = lease_millis;
1674
- this._key = registry_key(registry.events);
1675
+ this._key = shard ? `${registry_key(registry.events)}:${[...shard].sort().join(",")}` : registry_key(registry.events);
1675
1676
  this._dynamic_subscriptions = new LruMap(max_subscribed_streams);
1676
1677
  this._registry = registry;
1677
1678
  this._declared_lanes = declared_lanes;
@@ -1838,6 +1839,8 @@ var CorrelateCycle = class {
1838
1839
  priority: carried.priority,
1839
1840
  lane: carried.lane,
1840
1841
  upgraded,
1842
+ missed: !recorded,
1843
+ handler: reaction.handler.name,
1841
1844
  correlated_at: void 0
1842
1845
  };
1843
1846
  const seen_in_scan = correlated.has(resolved.target);
@@ -1856,6 +1859,7 @@ var CorrelateCycle = class {
1856
1859
  entry.priority = carried.priority;
1857
1860
  entry.lane = carried.lane;
1858
1861
  entry.upgraded = upgraded;
1862
+ entry.handler = reaction.handler.name;
1859
1863
  }
1860
1864
  if (this._in_fetch_window(resolved.source, event.stream))
1861
1865
  entry.correlated_at = event.id;
@@ -1869,6 +1873,7 @@ var CorrelateCycle = class {
1869
1873
  // actor-less, so plaintext here would be un-gated disclosure.
1870
1874
  { ...query, after, with_pii: false }
1871
1875
  );
1876
+ if (this._declared_lanes.size > 1) await this._recall(correlated);
1872
1877
  const streams = [];
1873
1878
  for (const [stream, entry] of correlated) {
1874
1879
  if (entry.upgraded || entry.correlated_at !== void 0)
@@ -1907,6 +1912,50 @@ var CorrelateCycle = class {
1907
1912
  this._armed = found === query.limit;
1908
1913
  return { subscribed: 0, last_id, marked: 0, scanned: true };
1909
1914
  }
1915
+ /**
1916
+ * Give each target this scan had no record of the record its row holds.
1917
+ *
1918
+ * A target with no record is either new or forgotten. Its row, when it
1919
+ * has one, says what lane and priority it was subscribed at; a resolution
1920
+ * that doesn't beat that priority carries the row's values, exactly as if
1921
+ * the record had never been evicted. One read per target with no record,
1922
+ * and only when more than one lane is declared: with one lane there is no
1923
+ * lane to lose, and the store already keeps the higher priority.
1924
+ */
1925
+ async _recall(correlated) {
1926
+ const missed = [...correlated].filter(([, entry]) => entry.missed);
1927
+ await Promise.all(
1928
+ missed.map(async ([stream, entry]) => {
1929
+ let row;
1930
+ await store().query_streams(
1931
+ (position) => {
1932
+ row = position;
1933
+ },
1934
+ { stream, stream_exact: true, limit: 1 }
1935
+ );
1936
+ if (!row) return;
1937
+ this._dynamic_subscriptions.set(stream, {
1938
+ floor: row.priority,
1939
+ priority: row.priority,
1940
+ lane: row.lane
1941
+ });
1942
+ if (entry.priority > row.priority) return;
1943
+ const held_lane = row.lane ?? DEFAULT_LANE;
1944
+ const resolved_lane = entry.lane ?? DEFAULT_LANE;
1945
+ if (entry.priority === row.priority && held_lane !== resolved_lane)
1946
+ report_lane_conflict(
1947
+ this._reported,
1948
+ entry.handler,
1949
+ stream,
1950
+ held_lane,
1951
+ resolved_lane
1952
+ );
1953
+ entry.priority = row.priority;
1954
+ entry.lane = row.lane;
1955
+ entry.upgraded = false;
1956
+ })
1957
+ );
1958
+ }
1910
1959
  /**
1911
1960
  * Hand the correlation lease back early.
1912
1961
  *
@@ -2066,9 +2115,8 @@ var DeferTimer = class {
2066
2115
  _timer;
2067
2116
  _on_wake;
2068
2117
  /**
2069
- * @param on_wake - invoked once each time the earliest due-time elapses,
2070
- * after the come-due entries have been removed. The consumer re-schedules
2071
- * for whatever is still parked.
2118
+ * @param on_wake - invoked once each time the earliest due-time elapses.
2119
+ * The consumer re-schedules for whatever is still parked.
2072
2120
  */
2073
2121
  constructor(on_wake) {
2074
2122
  this._on_wake = on_wake;
@@ -2098,6 +2146,16 @@ var DeferTimer = class {
2098
2146
  delete(stream) {
2099
2147
  this._due.delete(stream);
2100
2148
  }
2149
+ /**
2150
+ * Drop every entry due at or before `before`. The drain calls it when a
2151
+ * pass that started at `before` claimed nothing: whatever was due by then
2152
+ * was handled elsewhere (another worker) or has no work left, so waking
2153
+ * for it again would spin.
2154
+ */
2155
+ prune(before) {
2156
+ for (const [stream, at] of this._due)
2157
+ if (at <= before) this._due.delete(stream);
2158
+ }
2101
2159
  /**
2102
2160
  * (Re)schedule the wake timer at the earliest pending due-time. Idempotent
2103
2161
  * — collapses many parked streams into a single timer. A no-op clears any
@@ -2120,9 +2178,6 @@ var DeferTimer = class {
2120
2178
  );
2121
2179
  this._timer = setTimeout(() => {
2122
2180
  this._timer = void 0;
2123
- const now = Date.now();
2124
- for (const [stream, at] of this._due)
2125
- if (at <= now) this._due.delete(stream);
2126
2181
  this._on_wake();
2127
2182
  }, delay);
2128
2183
  this._timer.unref();
@@ -2891,6 +2946,9 @@ var DrainController = class {
2891
2946
  * schedule itself is persisted in the store; this runs the drain at the
2892
2947
  * earliest pending visit. A parked stream already carries its mark, so
2893
2948
  * the drain alone finds it, and on an idle app nothing else would.
2949
+ * Every drain re-aims it when it ends, and a due entry stays until a pass
2950
+ * handles it or a pass that started after it came due claims nothing, so
2951
+ * the wake keeps draining until every due stream is reached.
2894
2952
  * `stop()` cancels the timer and a stopped drain never re-schedules it,
2895
2953
  * so a wake never runs after a stop.
2896
2954
  */
@@ -2988,15 +3046,16 @@ var DrainController = class {
2988
3046
  const streamLimit = d.streamLimit ?? options.streamLimit ?? DEFAULT_STREAM_LIMIT;
2989
3047
  const eventLimit = d.eventLimit ?? options.eventLimit ?? DEFAULT_EVENT_LIMIT;
2990
3048
  const leaseMillis = d.leaseMillis ?? options.leaseMillis ?? DEFAULT_LEASE_MILLIS;
3049
+ if (this._deps.breaker.state(Date.now()) === "open")
3050
+ return EMPTY_DRAIN;
2991
3051
  try {
2992
- if (this._deps.breaker.state(Date.now()) === "open")
2993
- return EMPTY_DRAIN;
2994
3052
  this._locked = true;
2995
3053
  this._inflight = new Promise((done) => {
2996
3054
  this._inflight_done = done;
2997
3055
  });
2998
3056
  const lagging = Math.ceil(streamLimit * this._ratio);
2999
3057
  const leading = streamLimit - lagging;
3058
+ const started = Date.now();
3000
3059
  const cycle = await run_drain_cycle(this._deps, {
3001
3060
  lagging,
3002
3061
  leading,
@@ -3006,6 +3065,7 @@ var DrainController = class {
3006
3065
  store_failing: this._deps.breaker.failing
3007
3066
  });
3008
3067
  if (!cycle) {
3068
+ this._defer.prune(started);
3009
3069
  this._deps.breaker.passed();
3010
3070
  this._armed = false;
3011
3071
  return EMPTY_DRAIN;
@@ -3671,6 +3731,8 @@ var Act = class {
3671
3731
  // Declared lanes (not active ones): a lane another process claims is
3672
3732
  // still a valid destination.
3673
3733
  declared_lanes: new Set(this._declared_lanes),
3734
+ // Built after the controllers, so their keys are the active lanes.
3735
+ shard: this._drain_controllers.size < this._declared_lanes.length ? [...this._drain_controllers.keys()] : void 0,
3674
3736
  on_init: () => {
3675
3737
  if (this._drain && this._reactive_events.size > 0) this._arm_all();
3676
3738
  },
@@ -4367,11 +4429,11 @@ var Act = class {
4367
4429
  });
4368
4430
  }
4369
4431
  /**
4370
- * Atomically wipe the store and rebuild it from an async stream of
4371
- * committed events. The framework owns iteration, validation,
4372
- * `drop_snapshots` filtering, `on_progress`, and the per-call
4373
- * `old → new` causation remap; the adapter's {@link Store.restore}
4374
- * driver supplies the transaction lifecycle and per-event insert.
4432
+ * Atomically wipe the store and rebuild it from an event source. The
4433
+ * framework owns iteration, validation, `drop_snapshots` filtering,
4434
+ * `on_progress`, and the per-call `old → new` causation remap; the
4435
+ * adapter's {@link Store.restore} driver supplies the transaction
4436
+ * lifecycle and per-event insert.
4375
4437
  *
4376
4438
  * Throws if the adapter has no restore capability. Throws on the
4377
4439
  * first invalid event (negative version, malformed `created`) with
@@ -4379,33 +4441,30 @@ var Act = class {
4379
4441
  * the adapter means a failing restore leaves the store byte-for-byte
4380
4442
  * unchanged.
4381
4443
  *
4382
- * @param source - Async stream of events in target order. Streamed
4383
- * rather than buffered so multi-million-event backups don't OOM.
4384
- * Each event's original `id` is used as a causation lookup key but
4385
- * never written through — adapters renumber densely.
4444
+ * @param source - Any {@link EventSource}: a `CsvFile` backup or
4445
+ * another store. Read in batches rather than buffered, so
4446
+ * multi-million-event backups don't OOM. Each event's original `id`
4447
+ * is used as a causation lookup key but never written through —
4448
+ * adapters renumber densely.
4386
4449
  * @param opts - {@link ScanOptions}. `drop_snapshots` skips
4387
4450
  * `__snapshot__` events (counted in the result); `on_progress`
4388
4451
  * fires once per event.
4452
+ * @param sink - Where the events land. Defaults to this Act's store.
4389
4453
  * @returns {@link ScanResult} with `kept`, `duration_ms`, and
4390
4454
  * `dropped` per-category counters.
4391
4455
  *
4392
- * @example Round-trip a CSV backup
4456
+ * @example Restore a CSV backup into the connected store
4393
4457
  * ```typescript
4394
- * async function* parseCsv(blob: string) {
4395
- * for (const line of blob.split("\n").slice(1)) {
4396
- * const [id, name, data, stream, version, created, meta] = parse(line);
4397
- * yield {
4398
- * id: +id, name, data: JSON.parse(data), stream,
4399
- * version: +version, created: new Date(created),
4400
- * meta: JSON.parse(meta),
4401
- * };
4402
- * }
4403
- * }
4404
- * const result = await app.restore(parseCsv(csvBlob), {});
4458
+ * const result = await app.restore(new CsvFile({ path: "./backup.csv" }));
4405
4459
  * console.log(`Restored ${result.kept} events in ${result.duration_ms}ms`);
4406
4460
  * await cache().clear(); // operator's responsibility
4407
4461
  * ```
4408
4462
  *
4463
+ * @example Copy one store into another
4464
+ * ```typescript
4465
+ * await app.restore(pg, {}, sqlite);
4466
+ * ```
4467
+ *
4409
4468
  * @see {@link Store.restore} for the underlying driver-pattern primitive.
4410
4469
  */
4411
4470
  async restore(source, opts = {}, sink) {