@nmakarov/cli-toolkit 0.18.0 → 0.23.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.
Files changed (69) hide show
  1. package/README.md +9 -0
  2. package/dist/args.cjs +1 -4
  3. package/dist/args.cjs.map +1 -1
  4. package/dist/args.js +1 -1
  5. package/dist/args.js.map +1 -1
  6. package/dist/cli-runner.cjs +1493 -516
  7. package/dist/cli-runner.cjs.map +1 -1
  8. package/dist/cli-runner.js +1509 -531
  9. package/dist/cli-runner.js.map +1 -1
  10. package/dist/db.cjs +85 -157
  11. package/dist/db.cjs.map +1 -1
  12. package/dist/db.js +84 -150
  13. package/dist/db.js.map +1 -1
  14. package/dist/errors.cjs +2 -2
  15. package/dist/errors.cjs.map +1 -1
  16. package/dist/errors.js +2 -1
  17. package/dist/errors.js.map +1 -1
  18. package/dist/filedatabase.cjs +19 -19
  19. package/dist/filedatabase.cjs.map +1 -1
  20. package/dist/filedatabase.js +19 -16
  21. package/dist/filedatabase.js.map +1 -1
  22. package/dist/http-client.cjs +9 -11
  23. package/dist/http-client.cjs.map +1 -1
  24. package/dist/http-client.js +10 -9
  25. package/dist/http-client.js.map +1 -1
  26. package/dist/http-client2.cjs +34 -33
  27. package/dist/http-client2.cjs.map +1 -1
  28. package/dist/http-client2.js +34 -30
  29. package/dist/http-client2.js.map +1 -1
  30. package/dist/index.cjs +2063 -658
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.js +2063 -663
  33. package/dist/index.js.map +1 -1
  34. package/dist/init.cjs +97 -69
  35. package/dist/init.cjs.map +1 -1
  36. package/dist/init.js +112 -83
  37. package/dist/init.js.map +1 -1
  38. package/dist/logger.cjs +5 -5
  39. package/dist/logger.cjs.map +1 -1
  40. package/dist/logger.js +5 -4
  41. package/dist/logger.js.map +1 -1
  42. package/dist/mock-server.cjs +21 -33
  43. package/dist/mock-server.cjs.map +1 -1
  44. package/dist/mock-server.js +21 -28
  45. package/dist/mock-server.js.map +1 -1
  46. package/dist/params.cjs +22 -10
  47. package/dist/params.cjs.map +1 -1
  48. package/dist/params.js +22 -7
  49. package/dist/params.js.map +1 -1
  50. package/dist/s3.cjs +286 -0
  51. package/dist/s3.cjs.map +1 -0
  52. package/dist/s3.js +273 -0
  53. package/dist/s3.js.map +1 -0
  54. package/dist/screen.cjs +34 -39
  55. package/dist/screen.cjs.map +1 -1
  56. package/dist/screen.js +48 -46
  57. package/dist/screen.js.map +1 -1
  58. package/dist/tasks.cjs +1640 -416
  59. package/dist/tasks.cjs.map +1 -1
  60. package/dist/tasks.js +1614 -412
  61. package/dist/tasks.js.map +1 -1
  62. package/dist/utils.cjs +7 -8
  63. package/dist/utils.cjs.map +1 -1
  64. package/dist/utils.js +6 -6
  65. package/dist/utils.js.map +1 -1
  66. package/package.json +36 -44
  67. package/scripts/ssm/parse-cli.js +35 -0
  68. package/scripts/ssm/ssm-admin.js +151 -0
  69. package/scripts/ssm/ssm-pull.js +147 -0
