@nmakarov/cli-toolkit 0.47.0 → 0.50.0

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.
@@ -2687,6 +2687,9 @@ async function init(flow2, opts = {}) {
2687
2687
  if (context) {
2688
2688
  await runRegisteredCleanups(context);
2689
2689
  }
2690
+ if (process.exitCode && process.exitCode !== 0) {
2691
+ process.exit(process.exitCode);
2692
+ }
2690
2693
  }
2691
2694
  }
2692
2695
 
@@ -3966,6 +3969,51 @@ async function dropLegacyTaskNameColumn(db, tableNames, { dryRun, log, label })
3966
3969
  }
3967
3970
  return actions;
3968
3971
  }
3972
+ async function enqueueTask(context, options) {
3973
+ const db = getDb(context);
3974
+ const queueName = options.queueName ?? "tasks";
3975
+ const { tasksTable } = queueToTableNames(queueName);
3976
+ const id = randomUUID();
3977
+ const name = options.name ?? options.task;
3978
+ if (!name) {
3979
+ throw new Error("enqueueTask: name (or task) is required");
3980
+ }
3981
+ const schedule = options.schedule?.trim() ? options.schedule : null;
3982
+ let nextRunAt = null;
3983
+ if (options.nextRunAt !== void 0) {
3984
+ nextRunAt = options.nextRunAt == null ? null : new Date(options.nextRunAt);
3985
+ } else if (schedule) {
3986
+ nextRunAt = nextTimeMatch(schedule, /* @__PURE__ */ new Date());
3987
+ }
3988
+ const row = {
3989
+ id,
3990
+ name,
3991
+ params: toJsonColumn(options.params ?? null),
3992
+ opid: options.opid ?? null,
3993
+ priority: options.priority ?? 50,
3994
+ schedule,
3995
+ next_run_at: nextRunAt,
3996
+ service_group: options.serviceGroup ?? null,
3997
+ instance_number: options.instanceNumber ?? null,
3998
+ service_name: options.serviceName ?? null,
3999
+ server_name: options.serverName ?? null,
4000
+ status: "idle",
4001
+ status_changed_at: db.fn.now()
4002
+ };
4003
+ if (await tableHasLegacyTaskColumn(db, tasksTable)) {
4004
+ row.task = name;
4005
+ }
4006
+ await db(tasksTable).insert(row);
4007
+ return id;
4008
+ }
4009
+ var legacyTaskColumnCache = /* @__PURE__ */ new Map();
4010
+ async function tableHasLegacyTaskColumn(db, tableName) {
4011
+ const key = `${db?.config?.name ?? "db"}:${tableName}`;
4012
+ if (!legacyTaskColumnCache.has(key)) {
4013
+ legacyTaskColumnCache.set(key, await db.schema.hasColumn(tableName, "task"));
4014
+ }
4015
+ return legacyTaskColumnCache.get(key);
4016
+ }
3969
4017
  async function updateTaskProgress(context, tasksTable, taskId, progress) {
3970
4018
  const db = getDb(context);
3971
4019
  await db(tasksTable).where({ id: taskId }).update({
@@ -3981,6 +4029,19 @@ function getDb2(context) {
3981
4029
  }
3982
4030
  return db;
3983
4031
  }
4032
+ function parseMetadataColumn(value) {
4033
+ if (!value) return {};
4034
+ if (typeof value === "object" && !Array.isArray(value)) return value;
4035
+ if (typeof value === "string") {
4036
+ try {
4037
+ const p = JSON.parse(value);
4038
+ return p && typeof p === "object" && !Array.isArray(value) ? p : {};
4039
+ } catch {
4040
+ return {};
4041
+ }
4042
+ }
4043
+ return {};
4044
+ }
3984
4045
  var DEFAULT_GROUP_MAX_INSTANCES = {
3985
4046
  intake: 1,
3986
4047
  harvest: 1,
@@ -4179,11 +4240,33 @@ async function touchServicesRegistry(context, registration) {
4179
4240
  pid
4180
4241
  });
4181
4242
  }
4243
+ async function updateServicesRegistryMetadata(context, registration, patch) {
4244
+ const db = getDb2(context);
4245
+ const row = await db(registration.registryTable).where({ id: registration.rowId }).first();
4246
+ const prev = parseMetadataColumn(row?.metadata);
4247
+ const merged = { ...prev, ...patch };
4248
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
4249
+ metadata: toJsonColumn(merged),
4250
+ last_seen_at: db.fn.now()
4251
+ });
4252
+ context.logger.info?.(`[services-registry] metadata updated for ${registration.serviceName}`);
4253
+ }
4182
4254
  async function unregisterServicesRegistry(context, registration) {
4183
4255
  const db = getDb2(context);
4184
4256
  await db(registration.registryTable).where({ id: registration.rowId }).delete();
4185
4257
  context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
4186
4258
  }
