@nmakarov/cli-toolkit 0.18.0 → 0.21.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.
package/dist/index.js CHANGED
@@ -901,11 +901,11 @@ function buildBreadcrumb(parts) {
901
901
  if (parts.length === 1) return parts[0];
902
902
  return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
903
903
  }
904
- function buildDetailBreadcrumb(path4, suffix = "") {
905
- if (path4.length <= 1) {
906
- return suffix ? `\u2190 ${suffix}` : path4[0] || "";
904
+ function buildDetailBreadcrumb(path5, suffix = "") {
905
+ if (path5.length <= 1) {
906
+ return suffix ? `\u2190 ${suffix}` : path5[0] || "";
907
907
  }
908
- const breadcrumb = buildBreadcrumb(path4);
908
+ const breadcrumb = buildBreadcrumb(path5);
909
909
  return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
910
910
  }
911
911
  var init_utils = __esm({
@@ -1876,7 +1876,7 @@ var Params = class _Params {
1876
1876
  throw new ParamError(`default value "${defValObj.value}" type mismatch`);
1877
1877
  }
1878
1878
  type = type.default(defValObj.value);
1879
- } else if (str.match(/required/)) {
1879
+ } else if (str.match(/\s*required\s*/)) {
1880
1880
  type = type.required();
1881
1881
  } else {
1882
1882
  type = type.optional();
@@ -1952,6 +1952,8 @@ var Params = class _Params {
1952
1952
  /**
1953
1953
  * Get all parameters from definitions (main script).
1954
1954
  * Same as getAllForModule("script", defs). Processes left-to-right for cross-parameter references.
1955
+ * Libraries should use {@link getAllForModule} with an explicit module name (or {@link runWithModule}
1956
+ * around {@link get}) so --showUsedParams groups usage correctly.
1955
1957
  */
1956
1958
  getAll(defs) {
1957
1959
  return this.getAllForModule("script", defs);
@@ -1988,6 +1990,19 @@ var Params = class _Params {
1988
1990
  this._currentModule = prev;
1989
1991
  }
1990
1992
  }
1993
+ /**
1994
+ * Run a callback with {@link _currentModule} set so single {@link get} calls are tracked
1995
+ * under the same module (for --showUsedParams / getFiguredByModule).
1996
+ */
1997
+ runWithModule(moduleName, fn) {
1998
+ const prev = this._currentModule;
1999
+ this._currentModule = moduleName;
2000
+ try {
2001
+ return fn();
2002
+ } finally {
2003
+ this._currentModule = prev;
2004
+ }
2005
+ }
1991
2006
  /**
1992
2007
  * Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
1993
2008
  */
@@ -2001,9 +2016,9 @@ var Params = class _Params {
2001
2016
  if (!parenMatch) continue;
2002
2017
  const parts = parenMatch[1].split(":");
2003
2018
  if (parts.length < 3) continue;
2004
- const path4 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
2005
- if (!path4 || path4.includes(paramsIndexPath)) continue;
2006
- const srcMatch = path4.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
2019
+ const path5 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
2020
+ if (!path5 || path5.includes(paramsIndexPath)) continue;
2021
+ const srcMatch = path5.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
2007
2022
  if (srcMatch) return srcMatch[1];
2008
2023
  }
2009
2024
  return "script";
@@ -3605,7 +3620,7 @@ async function dbFindAndConnect(context, dbNameOrConnectionString) {
3605
3620
  dbConnectionString: "string",
3606
3621
  dbProfile: "boolean default false"
3607
3622
  };
3608
- const paramsConfig = context.params.getAllForModule(defs);
3623
+ const paramsConfig = context.params.getAll(defs);
3609
3624
  dbName = paramsConfig.dbName;
3610
3625
  dbConnectionString = paramsConfig.dbConnectionString;
3611
3626
  dbProfile = paramsConfig.dbProfile;
@@ -3991,6 +4006,12 @@ function toJsonColumn(value) {
3991
4006
  return JSON.stringify(value);
3992
4007
  }
3993
4008
 
4009
+ // src/tasks/servicesRegistry.ts
4010
+ import { randomUUID as randomUUID2 } from "crypto";
4011
+ import { mkdir, readFile, writeFile } from "fs/promises";
4012
+ import os from "os";
4013
+ import path4 from "path";
4014
+
3994
4015
  // src/tasks/taskUtils.ts
3995
4016
  import { randomUUID } from "crypto";
3996
4017
  function getDb(context) {
@@ -4009,6 +4030,12 @@ function queueToTableNames(queue) {
4009
4030
  historyTable: `${queue}_history`
4010
4031
  };
4011
4032
  }
4033
+ function servicesRegistryTable(queue) {
4034
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
4035
+ throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
4036
+ }
4037
+ return `${queue}_services_registry`;
4038
+ }
4012
4039
  async function ensureTaskTables(context, options = {}) {
4013
4040
  const queue = options.queue ?? "tasks";
4014
4041
  const recreate = options.recreate ?? false;
@@ -4045,18 +4072,6 @@ async function ensureTaskTables(context, options = {}) {
4045
4072
  t.index(["target", "task"], `${tasksTable}_target_task_idx`);
4046
4073
  });
4047
4074
  }
4048
- const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
4049
- if (!tasksHasOpid) {
4050
- await db.schema.alterTable(tasksTable, (t) => {
4051
- t.text("opid");
4052
- });
4053
- }
4054
- const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
4055
- if (!tasksHasPausedAt) {
4056
- await db.schema.alterTable(tasksTable, (t) => {
4057
- t.timestamp("paused_at").defaultTo(null);
4058
- });
4059
- }
4060
4075
  if (needsHistory) {
4061
4076
  await db.schema.createTable(historyTable, (t) => {
4062
4077
  t.uuid("id").notNullable();
@@ -4079,10 +4094,24 @@ async function ensureTaskTables(context, options = {}) {
4079
4094
  t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
4080
4095
  });
4081
4096
  }
4082
- const historyHasOpid = await db.schema.hasColumn(historyTable, "opid");
4083
- if (!historyHasOpid) {
4084
- await db.schema.alterTable(historyTable, (t) => {
4085
- t.text("opid");
4097
+ const registryTable = servicesRegistryTable(queue);
4098
+ const needsRegistry = !await db.tableExists(registryTable);
4099
+ if (needsRegistry) {
4100
+ await db.schema.createTable(registryTable, (t) => {
4101
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
4102
+ t.uuid("instance_id").notNullable().unique();
4103
+ t.text("queue").notNullable();
4104
+ t.text("service_group").notNullable();
4105
+ t.text("service_name").notNullable();
4106
+ t.text("target").notNullable();
4107
+ t.text("hostname");
4108
+ t.integer("pid");
4109
+ t.json("metadata");
4110
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
4111
+ t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
4112
+ t.unique(["queue", "service_name"], `${registryTable}_queue_service_name_uniq`);
4113
+ t.index(["queue", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
4114
+ t.index(["queue", "last_seen_at"], `${registryTable}_queue_seen_idx`);
4086
4115
  });
4087
4116
  }
4088
4117
  }
@@ -4109,6 +4138,260 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
4109
4138
  });
4110
4139
  }
4111
4140
 
4141
+ // src/tasks/servicesRegistry.ts
4142
+ function getDb2(context) {
4143
+ const db = context.db;
4144
+ if (!db) {
4145
+ throw new Error("Services registry requires context.db");
4146
+ }
4147
+ return db;
4148
+ }
4149
+ function parseMetadataColumn(value) {
4150
+ if (!value) return {};
4151
+ if (typeof value === "object" && !Array.isArray(value)) return value;
4152
+ if (typeof value === "string") {
4153
+ try {
4154
+ const p = JSON.parse(value);
4155
+ return p && typeof p === "object" && !Array.isArray(p) ? p : {};
4156
+ } catch {
4157
+ return {};
4158
+ }
4159
+ }
4160
+ return {};
4161
+ }
4162
+ var DEFAULT_GROUP_MAX_INSTANCES = {
4163
+ intake: 1,
4164
+ harvest: 1,
4165
+ loader: 0,
4166
+ photos: 0,
4167
+ photosprocessor: 0,
4168
+ ingest: 0
4169
+ };
4170
+ function sanitizeNamePart(raw) {
4171
+ const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
4172
+ return s.slice(0, 80) || "runner";
4173
+ }
4174
+ function identityFilePath(identityDir, queue, serviceGroup) {
4175
+ const safeQ = sanitizeNamePart(queue);
4176
+ const safeG = sanitizeNamePart(serviceGroup);
4177
+ return path4.join(identityDir, `${safeQ}_${safeG}.json`);
4178
+ }
4179
+ async function readIdentityFile(filePath) {
4180
+ try {
4181
+ const text = await readFile(filePath, "utf8");
4182
+ const parsed = JSON.parse(text);
4183
+ return parsed && typeof parsed === "object" ? parsed : {};
4184
+ } catch {
4185
+ return {};
4186
+ }
4187
+ }
4188
+ async function writeIdentityFile(filePath, data) {
4189
+ await mkdir(path4.dirname(filePath), { recursive: true });
4190
+ await writeFile(filePath, `${JSON.stringify(data, null, 2)}
4191
+ `, "utf8");
4192
+ }
4193
+ function resolveMaxInstances(serviceGroup, override) {
4194
+ if (override !== void 0 && Number.isFinite(override)) {
4195
+ return Math.max(0, Math.floor(Number(override)));
4196
+ }
4197
+ const g = serviceGroup.trim().toLowerCase();
4198
+ return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
4199
+ }
4200
+ async function countAliveInGroup(db, registryTable, queue, serviceGroup, staleMs, excludeInstanceId) {
4201
+ const cutoff = new Date(Date.now() - staleMs);
4202
+ let q = db(registryTable).where({ queue, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
4203
+ if (excludeInstanceId) {
4204
+ q = q.whereNot("instance_id", excludeInstanceId);
4205
+ }
4206
+ const row = await q.count("id as count").first();
4207
+ return Number(row?.count ?? 0);
4208
+ }
4209
+ function isUniqueViolation(error) {
4210
+ const code = error?.code ?? error?.errno;
4211
+ return code === "23505" || String(error?.message || "").includes("duplicate key");
4212
+ }
4213
+ async function registerInServicesRegistry(context, options) {
4214
+ const db = getDb2(context);
4215
+ const registryTable = servicesRegistryTable(options.queue);
4216
+ const serviceGroup = options.serviceGroup.trim();
4217
+ if (!serviceGroup) {
4218
+ throw new Error("registerInServicesRegistry: serviceGroup is required");
4219
+ }
4220
+ const identityPath = identityFilePath(options.identityDir, options.queue, serviceGroup);
4221
+ let identity = await readIdentityFile(identityPath);
4222
+ let instanceId = typeof identity.instanceId === "string" && identity.instanceId.trim() ? identity.instanceId.trim() : randomUUID2();
4223
+ identity.instanceId = instanceId;
4224
+ await writeIdentityFile(identityPath, identity);
4225
+ const hostname = os.hostname();
4226
+ const pid = typeof process.pid === "number" ? process.pid : null;
4227
+ const meta = toJsonColumn(options.metadata ?? null);
4228
+ const existing = await db(registryTable).where({ instance_id: instanceId }).first();
4229
+ if (existing) {
4230
+ await db(registryTable).where({ instance_id: instanceId }).update({
4231
+ target: options.target,
4232
+ hostname,
4233
+ pid,
4234
+ metadata: meta,
4235
+ last_seen_at: db.fn.now()
4236
+ });
4237
+ const serviceName = String(existing.service_name);
4238
+ identity.serviceName = serviceName;
4239
+ await writeIdentityFile(identityPath, identity);
4240
+ const reg = {
4241
+ instanceId,
4242
+ serviceName,
4243
+ serviceGroup,
4244
+ queue: options.queue,
4245
+ target: options.target,
4246
+ rowId: String(existing.id)
4247
+ };
4248
+ context.servicesRegistry = reg;
4249
+ context.runnerHeartbeat = reg;
4250
+ context.logger.info?.(
4251
+ `[services-registry] resumed instance_id=${instanceId} name=${serviceName} group=${serviceGroup} queue=${options.queue}`
4252
+ );
4253
+ return {
4254
+ instanceId,
4255
+ serviceName,
4256
+ serviceGroup,
4257
+ queue: options.queue,
4258
+ target: options.target,
4259
+ rowId: String(existing.id),
4260
+ registryTable
4261
+ };
4262
+ }
4263
+ const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
4264
+ const aliveOthers = await countAliveInGroup(
4265
+ db,
4266
+ registryTable,
4267
+ options.queue,
4268
+ serviceGroup,
4269
+ options.staleMs,
4270
+ instanceId
4271
+ );
4272
+ if (maxAllowed > 0 && aliveOthers >= maxAllowed) {
4273
+ const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveOthers} alive (max ${maxAllowed}, queue=${options.queue}).`;
4274
+ if (options.enforceMaxInstances) {
4275
+ throw new Error(msg);
4276
+ }
4277
+ context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
4278
+ }
4279
+ const explicitName = options.serviceName?.trim();
4280
+ const fromFile = typeof identity.serviceName === "string" ? identity.serviceName.trim() : "";
4281
+ const hostBase = sanitizeNamePart(hostname);
4282
+ const groupBase = sanitizeNamePart(serviceGroup);
4283
+ const baseCandidates = [];
4284
+ if (explicitName) baseCandidates.push(sanitizeNamePart(explicitName));
4285
+ if (fromFile) baseCandidates.push(sanitizeNamePart(fromFile));
4286
+ baseCandidates.push(`${groupBase}-${hostBase}`);
4287
+ baseCandidates.push(groupBase);
4288
+ function* eachServiceNameCandidate(bases) {
4289
+ const seen = /* @__PURE__ */ new Set();
4290
+ for (const rawBase of bases) {
4291
+ const base = sanitizeNamePart(rawBase);
4292
+ if (!base) continue;
4293
+ const seq = [base];
4294
+ for (let n = 2; n <= 500; n++) seq.push(`${base}-${n}`);
4295
+ for (const c of seq) {
4296
+ if (seen.has(c)) continue;
4297
+ seen.add(c);
4298
+ yield c;
4299
+ }
4300
+ }
4301
+ }
4302
+ let inserted;
4303
+ for (const candidate of eachServiceNameCandidate(baseCandidates)) {
4304
+ try {
4305
+ const rows = await db(registryTable).insert({
4306
+ instance_id: instanceId,
4307
+ queue: options.queue,
4308
+ service_group: serviceGroup,
4309
+ service_name: candidate,
4310
+ target: options.target,
4311
+ hostname,
4312
+ pid,
4313
+ metadata: meta,
4314
+ last_seen_at: db.fn.now()
4315
+ }).returning(["id", "service_name"]);
4316
+ const row = Array.isArray(rows) ? rows[0] : rows;
4317
+ if (row) {
4318
+ inserted = { id: String(row.id), service_name: String(row.service_name) };
4319
+ break;
4320
+ }
4321
+ } catch (error) {
4322
+ if (!isUniqueViolation(error)) {
4323
+ throw error;
4324
+ }
4325
+ }
4326
+ }
4327
+ if (!inserted) {
4328
+ throw new Error(
4329
+ `[services-registry] could not allocate a unique service_name for group=${serviceGroup} queue=${options.queue} (too many collisions).`
4330
+ );
4331
+ }
4332
+ identity.serviceName = inserted.service_name;
4333
+ await writeIdentityFile(identityPath, identity);
4334
+ const regNew = {
4335
+ instanceId,
4336
+ serviceName: inserted.service_name,
4337
+ serviceGroup,
4338
+ queue: options.queue,
4339
+ target: options.target,
4340
+ rowId: inserted.id
4341
+ };
4342
+ context.servicesRegistry = regNew;
4343
+ context.runnerHeartbeat = regNew;
4344
+ context.logger.info?.(
4345
+ `[services-registry] registered instance_id=${instanceId} name=${inserted.service_name} group=${serviceGroup} queue=${options.queue} target=${options.target}`
4346
+ );
4347
+ return {
4348
+ instanceId,
4349
+ serviceName: inserted.service_name,
4350
+ serviceGroup,
4351
+ queue: options.queue,
4352
+ target: options.target,
4353
+ rowId: inserted.id,
4354
+ registryTable
4355
+ };
4356
+ }
4357
+ async function touchServicesRegistry(context, registration) {
4358
+ const db = getDb2(context);
4359
+ const hostname = os.hostname();
4360
+ const pid = typeof process.pid === "number" ? process.pid : null;
4361
+ await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
4362
+ last_seen_at: db.fn.now(),
4363
+ hostname,
4364
+ pid
4365
+ });
4366
+ }
4367
+ async function updateServicesRegistryMetadata(context, registration, patch) {
4368
+ const db = getDb2(context);
4369
+ const row = await db(registration.registryTable).where({ instance_id: registration.instanceId }).first();
4370
+ const prev = parseMetadataColumn(row?.metadata);
4371
+ const merged = { ...prev, ...patch };
4372
+ await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
4373
+ metadata: toJsonColumn(merged),
4374
+ last_seen_at: db.fn.now()
4375
+ });
4376
+ context.logger.info?.(`[services-registry] metadata updated for ${registration.serviceName}`);
4377
+ }
4378
+ async function unregisterServicesRegistry(context, registration) {
4379
+ const db = getDb2(context);
4380
+ await db(registration.registryTable).where({ instance_id: registration.instanceId }).delete();
4381
+ context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} instance_id=${registration.instanceId}`);
4382
+ }
4383
+ async function listServicesRegistry(context, options = { queue: "tasks" }) {
4384
+ const db = getDb2(context);
4385
+ const staleMs = options.staleMs ?? 6e4;
4386
+ const cutoff = new Date(Date.now() - staleMs);
4387
+ const table = servicesRegistryTable(options.queue);
4388
+ let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
4389
+ if (options.serviceGroup?.trim()) {
4390
+ q = q.where({ service_group: options.serviceGroup.trim() });
4391
+ }
4392
+ return await q;
4393
+ }
4394
+
4112
4395
  // src/tasks/taskLogs.ts
4113
4396
  function getLogsState(context) {
4114
4397
  const holder = context;
@@ -4451,7 +4734,7 @@ var TaskShellCommand = class extends TaskMaster {
4451
4734
  };
4452
4735
 
4453
4736
  // src/tasks/coreTasks/TaskSystemInfo.ts
4454
- import os from "os";
4737
+ import os2 from "os";
4455
4738
  import fs4 from "fs/promises";
4456
4739
  function toGb(valueBytes) {
4457
4740
  return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
@@ -4473,10 +4756,10 @@ async function getDiskStats() {
4473
4756
  var TaskSystemInfo = class extends TaskMaster {
4474
4757
  async run() {
4475
4758
  try {
4476
- const totalMemory = os.totalmem();
4477
- const freeMemory = os.freemem();
4759
+ const totalMemory = os2.totalmem();
4760
+ const freeMemory = os2.freemem();
4478
4761
  const usedMemory = totalMemory - freeMemory;
4479
- const cpus = os.cpus();
4762
+ const cpus = os2.cpus();
4480
4763
  const cpuUtilization = cpus.map((cpu) => {
4481
4764
  const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
4482
4765
  const usage = (total - cpu.times.idle) / total * 100;
@@ -4502,10 +4785,10 @@ var TaskSystemInfo = class extends TaskMaster {
4502
4785
  utilization: cpuUtilization
4503
4786
  },
4504
4787
  runtime: {
4505
- platform: os.platform(),
4506
- arch: os.arch(),
4507
- uptimeSec: os.uptime(),
4508
- hostname: os.hostname()
4788
+ platform: os2.platform(),
4789
+ arch: os2.arch(),
4790
+ uptimeSec: os2.uptime(),
4791
+ hostname: os2.hostname()
4509
4792
  }
4510
4793
  };
4511
4794
  this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
@@ -4738,7 +5021,7 @@ async function runNodeTaskScript(context, options) {
4738
5021
  // src/tasks/index.ts
4739
5022
  var LOCKED_BY_ERROR_MESSAGE = "locked by error";
4740
5023
  var defaultTasksRegistry = TasksRegistry.withCoreTasks();
4741
- function getDb2(context) {
5024
+ function getDb3(context) {
4742
5025
  const db = context.db;
4743
5026
  if (!db) {
4744
5027
  throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
@@ -4782,7 +5065,7 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
4782
5065
  context.emitter.emit("stop", allowanceMs);
4783
5066
  }
4784
5067
  async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
4785
- const db = getDb2(context);
5068
+ const db = getDb3(context);
4786
5069
  const taskName = row.task;
4787
5070
  const TaskClass = registry.get(taskName);
4788
5071
  const { paused_at: _pausedAt, ...rowForHistory } = row;
@@ -4883,7 +5166,7 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4883
5166
  return { stopRunnerRequested, stopAllowanceMs };
4884
5167
  }
4885
5168
  async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
4886
- const db = getDb2(context);
5169
+ const db = getDb3(context);
4887
5170
  let query = db(tasksTable).whereNull("started_at").whereNull("paused_at").where({ target }).orderByRaw("CASE WHEN past_due IS NULL THEN 1 ELSE 0 END ASC").orderBy([{ column: "priority", order: "desc" }]).orderByRaw("CASE WHEN completed_at IS NULL THEN 0 ELSE 1 END ASC").orderBy([{ column: "completed_at", order: "asc" }, { column: "created_at", order: "asc" }]).limit(scanLimit);
4888
5171
  if (taskNames && taskNames.length > 0) {
4889
5172
  query = query.whereIn("task", taskNames);
@@ -4929,25 +5212,76 @@ async function runTasksLoop(context, options) {
4929
5212
  let stopRequested = false;
4930
5213
  let stopAllowanceMs = 5e3;
4931
5214
  context.__tasksRunnerStop = false;
4932
- while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
4933
- if (!runningStopControlPromise) {
4934
- const claimedStopTask = await claimNextRunnableTask(
4935
- context,
4936
- tasksTable,
4937
- target,
4938
- registry,
4939
- 10,
4940
- ["stopRunner", "stop"]
4941
- );
4942
- if (claimedStopTask) {
4943
- runningStopControlPromise = executeClaimedTask(
5215
+ let registryReg = null;
5216
+ let registryInterval = null;
5217
+ const hbGroup = options.runnerServiceGroup?.trim();
5218
+ if (hbGroup) {
5219
+ const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
5220
+ const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
5221
+ const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
5222
+ const defaultMeta = {
5223
+ component: "tasks-runner",
5224
+ allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
5225
+ };
5226
+ registryReg = await registerInServicesRegistry(context, {
5227
+ queue,
5228
+ target,
5229
+ serviceGroup: hbGroup,
5230
+ serviceName: options.runnerServiceName,
5231
+ identityDir,
5232
+ staleMs,
5233
+ groupMaxInstances: options.runnerGroupMaxInstances,
5234
+ enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
5235
+ metadata: options.runnerMetadata ?? defaultMeta
5236
+ });
5237
+ registryInterval = setInterval(() => {
5238
+ void touchServicesRegistry(context, registryReg).catch((err) => {
5239
+ context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
5240
+ });
5241
+ }, hbIntervalMs);
5242
+ }
5243
+ try {
5244
+ while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
5245
+ if (!runningStopControlPromise) {
5246
+ const claimedStopTask = await claimNextRunnableTask(
4944
5247
  context,
4945
5248
  tasksTable,
4946
- historyTable,
4947
- claimedStopTask,
5249
+ target,
4948
5250
  registry,
4949
- runningTaskInstances
4950
- ).then(async (outcome) => {
5251
+ 10,
5252
+ ["stopRunner", "stop"]
5253
+ );
5254
+ if (claimedStopTask) {
5255
+ runningStopControlPromise = executeClaimedTask(
5256
+ context,
5257
+ tasksTable,
5258
+ historyTable,
5259
+ claimedStopTask,
5260
+ registry,
5261
+ runningTaskInstances
5262
+ ).then(async (outcome) => {
5263
+ if (outcome.stopRunnerRequested && !stopRequested) {
5264
+ stopRequested = true;
5265
+ stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
5266
+ context.__tasksRunnerStop = true;
5267
+ await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
5268
+ }
5269
+ }).finally(() => {
5270
+ runningStopControlPromise = null;
5271
+ });
5272
+ }
5273
+ }
5274
+ while (runningPromises.size < maxParallel) {
5275
+ const claimed = await claimNextRunnableTask(
5276
+ context,
5277
+ tasksTable,
5278
+ target,
5279
+ registry,
5280
+ scanLimit,
5281
+ allowedTasks
5282
+ );
5283
+ if (!claimed) break;
5284
+ const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
4951
5285
  if (outcome.stopRunnerRequested && !stopRequested) {
4952
5286
  stopRequested = true;
4953
5287
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
@@ -4955,54 +5289,46 @@ async function runTasksLoop(context, options) {
4955
5289
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
4956
5290
  }
4957
5291
  }).finally(() => {
4958
- runningStopControlPromise = null;
5292
+ runningPromises.delete(p);
4959
5293
  });
5294
+ runningPromises.add(p);
5295
+ }
5296
+ await sleepMs(pollMs);
5297
+ }
5298
+ if (context.isStop() && !stopRequested) {
5299
+ await signalRunningTasksStop(context, runningTaskInstances, 5e3);
5300
+ }
5301
+ if (runningPromises.size > 0) {
5302
+ if (stopRequested) {
5303
+ await Promise.race([
5304
+ Promise.allSettled(Array.from(runningPromises)),
5305
+ sleepMs(stopAllowanceMs).then(() => {
5306
+ context.logger.warn?.(
5307
+ `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
5308
+ );
5309
+ })
5310
+ ]);
5311
+ } else {
5312
+ await Promise.allSettled(Array.from(runningPromises));
4960
5313
  }
4961
5314
  }
4962
- while (runningPromises.size < maxParallel) {
4963
- const claimed = await claimNextRunnableTask(
4964
- context,
4965
- tasksTable,
4966
- target,
4967
- registry,
4968
- scanLimit,
4969
- allowedTasks
4970
- );
4971
- if (!claimed) break;
4972
- const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
4973
- if (outcome.stopRunnerRequested && !stopRequested) {
4974
- stopRequested = true;
4975
- stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
4976
- context.__tasksRunnerStop = true;
4977
- await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
4978
- }
4979
- }).finally(() => {
4980
- runningPromises.delete(p);
4981
- });
4982
- runningPromises.add(p);
5315
+ } finally {
5316
+ if (registryInterval) {
5317
+ clearInterval(registryInterval);
5318
+ registryInterval = null;
4983
5319
  }
4984
- await sleepMs(pollMs);
4985
- }
4986
- if (context.isStop() && !stopRequested) {
4987
- await signalRunningTasksStop(context, runningTaskInstances, 5e3);
4988
- }
4989
- if (runningPromises.size > 0) {
4990
- if (stopRequested) {
4991
- await Promise.race([
4992
- Promise.allSettled(Array.from(runningPromises)),
4993
- sleepMs(stopAllowanceMs).then(() => {
4994
- context.logger.warn?.(
4995
- `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
4996
- );
4997
- })
4998
- ]);
4999
- } else {
5000
- await Promise.allSettled(Array.from(runningPromises));
5320
+ if (registryReg) {
5321
+ await unregisterServicesRegistry(context, registryReg).catch((err) => {
5322
+ context.logger.warn?.(`[services-registry] unregister failed: ${err?.message ?? String(err)}`);
5323
+ });
5324
+ registryReg = null;
5325
+ delete context.servicesRegistry;
5326
+ delete context.runnerHeartbeat;
5001
5327
  }
5002
5328
  }
5003
5329
  }
5004
5330
  async function waitForTaskResult(context, taskId, options = {}) {
5005
- const db = getDb2(context);
5331
+ const db = getDb3(context);
5006
5332
  const queue = options.queue ?? "tasks";
5007
5333
  const timeoutMs = options.timeoutMs ?? 6e4;
5008
5334
  const pollMs = options.pollMs ?? 500;
@@ -5030,6 +5356,14 @@ var TasksManager = class _TasksManager {
5030
5356
  scanLimit;
5031
5357
  allowedTasks;
5032
5358
  registry;
5359
+ runnerServiceGroup;
5360
+ runnerServiceName;
5361
+ runnerIdentityDir;
5362
+ runnerHeartbeatIntervalMs;
5363
+ runnerHeartbeatStaleMs;
5364
+ runnerGroupMaxInstances;
5365
+ runnerEnforceMaxInstances;
5366
+ runnerMetadata;
5033
5367
  constructor(context, options = {}) {
5034
5368
  this.context = context;
5035
5369
  this.queue = options.queue ?? "tasks";
@@ -5040,6 +5374,14 @@ var TasksManager = class _TasksManager {
5040
5374
  this.scanLimit = options.scanLimit ?? 100;
5041
5375
  this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
5042
5376
  this.registry = normalizeRegistry(options.registry);
5377
+ this.runnerServiceGroup = options.runnerServiceGroup;
5378
+ this.runnerServiceName = options.runnerServiceName;
5379
+ this.runnerIdentityDir = options.runnerIdentityDir;
5380
+ this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
5381
+ this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
5382
+ this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
5383
+ this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
5384
+ this.runnerMetadata = options.runnerMetadata;
5043
5385
  }
5044
5386
  static init(context, options = {}) {
5045
5387
  const defs = {
@@ -5049,7 +5391,14 @@ var TasksManager = class _TasksManager {
5049
5391
  pollMs: "number default 1000",
5050
5392
  maxParallel: "number default 1",
5051
5393
  scanLimit: "number default 100",
5052
- allowedTasks: "string"
5394
+ allowedTasks: "string",
5395
+ runnerServiceGroup: "string",
5396
+ runnerServiceName: "string",
5397
+ runnerIdentityDir: "string default ./data/runner-identities",
5398
+ runnerHeartbeatIntervalMs: "number default 10000",
5399
+ runnerHeartbeatStaleMs: "number default 45000",
5400
+ runnerGroupMaxInstances: "number",
5401
+ runnerEnforceMaxInstances: "boolean default true"
5053
5402
  };
5054
5403
  const discovered = context.params.getAllForModule(defs);
5055
5404
  const resolved = {
@@ -5060,6 +5409,13 @@ var TasksManager = class _TasksManager {
5060
5409
  maxParallel: discovered.maxParallel,
5061
5410
  scanLimit: discovered.scanLimit,
5062
5411
  allowedTasks: discovered.allowedTasks,
5412
+ runnerServiceGroup: discovered.runnerServiceGroup,
5413
+ runnerServiceName: discovered.runnerServiceName,
5414
+ runnerIdentityDir: discovered.runnerIdentityDir,
5415
+ runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
5416
+ runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
5417
+ runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
5418
+ runnerEnforceMaxInstances: discovered.runnerEnforceMaxInstances,
5063
5419
  ...options
5064
5420
  };
5065
5421
  return new _TasksManager(context, resolved);
@@ -5078,7 +5434,15 @@ var TasksManager = class _TasksManager {
5078
5434
  maxParallel: options.maxParallel ?? this.maxParallel,
5079
5435
  scanLimit: options.scanLimit ?? this.scanLimit,
5080
5436
  allowedTasks: options.allowedTasks ?? this.allowedTasks,
5081
- registry: options.registry ?? this.registry
5437
+ registry: options.registry ?? this.registry,
5438
+ runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
5439
+ runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
5440
+ runnerIdentityDir: options.runnerIdentityDir ?? this.runnerIdentityDir,
5441
+ runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
5442
+ runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
5443
+ runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
5444
+ runnerEnforceMaxInstances: options.runnerEnforceMaxInstances ?? this.runnerEnforceMaxInstances,
5445
+ runnerMetadata: options.runnerMetadata ?? this.runnerMetadata
5082
5446
  });
5083
5447
  }
5084
5448
  };
@@ -5132,13 +5496,19 @@ export {
5132
5496
  createElement2 as h,
5133
5497
  joiEdateType,
5134
5498
  joiStringArrayType,
5499
+ listServicesRegistry as listAliveRunnerHeartbeats,
5500
+ listServicesRegistry,
5135
5501
  listSources,
5136
5502
  listTables,
5137
5503
  load,
5138
5504
  organizeFooterMessages,
5139
5505
  queueToTableNames,
5506
+ registerInServicesRegistry,
5507
+ registerInServicesRegistry as registerRunnerHeartbeat,
5140
5508
  runNodeTaskScript,
5141
5509
  runTasksLoop,
5510
+ servicesRegistryTable as runnerHeartbeatsTable,
5511
+ servicesRegistryTable,
5142
5512
  setupContext,
5143
5513
  showListScreen,
5144
5514
  showMenuScreen,
@@ -5146,6 +5516,11 @@ export {
5146
5516
  showMultiColumnListWithPreviewScreen,
5147
5517
  showScreen,
5148
5518
  showWordGridScreen,
5519
+ touchServicesRegistry as touchRunnerHeartbeat,
5520
+ touchServicesRegistry,
5521
+ unregisterServicesRegistry as unregisterRunnerHeartbeat,
5522
+ unregisterServicesRegistry,
5523
+ updateServicesRegistryMetadata,
5149
5524
  updateTaskProgress,
5150
5525
  useCallback,
5151
5526
  useEffect3 as useEffect,