package/dist/tasks.js CHANGED
@@ -1,4 +1,7 @@
1
- // src/utils/date-utils.ts
1
+ // src/tasks/index.js
2
+ import os3 from "os";
3
+
4
+ // src/utils/date-utils.js
2
5
  function isTimestampFolder(folderName) {
3
6
  const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
4
7
  if (!isoRegex.test(folderName)) {
@@ -8,7 +11,7 @@ function isTimestampFolder(folderName) {
8
11
  return !isNaN(date.getTime()) && date.getTime() > 0;
9
12
  }
10
13
 
11
- // src/utils/fs-utils.ts
14
+ // src/utils/fs-utils.js
12
15
  import fs from "fs";
13
16
  import path from "path";
14
17
  async function ensurePath(...pathParts) {
@@ -32,7 +35,7 @@ function getFileExtension(dataType) {
32
35
  }
33
36
  }
34
37
 
35
- // src/utils/os-utils.ts
38
+ // src/utils/os-utils.js
36
39
  import fs2 from "fs";
37
40
  import path2 from "path";
38
41
  import { execSync } from "child_process";
@@ -61,7 +64,7 @@ function getFreeDiskSpace(targetPath) {
61
64
  }
62
65
  }
63
66
 
64
- // src/utils/format-utils.ts
67
+ // src/utils/format-utils.js
65
68
  function bytesToHumanReadable(bytes) {
66
69
  if (bytes === 0) return "0 B";
67
70
  const k = 1024;
@@ -70,7 +73,7 @@ function bytesToHumanReadable(bytes) {
70
73
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
71
74
  }
72
75
 
73
- // src/utils/core-utils.ts
76
+ // src/utils/core-utils.js
74
77
  function sleepMs(ms) {
75
78
  return new Promise((resolve) => setTimeout(resolve, ms));
76
79
  }
@@ -79,8 +82,82 @@ function toJsonColumn(value) {
79
82
  return JSON.stringify(value);
80
83
  }
81
84
 
82
- // src/tasks/taskUtils.ts
85
+ // src/tasks/servicesRegistry.js
86
+ import os from "os";
87
+
88
+ // src/tasks/taskUtils.js
83
89
  import { randomUUID } from "crypto";
90
+
91
+ // src/tasks/time-matcher.js
92
+ var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
93
+ function resolveAsterisks(field, range) {
94
+ return field.includes("*") ? field.replace("*", range) : field;
95
+ }
96
+ function resolveRanges(field) {
97
+ const regex = /(\d+)-(\d+)/;
98
+ let current = field;
99
+ while (true) {
100
+ const match = regex.exec(current);
101
+ if (!match) break;
102
+ const raw = match[0];
103
+ let first = Number(match[1]);
104
+ let last = Number(match[2]);
105
+ if (last < first) {
106
+ [first, last] = [last, first];
107
+ }
108
+ const values = [];
109
+ for (let i = first; i <= last; i += 1) {
110
+ values.push(i);
111
+ }
112
+ current = current.replace(raw, values.join(","));
113
+ }
114
+ return current;
115
+ }
116
+ function resolveSteps(field) {
117
+ const match = /^(.+)\/(\d+)$/.exec(field);
118
+ if (!match) return field;
119
+ const base = match[1];
120
+ const step = Number(match[2]);
121
+ if (!Number.isFinite(step) || step <= 0) return field;
122
+ return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
123
+ }
124
+ function convertPattern(pattern) {
125
+ const parts = pattern.trim().split(/\s+/);
126
+ if (parts.length !== 6) {
127
+ throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
128
+ }
129
+ return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
130
+ }
131
+ function fieldMatches(field, value) {
132
+ const allowed = field.split(",").map((v) => Number(v));
133
+ return allowed.includes(value);
134
+ }
135
+ function matchesParsedPattern(parsed, date) {
136
+ return fieldMatches(parsed[0], date.getSeconds()) && fieldMatches(parsed[1], date.getMinutes()) && fieldMatches(parsed[2], date.getHours()) && fieldMatches(parsed[3], date.getDate()) && fieldMatches(parsed[4], date.getMonth() + 1) && fieldMatches(parsed[5], date.getDay());
137
+ }
138
+ function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
139
+ const parsed = convertPattern(pattern);
140
+ return matchesParsedPattern(parsed, date);
141
+ }
142
+ var MS_PER_SECOND = 1e3;
143
+ var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
144
+ function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
145
+ const parsed = convertPattern(pattern);
146
+ let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
147
+ const end = t + maxSearchMs;
148
+ while (t <= end) {
149
+ const date = new Date(t);
150
+ if (matchesParsedPattern(parsed, date)) {
151
+ return date;
152
+ }
153
+ t += MS_PER_SECOND;
154
+ }
155
+ throw new Error(
156
+ `nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
157
+ );
158
+ }
159
+
160
+ // src/tasks/taskUtils.js
84
161
  function getDb(context) {
85
162
  const db = context.db;
86
163
  if (!db) {
@@ -88,105 +165,117 @@ function getDb(context) {
88
165
  }
89
166
  return db;
90
167
  }
91
- function queueToTableNames(queue) {
92
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
93
- throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
94
- }
168
+ function queueToTableNames(queueName) {
169
+ return {
170
+ tasksTable: queueName,
171
+ historyTable: `${queueName}_history`,
172
+ registryTable: `${queueName}_services_registry`
173
+ };
174
+ }
175
+ function defineTasksTable(t, db, tableNameForIndex) {
176
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
177
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
178
+ t.timestamp("started_at");
179
+ t.timestamp("completed_at");
180
+ t.integer("priority").notNullable().defaultTo(50);
181
+ t.text("schedule");
182
+ t.timestamp("next_run_at").defaultTo(null);
183
+ t.timestamp("past_due").defaultTo(null);
184
+ t.text("name").notNullable();
185
+ t.text("opid");
186
+ t.json("params");
187
+ t.text("service_group");
188
+ t.integer("instance_number");
189
+ t.text("service_name");
190
+ t.text("server_name");
191
+ t.text("status").notNullable().defaultTo("idle");
192
+ t.timestamp("status_changed_at").defaultTo(null);
193
+ t.text("progress");
194
+ t.boolean("success");
195
+ t.json("results");
196
+ t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
197
+ t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
198
+ }
199
+ function taskHistoryInsertFromQueueRow(row, overrides) {
200
+ const { id, ...snapshot } = row;
201
+ void id;
95
202
  return {
96
- tasksTable: queue,
97
- historyTable: `${queue}_history`
203
+ ...snapshot,
204
+ ...overrides
98
205
  };
99
206
  }
100
207
  async function ensureTaskTables(context, options = {}) {
101
- const queue = options.queue ?? "tasks";
208
+ const queueName = options.queueName ?? "tasks";
102
209
  const recreate = options.recreate ?? false;
103
210
  const db = getDb(context);
104
- const { tasksTable, historyTable } = queueToTableNames(queue);
211
+ const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
105
212
  const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
106
213
  const needsHistory = recreate ? true : !await db.tableExists(historyTable);
214
+ const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
107
215
  if (recreate) {
108
216
  await db.schema.dropTableIfExists(historyTable);
109
217
  await db.schema.dropTableIfExists(tasksTable);
218
+ await db.schema.dropTableIfExists(registryTable);
110
219
  }
111
220
  if (needsTasks) {
112
221
  await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
113
222
  await db.schema.createTable(tasksTable, (t) => {
114
- t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
115
- t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
116
- t.timestamp("started_at");
117
- t.timestamp("completed_at");
118
- t.integer("priority").notNullable().defaultTo(0);
119
- t.text("schedule");
120
- t.timestamp("past_due").defaultTo(null);
121
- t.text("target").notNullable();
122
- t.text("task").notNullable();
123
- t.json("params");
124
- t.text("opid");
125
- t.timestamp("paused_at").defaultTo(null);
126
- t.text("progress");
127
- t.boolean("success");
128
- t.json("results");
129
- });
130
- await db.schema.alterTable(tasksTable, (t) => {
131
- t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
132
- t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
133
- t.index(["target", "task"], `${tasksTable}_target_task_idx`);
134
- });
135
- }
136
- const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
137
- if (!tasksHasOpid) {
138
- await db.schema.alterTable(tasksTable, (t) => {
139
- t.text("opid");
140
- });
141
- }
142
- const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
143
- if (!tasksHasPausedAt) {
144
- await db.schema.alterTable(tasksTable, (t) => {
145
- t.timestamp("paused_at").defaultTo(null);
223
+ defineTasksTable(t, db, tasksTable);
146
224
  });
147
225
  }
148
226
  if (needsHistory) {
149
227
  await db.schema.createTable(historyTable, (t) => {
150
- t.uuid("id").notNullable();
151
- t.timestamp("created_at").notNullable();
152
- t.timestamp("started_at");
153
- t.timestamp("completed_at");
154
- t.integer("priority").notNullable().defaultTo(0);
155
- t.text("schedule");
156
- t.timestamp("past_due").defaultTo(null);
157
- t.text("target").notNullable();
158
- t.text("task").notNullable();
159
- t.json("params");
160
- t.text("opid");
161
- t.text("progress");
162
- t.boolean("success");
163
- t.json("results");
164
- });
165
- await db.schema.alterTable(historyTable, (t) => {
166
- t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
167
- t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
228
+ defineTasksTable(t, db, historyTable);
168
229
  });
169
230
  }
170
- const historyHasOpid = await db.schema.hasColumn(historyTable, "opid");
171
- if (!historyHasOpid) {
172
- await db.schema.alterTable(historyTable, (t) => {
173
- t.text("opid");
231
+ if (needsRegistry) {
232
+ await db.schema.createTable(registryTable, (t) => {
233
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
234
+ t.text("queue_name").notNullable();
235
+ t.text("service_group").notNullable();
236
+ t.integer("instance_number").notNullable().defaultTo(1);
237
+ t.text("service_name").notNullable();
238
+ t.text("server_name").notNullable();
239
+ t.integer("pid");
240
+ t.json("metadata");
241
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
242
+ t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
243
+ t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
244
+ t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
245
+ t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
174
246
  });
175
247
  }
176
248
  }
177
249
  async function enqueueTask(context, options) {
178
250
  const db = getDb(context);
179
- const queue = options.queue ?? "tasks";
180
- const { tasksTable } = queueToTableNames(queue);
251
+ const queueName = options.queueName ?? "tasks";
252
+ const { tasksTable } = queueToTableNames(queueName);
181
253
  const id = randomUUID();
254
+ const name = options.name ?? options.task;
255
+ if (!name) {
256
+ throw new Error("enqueueTask: name (or task) is required");
257
+ }
258
+ const schedule = options.schedule?.trim() ? options.schedule : null;
259
+ let nextRunAt = null;
260
+ if (options.nextRunAt !== void 0) {
261
+ nextRunAt = options.nextRunAt == null ? null : new Date(options.nextRunAt);
262
+ } else if (schedule) {
263
+ nextRunAt = nextTimeMatch(schedule, /* @__PURE__ */ new Date());
264
+ }
182
265
  await db(tasksTable).insert({
183
266
  id,
184
- target: options.target,
185
- task: options.task,
267
+ name,
186
268
  params: toJsonColumn(options.params ?? null),
187
269
  opid: options.opid ?? null,
188
- priority: options.priority ?? 0,
189
- schedule: options.schedule ?? null
270
+ priority: options.priority ?? 50,
271
+ schedule,
272
+ next_run_at: nextRunAt,
273
+ service_group: options.serviceGroup ?? null,
274
+ instance_number: options.instanceNumber ?? null,
275
+ service_name: options.serviceName ?? null,
276
+ server_name: options.serverName ?? null,
277
+ status: "idle",
278
+ status_changed_at: db.fn.now()
190
279
  });
191
280
  return id;
192
281
  }
@@ -197,11 +286,261 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
197
286
  });
198
287
  }
199
288
 
200
- // src/filedatabase/index.ts
289
+ // src/tasks/servicesRegistry.js
290
+ function getDb2(context) {
291
+ const db = context.db;
292
+ if (!db) {
293
+ throw new Error("Services registry requires context.db");
294
+ }
295
+ return db;
296
+ }
297
+ function parseMetadataColumn(value) {
298
+ if (!value) return {};
299
+ if (typeof value === "object" && !Array.isArray(value)) return value;
300
+ if (typeof value === "string") {
301
+ try {
302
+ const p = JSON.parse(value);
303
+ return p && typeof p === "object" && !Array.isArray(value) ? p : {};
304
+ } catch {
305
+ return {};
306
+ }
307
+ }
308
+ return {};
309
+ }
310
+ var DEFAULT_GROUP_MAX_INSTANCES = {
311
+ intake: 1,
312
+ harvest: 1,
313
+ harvester: 0,
314
+ loader: 0,
315
+ photos: 0,
316
+ photosprocessor: 0,
317
+ ingest: 0
318
+ };
319
+ function sanitizeNamePart(raw) {
320
+ const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
321
+ return s.slice(0, 80) || "runner";
322
+ }
323
+ function resolveMaxInstances(serviceGroup, override) {
324
+ if (override !== void 0 && Number.isFinite(override)) {
325
+ return Math.max(0, Math.floor(Number(override)));
326
+ }
327
+ const g = serviceGroup.trim().toLowerCase();
328
+ return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
329
+ }
330
+ async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
331
+ const cutoff = new Date(Date.now() - staleMs);
332
+ let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
333
+ if (excludeRowId) {
334
+ q = q.whereNot("id", excludeRowId);
335
+ }
336
+ const row = await q.count("id as count").first();
337
+ return Number(row?.count ?? 0);
338
+ }
339
+ async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
340
+ const cutoff = new Date(Date.now() - staleMs);
341
+ const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
342
+ const set = /* @__PURE__ */ new Set();
343
+ for (const r of rows) {
344
+ const n = Number(r.instance_number);
345
+ if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
346
+ }
347
+ return set;
348
+ }
349
+ function isUniqueViolation(error) {
350
+ const code = error?.code ?? error?.errno;
351
+ return code === "23505" || String(error?.message || "").includes("duplicate key");
352
+ }
353
+ function buildMetadata(options) {
354
+ const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
355
+ if (options.target) {
356
+ base.runnerTarget = options.target;
357
+ }
358
+ return toJsonColumn(Object.keys(base).length ? base : null);
359
+ }
360
+ function allocateInstanceNumber(occupied, explicit, maxSlots) {
361
+ if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
362
+ const e = Math.max(1, Math.floor(Number(explicit)));
363
+ if (occupied.has(e)) {
364
+ throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
365
+ }
366
+ if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
367
+ throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
368
+ }
369
+ return e;
370
+ }
371
+ const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
372
+ for (let n = 1; n <= cap; n++) {
373
+ if (!occupied.has(n)) return n;
374
+ }
375
+ throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
376
+ }
377
+ function defaultServiceName(groupBase, hostBase, instanceNumber) {
378
+ return `${groupBase}-${hostBase}-${instanceNumber}`;
379
+ }
380
+ async function registerInServicesRegistry(context, options) {
381
+ const db = getDb2(context);
382
+ const registryTable = queueToTableNames(options.queueName).registryTable;
383
+ const serviceGroup = options.serviceGroup.trim();
384
+ if (!serviceGroup) {
385
+ throw new Error("registerInServicesRegistry: serviceGroup is required");
386
+ }
387
+ const serverName = os.hostname();
388
+ const pid = typeof process.pid === "number" ? process.pid : null;
389
+ const meta = buildMetadata(options);
390
+ const groupBase = sanitizeNamePart(serviceGroup);
391
+ const hostBase = sanitizeNamePart(serverName);
392
+ const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
393
+ const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
394
+ if (maxAllowed > 0 && aliveCount >= maxAllowed) {
395
+ const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
396
+ if (options.enforceMaxInstances) {
397
+ throw new Error(msg);
398
+ }
399
+ context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
400
+ }
401
+ const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
402
+ const cutoff = new Date(Date.now() - options.staleMs);
403
+ const MAX_ATTEMPTS = 8;
404
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
405
+ const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
406
+ const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
407
+ const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
408
+ const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
409
+ if (existing) {
410
+ const lastSeen = new Date(existing.last_seen_at);
411
+ const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
412
+ if (isAlive) {
413
+ if (options.serviceName?.trim()) {
414
+ throw new Error(
415
+ `[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
416
+ );
417
+ }
418
+ context.logger.warn?.(
419
+ `[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
420
+ );
421
+ if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
422
+ throw new Error(
423
+ `[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
424
+ );
425
+ }
426
+ await new Promise((r) => setTimeout(r, 50 + attempt * 30));
427
+ continue;
428
+ }
429
+ await db(registryTable).where({ id: existing.id }).update({
430
+ server_name: serverName,
431
+ pid,
432
+ metadata: meta,
433
+ service_group: serviceGroup,
434
+ instance_number: instanceNumber,
435
+ last_seen_at: db.fn.now()
436
+ });
437
+ const reg = {
438
+ serviceName: serviceNameRaw,
439
+ serviceGroup,
440
+ queueName: options.queueName,
441
+ target: options.target,
442
+ rowId: String(existing.id),
443
+ registryTable,
444
+ instanceNumber
445
+ };
446
+ context.servicesRegistry = reg;
447
+ context.runnerHeartbeat = reg;
448
+ context.logger.info?.(
449
+ `[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
450
+ );
451
+ return reg;
452
+ }
453
+ try {
454
+ const rows = await db(registryTable).insert({
455
+ queue_name: options.queueName,
456
+ service_group: serviceGroup,
457
+ instance_number: instanceNumber,
458
+ service_name: serviceNameRaw,
459
+ server_name: serverName,
460
+ pid,
461
+ metadata: meta,
462
+ last_seen_at: db.fn.now(),
463
+ created_at: db.fn.now()
464
+ }).returning(["id", "service_name"]);
465
+ const row = Array.isArray(rows) ? rows[0] : rows;
466
+ let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
467
+ if (!rowId) {
468
+ const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
469
+ rowId = again?.id != null ? String(again.id) : "";
470
+ }
471
+ if (!rowId) continue;
472
+ const regNew = {
473
+ serviceName: String(row?.service_name ?? serviceNameRaw),
474
+ serviceGroup,
475
+ queueName: options.queueName,
476
+ target: options.target,
477
+ rowId,
478
+ registryTable,
479
+ instanceNumber
480
+ };
481
+ context.servicesRegistry = regNew;
482
+ context.runnerHeartbeat = regNew;
483
+ context.logger.info?.(
484
+ `[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
485
+ );
486
+ return regNew;
487
+ } catch (error) {
488
+ if (!isUniqueViolation(error)) {
489
+ throw error;
490
+ }
491
+ context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
492
+ }
493
+ }
494
+ throw new Error(
495
+ `[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
496
+ );
497
+ }
498
+ async function touchServicesRegistry(context, registration) {
499
+ const db = getDb2(context);
500
+ const serverName = os.hostname();
501
+ const pid = typeof process.pid === "number" ? process.pid : null;
502
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
503
+ last_seen_at: db.fn.now(),
504
+ server_name: serverName,
505
+ pid
506
+ });
507
+ }
508
+ async function updateServicesRegistryMetadata(context, registration, patch) {
509
+ const db = getDb2(context);
510
+ const row = await db(registration.registryTable).where({ id: registration.rowId }).first();
511
+ const prev = parseMetadataColumn(row?.metadata);
512
+ const merged = { ...prev, ...patch };
513
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
514
+ metadata: toJsonColumn(merged),
515
+ last_seen_at: db.fn.now()
516
+ });
517
+ context.logger.info?.(`[services-registry] metadata updated for ${registration.serviceName}`);
518
+ }
519
+ async function unregisterServicesRegistry(context, registration) {
520
+ const db = getDb2(context);
521
+ await db(registration.registryTable).where({ id: registration.rowId }).delete();
522
+ context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
523
+ }
524
+ async function listServicesRegistry(context, options = { queueName: "tasks" }) {
525
+ const db = getDb2(context);
526
+ const staleMs = options.staleMs ?? 6e4;
527
+ const cutoff = new Date(Date.now() - staleMs);
528
+ const table = queueToTableNames(options.queueName).registryTable;
529
+ let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
530
+ if (options.serviceGroup?.trim()) {
531
+ q = q.where({ service_group: options.serviceGroup.trim() });
532
+ }
533
+ return await q;
534
+ }
535
+
536
+ // src/tasks/taskLogs.js
537
+ import path4 from "path";
538
+
539
+ // src/filedatabase/index.js
201
540
  import fs3 from "fs";
202
541
  import path3 from "path";
203
542
 
204
- // src/filedatabase/serializers.ts
543
+ // src/filedatabase/serializers.js
205
544
  function detectDataType(data) {
206
545
  if (Array.isArray(data)) {
207
546
  return "json-array";
@@ -233,7 +572,7 @@ function deserializeData(rawData, dataType) {
233
572
  }
234
573
  }
235
574
 
236
- // src/errors.ts
575
+ // src/errors.js
237
576
  var FrameworkError = class extends Error {
238
577
  constructor(message) {
239
578
  super(message);
@@ -253,7 +592,7 @@ var FileDatabaseError = class extends FrameworkError {
253
592
  }
254
593
  };
255
594
 
256
- // src/filedatabase/index.ts
595
+ // src/filedatabase/index.js
257
596
  var FileDatabase = class _FileDatabase {
258
597
  basePath;
259
598
  namespace;
@@ -339,7 +678,7 @@ var FileDatabase = class _FileDatabase {
339
678
  if (errors.length) {
340
679
  throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
341
680
  }
342
- let parts = [this.basePath, this.namespace];
681
+ const parts = [this.basePath, this.namespace];
343
682
  if (this.tableName) {
344
683
  parts.push(...this.tableName.split("/"));
345
684
  }
@@ -832,14 +1171,17 @@ var FileDatabase = class _FileDatabase {
832
1171
  * Prepare the instance for read or write operations
833
1172
  * This discovers state and sets up internal members based on mode and current data
834
1173
  */
835
- async prepare({ write, read, version }) {
1174
+ async prepare(options) {
1175
+ const { write, read, version, deferInitialVersion } = options;
836
1176
  if (write) {
837
1177
  if (this.versioned) {
838
1178
  if (this.currentVersion === null) {
839
- await this.makeNewVersion();
840
- this.metadata = this.getDefaultMetadata();
841
- this.metadata.version = this.currentVersion;
842
- this.makeNewFile();
1179
+ if (!deferInitialVersion) {
1180
+ await this.makeNewVersion();
1181
+ this.metadata = this.getDefaultMetadata();
1182
+ this.metadata.version = this.currentVersion;
1183
+ this.makeNewFile();
1184
+ }
843
1185
  } else {
844
1186
  if (!this.metadata.files.length) {
845
1187
  this.metadata = await this.figureMetadata(this.currentVersion);
@@ -949,7 +1291,7 @@ var FileDatabase = class _FileDatabase {
949
1291
  if (options.forceNewVersion && !this.versioned) {
950
1292
  throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
951
1293
  }
952
- await this.prepare({ write: true });
1294
+ await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
953
1295
  const incomingDataType = detectDataType(data);
954
1296
  this.metadata.dataType = incomingDataType;
955
1297
  if (options.forceNewVersion) {
@@ -1243,7 +1585,7 @@ var FileDatabase = class _FileDatabase {
1243
1585
  }
1244
1586
  };
1245
1587
 
1246
- // src/tasks/taskLogs.ts
1588
+ // src/tasks/taskLogs.js
1247
1589
  function getLogsState(context) {
1248
1590
  const holder = context;
1249
1591
  if (holder.__tasksLogsState) return holder.__tasksLogsState;
@@ -1296,6 +1638,96 @@ function getLogsState(context) {
1296
1638
  holder.__tasksLogsState = state;
1297
1639
  return state;
1298
1640
  }
1641
+ function ipcLogTargetKey(target) {
1642
+ const bp = target.basePath ?? "";
1643
+ const ns = target.namespace ?? "";
1644
+ return `${bp}::${ns}::${target.tableName}`;
1645
+ }
1646
+ function ipcFileLogsTableNameForSourceResource(source, resource) {
1647
+ const seg = (s) => {
1648
+ const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
1649
+ return t.length ? t : "x";
1650
+ };
1651
+ return `${seg(source)}/${seg(resource)}`;
1652
+ }
1653
+ async function readTaskIpcLogsSnapshot(context, options) {
1654
+ const holder = context;
1655
+ const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
1656
+ const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
1657
+ const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
1658
+ const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
1659
+ const fd = new FileDatabase({
1660
+ basePath,
1661
+ namespace,
1662
+ tableName,
1663
+ versioned: true,
1664
+ useMetadata: true,
1665
+ maxVersions: 30,
1666
+ pageSize: 2e3,
1667
+ logger: holder.logger
1668
+ });
1669
+ const versions = await fd.getVersions();
1670
+ if (versions.length === 0) {
1671
+ return { records: [], latestTs: null };
1672
+ }
1673
+ const latest = versions[versions.length - 1];
1674
+ const raw = await fd.read({ version: latest });
1675
+ const arr = Array.isArray(raw) ? raw : [];
1676
+ let filtered = arr;
1677
+ if (options.afterTs && String(options.afterTs).trim()) {
1678
+ const cut = String(options.afterTs).trim();
1679
+ filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
1680
+ }
1681
+ let latestTs = null;
1682
+ for (const r of filtered) {
1683
+ const ts = typeof r?.ts === "string" ? String(r.ts) : null;
1684
+ if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
1685
+ }
1686
+ const incremental = !!(options.afterTs && String(options.afterTs).trim());
1687
+ const maxReturn = incremental ? 1e4 : tail;
1688
+ const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
1689
+ return { records: sliced, latestTs };
1690
+ }
1691
+ function resolveIpcFileLogsDir(context, target) {
1692
+ const holder = context;
1693
+ const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
1694
+ const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
1695
+ const segments = target.tableName.split("/").filter(Boolean);
1696
+ return path4.resolve(basePath, namespace, ...segments);
1697
+ }
1698
+ function getLogsStateForTarget(context, target) {
1699
+ const holder = context;
1700
+ const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
1701
+ const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
1702
+ if (!enabled) return null;
1703
+ if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
1704
+ const map = holder.__tasksLogsTargetStates;
1705
+ const key = ipcLogTargetKey(target);
1706
+ if (map.has(key)) return map.get(key);
1707
+ const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
1708
+ const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
1709
+ const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
1710
+ const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
1711
+ const db = new FileDatabase({
1712
+ basePath,
1713
+ namespace,
1714
+ tableName: target.tableName,
1715
+ versioned: true,
1716
+ useMetadata: true,
1717
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
1718
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
1719
+ logger: holder.logger
1720
+ });
1721
+ const state = {
1722
+ db,
1723
+ errorDb: null,
1724
+ queue: Promise.resolve(),
1725
+ initialized: false,
1726
+ errorInitialized: false
1727
+ };
1728
+ map.set(key, state);
1729
+ return state;
1730
+ }
1299
1731
  function isErrorPayload(payload) {
1300
1732
  if (!payload) return false;
1301
1733
  if (typeof payload === "object") {
@@ -1315,14 +1747,26 @@ function buildLogRecord(task, payload) {
1315
1747
  ts: (/* @__PURE__ */ new Date()).toISOString(),
1316
1748
  opid: task.opid ?? null,
1317
1749
  taskId: task.id,
1318
- taskName: task.task,
1319
- target: task.target,
1750
+ taskName: task.name,
1751
+ target: task.service_group,
1320
1752
  source: typeof params.source === "string" ? params.source : null,
1321
1753
  resource: typeof params.resource === "string" ? params.resource : null,
1322
1754
  payload
1323
1755
  };
1324
1756
  }
1325
- function appendTaskIpcLog(context, task, payload) {
1757
+ function appendTaskIpcLog(context, task, payload, target) {
1758
+ if (target) {
1759
+ const state2 = getLogsStateForTarget(context, target);
1760
+ if (!state2?.db) return;
1761
+ const record2 = buildLogRecord(task, payload);
1762
+ state2.queue = state2.queue.then(async () => {
1763
+ await state2.db.write([record2], { forceNewVersion: !state2.initialized });
1764
+ state2.initialized = true;
1765
+ }).catch((error) => {
1766
+ context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
1767
+ });
1768
+ return;
1769
+ }
1326
1770
  const state = getLogsState(context);
1327
1771
  if (!state.db && !state.errorDb) return;
1328
1772
  const record = buildLogRecord(task, payload);
@@ -1339,98 +1783,315 @@ function appendTaskIpcLog(context, task, payload) {
1339
1783
  context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
1340
1784
  });
1341
1785
  }
1342
-
1343
- // src/tasks/time-matcher.ts
1344
- var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
1345
- function resolveAsterisks(field, range) {
1346
- return field.includes("*") ? field.replace("*", range) : field;
1347
- }
1348
- function resolveRanges(field) {
1349
- const regex = /(\d+)-(\d+)/;
1350
- let current = field;
1351
- while (true) {
1352
- const match = regex.exec(current);
1353
- if (!match) break;
1354
- const raw = match[0];
1355
- let first = Number(match[1]);
1356
- let last = Number(match[2]);
1357
- if (last < first) {
1358
- [first, last] = [last, first];
1359
- }
1360
- const values = [];
1361
- for (let i = first; i <= last; i += 1) {
1362
- values.push(i);
1786
+ async function flushTaskIpcLogs(context) {
1787
+ const holder = context;
1788
+ const promises = [];
1789
+ if (holder.__tasksLogsState?.queue) promises.push(holder.__tasksLogsState.queue);
1790
+ const map = holder.__tasksLogsTargetStates;
1791
+ if (map) {
1792
+ for (const s of map.values()) {
1793
+ if (s.queue) promises.push(s.queue);
1363
1794
  }
1364
- current = current.replace(raw, values.join(","));
1365
- }
1366
- return current;
1367
- }
1368
- function resolveSteps(field) {
1369
- const match = /^(.+)\/(\d+)$/.exec(field);
1370
- if (!match) return field;
1371
- const base = match[1];
1372
- const step = Number(match[2]);
1373
- if (!Number.isFinite(step) || step <= 0) return field;
1374
- return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
1375
- }
1376
- function convertPattern(pattern) {
1377
- const parts = pattern.trim().split(/\s+/);
1378
- if (parts.length !== 6) {
1379
- throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
1380
1795
  }
1381
- return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
1382
- }
1383
- function fieldMatches(field, value) {
1384
- const allowed = field.split(",").map((v) => Number(v));
1385
- return allowed.includes(value);
1386
- }
1387
- function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
1388
- const parsed = convertPattern(pattern);
1389
- return fieldMatches(parsed[0], date.getSeconds()) && fieldMatches(parsed[1], date.getMinutes()) && fieldMatches(parsed[2], date.getHours()) && fieldMatches(parsed[3], date.getDate()) && fieldMatches(parsed[4], date.getMonth() + 1) && fieldMatches(parsed[5], date.getDay());
1796
+ await Promise.all(promises);
1390
1797
  }
1391
1798
 
1392
- // src/tasks/TaskMaster.ts
1393
- var TaskMaster = class {
1394
- context;
1395
- task;
1799
+ // src/tasks/AbstractTask.js
1800
+ var AbstractTask = class _AbstractTask {
1801
+ /**
1802
+ * Whether `send-task` should wait for completion (and print a result
1803
+ * report) when no explicit `--wait` / `--noWait` flag is given. Defaults
1804
+ * to false; short-lived probe tasks (e.g. `ping`) override to true.
1805
+ *
1806
+ * @type {boolean}
1807
+ */
1808
+ static defaultWaitForResult = false;
1809
+ /**
1810
+ * @param {object} context Runner context (db, logger, params, emitter...).
1811
+ * @param {object} task Task row as claimed from the queue.
1812
+ */
1396
1813
  constructor(context, task) {
1397
1814
  this.context = context;
1398
1815
  this.task = task;
1399
1816
  }
1817
+ /**
1818
+ * Return a short reason string when the task should be deferred (e.g. "locked
1819
+ * by source"), or `false`/falsy when it is free to run. Default: always `false`.
1820
+ *
1821
+ * @returns {string | false | Promise<string | false>}
1822
+ */
1400
1823
  cantRunReason() {
1401
1824
  return false;
1402
1825
  }
1826
+ /**
1827
+ * Called by the runner when a stop has been requested. Subclasses running
1828
+ * long loops should flip a flag here and check it between iterations.
1829
+ *
1830
+ * @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
1831
+ */
1403
1832
  requestStop(_allowanceMs) {
1404
1833
  }
1405
- };
1406
-
1407
- // src/tasks/coreTasks/TaskPing.ts
1408
- var TaskPing = class extends TaskMaster {
1409
- async run() {
1410
- this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
1411
- return { success: true, results: "pong" };
1834
+ /**
1835
+ * Perform the task. Must be implemented by subclasses.
1836
+ *
1837
+ * @param {(progress: unknown) => Promise<void>} _reportProgress
1838
+ * Updates the DB `progress` column. Accepts any serializable value;
1839
+ * strings are stored verbatim, objects are JSON-stringified.
1840
+ * @returns {Promise<{ success: boolean, results: unknown }>}
1841
+ */
1842
+ async run(_reportProgress) {
1843
+ throw new Error("AbstractTask.run must be implemented by subclass");
1412
1844
  }
1413
- };
1414
-
1415
- // src/tasks/coreTasks/TaskSampleProcess.ts
1416
- var TaskSampleProcess = class extends TaskMaster {
1417
- stopRequested = false;
1418
- stopAllowanceMs = 0;
1419
- stopDecisionLogged = false;
1420
- requestStop(allowanceMs) {
1421
- this.stopRequested = true;
1422
- this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
1423
- this.context.logger.warn?.(
1424
- `[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
1425
- );
1845
+ /**
1846
+ * Resolve a complete row payload for this task — envelope fields (queue,
1847
+ * priority, targeting, schedule…) plus the inner `params` blob produced by
1848
+ * {@link AbstractTask.resolveCustomParams}. Output shape matches
1849
+ * {@link enqueueTask}'s `options` argument, so the typical call is:
1850
+ *
1851
+ * const payload = await TaskClass.resolveParams(context, { name });
1852
+ * await enqueueTask(context, payload);
1853
+ *
1854
+ * Validation failures throw {@link ParamError} so the script aborts before
1855
+ * a malformed row hits the DB.
1856
+ *
1857
+ * @param {object} context
1858
+ * @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
1859
+ * Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
1860
+ * `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
1861
+ * `nextRunAt`, plus `params` (object — overlay onto inner blob).
1862
+ * @returns {Promise<object>}
1863
+ */
1864
+ static async resolveParams(context, overrides = {}) {
1865
+ const main = _AbstractTask._resolveMainFields(context, overrides);
1866
+ const params = await this.resolveCustomParams(context, overrides);
1867
+ return { ...main, params };
1426
1868
  }
1427
- async run(reportProgress) {
1428
- const totalRaw = this.task?.params?.total ?? 10;
1429
- const delayRaw = this.task?.params?.delay ?? 1e3;
1430
- const nameRaw = this.task?.params?.name;
1431
- const total = Number(totalRaw);
1432
- const delay = Number(delayRaw);
1433
- const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "sampleProcess";
1869
+ /**
1870
+ * Resolve the inner JSON blob stored in the `params` column. Default
1871
+ * implementation passes through `--paramsJson` (parsed as a JSON object)
1872
+ * overlaid with `overrides.params` when supplied; returns `null` when
1873
+ * neither is provided.
1874
+ *
1875
+ * Subclasses with typed fields should override and call
1876
+ * {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
1877
+ * resolution, then validate and throw {@link ParamError} on bad input.
1878
+ *
1879
+ * @param {object} context
1880
+ * @param {Record<string, unknown>} [overrides]
1881
+ * @returns {Promise<object|null>}
1882
+ */
1883
+ static async resolveCustomParams(context, overrides = {}) {
1884
+ return _AbstractTask._defaultParamsBlob(context, overrides);
1885
+ }
1886
+ /**
1887
+ * Read main task envelope fields from `context.params` (CLI/env), with
1888
+ * any matching key on `overrides` taking precedence. Internal; called by
1889
+ * {@link AbstractTask.resolveParams}.
1890
+ *
1891
+ * @param {object} context
1892
+ * @param {Record<string, unknown>} [overrides]
1893
+ * @returns {object}
1894
+ */
1895
+ static _resolveMainFields(context, overrides = {}) {
1896
+ const defs = {
1897
+ queueName: "string default tasks",
1898
+ priority: "number default 50",
1899
+ serviceGroup: "string",
1900
+ serviceName: "string",
1901
+ instanceNumber: "number",
1902
+ serverName: "string",
1903
+ opid: "string",
1904
+ schedule: "string"
1905
+ };
1906
+ const cli = context.params.getAllForModule("task-envelope", defs);
1907
+ const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
1908
+ if (!name) {
1909
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
1910
+ }
1911
+ let instanceNumber;
1912
+ const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
1913
+ if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
1914
+ const n = Number(rawInstance);
1915
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
1916
+ throw new ParamError("--instanceNumber must be a positive integer when set");
1917
+ }
1918
+ instanceNumber = n;
1919
+ } else {
1920
+ instanceNumber = null;
1921
+ }
1922
+ const priorityRaw = overrides.priority ?? cli.priority ?? 50;
1923
+ const priority = Number(priorityRaw);
1924
+ if (!Number.isFinite(priority)) {
1925
+ throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
1926
+ }
1927
+ return {
1928
+ name,
1929
+ queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
1930
+ priority,
1931
+ serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
1932
+ serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
1933
+ instanceNumber,
1934
+ serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
1935
+ opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
1936
+ schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
1937
+ nextRunAt: overrides.nextRunAt ?? null
1938
+ };
1939
+ }
1940
+ /**
1941
+ * Default inner-params resolver: parses `--paramsJson` (must be a JSON
1942
+ * object), then overlays `overrides.params` on top. Returns `null` when
1943
+ * neither is provided.
1944
+ *
1945
+ * @param {object} context
1946
+ * @param {Record<string, unknown>} [overrides]
1947
+ * @returns {object|null}
1948
+ */
1949
+ static _defaultParamsBlob(context, overrides = {}) {
1950
+ const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
1951
+ const fromJson = parseParamsJson(cli.paramsJson);
1952
+ const fromOverride = pickParamsObject(overrides);
1953
+ if (!fromJson && !fromOverride) return null;
1954
+ return { ...fromJson ?? {}, ...fromOverride ?? {} };
1955
+ }
1956
+ /**
1957
+ * Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
1958
+ * params (per `defs`) plus `--paramsJson` under a module namespace, then
1959
+ * merges them with explicit `overrides.params` in increasing priority:
1960
+ *
1961
+ * typed CLI flags → --paramsJson → overrides.params
1962
+ *
1963
+ * Undefined values are dropped so defaults declared in `defs` aren't
1964
+ * overwritten by missing-flag noise. Returns the merged object; the
1965
+ * caller is responsible for validation and throwing `ParamError`.
1966
+ *
1967
+ * @param {object} context
1968
+ * @param {string} moduleName Namespace for `--showUsedParams` grouping.
1969
+ * @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
1970
+ * @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
1971
+ * @returns {Record<string, unknown>}
1972
+ */
1973
+ static _mergeTypedParams(context, moduleName, defs, overrides = {}) {
1974
+ const fullDefs = { ...defs, paramsJson: "string" };
1975
+ const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
1976
+ const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
1977
+ const fromCli = {};
1978
+ for (const [k, v] of Object.entries(cliRaw)) {
1979
+ if (k === "paramsJson") continue;
1980
+ if (v !== void 0 && v !== null) fromCli[k] = v;
1981
+ }
1982
+ const fromOverride = pickParamsObject(overrides) ?? {};
1983
+ return { ...fromCli, ...fromJson, ...fromOverride };
1984
+ }
1985
+ };
1986
+ function emptyToUndef(s) {
1987
+ if (s === void 0 || s === null) return void 0;
1988
+ if (typeof s !== "string") return s;
1989
+ const t = s.trim();
1990
+ return t.length ? t : void 0;
1991
+ }
1992
+ function parseParamsJson(raw) {
1993
+ if (raw == null) return null;
1994
+ const t = String(raw).trim();
1995
+ if (!t) return null;
1996
+ let parsed;
1997
+ try {
1998
+ parsed = JSON.parse(t);
1999
+ } catch (e) {
2000
+ throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
2001
+ }
2002
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2003
+ throw new ParamError("--paramsJson must be a JSON object");
2004
+ }
2005
+ return parsed;
2006
+ }
2007
+ function pickParamsObject(overrides) {
2008
+ const p = overrides?.params;
2009
+ if (p && typeof p === "object" && !Array.isArray(p)) return p;
2010
+ return void 0;
2011
+ }
2012
+
2013
+ // src/tasks/coreTasks/TaskPing.js
2014
+ var TaskPing = class extends AbstractTask {
2015
+ /** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
2016
+ static defaultWaitForResult = true;
2017
+ /** Ping takes no params. */
2018
+ static async resolveCustomParams() {
2019
+ return null;
2020
+ }
2021
+ /**
2022
+ * @returns {Promise<{ success: true, results: "pong" }>}
2023
+ */
2024
+ async run() {
2025
+ this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
2026
+ return { success: true, results: "pong" };
2027
+ }
2028
+ };
2029
+
2030
+ // src/tasks/coreTasks/TaskSampleProcess.js
2031
+ var TaskSampleProcess = class extends AbstractTask {
2032
+ /**
2033
+ * @param {object} context
2034
+ * @param {Record<string, unknown>} [overrides]
2035
+ * @returns {Promise<{ total: number, delay: number, name?: string }>}
2036
+ */
2037
+ static async resolveCustomParams(context, overrides = {}) {
2038
+ const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
2039
+ total: "number default 10",
2040
+ delay: "number default 1000",
2041
+ name: "string"
2042
+ }, overrides);
2043
+ const total = Number(merged.total);
2044
+ const delay = Number(merged.delay);
2045
+ if (!Number.isInteger(total) || total <= 0) {
2046
+ throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
2047
+ }
2048
+ if (!Number.isInteger(delay) || delay < 0) {
2049
+ throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
2050
+ }
2051
+ const out = { total, delay };
2052
+ if (typeof merged.name === "string" && merged.name.trim()) {
2053
+ out.name = merged.name.trim();
2054
+ }
2055
+ return out;
2056
+ }
2057
+ /**
2058
+ * @param {object} context
2059
+ * @param {object} task
2060
+ */
2061
+ constructor(context, task) {
2062
+ super(context, task);
2063
+ this.stopRequested = false;
2064
+ this.stopAllowanceMs = 0;
2065
+ this.stopDecisionLogged = false;
2066
+ }
2067
+ /**
2068
+ * Runner-facing stop signal. Records the allowance window so the main loop
2069
+ * can decide per-iteration whether to finish or abort early.
2070
+ *
2071
+ * @param {number} allowanceMs
2072
+ */
2073
+ requestStop(allowanceMs) {
2074
+ this.stopRequested = true;
2075
+ this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
2076
+ this.context.logger.warn?.(
2077
+ `[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
2078
+ );
2079
+ }
2080
+ /**
2081
+ * Iterate `total` times, sleeping `delay` ms between ticks and reporting
2082
+ * progress every iteration. Validates params up front; invalid values short-
2083
+ * circuit to a structured failure without starting the loop.
2084
+ *
2085
+ * @param {(progress: object) => Promise<void>} reportProgress
2086
+ * @returns {Promise<{ success: boolean, results: unknown }>}
2087
+ */
2088
+ async run(reportProgress) {
2089
+ const totalRaw = this.task?.params?.total ?? 10;
2090
+ const delayRaw = this.task?.params?.delay ?? 1e3;
2091
+ const nameRaw = this.task?.params?.name;
2092
+ const total = Number(totalRaw);
2093
+ const delay = Number(delayRaw);
2094
+ const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "sampleProcess";
1434
2095
  const errors = [];
1435
2096
  if (!Number.isInteger(total) || total <= 0) {
1436
2097
  errors.push('param "total" must be a positive integer');
@@ -1505,7 +2166,7 @@ var TaskSampleProcess = class extends TaskMaster {
1505
2166
  }
1506
2167
  };
1507
2168
 
1508
- // src/tasks/coreTasks/TaskShellCommand.ts
2169
+ // src/tasks/coreTasks/TaskShellCommand.js
1509
2170
  import { spawn } from "child_process";
1510
2171
  function runShellCommand(command, cwd) {
1511
2172
  return new Promise((resolve, reject) => {
@@ -1535,7 +2196,27 @@ function runShellCommand(command, cwd) {
1535
2196
  });
1536
2197
  });
1537
2198
  }
1538
- var TaskShellCommand = class extends TaskMaster {
2199
+ var TaskShellCommand = class extends AbstractTask {
2200
+ /**
2201
+ * @param {object} context
2202
+ * @param {Record<string, unknown>} [overrides]
2203
+ * @returns {Promise<{ command: string, cwd?: string }>}
2204
+ */
2205
+ static async resolveCustomParams(context, overrides = {}) {
2206
+ const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
2207
+ command: "string",
2208
+ cwd: "string"
2209
+ }, overrides);
2210
+ const command = typeof merged.command === "string" ? merged.command.trim() : "";
2211
+ if (!command) {
2212
+ throw new ParamError('shellCommand: param "command" must be a non-empty string');
2213
+ }
2214
+ const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
2215
+ return cwd ? { command, cwd } : { command };
2216
+ }
2217
+ /**
2218
+ * @returns {Promise<{ success: boolean, results: unknown }>}
2219
+ */
1539
2220
  async run() {
1540
2221
  const params = this.task?.params;
1541
2222
  const commandRaw = typeof params === "string" ? params : params?.command;
@@ -1584,8 +2265,8 @@ var TaskShellCommand = class extends TaskMaster {
1584
2265
  }
1585
2266
  };
1586
2267
 
1587
- // src/tasks/coreTasks/TaskSystemInfo.ts
1588
- import os from "os";
2268
+ // src/tasks/coreTasks/TaskSystemInfo.js
2269
+ import os2 from "os";
1589
2270
  import fs4 from "fs/promises";
1590
2271
  function toGb(valueBytes) {
1591
2272
  return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
@@ -1604,13 +2285,22 @@ async function getDiskStats() {
1604
2285
  free: toGb(free)
1605
2286
  };
1606
2287
  }
1607
- var TaskSystemInfo = class extends TaskMaster {
2288
+ var TaskSystemInfo = class extends AbstractTask {
2289
+ /** Same UX expectation as `ping` — short probe, print the result. */
2290
+ static defaultWaitForResult = true;
2291
+ /** systemInfo takes no params. */
2292
+ static async resolveCustomParams() {
2293
+ return null;
2294
+ }
2295
+ /**
2296
+ * @returns {Promise<{ success: boolean, results: unknown }>}
2297
+ */
1608
2298
  async run() {
1609
2299
  try {
1610
- const totalMemory = os.totalmem();
1611
- const freeMemory = os.freemem();
2300
+ const totalMemory = os2.totalmem();
2301
+ const freeMemory = os2.freemem();
1612
2302
  const usedMemory = totalMemory - freeMemory;
1613
- const cpus = os.cpus();
2303
+ const cpus = os2.cpus();
1614
2304
  const cpuUtilization = cpus.map((cpu) => {
1615
2305
  const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
1616
2306
  const usage = (total - cpu.times.idle) / total * 100;
@@ -1636,10 +2326,10 @@ var TaskSystemInfo = class extends TaskMaster {
1636
2326
  utilization: cpuUtilization
1637
2327
  },
1638
2328
  runtime: {
1639
- platform: os.platform(),
1640
- arch: os.arch(),
1641
- uptimeSec: os.uptime(),
1642
- hostname: os.hostname()
2329
+ platform: os2.platform(),
2330
+ arch: os2.arch(),
2331
+ uptimeSec: os2.uptime(),
2332
+ hostname: os2.hostname()
1643
2333
  }
1644
2334
  };
1645
2335
  this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
@@ -1656,8 +2346,31 @@ var TaskSystemInfo = class extends TaskMaster {
1656
2346
  }
1657
2347
  };
1658
2348
 
1659
- // src/tasks/coreTasks/TaskSumAB.ts
1660
- var TaskSumAB = class extends TaskMaster {
2349
+ // src/tasks/coreTasks/TaskSumAB.js
2350
+ var TaskSumAB = class extends AbstractTask {
2351
+ /** Short, deterministic — wait by default so callers see the sum. */
2352
+ static defaultWaitForResult = true;
2353
+ /**
2354
+ * @param {object} context
2355
+ * @param {Record<string, unknown>} [overrides]
2356
+ * @returns {Promise<{ a: number, b: number }>}
2357
+ */
2358
+ static async resolveCustomParams(context, overrides = {}) {
2359
+ const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
2360
+ a: "number",
2361
+ b: "number"
2362
+ }, overrides);
2363
+ if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
2364
+ throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
2365
+ }
2366
+ if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
2367
+ throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
2368
+ }
2369
+ return { a: merged.a, b: merged.b };
2370
+ }
2371
+ /**
2372
+ * @returns {Promise<{ success: boolean, results: unknown }>}
2373
+ */
1661
2374
  async run() {
1662
2375
  const a = this.task?.params?.a;
1663
2376
  const b = this.task?.params?.b;
@@ -1688,8 +2401,46 @@ var TaskSumAB = class extends TaskMaster {
1688
2401
  }
1689
2402
  };
1690
2403
 
1691
- // src/tasks/coreTasks/TaskStopRunner.ts
1692
- var TaskStopRunner = class extends TaskMaster {
2404
+ // src/tasks/coreTasks/TaskStopRunner.js
2405
+ var TaskStopRunner = class extends AbstractTask {
2406
+ /**
2407
+ * Stop tasks must target a concrete instance — without `serviceName` the
2408
+ * row would race against any worker on the queue. Layered on top of the
2409
+ * envelope built by {@link AbstractTask.resolveParams}.
2410
+ *
2411
+ * @param {object} context
2412
+ * @param {Record<string, unknown>} [overrides]
2413
+ * @returns {Promise<object>}
2414
+ */
2415
+ static async resolveParams(context, overrides = {}) {
2416
+ const main = await super.resolveParams(context, overrides);
2417
+ if (!main.serviceName) {
2418
+ throw new ParamError(
2419
+ "stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
2420
+ );
2421
+ }
2422
+ return main;
2423
+ }
2424
+ /**
2425
+ * @param {object} context
2426
+ * @param {Record<string, unknown>} [overrides]
2427
+ * @returns {Promise<{ allowanceMs: number }>}
2428
+ */
2429
+ static async resolveCustomParams(context, overrides = {}) {
2430
+ const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
2431
+ allowanceMs: "number default 5000"
2432
+ }, overrides);
2433
+ const allowanceMs = Number(merged.allowanceMs);
2434
+ if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
2435
+ throw new ParamError(
2436
+ `stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
2437
+ );
2438
+ }
2439
+ return { allowanceMs };
2440
+ }
2441
+ /**
2442
+ * @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
2443
+ */
1693
2444
  async run() {
1694
2445
  const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
1695
2446
  this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
@@ -1704,175 +2455,399 @@ var TaskStopRunner = class extends TaskMaster {
1704
2455
  }
1705
2456
  };