4259
+ async function listServicesRegistry(context, options = { queueName: "tasks" }) {
4260
+ const db = getDb2(context);
4261
+ const staleMs = options.staleMs ?? 6e4;
4262
+ const cutoff = new Date(Date.now() - staleMs);
4263
+ const table = queueToTableNames(options.queueName).registryTable;
4264
+ let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
4265
+ if (options.serviceGroup?.trim()) {
4266
+ q = q.where({ service_group: options.serviceGroup.trim() });
4267
+ }
4268
+ return await q;
4269
+ }
4187
4270
 
4188
4271
  // src/tasks/taskLogs.js
4189
4272
  import path4 from "path";
@@ -6142,6 +6225,273 @@ var TaskGetLogs = class extends AbstractTask {
6142
6225
  }
6143
6226
  };
6144
6227
 
6228
+ // src/tasks/runtimeParams.js
6229
+ var LOOP_RUNTIME_KEYS = ["maxParallel", "pollMs", "claimJitterMs", "scanLimit"];
6230
+ var LOGGER_RUNTIME_KEYS = [
6231
+ "levels",
6232
+ "silent",
6233
+ "showLevel",
6234
+ "timestamp",
6235
+ "mode",
6236
+ "route",
6237
+ "prefix",
6238
+ "progressWithTimes",
6239
+ "progressThrottleMs"
6240
+ ];
6241
+ var CONTROL_LANE_TASK_NAMES = ["stopRunner", "stop", "setRuntimeParam", "setRunnerParam"];
6242
+ function controlLaneTaskNames() {
6243
+ return [...CONTROL_LANE_TASK_NAMES];
6244
+ }
6245
+ function asPositiveInt(value, key, { min = 1 } = {}) {
6246
+ const n = Number(value);
6247
+ if (!Number.isFinite(n) || n < min) {
6248
+ throw new ParamError(
6249
+ `setRuntimeParam: ${key} must be a number >= ${min} (got ${JSON.stringify(value)})`
6250
+ );
6251
+ }
6252
+ return Math.floor(n);
6253
+ }
6254
+ function coerceRuntimeValue(key, value) {
6255
+ switch (key) {
6256
+ case "maxParallel":
6257
+ return asPositiveInt(value, key, { min: 1 });
6258
+ case "pollMs":
6259
+ return asPositiveInt(value, key, { min: 50 });
6260
+ case "claimJitterMs":
6261
+ return asPositiveInt(value, key, { min: 0 });
6262
+ case "scanLimit":
6263
+ return asPositiveInt(value, key, { min: 1 });
6264
+ case "silent":
6265
+ case "showLevel":
6266
+ case "timestamp":
6267
+ case "progressWithTimes":
6268
+ if (typeof value === "boolean") return value;
6269
+ if (value === "true" || value === "1") return true;
6270
+ if (value === "false" || value === "0") return false;
6271
+ throw new ParamError(
6272
+ `setRuntimeParam: ${key} must be boolean (got ${JSON.stringify(value)})`
6273
+ );
6274
+ case "progressThrottleMs":
6275
+ return asPositiveInt(value, key, { min: 0 });
6276
+ case "levels":
6277
+ case "mode":
6278
+ case "route":
6279
+ case "prefix":
6280
+ return value;
6281
+ default:
6282
+ return value;
6283
+ }
6284
+ }
6285
+ function ensureTasksRuntime(context, seed = {}) {
6286
+ if (!context.tasksRuntime || typeof context.tasksRuntime !== "object") {
6287
+ context.tasksRuntime = {};
6288
+ }
6289
+ const rt = context.tasksRuntime;
6290
+ if (rt.maxParallel === void 0) rt.maxParallel = seed.maxParallel ?? 32;
6291
+ if (rt.pollMs === void 0) rt.pollMs = seed.pollMs ?? 1e3;
6292
+ if (rt.claimJitterMs === void 0) rt.claimJitterMs = seed.claimJitterMs ?? 0;
6293
+ if (rt.scanLimit === void 0) rt.scanLimit = seed.scanLimit ?? 100;
6294
+ return rt;
6295
+ }
6296
+ async function applyRuntimeParam(context, key, value) {
6297
+ const k = String(key ?? "").trim();
6298
+ if (!k) throw new ParamError("setRuntimeParam: key is required");
6299
+ const runtime = ensureTasksRuntime(context);
6300
+ const next = coerceRuntimeValue(k, value);
6301
+ const previous = runtime[k];
6302
+ const applied = [];
6303
+ runtime[k] = next;
6304
+ applied.push("tasksRuntime");
6305
+ if (LOGGER_RUNTIME_KEYS.includes(k) && context.logger?.configure) {
6306
+ context.logger.configure({ [k]: next });
6307
+ applied.push("logger");
6308
+ }
6309
+ const hook = context.tasksRuntimeOnParam;
6310
+ if (typeof hook === "function") {
6311
+ await hook(k, next, runtime, context);
6312
+ applied.push("onRuntimeParam");
6313
+ }
6314
+ const reg = context.servicesRegistry;
6315
+ if (reg?.rowId && reg?.registryTable) {
6316
+ try {
6317
+ const loopSnapshot = {};
6318
+ for (const lk of LOOP_RUNTIME_KEYS) {
6319
+ if (runtime[lk] !== void 0) loopSnapshot[lk] = runtime[lk];
6320
+ }
6321
+ await updateServicesRegistryMetadata(context, reg, {
6322
+ runtime: loopSnapshot,
6323
+ runtimeUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
6324
+ });
6325
+ applied.push("servicesRegistry");
6326
+ } catch (err) {
6327
+ context.logger?.warn?.(
6328
+ `[setRuntimeParam] registry metadata update failed: ${err?.message ?? String(err)}`
6329
+ );
6330
+ }
6331
+ }
6332
+ return { key: k, previous, next, applied };
6333
+ }
6334
+ async function applyRuntimePatch(context, patch) {
6335
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
6336
+ throw new ParamError("setRuntimeParam: patch must be a plain object");
6337
+ }
6338
+ const entries = Object.entries(patch);
6339
+ if (entries.length === 0) {
6340
+ throw new ParamError("setRuntimeParam: patch is empty");
6341
+ }
6342
+ const out = [];
6343
+ for (const [key, value] of entries) {
6344
+ out.push(await applyRuntimeParam(context, key, value));
6345
+ }
6346
+ return out;
6347
+ }
6348
+ function readLoopRuntime(context) {
6349
+ const rt = ensureTasksRuntime(context);
6350
+ return {
6351
+ maxParallel: Math.max(1, Number(rt.maxParallel) || 1),
6352
+ pollMs: Math.max(50, Number(rt.pollMs) || 1e3),
6353
+ claimJitterMs: Math.max(0, Number(rt.claimJitterMs) || 0),
6354
+ scanLimit: Math.max(1, Number(rt.scanLimit) || 100)
6355
+ };
6356
+ }
6357
+
6358
+ // src/tasks/coreTasks/TaskSetRuntimeParam.js
6359
+ var TaskSetRuntimeParam = class extends AbstractTask {
6360
+ static defaultWaitForResult = true;
6361
+ /**
6362
+ * @param {object} context
6363
+ * @param {Record<string, unknown>} [overrides]
6364
+ * @returns {Promise<object>}
6365
+ */
6366
+ static async resolveParams(context, overrides = {}) {
6367
+ const main = await super.resolveParams(context, overrides);
6368
+ if (!main.serviceName && !main.serviceGroup) {
6369
+ throw new ParamError(
6370
+ "setRuntimeParam requires --serviceName (one instance) or --serviceGroup (broadcast to alive instances)"
6371
+ );
6372
+ }
6373
+ return main;
6374
+ }
6375
+ /**
6376
+ * @param {object} context
6377
+ * @param {Record<string, unknown>} [overrides]
6378
+ * @returns {Promise<{ key?: string, value?: unknown, patch?: Record<string, unknown> }>}
6379
+ */
6380
+ static async resolveCustomParams(context, overrides = {}) {
6381
+ const merged = AbstractTask._mergeTypedParams(
6382
+ context,
6383
+ "task-set-runtime-param",
6384
+ {
6385
+ paramKey: "string",
6386
+ paramValue: "string",
6387
+ key: "string",
6388
+ value: "string"
6389
+ },
6390
+ overrides
6391
+ );
6392
+ if (merged.patch && typeof merged.patch === "object" && !Array.isArray(merged.patch)) {
6393
+ if (Object.keys(merged.patch).length === 0) {
6394
+ throw new ParamError("setRuntimeParam: patch is empty");
6395
+ }
6396
+ return { patch: { ...merged.patch } };
6397
+ }
6398
+ const key = merged.paramKey || merged.key;
6399
+ const value = merged.paramValue !== void 0 ? merged.paramValue : merged.value;
6400
+ if (!key) {
6401
+ throw new ParamError(
6402
+ `setRuntimeParam requires --paramKey/--paramValue, or --paramsJson '{"key":"maxParallel","value":16}' / '{"patch":{...}}'`
6403
+ );
6404
+ }
6405
+ let parsed = value;
6406
+ if (typeof value === "string") {
6407
+ const t = value.trim();
6408
+ if (t === "true") parsed = true;
6409
+ else if (t === "false") parsed = false;
6410
+ else if (t !== "" && !Number.isNaN(Number(t)) && /^-?\d+(\.\d+)?$/.test(t)) {
6411
+ parsed = Number(t);
6412
+ } else if (t.startsWith("{") && t.endsWith("}") || t.startsWith("[") && t.endsWith("]") || t.startsWith('"') && t.endsWith('"')) {
6413
+ try {
6414
+ parsed = JSON.parse(t);
6415
+ } catch {
6416
+ parsed = value;
6417
+ }
6418
+ }
6419
+ }
6420
+ return { key: String(key), value: parsed };
6421
+ }
6422
+ /**
6423
+ * Enqueue one or many setRuntimeParam tasks. Prefer this over a bare
6424
+ * `enqueueTask` when broadcasting to a service group.
6425
+ *
6426
+ * @param {object} context
6427
+ * @param {Record<string, unknown>} [overrides]
6428
+ * @returns {Promise<{ ids: string[], targets: string[] }>}
6429
+ */
6430
+ static async enqueue(context, overrides = {}) {
6431
+ const payload = await this.resolveParams(context, { ...overrides, name: "setRuntimeParam" });
6432
+ const queueName = payload.queueName ?? "tasks";
6433
+ if (payload.serviceName) {
6434
+ const id = await enqueueTask(context, payload);
6435
+ return { ids: [id], targets: [payload.serviceName] };
6436
+ }
6437
+ const group = String(payload.serviceGroup || "").trim();
6438
+ if (!group) {
6439
+ throw new ParamError("setRuntimeParam.enqueue: serviceGroup required for broadcast");
6440
+ }
6441
+ const alive = await listServicesRegistry(context, {
6442
+ queueName,
6443
+ serviceGroup: group,
6444
+ staleMs: overrides.staleMs ?? 45e3
6445
+ });
6446
+ if (!alive.length) {
6447
+ throw new ParamError(
6448
+ `setRuntimeParam: no alive services in group="${group}" queue="${queueName}"`
6449
+ );
6450
+ }
6451
+ const ids = [];
6452
+ const targets = [];
6453
+ for (const reg of alive) {
6454
+ const id = await enqueueTask(context, {
6455
+ ...payload,
6456
+ serviceGroup: group,
6457
+ serviceName: reg.service_name,
6458
+ serverName: reg.server_name ?? null,
6459
+ instanceNumber: reg.instance_number ?? null
6460
+ });
6461
+ ids.push(id);
6462
+ targets.push(reg.service_name);
6463
+ }
6464
+ context.logger?.info?.(
6465
+ `[setRuntimeParam] broadcast to ${targets.length} instance(s) in group=${group}: ${targets.join(", ")}`
6466
+ );
6467
+ return { ids, targets };
6468
+ }
6469
+ /**
6470
+ * @returns {Promise<{ success: true, results: object }>}
6471
+ */
6472
+ async run() {
6473
+ const params = this.task?.params ?? {};
6474
+ let changes;
6475
+ if (params.patch && typeof params.patch === "object") {
6476
+ changes = await applyRuntimePatch(this.context, params.patch);
6477
+ } else {
6478
+ changes = [await applyRuntimeParam(this.context, params.key, params.value)];
6479
+ }
6480
+ const summary = changes.map((c) => `${c.key}: ${JSON.stringify(c.previous)} \u2192 ${JSON.stringify(c.next)}`);
6481
+ this.context.logger.warn?.(
6482
+ `[TaskSetRuntimeParam] applied on ${this.context.servicesRegistry?.serviceName ?? "runner"}: ${summary.join("; ")}`
6483
+ );
6484
+ return {
6485
+ success: true,
6486
+ results: {
6487
+ runtimeParamApplied: true,
6488
+ changes,
6489
+ runtime: { ...this.context.tasksRuntime ?? {} }
6490
+ }
6491
+ };
6492
+ }
6493
+ };
6494
+
6145
6495
  // src/tasks/TasksRegistry.js
