@rotorsoft/act 1.32.22 → 1.32.24

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
@@ -1142,25 +1142,19 @@ var InMemoryStore = class {
1142
1142
  */
1143
1143
  async defer(input, deferred_at) {
1144
1144
  await sleep();
1145
- let count = 0;
1146
- if (Array.isArray(input)) {
1147
- for (const name of new Set(input)) {
1148
- const s = this._streams.get(name);
1149
- if (s) {
1150
- s.defer(deferred_at);
1151
- count++;
1152
- }
1153
- }
1154
- } else {
1155
- const matches = this._filter_predicate(input);
1156
- for (const s of this._streams.values()) {
1157
- if (matches(s)) {
1158
- s.defer(deferred_at);
1159
- count++;
1160
- }
1161
- }
1162
- }
1163
- return count;
1145
+ const selected = this._select(input);
1146
+ for (const s of selected) s.defer(deferred_at);
1147
+ return selected.length;
1148
+ }
1149
+ /**
1150
+ * The streams named by `input`: an explicit list (de-duplicated, so a
1151
+ * repeated name counts once, matching the SQL adapters' set-based
1152
+ * `stream = ANY(...)`; unknown names are skipped) or a {@link StreamFilter}.
1153
+ */
1154
+ _select(input) {
1155
+ if (Array.isArray(input))
1156
+ return [...new Set(input)].map((name) => this._streams.get(name)).filter((s) => s !== void 0);
1157
+ return [...this._streams.values()].filter(this._filter_predicate(input));
1164
1158
  }
1165
1159
  /**
1166
1160
  * Build a predicate from a {@link StreamFilter}. Compiled regexes are
@@ -1196,37 +1190,10 @@ var InMemoryStore = class {
1196
1190
  */
1197
1191
  async reset(input) {
1198
1192
  await sleep();
1199
- let count = 0;
1200
- if (Array.isArray(input)) {
1201
- for (const name of new Set(input)) {
1202
- const s = this._streams.get(name);
1203
- if (s) {
1204
- s.reset();
1205
- count++;
1206
- }
1207
- }
1208
- } else {
1209
- const matches = this._filter_predicate(input);
1210
- for (const s of this._streams.values()) {
1211
- if (matches(s)) {
1212
- s.reset();
1213
- count++;
1214
- }
1215
- }
1216
- }
1217
- return count;
1193
+ const selected = this._select(input);
1194
+ for (const s of selected) s.reset();
1195
+ return selected.length;
1218
1196
  }
1219
- /**
1220
- * Clear the blocked flag (and retry / error / lease) on the matched
1221
- * streams without touching the watermark. Streams that aren't blocked
1222
- * at call time are silently skipped. Accepts either an explicit list
1223
- * of names or a {@link StreamFilter}. The filter form always restricts
1224
- * to blocked streams — passing `blocked: false` matches nothing.
1225
- * See {@link Store.unblock}.
1226
- *
1227
- * @param input - Stream names or a filter selecting the streams to unblock.
1228
- * @returns Count of streams that were actually flipped (were blocked).
1229
- */
1230
1197
  /**
1231
1198
  * Wipe the sensitive-data payload for every event on the stream — see
1232
1199
  * {@link Store.forget_pii}. O(1) drop of the stream's inner Map; the size of
@@ -1242,21 +1209,23 @@ var InMemoryStore = class {
1242
1209
  this._pii.delete(stream);
1243
1210
  return count;
1244
1211
  }
1212
+ /**
1213
+ * Clear the blocked flag (and retry / error / lease) on the matched
1214
+ * streams without touching the watermark. Streams that aren't blocked
1215
+ * at call time are silently skipped. Accepts either an explicit list
1216
+ * of names or a {@link StreamFilter}. The filter form always restricts
1217
+ * to blocked streams — passing `blocked: false` matches nothing.
1218
+ * See {@link Store.unblock}.
1219
+ *
1220
+ * @param input - Stream names or a filter selecting the streams to unblock.
1221
+ * @returns Count of streams that were actually flipped (were blocked).
1222
+ */
1245
1223
  async unblock(input) {
1246
1224
  await sleep();
1247
- let count = 0;
1248
- if (Array.isArray(input)) {
1249
- for (const name of input) {
1250
- const s = this._streams.get(name);
1251
- if (s?.unblock()) count++;
1252
- }
1253
- } else {
1254
- const matches = this._filter_predicate({ ...input, blocked: true });
1255
- for (const s of this._streams.values()) {
1256
- if (matches(s) && s.unblock()) count++;
1257
- }
1258
- }
1259
- return count;
1225
+ const selected = this._select(
1226
+ Array.isArray(input) ? input : { ...input, blocked: true }
1227
+ );
1228
+ return selected.filter((s) => s.unblock()).length;
1260
1229
  }
1261
1230
  /**
1262
1231
  * Bulk-update priority of streams matching `filter`. Mirrors
@@ -1269,16 +1238,9 @@ var InMemoryStore = class {
1269
1238
  */
1270
1239
  async prioritize(filter, priority) {
1271
1240
  await sleep();
1272
- const matches = this._filter_predicate(filter);
1273
- let count = 0;
1274
- for (const s of this._streams.values()) {
1275
- if (!matches(s)) continue;
1276
- if (s.priority !== priority) {
1277
- s.set_priority(priority);
1278
- count++;
1279
- }
1280
- }
1281
- return count;
1241
+ const changed = this._select(filter).filter((s) => s.priority !== priority);
1242
+ for (const s of changed) s.set_priority(priority);
1243
+ return changed.length;
1282
1244
  }
1283
1245
  /**
1284
1246
  * Streams registered subscription positions to the callback, ordered by
@@ -1289,10 +1251,8 @@ var InMemoryStore = class {
1289
1251
  await sleep();
1290
1252
  const limit = query?.limit ?? 100;
1291
1253
  const after = query?.after;
1292
- const blocked = query?.blocked;
1293
1254
  const source_matches = query?.source_matches;
1294
- const stream_re = query?.stream !== void 0 && !query.stream_exact ? new RegExp(query.stream) : void 0;
1295
- const source_re = query?.source !== void 0 && !query.source_exact ? new RegExp(query.source) : void 0;
1255
+ const matches = this._filter_predicate(query ?? {});
1296
1256
  const reverse_cache = /* @__PURE__ */ new Map();
1297
1257
  const reverse_match = (source) => {
1298
1258
  let re = reverse_cache.get(source);
@@ -1306,20 +1266,10 @@ var InMemoryStore = class {
1306
1266
  let count = 0;
1307
1267
  for (const s of sorted) {
1308
1268
  if (after !== void 0 && s.stream <= after) continue;
1309
- if (query?.stream !== void 0) {
1310
- if (query.stream_exact ? s.stream !== query.stream : !stream_re.test(s.stream))
1311
- continue;
1312
- }
1313
- if (query?.source !== void 0) {
1314
- if (s.source === void 0) continue;
1315
- if (query.source_exact ? s.source !== query.source : !source_re.test(s.source))
1316
- continue;
1317
- }
1269
+ if (!matches(s)) continue;
1318
1270
  if (source_matches !== void 0) {
1319
1271
  if (s.source && !reverse_match(s.source)) continue;
1320
1272
  }
1321
- if (blocked !== void 0 && s.blocked !== blocked) continue;
1322
- if (query?.lane !== void 0 && s.lane !== query.lane) continue;
1323
1273
  if (count >= limit) break;
1324
1274
  callback({
1325
1275
  stream: s.stream,
@@ -3178,71 +3128,27 @@ function report_lane_conflict(seen, handler, target, kept, dropped) {
3178
3128
  var CorrelateCycle = class {
3179
3129
  _checkpoint = -1;
3180
3130
  _initialized = false;
3181
- /**
3182
- * This worker's identity for the correlation lease. A per-instance
3183
- * UUID, matching the drain's convention, so a renewal is recognised as the
3184
- * same holder and a restarted process never inherits a stale claim.
3185
- */
3131
+ /** Lease holder id; per instance, so a restart never inherits a stale claim. */
3186
3132
  _by = (0, import_node_crypto.randomUUID)();
3187
- /**
3188
- * Which correlator this is. Computed once from the registry so identical
3189
- * workers share a lease and unrelated applications never do.
3190
- */
3191
3133
  _key;
3192
- /**
3193
- * When this worker's correlation lease runs out, as a local clock reading,
3194
- * or 0 when it holds none.
3195
- *
3196
- * Cached so the holder doesn't ask the store on every pass. A stale belief
3197
- * can only cause a duplicate scan, and marks are idempotent, so it costs
3198
- * work, never correctness.
3199
- */
3134
+ /** Local expiry of the held lease (0 = none); stale only costs a duplicate scan. */
3200
3135
  _lease_until = 0;
3201
- /**
3202
- * Whether a scan might find anything, like the drain's flag: a commit or
3203
- * notification raises it, a short page lowers it, and a disarmed scan
3204
- * returns without touching the store.
3205
- *
3206
- * Starts armed: the log may already hold events this process has never
3207
- * correlated, and only a scan can find out.
3208
- */
3136
+ /** Raised by a commit or notification, lowered by a short page. Starts armed. */
3209
3137
  _armed = true;
3210
- /** In-flight init, memoized for single-flight and cleared on failure. */
3211
3138
  _init_promise;
3212
- // Dynamically discovered targets → what each was last subscribed at,
3213
- // bounded by `maxSubscribedStreams`. It decides *what* a scan sends with a
3214
- // target: priority/lane only when the resolution beats the recorded floor,
3215
- // otherwise the row's own values. See {@link Subscription}.
3139
+ // Bounded by `maxSubscribedStreams`; static records live apart so eviction
3140
+ // never drops their +Infinity floor.
3216
3141
  _dynamic_subscriptions;
3217
- /**
3218
- * What each static target was subscribed at by `init`. A plain map, never
3219
- * evicted: the collection is the build-time `_static_targets` list, so it
3220
- * is already bounded by the registry and costs nothing the registry does
3221
- * not already hold.
3222
- *
3223
- * Kept out of the LRU so eviction can never drop a static target's
3224
- * `+Infinity` floor and let a dynamic resolution re-lane it.
3225
- */
3226
3142
  _static_subscriptions = /* @__PURE__ */ new Map();
3227
- /** Compiled pattern sources, bounded by {@link PATTERN_CACHE_SIZE}. */
3228
3143
  _patterns = new LruMap(PATTERN_CACHE_SIZE);
3229
3144
  _registry;
3230
3145
  _static_targets;
3231
3146
  _cd;
3232
3147
  _on_init;
3233
- /**
3234
- * Async cold-start hook, run once after `on_init` inside `init()`. The
3235
- * orchestrator re-seeds its defer timers from the persisted `deferred_at`
3236
- * here, so an idle deferred stream re-arms across a restart.
3237
- */
3148
+ /** Runs once after `on_init`; re-seeds defer timers across a restart. */
3238
3149
  _on_init_async;
3239
- /** Lanes a controller exists for. See {@link CorrelateCycleDeps}. */
3240
3150
  _declared_lanes;
3241
- /**
3242
- * Offending declarations already reported, so a resolver firing for every
3243
- * matching event reports once. Owned by the instance rather than the
3244
- * module: `internal/` holds no module-level state.
3245
- */
3151
+ /** Report-once keys; per instance because `internal/` holds no module state. */
3246
3152
  _reported = /* @__PURE__ */ new Set();
3247
3153
  constructor({
3248
3154
  registry,
@@ -3263,28 +3169,13 @@ var CorrelateCycle = class {
3263
3169
  this._on_init = on_init;
3264
3170
  this._on_init_async = on_init_async;
3265
3171
  }
3266
- /** Last correlated event id. */
3267
3172
  get checkpoint() {
3268
3173
  return this._checkpoint;
3269
3174
  }
3270
- /**
3271
- * Signal that a commit (local or remote) may have produced events this
3272
- * process has not correlated. Cheap and idempotent — the orchestrator calls
3273
- * it on every commit and every notification.
3274
- */
3175
+ /** A commit (local or remote) may have produced uncorrelated events. */
3275
3176
  arm() {
3276
3177
  this._armed = true;
3277
3178
  }
3278
- /**
3279
- * Initialize correlation state on first call.
3280
- * - Reads the durable correlate checkpoint (and max(at)) from the store,
3281
- * flooring a first boot at `watermark - back_scan` so an event
3282
- * committed-but-not-correlated before a crash is re-scanned on
3283
- * restart instead of skipped
3284
- * - Subscribes static resolver targets (idempotent upsert)
3285
- * - Populates the subscribed-streams LRU
3286
- * - Fires `on_init` once (Act uses this to flag a cold-start drain)
3287
- */
3288
3179
  async init() {
3289
3180
  if (this._initialized) return;
3290
3181
  if (!this._init_promise) {
@@ -3311,16 +3202,7 @@ var CorrelateCycle = class {
3311
3202
  }
3312
3203
  await this._on_init_async?.();
3313
3204
  }
3314
- /**
3315
- * Start correlation over after the event log was replaced wholesale
3316
- * (`restore`). The restored log is renumbered from the start and its
3317
- * subscription rows are gone, so the in-memory scan position and both
3318
- * subscription records describe a log that no longer exists. Dropping them
3319
- * and the init latch makes the next pass run cold-start again: static
3320
- * targets are re-subscribed, the checkpoint is re-read from the store
3321
- * (which `restore` reset), and dynamic targets are rediscovered by the
3322
- * scan.
3323
- */
3205
+ /** After `restore` replaced the log: forget everything and cold-start again. */
3324
3206
  restart() {
3325
3207
  this._initialized = false;
3326
3208
  this._init_promise = void 0;
@@ -3329,21 +3211,11 @@ var CorrelateCycle = class {
3329
3211
  this._static_subscriptions.clear();
3330
3212
  this._armed = true;
3331
3213
  }
3332
- /**
3333
- * Forget dynamic targets whose subscription rows no longer exist (a full
3334
- * close deletes the row), so a later scan re-subscribes them. Static
3335
- * targets keep their records.
3336
- */
3214
+ /** Forget dynamic targets whose rows a full close deleted, so they re-subscribe. */
3337
3215
  forget_subscribed(streams) {
3338
3216
  for (const stream of streams) this._dynamic_subscriptions.delete(stream);
3339
3217
  }
3340
- /**
3341
- * Would an event from `stream` be fetched for a target subscribed with
3342
- * `source`? The subscription's source is the filter `fetch` queries with
3343
- * — literal names by equality, patterns compiled as a `RegExp` — so an
3344
- * event outside it is not work for that target and must not mark it.
3345
- * No source means the target consumes every stream.
3346
- */
3218
+ /** Would the target's own `fetch` (filtered by `source`) return this event? */
3347
3219
  _in_fetch_window(source, stream) {
3348
3220
  if (source === void 0 || source === stream) return true;
3349
3221
  if (is_literal_source(source)) return false;
@@ -3354,16 +3226,7 @@ var CorrelateCycle = class {
3354
3226
  }
3355
3227
  return pattern.test(stream);
3356
3228
  }
3357
- /**
3358
- * Scan the events past the checkpoint, resolve each to its target
3359
- * streams, and record what it found through `cd.subscribe` — new dynamic
3360
- * targets get registered, and every target an event resolved to gets its
3361
- * **work mark** raised to that event's id.
3362
- *
3363
- * Both resolver kinds are walked. A static target is already subscribed
3364
- * at init, but marking it is what makes it claimable without probing the
3365
- * event log, so the scan runs for every app.
3366
- */
3229
+ /** Scan past the checkpoint, subscribe new targets and raise their work marks. */
3367
3230
  async correlate(query = { after: -1, limit: 10 }, lease = false) {
3368
3231
  await this.init();
3369
3232
  if (!this._armed)
@@ -3448,10 +3311,8 @@ var CorrelateCycle = class {
3448
3311
  }
3449
3312
  }
3450
3313
  },
3451
- // Decline the sensitive payload: the scan only needs name/stream/id,
3452
- // and one undecryptable row would otherwise stop every stream's
3453
- // reactions. Resolvers therefore see `pii: null`; correlate runs
3454
- // actor-less, so plaintext here would be un-gated disclosure.
3314
+ // Resolvers see `pii: null`: correlate runs actor-less, and one
3315
+ // undecryptable row would otherwise stop every stream's reactions.
3455
3316
  { ...query, after, with_pii: false }
3456
3317
  );
3457
3318
  if (this._declared_lanes.size > 1) await this._recall(correlated);
@@ -3494,14 +3355,9 @@ var CorrelateCycle = class {
3494
3355
  return { subscribed: 0, last_id, marked: 0, scanned: true };
3495
3356
  }
3496
3357
  /**
3497
- * Give each target this scan had no record of the record its row holds.
3498
- *
3499
- * A target with no record is either new or forgotten. Its row, when it
3500
- * has one, says what lane and priority it was subscribed at; a resolution
3501
- * that doesn't beat that priority carries the row's values, exactly as if
3502
- * the record had never been evicted. One read per target with no record,
3503
- * and only when more than one lane is declared: with one lane there is no
3504
- * lane to lose, and the store already keeps the higher priority.
3358
+ * Read the row of each target this scan had no record of (new or evicted),
3359
+ * so a resolution that doesn't beat its priority keeps the row's lane.
3360
+ * Only called with more than one lane declared.
3505
3361
  */
3506
3362
  async _recall(correlated) {
3507
3363
  const missed = [...correlated].filter(([, entry]) => entry.missed);
@@ -3537,13 +3393,7 @@ var CorrelateCycle = class {
3537
3393
  })
3538
3394
  );
3539
3395
  }
3540
- /**
3541
- * Hand the correlation lease back early.
3542
- *
3543
- * The port has no release verb; re-acquiring as the same holder with a
3544
- * zero duration releases it. Best-effort: a failure costs the lease's
3545
- * remaining lifetime, as a crash would.
3546
- */
3396
+ /** Release the lease early by re-acquiring it for zero ms. Best-effort. */
3547
3397
  async release_correlation() {
3548
3398
  try {
3549
3399
  this._lease_until = 0;
@@ -4020,11 +3870,9 @@ async function load(me, target, callback, patch_fn = bare_patch) {
4020
3870
  {
4021
3871
  stream,
4022
3872
  stream_exact: true,
4023
- // `with_snaps` on every non-time-travel load. Cold, it resumes from the
4024
- // latest snapshot (this is the only place that decides it; stores just
4025
- // apply it). Warm, it keeps a snapshot above a stale checkpoint in the
4026
- // result, so a windowed close that pruned the events in between can't
4027
- // make the fold apply the tail onto stale state.
3873
+ // `with_snaps` on every non-time-travel load: cold, it resumes from the
3874
+ // latest snapshot; warm, it keeps a snapshot above a stale checkpoint,
3875
+ // so a windowed close's prune can't fold the tail onto stale state.
4028
3876
  ...cached ? { after: cached.event_id, with_snaps: true } : { ...time_travel ? {} : { with_snaps: true }, ...asOf }
4029
3877
  }
4030
3878
  );
@@ -4048,10 +3896,7 @@ async function load(me, target, callback, patch_fn = bare_patch) {
4048
3896
  event,
4049
3897
  state: state2,
4050
3898
  version: event?.version ?? cached?.version ?? -1,
4051
- // Head event id, captured atomically with `state`: the last replayed
4052
- // event's id on a cache miss, or the cached checkpoint's event_id on a
4053
- // warm hit with no new events. Consumers read this instead of a
4054
- // separate cache lookup that could race a concurrent commit.
3899
+ // Captured with `state`, so consumers never race a separate cache lookup.
4055
3900
  id: event?.id ?? cached?.event_id ?? -1,
4056
3901
  patches,
4057
3902
  snaps,
@@ -4113,8 +3958,7 @@ async function action(me, action2, target, payload, options, patch_fn = bare_pat
4113
3958
  action: {
4114
3959
  name: action2,
4115
3960
  ...target
4116
- // payload intentionally omitted from causation metadata —
4117
- // callers correlate via the correlation id when they need it.
3961
+ // payload intentionally omitted
4118
3962
  },
4119
3963
  event: reactingTo ? {
4120
3964
  id: reactingTo.id,
@@ -4517,61 +4361,35 @@ var DrainController = class {
4517
4361
  _locked = false;
4518
4362
  _ratio = 0.5;
4519
4363
  /**
4520
- * Local wake for streams this worker parked (backoff or defer). The
4521
- * schedule itself is persisted in the store; this runs the drain at the
4522
- * earliest pending visit. A parked stream already carries its mark, so
4523
- * the drain alone finds it, and on an idle app nothing else would.
4524
- * Every drain re-aims it when it ends, and a due entry stays until a pass
4525
- * handles it or a pass that started after it came due claims nothing, so
4526
- * the wake keeps draining until every due stream is reached.
4527
- * `stop()` cancels the timer and a stopped drain never re-schedules it,
4528
- * so a wake never runs after a stop.
4364
+ * Local wake at the earliest parked (backoff or defer) visit; the schedule
4365
+ * itself is persisted. On an idle app nothing else would drain it. A due
4366
+ * entry stays until handled or until a later pass claims nothing.
4529
4367
  */
4530
4368
  _defer = new DeferTimer(() => {
4531
4369
  this._armed = true;
4532
4370
  void this._deps.run_scoped(() => this.drain());
4533
4371
  });
4534
- /** Worker timer. Set when `start()` is active, undefined otherwise. */
4535
4372
  _worker;
4536
- /**
4537
- * Misroutings this controller has reported. A resolver returning a
4538
- * projection's target does so for every matching event; one line per event
4539
- * would bury the signal it exists to raise.
4540
- */
4541
4373
  _misrouted = /* @__PURE__ */ new Set();
4542
4374
  _stopped = false;
4543
- /**
4544
- * Resolves when the cycle in flight finishes, so a graceful shutdown can
4545
- * wait for it. Never rejects.
4546
- */
4375
+ /** Never rejects; graceful shutdown waits on it. */
4547
4376
  _inflight;
4548
4377
  _inflight_done;
4549
4378
  _deps;
4550
4379
  constructor(deps) {
4551
4380
  this._deps = deps;
4552
4381
  }
4553
- /**
4554
- * Signal that a commit (or reset / cold-start) may have produced work.
4555
- * Subsequent `drain()` calls will run the pipeline; once the pipeline
4556
- * settles to no-progress, the controller disarms itself.
4557
- */
4558
4382
  arm() {
4559
4383
  this._armed = true;
4560
4384
  }
4561
- /**
4562
- * Re-seed a persisted `deferred_at` into the local wake at cold start, so
4563
- * an idle deferred stream re-arms at its due-time with no new commit.
4564
- * Seeds collapse into one timer; the earliest wins.
4565
- */
4385
+ /** Cold start: re-seed a persisted `deferred_at` into the local wake. */
4566
4386
  seed_defer(stream, at) {
4567
4387
  this._defer.set(stream, at);
4568
4388
  this._defer.schedule();
4569
4389
  }
4570
- /** Read-only flag — true while a commit / reset is unprocessed. */
4571
4390
  get armed() {
4572
4391
  return this._armed;
4573
4392
  }
4574
- /** The cycle in flight, or `undefined` when idle. */
4575
4393
  get inflight() {
4576
4394
  return this._inflight;
4577
4395
  }
@@ -4579,19 +4397,10 @@ var DrainController = class {
4579
4397
  get lease_millis() {
4580
4398
  return this._deps.defaults?.leaseMillis;
4581
4399
  }
4582
- /** Lane this controller drains (undefined spans all lanes). */
4583
4400
  get lane() {
4584
4401
  return this._deps.lane;
4585
4402
  }
4586
- /**
4587
- * Start a per-lane worker that drains at the lane's `cycleMs`
4588
- * cadence. When armed, the worker calls `drain()` on every
4589
- * tick and re-schedules; when not armed, it still re-schedules at
4590
- * `cycleMs` so a future `arm()` is picked up on the next tick.
4591
- *
4592
- * The setTimeout chain uses `unref()` so it doesn't keep the process
4593
- * alive on its own.
4594
- */
4403
+ /** Drains every `cycleMs` while armed; the unref'd timer never holds the process. */
4595
4404
  start(cycleMs) {
4596
4405
  if (this._worker || this._stopped) return;
4597
4406
  const run = this._deps.run_scoped;
@@ -4604,7 +4413,6 @@ var DrainController = class {
4604
4413
  this._worker = setTimeout(tick, cycleMs);
4605
4414
  this._worker.unref();
4606
4415
  }
4607
- /** Stop the per-lane worker. Idempotent. */
4608
4416
  stop() {
4609
4417
  this._stopped = true;
4610
4418
  if (this._worker) {
@@ -4613,7 +4421,6 @@ var DrainController = class {
4613
4421
  }
4614
4422
  this._defer.stop();
4615
4423
  }
4616
- /** Run one drain pass. Short-circuits when not armed or already running. */
4617
4424
  async drain(options = {}) {
4618
4425
  if (!this._armed) return EMPTY_DRAIN;
4619
4426
  if (this._locked) return EMPTY_DRAIN;