1706
2457
 
1707
- // src/tasks/TasksRegistry.ts
2458
+ // src/tasks/coreTasks/TaskGetLogs.js
2459
+ var TaskGetLogs = class extends AbstractTask {
2460
+ /**
2461
+ * @param {object} context
2462
+ * @param {Record<string, unknown>} [overrides]
2463
+ * @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
2464
+ */
2465
+ static async resolveCustomParams(context, overrides = {}) {
2466
+ const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
2467
+ source: "string",
2468
+ resource: "string",
2469
+ tail: "number default 100",
2470
+ afterTs: "string"
2471
+ }, overrides);
2472
+ const source = typeof merged.source === "string" ? merged.source.trim() : "";
2473
+ const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
2474
+ if (!source) throw new ParamError('getLogs: param "source" is required');
2475
+ if (!resource) throw new ParamError('getLogs: param "resource" is required');
2476
+ let tail = Number(merged.tail);
2477
+ if (!Number.isFinite(tail) || tail < 1) tail = 100;
2478
+ tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
2479
+ const out = { source, resource, tail };
2480
+ if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
2481
+ out.afterTs = merged.afterTs.trim();
2482
+ }
2483
+ return out;
2484
+ }
2485
+ /**
2486
+ * @param {unknown} _reportProgress Unused (single-shot read, no progress events).
2487
+ * @returns {Promise<{ success: boolean, results: unknown }>}
2488
+ */
2489
+ async run(_reportProgress) {
2490
+ const p = this.task.params ?? {};
2491
+ const source = String(p.source ?? "").trim();
2492
+ const resource = String(p.resource ?? "").trim();
2493
+ const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
2494
+ const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
2495
+ if (!source || !resource) {
2496
+ return {
2497
+ success: false,
2498
+ results: { error: 'getLogs requires params "source" and "resource"' }
2499
+ };
2500
+ }
2501
+ try {
2502
+ const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
2503
+ source,
2504
+ resource,
2505
+ tail,
2506
+ afterTs
2507
+ });
2508
+ return {
2509
+ success: true,
2510
+ results: { records, latestTs, source, resource }
2511
+ };
2512
+ } catch (e) {
2513
+ return {
2514
+ success: false,
2515
+ results: { error: e?.message ?? String(e) }
2516
+ };
2517
+ }
2518
+ }
2519
+ };
2520
+
2521
+ // src/tasks/TasksRegistry.js
1708
2522
  var TasksRegistry = class _TasksRegistry {
1709
- map = {};
2523
+ /**
2524
+ * @param {Record<string, Function>} [initial] Optional seed entries to copy in.
2525
+ */
1710
2526
  constructor(initial) {
2527
+ this.map = {};
1711
2528
  if (initial) {
1712
2529
  this.addMany(initial);
1713
2530
  }
1714
2531
  }
2532
+ /**
2533
+ * Build a registry pre-populated with every core task plus legacy aliases.
2534
+ * Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
2535
+ *
2536
+ * @returns {TasksRegistry}
2537
+ */
1715
2538
  static withCoreTasks() {
1716
- return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
2539
+ 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);
1717
2540
  }