6146
6496
  var TasksRegistry = class _TasksRegistry {
6147
6497
  /**
@@ -6160,7 +6510,7 @@ var TasksRegistry = class _TasksRegistry {
6160
6510
  * @returns {TasksRegistry}
6161
6511
  */
6162
6512
  static withCoreTasks() {
6163
- return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("info", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner).add("getLogs", TaskGetLogs);
6513
+ return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("info", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner).add("getLogs", TaskGetLogs).add("setRuntimeParam", TaskSetRuntimeParam).add("setRunnerParam", TaskSetRuntimeParam);
6164
6514
  }
6165
6515
  /**
6166
6516
  * Register a single task class under a name. Overwrites any previous entry.
@@ -6489,18 +6839,23 @@ async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry
6489
6839
  async function runTasksLoop(context, options) {
6490
6840
  const queueName = options.queueName ?? "tasks";
6491
6841
  const target = options.target;
6492
- const pollMs = options.pollMs ?? 1e3;
6493
- const claimJitterMs = options.claimJitterMs ?? 0;
6494
- const maxParallel = options.maxParallel ?? 32;
6495
- const scanLimit = options.scanLimit ?? 100;
6496
6842
  const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
6497
6843
  const registry = normalizeRegistry(options.registry);
6498
6844
  const { tasksTable, historyTable } = queueToTableNames(queueName);
6499
6845
  if (!target) throw new Error("runTasksLoop: target is required");
6500
6846
  context.tasksQueueName = queueName;
6847
+ ensureTasksRuntime(context, {
6848
+ maxParallel: options.maxParallel ?? 32,
6849
+ pollMs: options.pollMs ?? 1e3,
6850
+ claimJitterMs: options.claimJitterMs ?? 0,
6851
+ scanLimit: options.scanLimit ?? 100
6852
+ });
6853
+ if (typeof options.onRuntimeParam === "function") {
6854
+ context.tasksRuntimeOnParam = options.onRuntimeParam;
6855
+ }
6501
6856
  const runningPromises = /* @__PURE__ */ new Set();
6502
6857
  const runningTaskInstances = /* @__PURE__ */ new Map();
6503
- let runningStopControlPromise = null;
6858
+ let runningControlPromise = null;
6504
6859
  let stopRequested = false;
6505
6860
  let stopAllowanceMs = 5e3;
6506
6861
  context.tasksRunnerStop = false;
@@ -6511,9 +6866,16 @@ async function runTasksLoop(context, options) {
6511
6866
  if (hbGroup) {
6512
6867
  const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
6513
6868
  const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
6869
+ const loop0 = readLoopRuntime(context);
6514
6870
  const defaultMeta = {
6515
6871
  component: "tasks-runner",
6516
- allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
6872
+ allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all",
6873
+ runtime: {
6874
+ maxParallel: loop0.maxParallel,
6875
+ pollMs: loop0.pollMs,
6876
+ claimJitterMs: loop0.claimJitterMs,
6877
+ scanLimit: loop0.scanLimit
6878
+ }
6517
6879
  };
6518
6880
  registryReg = await registerInServicesRegistry(context, {
6519
6881
  queueName,
@@ -6526,6 +6888,7 @@ async function runTasksLoop(context, options) {
6526
6888
  enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
6527
6889
  metadata: options.runnerMetadata ?? defaultMeta
6528
6890
  });
6891
+ context.servicesRegistry = registryReg;
6529
6892
  runnerIdentity = {
6530
6893
  service_name: registryReg.serviceName,
6531
6894
  server_name: os3.hostname(),
@@ -6539,22 +6902,23 @@ async function runTasksLoop(context, options) {
6539
6902
  }
6540
6903
  try {
6541
6904
  while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
6542
- if (!runningStopControlPromise) {
6543
- const claimedStopTask = await claimNextRunnableTask(
6905
+ const { maxParallel, pollMs, claimJitterMs, scanLimit } = readLoopRuntime(context);
6906
+ if (!runningControlPromise) {
6907
+ const claimedControlTask = await claimNextRunnableTask(
6544
6908
  context,
6545
6909
  tasksTable,
6546
6910
  target,
6547
6911
  registry,
6548
6912
  10,
6549
- ["stopRunner", "stop"],
6913
+ controlLaneTaskNames(),
6550
6914
  runnerIdentity
6551
6915
  );
6552
- if (claimedStopTask) {
6553
- runningStopControlPromise = executeClaimedTask(
6916
+ if (claimedControlTask) {
6917
+ runningControlPromise = executeClaimedTask(
6554
6918
  context,
6555
6919
  tasksTable,
6556
6920
  historyTable,
6557
- claimedStopTask,
6921
+ claimedControlTask,
6558
6922
  registry,
6559
6923
  runningTaskInstances
6560
6924
  ).then(async (outcome) => {
@@ -6565,7 +6929,7 @@ async function runTasksLoop(context, options) {
6565
6929
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
6566
6930
  }
6567
6931
  }).finally(() => {
6568
- runningStopControlPromise = null;
6932
+ runningControlPromise = null;
6569
6933
  });
6570
6934
  }
6571
6935
  }
@@ -6596,8 +6960,8 @@ async function runTasksLoop(context, options) {
6596
6960
  runningPromises.add(p);
6597
6961
  }
6598
6962
  const wakePromises = [...runningPromises];
6599
- if (runningStopControlPromise) {
6600
- wakePromises.push(runningStopControlPromise);
6963
+ if (runningControlPromise) {
6964
+ wakePromises.push(runningControlPromise);
6601
6965
  }
6602
6966
  if (wakePromises.length === 0) {
6603
6967
  await sleepMs(pollMs);