2541
+ /**
2542
+ * Register a single task class under a name. Overwrites any previous entry.
2543
+ *
2544
+ * @param {string} taskName
2545
+ * @param {Function} taskClass Subclass of `AbstractTask`.
2546
+ * @returns {this}
2547
+ */
1718
2548
  add(taskName, taskClass) {
1719
2549
  this.map[taskName] = taskClass;
1720
2550
  return this;
1721
2551
  }
2552
+ /**
2553
+ * Bulk-register a name → class map. Later calls override earlier ones.
2554
+ *
2555
+ * @param {Record<string, Function>} entries
2556
+ * @returns {this}
2557
+ */
1722
2558
  addMany(entries) {
1723
2559
  for (const [name, klass] of Object.entries(entries)) {
1724
2560
  this.add(name, klass);
1725
2561
  }
1726
2562
  return this;
1727
2563
  }
2564
+ /**
2565
+ * Look up a task class by name. Returns `undefined` when the name is unknown;
2566
+ * the runner treats that as "some other worker may handle this" and skips.
2567
+ *
2568
+ * @param {string} taskName
2569
+ * @returns {Function | undefined}
2570
+ */
1728
2571
  get(taskName) {
1729
2572
  return this.map[taskName];
1730
2573
  }
2574
+ /**
2575
+ * Strict variant of {@link get}: throws {@link ParamError} (with the list
2576
+ * of supported names) when `taskName` is unknown. Use from enqueuer code
2577
+ * paths where an unknown name is a hard CLI/programmer error.
2578
+ *
2579
+ * @param {string} taskName
2580
+ * @returns {Function}
2581
+ */
2582
+ requireClass(taskName) {
2583
+ const TaskClass = taskName ? this.map[taskName] : void 0;
2584
+ if (!TaskClass) {
2585
+ const supported = this.listSupportedTasks().join(", ") || "(none)";
2586
+ throw new ParamError(
2587
+ `Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
2588
+ );
2589
+ }
2590
+ return TaskClass;
2591
+ }
2592
+ /**
2593
+ * Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
2594
+ * from `overrides` or `context.params`, look up the class, and delegate
2595
+ * to its static {@link AbstractTask.resolveParams} with `name` seeded into
2596
+ * the overrides. The returned object is shaped for {@link enqueueTask}.
2597
+ *
2598
+ * Validation failures (unknown task, missing required custom params, etc.)
2599
+ * surface as {@link ParamError} so the caller aborts cleanly before any
2600
+ * row is inserted.
2601
+ *
2602
+ * @param {object} context
2603
+ * @param {Record<string, unknown>} [overrides]
2604
+ * @returns {Promise<object>}
2605
+ */
2606
+ async resolveTaskParams(context, overrides = {}) {
2607
+ const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
2608
+ const fromCli = context.params.get("name", "string");
2609
+ const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
2610
+ const name = overrideName || cliName;
2611
+ if (!name) {
2612
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
2613
+ }
2614
+ const TaskClass = this.requireClass(name);
2615
+ return TaskClass.resolveParams(context, { ...overrides, name });
2616
+ }
2617
+ /**
2618
+ * Names of every registered task, sorted alphabetically (useful for CLI output
2619
+ * and allowlist sanity checks).
2620
+ *
2621
+ * @returns {string[]}
2622
+ */
1731
2623
  listSupportedTasks() {
1732
2624
  return Object.keys(this.map).sort();
1733
2625
  }
2626
+ /**
2627
+ * Shallow copy of the internal map, for handing to `addMany` on another registry
2628
+ * or for serialization.
2629
+ *
2630
+ * @returns {Record<string, Function>}
2631
+ */
1734
2632
  toObject() {
1735
2633
  return { ...this.map };
1736
2634
  }
1737
2635
  };
1738
2636
 
1739
- // src/tasks/taskScriptRunner.ts
2637
+ // src/tasks/serviceTaskAllowlist.js
2638
+ var SERVICE_TASK_NAMES = [
2639
+ "ping",
2640
+ "stop",
2641
+ "stopRunner",
2642
+ "shellCommand",
2643
+ "systemInfo",
2644
+ "info",
2645
+ "getLogs"
2646
+ ];
2647
+ function normalizeAllowedTasks(value) {
2648
+ if (!value) return void 0;
2649
+ if (Array.isArray(value)) {
2650
+ const out2 = value.map((v) => String(v).trim()).filter(Boolean);
2651
+ return out2.length ? out2 : void 0;
2652
+ }
2653
+ const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
2654
+ return out.length ? out : void 0;
2655
+ }
2656
+ function mergeAllowedTasksWithServiceTasks(names) {
2657
+ const set = /* @__PURE__ */ new Set([...SERVICE_TASK_NAMES, ...names ?? []]);
2658
+ return Array.from(set).sort();
2659
+ }
2660
+
2661
+ // src/tasks/taskScriptRunner.js
1740
2662
  import { spawn as spawn2 } from "child_process";
2663
+ var MAX_PROGRESS_TEXT_LEN = 4e3;
1741
2664
  function toCliArgs(args = []) {
1742
2665
  return args.filter((a) => typeof a === "string" && a.length > 0);
1743
2666
  }
1744
2667
  function formatChildLogPrefix(task) {
1745
- return `${task.task}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
2668
+ return `${task.name}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
1746
2669
  }
1747
- async function runNodeTaskScript(context, options) {
1748
- const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
2670
+ function isProgressPayload(payload) {
2671
+ if (!payload || typeof payload !== "object") return false;
2672
+ if (payload.level !== "progress") return false;
2673
+ const count = Number(payload.count);
2674
+ const total = Number(payload.total);
2675
+ return Number.isFinite(count) && Number.isFinite(total) && total > 0;
2676
+ }
2677
+ function formatProgressText(payload, fallbackPrefix) {
2678
+ const pfx = payload.prefix ? `${payload.prefix} ` : fallbackPrefix ? `${fallbackPrefix} ` : "";
2679
+ const label = typeof payload.message === "string" && payload.message ? `${payload.message} ` : "";
2680
+ return `${pfx}${label}${payload.count}/${payload.total}`;
2681
+ }
2682
+ function forwardChildLogToParent(context, prefix, message) {
2683
+ if (!message || typeof message !== "object") return;
2684
+ const text = typeof message.message === "string" ? message.message : null;
2685
+ if (!text) return;
2686
+ const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
2687
+ const line = `[child:${prefix}] ${text}`;
2688
+ const logger = context.logger;
2689
+ switch (level) {
2690
+ case "error":
2691
+ case "fatal":
2692
+ logger.error?.(line);
2693
+ return;
2694
+ case "warn":
2695
+ case "warning":
2696
+ logger.warn?.(line);
2697
+ return;
2698
+ case "debug":
2699
+ logger.debug?.(line);
2700
+ return;
2701
+ case "info":
2702
+ default:
2703
+ logger.info?.(line);
2704
+ }
2705
+ }
2706
+ function buildNodeArgs(scriptPath, cliArgs) {
1749
2707
  const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
1750
2708
  const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
1751
- const nodeArgs = hasTsRuntimeInParent ? [...inheritedExecArgs, options.scriptPath, ...cliArgs] : ["--import", "tsx", options.scriptPath, ...cliArgs];
1752
- const child = spawn2(
1753
- process.execPath,
1754
- nodeArgs,
1755
- {
1756
- cwd: options.cwd || process.cwd(),
1757
- stdio: ["ignore", "pipe", "pipe", "ipc"],
1758
- env: {
1759
- ...process.env,
1760
- TASK_ID: options.task.id,
1761
- TASK_NAME: options.task.task,
1762
- TASK_OPID: options.task.opid || ""
1763
- }
1764
- }
2709
+ return hasTsRuntimeInParent ? [...inheritedExecArgs, scriptPath, ...cliArgs] : ["--import", "tsx", scriptPath, ...cliArgs];
2710
+ }
2711
+ function resolveTasksTableName(context) {
2712
+ return context.tasksQueueName || context.params?.get?.("table") || "tasks";
2713
+ }
2714
+ function announceIpcFileLogsTarget(context, options) {
2715
+ if (!options.ipcFileLogs) return;
2716
+ const logsDir = resolveIpcFileLogsDir(context, options.ipcFileLogs);
2717
+ const enabledRaw = context.params?.get?.("tasksLogsEnabled");
2718
+ const logsEnabled = enabledRaw === void 0 ? true : !!enabledRaw;
2719
+ context.logger.info?.(
2720
+ `[tasks] IPC file logs: ${logsDir}` + (logsEnabled ? "" : " (tasksLogsEnabled=false; not persisted)")
1765
2721
  );
1766
- let stdout = "";
1767
- let stderr = "";
1768
- let workerResult = null;
1769
- let hadErrorMessage = false;
1770
- const prefix = formatChildLogPrefix(options.task);
1771
- const db = context.db;
1772
- const tasksTable = context.params?.get?.("table") || "tasks";
1773
- let progressWriteChain = Promise.resolve();
1774
- let progressCallbackChain = Promise.resolve();
1775
- const updateProgress = (text) => {
1776
- if (!db || !text || !text.trim()) return;
1777
- progressWriteChain = progressWriteChain.then(async () => {
1778
- await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
1779
- }).catch((error) => {
1780
- context.logger.warn?.(
1781
- `[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
1782
- );
1783
- });
1784
- if (options.onProgress) {
1785
- progressCallbackChain = progressCallbackChain.then(async () => {
1786
- await options.onProgress?.(text.slice(0, 4e3));
1787
- }).catch((error) => {
1788
- context.logger.warn?.(
1789
- `[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
1790
- );
2722
+ }
2723
+ function createSerializedQueue() {
2724
+ let chain = Promise.resolve();
2725
+ return {
2726
+ push(fn) {
2727
+ chain = chain.then(fn, () => {
2728
+ }).catch(() => {
2729
+ });
2730
+ return chain;
2731
+ },
2732
+ drain() {
2733
+ return chain.catch(() => {
1791
2734
  });
1792
2735
  }
1793
2736
  };
1794
- const payloadToProgressText = (payload) => {
1795
- if (!payload) return "";
1796
- if (typeof payload === "string") return payload;
1797
- if (typeof payload.message === "string" && payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
1798
- const pfx = payload.prefix ? `${payload.prefix} ` : "";
1799
- return `${pfx}${payload.message} ${payload.count}/${payload.total}`;
1800
- }
1801
- if (typeof payload.message === "string") return payload.message;
1802
- if (payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
1803
- const pfx = payload.prefix ? `${payload.prefix} ` : "";
1804
- return `${pfx}${payload.count}/${payload.total}`;
1805
- }
1806
- return "";
2737
+ }
2738
+ async function runNodeTaskScript(context, options) {
2739
+ const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
2740
+ const nodeArgs = buildNodeArgs(options.scriptPath, cliArgs);
2741
+ const child = spawn2(process.execPath, nodeArgs, {
2742
+ cwd: options.cwd || process.cwd(),
2743
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
2744
+ env: {
2745
+ ...process.env,
2746
+ TASK_ID: options.task.id,
2747
+ TASK_NAME: options.task.name,
2748
+ TASK_OPID: options.task.opid || ""
2749
+ }
2750
+ });
2751
+ announceIpcFileLogsTarget(context, options);
2752
+ const prefix = formatChildLogPrefix(options.task);
2753
+ const tasksTable = resolveTasksTableName(context);
2754
+ const progressQueue = createSerializedQueue();
2755
+ const state = {
2756
+ stdout: "",
2757
+ stderr: "",
2758
+ workerResult: null,
2759
+ hadErrorMessage: false
1807
2760
  };
1808
- child.stdout.on("data", (chunk) => {
2761
+ const writeProgress = (text) => {
2762
+ const trimmed = typeof text === "string" ? text.slice(0, MAX_PROGRESS_TEXT_LEN) : "";
2763
+ if (!trimmed) return;
2764
+ progressQueue.push(async () => {
2765
+ const db = context.db;
2766
+ if (db) {
2767
+ try {
2768
+ await db(tasksTable).where({ id: options.task.id }).update({ progress: trimmed });
2769
+ } catch (error) {
2770
+ context.logger.warn?.(
2771
+ `[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
2772
+ );
2773
+ }
2774
+ }
2775
+ if (options.onProgress) {
2776
+ try {
2777
+ await options.onProgress(trimmed);
2778
+ } catch (error) {
2779
+ context.logger.warn?.(
2780
+ `[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
2781
+ );
2782
+ }
2783
+ }
2784
+ });
2785
+ };
2786
+ child.stdout?.on("data", (chunk) => {
1809
2787
  const text = String(chunk);
1810
- stdout += text;
2788
+ state.stdout += text;
1811
2789
  if (text.trim()) {
1812
2790
  context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
1813
- updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
1814
2791
  }
1815
2792
  });
1816
- child.stderr.on("data", (chunk) => {
2793
+ child.stderr?.on("data", (chunk) => {
1817
2794
  const text = String(chunk);
1818
- stderr += text;
2795
+ state.stderr += text;
1819
2796
  if (text.trim()) {
1820
2797
  context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
1821
2798
  }
1822
2799
  });
1823
2800
  child.on("message", (message) => {
1824
2801
  if (message && typeof message === "object" && "__taskWorkerResult" in message) {
1825
- workerResult = message.__taskWorkerResult;
2802
+ state.workerResult = message.__taskWorkerResult;
1826
2803
  return;
1827
2804
  }
1828
2805
  if (message && typeof message === "object") {
1829
2806
  const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
1830
2807
  if (level === "error" || level === "fatal") {
1831
- hadErrorMessage = true;
2808
+ state.hadErrorMessage = true;
1832
2809
  }
1833
2810
  }
1834
- appendTaskIpcLog(context, options.task, message);
1835
- const progressText = payloadToProgressText(message);
1836
- if (progressText) {
1837
- updateProgress(progressText);
1838
- if (typeof message === "object" && message?.level === "progress" && message.count !== void 0 && message.total !== void 0) {
1839
- const countNum = Number(String(message.count).trim());
1840
- const totalNum = Number(message.total);
1841
- if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
1842
- context.logger.progress(message.message || "progress", {
1843
- prefix: message.prefix || prefix,
1844
- count: countNum,
1845
- total: totalNum
1846
- });
1847
- } else {
1848
- context.logger.info?.(`[child:${prefix}] ${progressText}`);
1849
- }
1850
- } else {
1851
- context.logger.info?.(`[child:${prefix}] ${progressText}`);
1852
- }
2811
+ appendTaskIpcLog(context, options.task, message, options.ipcFileLogs);
2812
+ try {
2813
+ options.onChildIpcMessage?.(message);
2814
+ } catch (e) {
2815
+ context.logger.warn?.(`[tasks] onChildIpcMessage failed: ${e?.message ?? String(e)}`);
1853
2816
  }
2817
+ if (isProgressPayload(message)) {
2818
+ context.logger.progress(message.message || "progress", {
2819
+ prefix: message.prefix || prefix,
2820
+ count: Number(message.count),
2821
+ total: Number(message.total)
2822
+ });
2823
+ writeProgress(formatProgressText(message, prefix));
2824
+ return;
2825
+ }
2826
+ forwardChildLogToParent(context, prefix, message);
1854
2827
  });
1855
2828
  return await new Promise((resolve, reject) => {
1856
2829
  child.on("error", (error) => reject(error));
1857
2830
  child.on("close", (exitCode, signal) => {
1858
- Promise.allSettled([progressWriteChain, progressCallbackChain]).finally(() => {
2831
+ void (async () => {
2832
+ await flushTaskIpcLogs(context);
2833
+ await progressQueue.drain();
1859
2834
  resolve({
1860
2835
  exitCode,
1861
2836
  signal,
1862
- stdout: stdout.trim(),
1863
- stderr: stderr.trim(),
1864
- workerResult,
1865
- hadErrorMessage
2837
+ stdout: state.stdout.trim(),
2838
+ stderr: state.stderr.trim(),
2839
+ workerResult: state.workerResult,
2840
+ hadErrorMessage: state.hadErrorMessage
1866
2841
  });
1867
- });
2842
+ })();
1868
2843
  });
1869
2844
  });
1870
2845
  }
1871
2846
 
1872
- // src/tasks/index.ts
2847
+ // src/tasks/index.js
1873
2848
  var LOCKED_BY_ERROR_MESSAGE = "locked by error";
1874
2849
  var defaultTasksRegistry = TasksRegistry.withCoreTasks();
1875
- function getDb2(context) {
2850
+ function getDb3(context) {
1876
2851
  const db = context.db;
1877
2852
  if (!db) {
1878
2853
  throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
@@ -1884,22 +2859,13 @@ function normalizeRegistry(registry) {
1884
2859
  if (registry instanceof TasksRegistry) return registry;
1885
2860
  return new TasksRegistry().addMany(registry);
1886
2861
  }
1887
- function normalizeAllowedTasks(value) {
1888
- if (!value) return void 0;
1889
- if (Array.isArray(value)) {
1890
- const out2 = value.map((v) => String(v).trim()).filter(Boolean);
1891
- return out2.length ? out2 : void 0;
1892
- }
1893
- const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
1894
- return out.length ? out : void 0;
1895
- }
1896
- async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
2862
+ async function enqueueStopTask(context, serviceGroup, queueName = "tasks", allowanceMs = 5e3) {
1897
2863
  return enqueueTask(context, {
1898
- queue,
1899
- target,
1900
- task: "stopRunner",
2864
+ queueName,
2865
+ name: "stopRunner",
1901
2866
  params: { allowanceMs },
1902
- priority: 1e6
2867
+ priority: 0,
2868
+ serviceGroup
1903
2869
  });
1904
2870
  }
1905
2871
  async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
@@ -1916,19 +2882,21 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
1916
2882
  context.emitter.emit("stop", allowanceMs);
1917
2883
  }
1918
2884
  async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
1919
- const db = getDb2(context);
1920
- const taskName = row.task;
2885
+ const db = getDb3(context);
2886
+ const taskName = row.name;
1921
2887
  const TaskClass = registry.get(taskName);
1922
- const { paused_at: _pausedAt, ...rowForHistory } = row;
1923
2888
  if (!TaskClass) {
1924
2889
  const err = { message: `Unknown task "${taskName}"` };
1925
- await db(historyTable).insert({
1926
- ...rowForHistory,
1927
- completed_at: /* @__PURE__ */ new Date(),
1928
- success: false,
1929
- params: toJsonColumn(row.params),
1930
- results: toJsonColumn(err)
1931
- });
2890
+ await db(historyTable).insert(
2891
+ taskHistoryInsertFromQueueRow(row, {
2892
+ completed_at: /* @__PURE__ */ new Date(),
2893
+ success: false,
2894
+ status: "failed",
2895
+ status_changed_at: db.fn.now(),
2896
+ params: toJsonColumn(row.params),
2897
+ results: toJsonColumn(err)
2898
+ })
2899
+ );
1932
2900
  if (row.schedule) {
1933
2901
  await db(tasksTable).where({ id: row.id }).update({
1934
2902
  started_at: null,
@@ -1936,7 +2904,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
1936
2904
  success: false,
1937
2905
  results: toJsonColumn(err),
1938
2906
  past_due: null,
1939
- paused_at: db.fn.now(),
2907
+ status: "paused",
2908
+ status_changed_at: db.fn.now(),
1940
2909
  progress: LOCKED_BY_ERROR_MESSAGE
1941
2910
  });
1942
2911
  } else {
@@ -1963,13 +2932,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
1963
2932
  } finally {
1964
2933
  runningTaskInstances.delete(row.id);
1965
2934
  }
1966
- await db(historyTable).insert({
1967
- ...rowForHistory,
1968
- completed_at: /* @__PURE__ */ new Date(),
1969
- success,
1970
- params: toJsonColumn(row.params),
1971
- results: toJsonColumn(results)
1972
- });
2935
+ await db(historyTable).insert(
2936
+ taskHistoryInsertFromQueueRow(row, {
2937
+ completed_at: /* @__PURE__ */ new Date(),
2938
+ success,
2939
+ status: success ? "completed" : "failed",
2940
+ status_changed_at: db.fn.now(),
2941
+ params: toJsonColumn(row.params),
2942
+ results: toJsonColumn(results)
2943
+ })
2944
+ );
1973
2945
  if (!success) {
1974
2946
  const dbName = String(context?.params?.get?.("dbName") || "local");
1975
2947
  const tableName = String(context?.params?.get?.("table") || "tasks");
@@ -1984,19 +2956,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
1984
2956
  const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
1985
2957
  appendTaskIpcLog(context, row, {
1986
2958
  level: "error",
1987
- message: `[tasks] task failed: ${row.task} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
2959
+ message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
1988
2960
  details: results
1989
2961
  });
1990
2962
  }
1991
2963
  if (row.schedule) {
1992
2964
  if (success) {
2965
+ let nextRunAt = null;
2966
+ try {
2967
+ nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
2968
+ } catch (e) {
2969
+ context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
2970
+ }
1993
2971
  await db(tasksTable).where({ id: row.id }).update({
1994
2972
  started_at: null,
1995
2973
  completed_at: /* @__PURE__ */ new Date(),
1996
2974
  success,
1997
2975
  results: toJsonColumn(results),
1998
2976
  progress: null,
1999
- past_due: null
2977
+ past_due: null,
2978
+ status: "idle",
2979
+ status_changed_at: db.fn.now(),
2980
+ next_run_at: nextRunAt,
2981
+ // Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
2982
+ service_name: null,
2983
+ server_name: null,
2984
+ instance_number: null
2000
2985
  });
2001
2986
  } else {
2002
2987
  await db(tasksTable).where({ id: row.id }).update({
@@ -2004,7 +2989,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
2004
2989
  completed_at: /* @__PURE__ */ new Date(),
2005
2990
  success,
2006
2991
  results: toJsonColumn(results),
2007
- paused_at: db.fn.now(),
2992
+ status: "paused",
2993
+ status_changed_at: db.fn.now(),
2008
2994
  progress: LOCKED_BY_ERROR_MESSAGE,
2009
2995
  past_due: null
2010
2996
  });
@@ -2016,208 +3002,401 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
2016
3002
  const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
2017
3003
  return { stopRunnerRequested, stopAllowanceMs };
2018
3004
  }
2019
- async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
2020
- const db = getDb2(context);
2021
- 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);
3005
+ function shuffleTaskRowsInPlace(rows) {
3006
+ for (let i = rows.length - 1; i > 0; i--) {
3007
+ const j = Math.floor(Math.random() * (i + 1));
3008
+ const t = rows[i];
3009
+ rows[i] = rows[j];
3010
+ rows[j] = t;
3011
+ }
3012
+ }
3013
+ async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
3014
+ const db = getDb3(context);
3015
+ let query = db(tasksTable).where({ status: "idle" }).where(function() {
3016
+ this.whereNull("service_group").orWhere({ service_group: serviceGroup });
3017
+ }).orderByRaw("CASE WHEN past_due IS NULL THEN 1 ELSE 0 END ASC").orderBy([{ column: "priority", order: "asc" }]).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);
2022
3018
  if (taskNames && taskNames.length > 0) {
2023
- query = query.whereIn("task", taskNames);
3019
+ query = query.whereIn("name", taskNames);
3020
+ }
3021
+ if (runnerIdentity) {
3022
+ query = query.where(function() {
3023
+ this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
3024
+ }).where(function() {
3025
+ this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
3026
+ }).where(function() {
3027
+ this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
3028
+ });
3029
+ } else {
3030
+ query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
2024
3031
  }
2025
3032
  const candidates = await query;
3033
+ shuffleTaskRowsInPlace(candidates);
2026
3034
  for (const row of candidates) {
2027
3035
  if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
2028
3036
  continue;
2029
3037
  }
2030
- const TaskClass = registry.get(row.task);
2031
- if (TaskClass) {
2032
- const taskInstance = new TaskClass(context, row);
2033
- const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
2034
- if (reason) {
2035
- if (!row.past_due) {
2036
- await db(tasksTable).where({ id: row.id }).update({
2037
- past_due: db.fn.now(),
2038
- progress: String(reason)
2039
- });
2040
- }
2041
- continue;
3038
+ const TaskClass = registry.get(row.name);
3039
+ if (!TaskClass) {
3040
+ continue;
3041
+ }
3042
+ const taskInstance = new TaskClass(context, row);
3043
+ const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
3044
+ if (reason) {
3045
+ if (!row.past_due) {
3046
+ await db(tasksTable).where({ id: row.id }).update({
3047
+ past_due: db.fn.now(),
3048
+ progress: String(reason)
3049
+ });
2042
3050
  }
3051
+ continue;
2043
3052
  }
2044
- const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
3053
+ const claimPatch = {
3054
+ started_at: db.fn.now(),
3055
+ status: "running",
3056
+ status_changed_at: db.fn.now()
3057
+ };
3058
+ if (runnerIdentity) {
3059
+ claimPatch.service_name = runnerIdentity.service_name;
3060
+ claimPatch.server_name = runnerIdentity.server_name;
3061
+ claimPatch.instance_number = runnerIdentity.instance_number;
3062
+ }
3063
+ const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
2045
3064
  const claimed = Array.isArray(updated) ? updated[0] : null;
2046
3065
  if (claimed) return claimed;
2047
3066
  }
2048
3067
  return null;
2049
3068
  }
2050
3069
  async function runTasksLoop(context, options) {
2051
- const queue = options.queue ?? "tasks";
3070
+ const queueName = options.queueName ?? "tasks";
2052
3071
  const target = options.target;
2053
3072
  const pollMs = options.pollMs ?? 1e3;
2054
- const maxParallel = options.maxParallel ?? 1;
3073
+ const claimJitterMs = options.claimJitterMs ?? 0;
3074
+ const maxParallel = options.maxParallel ?? 32;
2055
3075
  const scanLimit = options.scanLimit ?? 100;
2056
3076
  const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
2057
3077
  const registry = normalizeRegistry(options.registry);
2058
- const { tasksTable, historyTable } = queueToTableNames(queue);
3078
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
2059
3079
  if (!target) throw new Error("runTasksLoop: target is required");
3080
+ context.tasksQueueName = queueName;
2060
3081
  const runningPromises = /* @__PURE__ */ new Set();
2061
3082
  const runningTaskInstances = /* @__PURE__ */ new Map();
2062
3083
  let runningStopControlPromise = null;
2063
3084
  let stopRequested = false;
2064
3085
  let stopAllowanceMs = 5e3;
2065
- context.__tasksRunnerStop = false;
2066
- while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
2067
- if (!runningStopControlPromise) {
2068
- const claimedStopTask = await claimNextRunnableTask(
2069
- context,
2070
- tasksTable,
2071
- target,
2072
- registry,
2073
- 10,
2074
- ["stopRunner", "stop"]
2075
- );
2076
- if (claimedStopTask) {
2077
- runningStopControlPromise = executeClaimedTask(
3086
+ context.tasksRunnerStop = false;
3087
+ let registryReg = null;
3088
+ let registryInterval = null;
3089
+ let runnerIdentity = null;
3090
+ const hbGroup = options.runnerServiceGroup?.trim();
3091
+ if (hbGroup) {
3092
+ const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
3093
+ const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
3094
+ const defaultMeta = {
3095
+ component: "tasks-runner",
3096
+ allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
3097
+ };
3098
+ registryReg = await registerInServicesRegistry(context, {
3099
+ queueName,
3100
+ target,
3101
+ serviceGroup: hbGroup,
3102
+ serviceName: options.runnerServiceName,
3103
+ instanceNumber: options.runnerInstanceNumber,
3104
+ staleMs,
3105
+ groupMaxInstances: options.runnerGroupMaxInstances,
3106
+ enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
3107
+ metadata: options.runnerMetadata ?? defaultMeta
3108
+ });
3109
+ runnerIdentity = {
3110
+ service_name: registryReg.serviceName,
3111
+ server_name: os3.hostname(),
3112
+ instance_number: registryReg.instanceNumber
3113
+ };
3114
+ registryInterval = setInterval(() => {
3115
+ void touchServicesRegistry(context, registryReg).catch((err) => {
3116
+ context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
3117
+ });
3118
+ }, hbIntervalMs);
3119
+ }
3120
+ try {
3121
+ while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
3122
+ if (!runningStopControlPromise) {
3123
+ const claimedStopTask = await claimNextRunnableTask(
2078
3124
  context,
2079
3125
  tasksTable,
2080
- historyTable,
2081
- claimedStopTask,
3126
+ target,
2082
3127
  registry,
2083
- runningTaskInstances
2084
- ).then(async (outcome) => {
3128
+ 10,
3129
+ ["stopRunner", "stop"],
3130
+ runnerIdentity
3131
+ );
3132
+ if (claimedStopTask) {
3133
+ runningStopControlPromise = executeClaimedTask(
3134
+ context,
3135
+ tasksTable,
3136
+ historyTable,
3137
+ claimedStopTask,
3138
+ registry,
3139
+ runningTaskInstances
3140
+ ).then(async (outcome) => {
3141
+ if (outcome.stopRunnerRequested && !stopRequested) {
3142
+ stopRequested = true;
3143
+ stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
3144
+ context.tasksRunnerStop = true;
3145
+ await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
3146
+ }
3147
+ }).finally(() => {
3148
+ runningStopControlPromise = null;
3149
+ });
3150
+ }
3151
+ }
3152
+ if (claimJitterMs > 0) {
3153
+ await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
3154
+ }
3155
+ while (runningPromises.size < maxParallel) {
3156
+ const claimed = await claimNextRunnableTask(
3157
+ context,
3158
+ tasksTable,
3159
+ target,
3160
+ registry,
3161
+ scanLimit,
3162
+ allowedTasks,
3163
+ runnerIdentity
3164
+ );
3165
+ if (!claimed) break;
3166
+ const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
2085
3167
  if (outcome.stopRunnerRequested && !stopRequested) {
2086
3168
  stopRequested = true;
2087
3169
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
2088
- context.__tasksRunnerStop = true;
3170
+ context.tasksRunnerStop = true;
2089
3171
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
2090
3172
  }
2091
3173
  }).finally(() => {
2092
- runningStopControlPromise = null;
3174
+ runningPromises.delete(p);
2093
3175
  });
3176
+ runningPromises.add(p);
3177
+ }
3178
+ const wakePromises = [...runningPromises];
3179
+ if (runningStopControlPromise) {
3180
+ wakePromises.push(runningStopControlPromise);
3181
+ }
3182
+ if (wakePromises.length === 0) {
3183
+ await sleepMs(pollMs);
3184
+ } else {
3185
+ const safe = wakePromises.map((p) => p.catch(() => void 0));
3186
+ await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
2094
3187
  }
2095
3188
  }
2096
- while (runningPromises.size < maxParallel) {
2097
- const claimed = await claimNextRunnableTask(
2098
- context,
2099
- tasksTable,
2100
- target,
2101
- registry,
2102
- scanLimit,
2103
- allowedTasks
2104
- );
2105
- if (!claimed) break;
2106
- const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
2107
- if (outcome.stopRunnerRequested && !stopRequested) {
2108
- stopRequested = true;
2109
- stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
2110
- context.__tasksRunnerStop = true;
2111
- await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
2112
- }
2113
- }).finally(() => {
2114
- runningPromises.delete(p);
2115
- });
2116
- runningPromises.add(p);
3189
+ if (context.isStop() && !stopRequested) {
3190
+ await signalRunningTasksStop(context, runningTaskInstances, 5e3);
2117
3191
  }
2118
- await sleepMs(pollMs);
2119
- }
2120
- if (context.isStop() && !stopRequested) {
2121
- await signalRunningTasksStop(context, runningTaskInstances, 5e3);
2122
- }
2123
- if (runningPromises.size > 0) {
2124
- if (stopRequested) {
2125
- await Promise.race([
2126
- Promise.allSettled(Array.from(runningPromises)),
2127
- sleepMs(stopAllowanceMs).then(() => {
2128
- context.logger.warn?.(
2129
- `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
2130
- );
2131
- })
2132
- ]);
2133
- } else {
2134
- await Promise.allSettled(Array.from(runningPromises));
3192
+ if (runningPromises.size > 0) {
3193
+ if (stopRequested) {
3194
+ await Promise.race([
3195
+ Promise.allSettled(Array.from(runningPromises)),
3196
+ sleepMs(stopAllowanceMs).then(() => {
3197
+ context.logger.warn?.(
3198
+ `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
3199
+ );
3200
+ })
3201
+ ]);
3202
+ } else {
3203
+ await Promise.allSettled(Array.from(runningPromises));
3204
+ }
3205
+ }
3206
+ } finally {
3207
+ if (registryInterval) {
3208
+ clearInterval(registryInterval);
3209
+ registryInterval = null;
3210
+ }
3211
+ if (registryReg) {
3212
+ await unregisterServicesRegistry(context, registryReg).catch((err) => {
3213
+ context.logger.warn?.(`[services-registry] unregister failed: ${err?.message ?? String(err)}`);
3214
+ });
3215
+ registryReg = null;
3216
+ delete context.servicesRegistry;
3217
+ delete context.runnerHeartbeat;
2135
3218
  }
2136
3219
  }
2137
3220
  }
2138
3221
  async function waitForTaskResult(context, taskId, options = {}) {
2139
- const db = getDb2(context);
2140
- const queue = options.queue ?? "tasks";
3222
+ const db = getDb3(context);
3223
+ const queueName = options.queueName ?? "tasks";
2141
3224
  const timeoutMs = options.timeoutMs ?? 6e4;
2142
3225
  const pollMs = options.pollMs ?? 500;
2143
- const { tasksTable, historyTable } = queueToTableNames(queue);
3226
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
2144
3227
  const deadline = Date.now() + timeoutMs;
3228
+ const waitStartedAt = /* @__PURE__ */ new Date();
3229
+ let cachedNameOpid = null;
3230
+ async function historySinceWait(name, opid) {
3231
+ let q = db(historyTable).where({ name }).where("completed_at", ">=", waitStartedAt);
3232
+ if (opid == null || opid === "") {
3233
+ q = q.whereNull("opid");
3234
+ } else {
3235
+ q = q.where({ opid });
3236
+ }
3237
+ return await q.orderBy("completed_at", "desc").first();
3238
+ }
2145
3239
  while (Date.now() <= deadline) {
2146
- const done = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
2147
- if (done) return done;
3240
+ const legacy = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
3241
+ if (legacy) {
3242
+ return legacy;
3243
+ }
2148
3244
  const pending = await db(tasksTable).where({ id: taskId }).first();
2149
- if (!pending) {
2150
- const maybeDone = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
2151
- return maybeDone ?? null;
3245
+ if (pending) {
3246
+ cachedNameOpid = { name: pending.name, opid: pending.opid };
3247
+ const done = await historySinceWait(pending.name, pending.opid);
3248
+ if (done) {
3249
+ return done;
3250
+ }
3251
+ } else if (cachedNameOpid) {
3252
+ const done = await historySinceWait(cachedNameOpid.name, cachedNameOpid.opid);
3253
+ if (done) {
3254
+ return done;
3255
+ }
3256
+ return null;
3257
+ } else {
3258
+ return null;
2152
3259
  }
2153
3260
  await sleepMs(pollMs);
2154
3261
  }
2155
3262
  return null;
2156
3263
  }
2157
3264
  var TasksManager = class _TasksManager {
2158
- context;
2159
- queue;
2160
- target;
2161
- recreateTaskTables;
2162
- pollMs;
2163
- maxParallel;
2164
- scanLimit;
2165
- allowedTasks;
2166
- registry;
3265
+ /**
3266
+ * @param {object} context
3267
+ * @param {{
3268
+ * queueName?: string,
3269
+ * target?: string,
3270
+ * recreateTaskTables?: boolean,
3271
+ * pollMs?: number,
3272
+ * claimJitterMs?: number,
3273
+ * maxParallel?: number,
3274
+ * scanLimit?: number,
3275
+ * allowedTasks?: string | string[],
3276
+ * registry?: TasksRegistry | Record<string, Function>,
3277
+ * runnerServiceGroup?: string,
3278
+ * runnerServiceName?: string,
3279
+ * runnerInstanceNumber?: number,
3280
+ * runnerHeartbeatIntervalMs?: number,
3281
+ * runnerHeartbeatStaleMs?: number,
3282
+ * runnerGroupMaxInstances?: number,
3283
+ * runnerEnforceMaxInstances?: boolean,
3284
+ * runnerMetadata?: Record<string, unknown>,
3285
+ * }} [options]
3286
+ */
2167
3287
  constructor(context, options = {}) {
2168
3288
  this.context = context;
2169
- this.queue = options.queue ?? "tasks";
3289
+ this.queueName = options.queueName ?? "tasks";
2170
3290
  this.target = options.target ?? "localRunner";
2171
3291
  this.recreateTaskTables = options.recreateTaskTables ?? false;
2172
3292
  this.pollMs = options.pollMs ?? 1e3;
3293
+ this.claimJitterMs = options.claimJitterMs ?? 0;
2173
3294
  this.maxParallel = options.maxParallel ?? 1;
2174
3295
  this.scanLimit = options.scanLimit ?? 100;
2175
3296
  this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
2176
3297
  this.registry = normalizeRegistry(options.registry);
3298
+ this.runnerServiceGroup = options.runnerServiceGroup;
3299
+ this.runnerServiceName = options.runnerServiceName;
3300
+ this.runnerInstanceNumber = options.runnerInstanceNumber;
3301
+ this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
3302
+ this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
3303
+ this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
3304
+ this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
3305
+ this.runnerMetadata = options.runnerMetadata;
2177
3306
  }
3307
+ /**
3308
+ * Preferred factory: reads defaults from `context.params` (module namespace
3309
+ * `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
3310
+ * and inline options in one consistent resolver.
3311
+ *
3312
+ * @param {object} context
3313
+ * @param {ConstructorParameters<typeof TasksManager>[1]} [options]
3314
+ * @returns {TasksManager}
3315
+ */
2178
3316
  static init(context, options = {}) {
2179
3317
  const defs = {
2180
3318
  table: "string default tasks",
2181
3319
  target: "string default localRunner",
2182
3320
  recreateTaskTables: "boolean default false",
2183
3321
  pollMs: "number default 1000",
3322
+ claimJitterMs: "number default 0",
2184
3323
  maxParallel: "number default 1",
2185
3324
  scanLimit: "number default 100",
2186
- allowedTasks: "string"
3325
+ allowedTasks: "string",
3326
+ runnerServiceGroup: "string",
3327
+ runnerServiceName: "string",
3328
+ runnerInstanceNumber: "number",
3329
+ runnerHeartbeatIntervalMs: "number default 10000",
3330
+ runnerHeartbeatStaleMs: "number default 45000",
3331
+ runnerGroupMaxInstances: "number",
3332
+ runnerEnforceMaxInstances: "boolean default true"
2187
3333
  };
2188
- const discovered = context.params.getAllForModule(defs);
3334
+ const discovered = context.params.getAllForModule("tasks", defs);
2189
3335
  const resolved = {
2190
- queue: discovered.table,
3336
+ queueName: discovered.table,
2191
3337
  target: discovered.target,
2192
3338
  recreateTaskTables: discovered.recreateTaskTables,
2193
3339
  pollMs: discovered.pollMs,
3340
+ claimJitterMs: discovered.claimJitterMs,
2194
3341
  maxParallel: discovered.maxParallel,
2195
3342
  scanLimit: discovered.scanLimit,
2196
3343
  allowedTasks: discovered.allowedTasks,
3344
+ runnerServiceGroup: discovered.runnerServiceGroup,
3345
+ runnerServiceName: discovered.runnerServiceName,
3346
+ runnerInstanceNumber: discovered.runnerInstanceNumber,
3347
+ runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
3348
+ runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
3349
+ runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
3350
+ runnerEnforceMaxInstances: discovered.runnerEnforceMaxInstances,
2197
3351
  ...options
2198
3352
  };
2199
3353
  return new _TasksManager(context, resolved);
2200
3354
  }
3355
+ /**
3356
+ * Idempotently ensure the three backing tables exist for this queue.
3357
+ *
3358
+ * @param {{ recreate?: boolean }} [options]
3359
+ * @returns {Promise<void>}
3360
+ */
2201
3361
  async ensureTaskTables(options = {}) {
2202
3362
  await ensureTaskTables(this.context, {
2203
- queue: this.queue,
3363
+ queueName: this.queueName,
2204
3364
  recreate: options.recreate ?? this.recreateTaskTables
2205
3365
  });
2206
3366
  }
3367
+ /**
3368
+ * Start the runner loop using this manager's resolved config. Per-call
3369
+ * options override the stored defaults, but `runnerMetadata` still falls
3370
+ * through when omitted.
3371
+ *
3372
+ * @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
3373
+ * @returns {Promise<void>}
3374
+ */
2207
3375
  async runTasksLoop(options = {}) {
2208
3376
  await runTasksLoop(this.context, {
2209
- queue: options.queue ?? this.queue,
3377
+ queueName: options.queueName ?? this.queueName,
2210
3378
  target: options.target ?? this.target,
2211
3379
  pollMs: options.pollMs ?? this.pollMs,
3380
+ claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
2212
3381
  maxParallel: options.maxParallel ?? this.maxParallel,
2213
3382
  scanLimit: options.scanLimit ?? this.scanLimit,
2214
3383
  allowedTasks: options.allowedTasks ?? this.allowedTasks,
2215
- registry: options.registry ?? this.registry
3384
+ registry: options.registry ?? this.registry,
3385
+ runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
3386
+ runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
3387
+ runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
3388
+ runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
3389
+ runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
3390
+ runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
3391
+ runnerEnforceMaxInstances: options.runnerEnforceMaxInstances ?? this.runnerEnforceMaxInstances,
3392
+ runnerMetadata: options.runnerMetadata ?? this.runnerMetadata
2216
3393
  });
2217
3394
  }
2218
3395
  };
2219
3396
  export {
2220
- TaskMaster,
3397
+ AbstractTask,
3398
+ SERVICE_TASK_NAMES,
3399
+ TaskGetLogs,
2221
3400
  TaskPing,
2222
3401
  TaskSampleProcess,
2223
3402
  TaskShellCommand,
@@ -2227,13 +3406,36 @@ export {
2227
3406
  TasksManager,
2228
3407
  TasksRegistry,
2229
3408
  appendTaskIpcLog,
3409
+ convertPattern,
2230
3410
  defaultTasksRegistry,
2231
3411
  enqueueStopTask,
2232
3412
  enqueueTask,
2233
3413
  ensureTaskTables,
3414
+ flushTaskIpcLogs,
3415
+ ipcFileLogsTableNameForSourceResource,
3416
+ listServicesRegistry as listAliveRunnerHeartbeats,
3417
+ listServicesRegistry,
3418
+ matchesParsedPattern,
3419
+ mergeAllowedTasksWithServiceTasks,
3420
+ nextTimeMatch,
3421
+ normalizeAllowedTasks,
2234
3422
  queueToTableNames,
3423
+ readTaskIpcLogsSnapshot,
3424
+ registerInServicesRegistry,
3425
+ registerInServicesRegistry as registerRunnerHeartbeat,
3426
+ resolveAsterisks,
3427
+ resolveIpcFileLogsDir,
3428
+ resolveRanges,
3429
+ resolveSteps,
2235
3430
  runNodeTaskScript,
2236
3431
  runTasksLoop,
3432
+ taskHistoryInsertFromQueueRow,
3433
+ timeMatcher,
3434
+ touchServicesRegistry as touchRunnerHeartbeat,
3435
+ touchServicesRegistry,
3436
+ unregisterServicesRegistry as unregisterRunnerHeartbeat,
3437
+ unregisterServicesRegistry,
3438
+ updateServicesRegistryMetadata,
2237
3439
  updateTaskProgress,
2238
3440
  waitForTaskResult
2239
3441
  };