@nmakarov/cli-toolkit 0.49.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tasks.js CHANGED
@@ -2707,6 +2707,273 @@ var TaskGetLogs = class extends AbstractTask {
2707
2707
  }
2708
2708
  };
2709
2709
 
2710
+ // src/tasks/runtimeParams.js
2711
+ var LOOP_RUNTIME_KEYS = ["maxParallel", "pollMs", "claimJitterMs", "scanLimit"];
2712
+ var LOGGER_RUNTIME_KEYS = [
2713
+ "levels",
2714
+ "silent",
2715
+ "showLevel",
2716
+ "timestamp",
2717
+ "mode",
2718
+ "route",
2719
+ "prefix",
2720
+ "progressWithTimes",
2721
+ "progressThrottleMs"
2722
+ ];
2723
+ var CONTROL_LANE_TASK_NAMES = ["stopRunner", "stop", "setRuntimeParam", "setRunnerParam"];
2724
+ function controlLaneTaskNames() {
2725
+ return [...CONTROL_LANE_TASK_NAMES];
2726
+ }
2727
+ function asPositiveInt(value, key, { min = 1 } = {}) {
2728
+ const n = Number(value);
2729
+ if (!Number.isFinite(n) || n < min) {
2730
+ throw new ParamError(
2731
+ `setRuntimeParam: ${key} must be a number >= ${min} (got ${JSON.stringify(value)})`
2732
+ );
2733
+ }
2734
+ return Math.floor(n);
2735
+ }
2736
+ function coerceRuntimeValue(key, value) {
2737
+ switch (key) {
2738
+ case "maxParallel":
2739
+ return asPositiveInt(value, key, { min: 1 });
2740
+ case "pollMs":
2741
+ return asPositiveInt(value, key, { min: 50 });
2742
+ case "claimJitterMs":
2743
+ return asPositiveInt(value, key, { min: 0 });
2744
+ case "scanLimit":
2745
+ return asPositiveInt(value, key, { min: 1 });
2746
+ case "silent":
2747
+ case "showLevel":
2748
+ case "timestamp":
2749
+ case "progressWithTimes":
2750
+ if (typeof value === "boolean") return value;
2751
+ if (value === "true" || value === "1") return true;
2752
+ if (value === "false" || value === "0") return false;
2753
+ throw new ParamError(
2754
+ `setRuntimeParam: ${key} must be boolean (got ${JSON.stringify(value)})`
2755
+ );
2756
+ case "progressThrottleMs":
2757
+ return asPositiveInt(value, key, { min: 0 });
2758
+ case "levels":
2759
+ case "mode":
2760
+ case "route":
2761
+ case "prefix":
2762
+ return value;
2763
+ default:
2764
+ return value;
2765
+ }
2766
+ }
2767
+ function ensureTasksRuntime(context, seed = {}) {
2768
+ if (!context.tasksRuntime || typeof context.tasksRuntime !== "object") {
2769
+ context.tasksRuntime = {};
2770
+ }
2771
+ const rt = context.tasksRuntime;
2772
+ if (rt.maxParallel === void 0) rt.maxParallel = seed.maxParallel ?? 32;
2773
+ if (rt.pollMs === void 0) rt.pollMs = seed.pollMs ?? 1e3;
2774
+ if (rt.claimJitterMs === void 0) rt.claimJitterMs = seed.claimJitterMs ?? 0;
2775
+ if (rt.scanLimit === void 0) rt.scanLimit = seed.scanLimit ?? 100;
2776
+ return rt;
2777
+ }
2778
+ async function applyRuntimeParam(context, key, value) {
2779
+ const k = String(key ?? "").trim();
2780
+ if (!k) throw new ParamError("setRuntimeParam: key is required");
2781
+ const runtime = ensureTasksRuntime(context);
2782
+ const next = coerceRuntimeValue(k, value);
2783
+ const previous = runtime[k];
2784
+ const applied = [];
2785
+ runtime[k] = next;
2786
+ applied.push("tasksRuntime");
2787
+ if (LOGGER_RUNTIME_KEYS.includes(k) && context.logger?.configure) {
2788
+ context.logger.configure({ [k]: next });
2789
+ applied.push("logger");
2790
+ }
2791
+ const hook = context.tasksRuntimeOnParam;
2792
+ if (typeof hook === "function") {
2793
+ await hook(k, next, runtime, context);
2794
+ applied.push("onRuntimeParam");
2795
+ }
2796
+ const reg = context.servicesRegistry;
2797
+ if (reg?.rowId && reg?.registryTable) {
2798
+ try {
2799
+ const loopSnapshot = {};
2800
+ for (const lk of LOOP_RUNTIME_KEYS) {
2801
+ if (runtime[lk] !== void 0) loopSnapshot[lk] = runtime[lk];
2802
+ }
2803
+ await updateServicesRegistryMetadata(context, reg, {
2804
+ runtime: loopSnapshot,
2805
+ runtimeUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
2806
+ });
2807
+ applied.push("servicesRegistry");
2808
+ } catch (err) {
2809
+ context.logger?.warn?.(
2810
+ `[setRuntimeParam] registry metadata update failed: ${err?.message ?? String(err)}`
2811
+ );
2812
+ }
2813
+ }
2814
+ return { key: k, previous, next, applied };
2815
+ }
2816
+ async function applyRuntimePatch(context, patch) {
2817
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
2818
+ throw new ParamError("setRuntimeParam: patch must be a plain object");
2819
+ }
2820
+ const entries = Object.entries(patch);
2821
+ if (entries.length === 0) {
2822
+ throw new ParamError("setRuntimeParam: patch is empty");
2823
+ }
2824
+ const out = [];
2825
+ for (const [key, value] of entries) {
2826
+ out.push(await applyRuntimeParam(context, key, value));
2827
+ }
2828
+ return out;
2829
+ }
2830
+ function readLoopRuntime(context) {
2831
+ const rt = ensureTasksRuntime(context);
2832
+ return {
2833
+ maxParallel: Math.max(1, Number(rt.maxParallel) || 1),
2834
+ pollMs: Math.max(50, Number(rt.pollMs) || 1e3),
2835
+ claimJitterMs: Math.max(0, Number(rt.claimJitterMs) || 0),
2836
+ scanLimit: Math.max(1, Number(rt.scanLimit) || 100)
2837
+ };
2838
+ }
2839
+
2840
+ // src/tasks/coreTasks/TaskSetRuntimeParam.js
2841
+ var TaskSetRuntimeParam = class extends AbstractTask {
2842
+ static defaultWaitForResult = true;
2843
+ /**
2844
+ * @param {object} context
2845
+ * @param {Record<string, unknown>} [overrides]
2846
+ * @returns {Promise<object>}
2847
+ */
2848
+ static async resolveParams(context, overrides = {}) {
2849
+ const main = await super.resolveParams(context, overrides);
2850
+ if (!main.serviceName && !main.serviceGroup) {
2851
+ throw new ParamError(
2852
+ "setRuntimeParam requires --serviceName (one instance) or --serviceGroup (broadcast to alive instances)"
2853
+ );
2854
+ }
2855
+ return main;
2856
+ }
2857
+ /**
2858
+ * @param {object} context
2859
+ * @param {Record<string, unknown>} [overrides]
2860
+ * @returns {Promise<{ key?: string, value?: unknown, patch?: Record<string, unknown> }>}
2861
+ */
2862
+ static async resolveCustomParams(context, overrides = {}) {
2863
+ const merged = AbstractTask._mergeTypedParams(
2864
+ context,
2865
+ "task-set-runtime-param",
2866
+ {
2867
+ paramKey: "string",
2868
+ paramValue: "string",
2869
+ key: "string",
2870
+ value: "string"
2871
+ },
2872
+ overrides
2873
+ );
2874
+ if (merged.patch && typeof merged.patch === "object" && !Array.isArray(merged.patch)) {
2875
+ if (Object.keys(merged.patch).length === 0) {
2876
+ throw new ParamError("setRuntimeParam: patch is empty");
2877
+ }
2878
+ return { patch: { ...merged.patch } };
2879
+ }
2880
+ const key = merged.paramKey || merged.key;
2881
+ const value = merged.paramValue !== void 0 ? merged.paramValue : merged.value;
2882
+ if (!key) {
2883
+ throw new ParamError(
2884
+ `setRuntimeParam requires --paramKey/--paramValue, or --paramsJson '{"key":"maxParallel","value":16}' / '{"patch":{...}}'`
2885
+ );
2886
+ }
2887
+ let parsed = value;
2888
+ if (typeof value === "string") {
2889
+ const t = value.trim();
2890
+ if (t === "true") parsed = true;
2891
+ else if (t === "false") parsed = false;
2892
+ else if (t !== "" && !Number.isNaN(Number(t)) && /^-?\d+(\.\d+)?$/.test(t)) {
2893
+ parsed = Number(t);
2894
+ } else if (t.startsWith("{") && t.endsWith("}") || t.startsWith("[") && t.endsWith("]") || t.startsWith('"') && t.endsWith('"')) {
2895
+ try {
2896
+ parsed = JSON.parse(t);
2897
+ } catch {
2898
+ parsed = value;
2899
+ }
2900
+ }
2901
+ }
2902
+ return { key: String(key), value: parsed };
2903
+ }
2904
+ /**
2905
+ * Enqueue one or many setRuntimeParam tasks. Prefer this over a bare
2906
+ * `enqueueTask` when broadcasting to a service group.
2907
+ *
2908
+ * @param {object} context
2909
+ * @param {Record<string, unknown>} [overrides]
2910
+ * @returns {Promise<{ ids: string[], targets: string[] }>}
2911
+ */
2912
+ static async enqueue(context, overrides = {}) {
2913
+ const payload = await this.resolveParams(context, { ...overrides, name: "setRuntimeParam" });
2914
+ const queueName = payload.queueName ?? "tasks";
2915
+ if (payload.serviceName) {
2916
+ const id = await enqueueTask(context, payload);
2917
+ return { ids: [id], targets: [payload.serviceName] };
2918
+ }
2919
+ const group = String(payload.serviceGroup || "").trim();
2920
+ if (!group) {
2921
+ throw new ParamError("setRuntimeParam.enqueue: serviceGroup required for broadcast");
2922
+ }
2923
+ const alive = await listServicesRegistry(context, {
2924
+ queueName,
2925
+ serviceGroup: group,
2926
+ staleMs: overrides.staleMs ?? 45e3
2927
+ });
2928
+ if (!alive.length) {
2929
+ throw new ParamError(
2930
+ `setRuntimeParam: no alive services in group="${group}" queue="${queueName}"`
2931
+ );
2932
+ }
2933
+ const ids = [];
2934
+ const targets = [];
2935
+ for (const reg of alive) {
2936
+ const id = await enqueueTask(context, {
2937
+ ...payload,
2938
+ serviceGroup: group,
2939
+ serviceName: reg.service_name,
2940
+ serverName: reg.server_name ?? null,
2941
+ instanceNumber: reg.instance_number ?? null
2942
+ });
2943
+ ids.push(id);
2944
+ targets.push(reg.service_name);
2945
+ }
2946
+ context.logger?.info?.(
2947
+ `[setRuntimeParam] broadcast to ${targets.length} instance(s) in group=${group}: ${targets.join(", ")}`
2948
+ );
2949
+ return { ids, targets };
2950
+ }
2951
+ /**
2952
+ * @returns {Promise<{ success: true, results: object }>}
2953
+ */
2954
+ async run() {
2955
+ const params = this.task?.params ?? {};
2956
+ let changes;
2957
+ if (params.patch && typeof params.patch === "object") {
2958
+ changes = await applyRuntimePatch(this.context, params.patch);
2959
+ } else {
2960
+ changes = [await applyRuntimeParam(this.context, params.key, params.value)];
2961
+ }
2962
+ const summary = changes.map((c) => `${c.key}: ${JSON.stringify(c.previous)} \u2192 ${JSON.stringify(c.next)}`);
2963
+ this.context.logger.warn?.(
2964
+ `[TaskSetRuntimeParam] applied on ${this.context.servicesRegistry?.serviceName ?? "runner"}: ${summary.join("; ")}`
2965
+ );
2966
+ return {
2967
+ success: true,
2968
+ results: {
2969
+ runtimeParamApplied: true,
2970
+ changes,
2971
+ runtime: { ...this.context.tasksRuntime ?? {} }
2972
+ }
2973
+ };
2974
+ }
2975
+ };
2976
+
2710
2977
  // src/tasks/TasksRegistry.js
2711
2978
  var TasksRegistry = class _TasksRegistry {
2712
2979
  /**
@@ -2725,7 +2992,7 @@ var TasksRegistry = class _TasksRegistry {
2725
2992
  * @returns {TasksRegistry}
2726
2993
  */
2727
2994
  static withCoreTasks() {
2728
- 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);
2995
+ return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("info", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner).add("getLogs", TaskGetLogs).add("setRuntimeParam", TaskSetRuntimeParam).add("setRunnerParam", TaskSetRuntimeParam);
2729
2996
  }
2730
2997
  /**
2731
2998
  * Register a single task class under a name. Overwrites any previous entry.
@@ -2831,7 +3098,9 @@ var SERVICE_TASK_NAMES = [
2831
3098
  "shellCommand",
2832
3099
  "systemInfo",
2833
3100
  "info",
2834
- "getLogs"
3101
+ "getLogs",
3102
+ "setRuntimeParam",
3103
+ "setRunnerParam"
2835
3104
  ];
2836
3105
  function normalizeAllowedTasks(value) {
2837
3106
  if (!value) return void 0;
@@ -3258,18 +3527,23 @@ async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry
3258
3527
  async function runTasksLoop(context, options) {
3259
3528
  const queueName = options.queueName ?? "tasks";
3260
3529
  const target = options.target;
3261
- const pollMs = options.pollMs ?? 1e3;
3262
- const claimJitterMs = options.claimJitterMs ?? 0;
3263
- const maxParallel = options.maxParallel ?? 32;
3264
- const scanLimit = options.scanLimit ?? 100;
3265
3530
  const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
3266
3531
  const registry = normalizeRegistry(options.registry);
3267
3532
  const { tasksTable, historyTable } = queueToTableNames(queueName);
3268
3533
  if (!target) throw new Error("runTasksLoop: target is required");
3269
3534
  context.tasksQueueName = queueName;
3535
+ ensureTasksRuntime(context, {
3536
+ maxParallel: options.maxParallel ?? 32,
3537
+ pollMs: options.pollMs ?? 1e3,
3538
+ claimJitterMs: options.claimJitterMs ?? 0,
3539
+ scanLimit: options.scanLimit ?? 100
3540
+ });
3541
+ if (typeof options.onRuntimeParam === "function") {
3542
+ context.tasksRuntimeOnParam = options.onRuntimeParam;
3543
+ }
3270
3544
  const runningPromises = /* @__PURE__ */ new Set();
3271
3545
  const runningTaskInstances = /* @__PURE__ */ new Map();
3272
- let runningStopControlPromise = null;
3546
+ let runningControlPromise = null;
3273
3547
  let stopRequested = false;
3274
3548
  let stopAllowanceMs = 5e3;
3275
3549
  context.tasksRunnerStop = false;
@@ -3280,9 +3554,16 @@ async function runTasksLoop(context, options) {
3280
3554
  if (hbGroup) {
3281
3555
  const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
3282
3556
  const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
3557
+ const loop0 = readLoopRuntime(context);
3283
3558
  const defaultMeta = {
3284
3559
  component: "tasks-runner",
3285
- allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
3560
+ allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all",
3561
+ runtime: {
3562
+ maxParallel: loop0.maxParallel,
3563
+ pollMs: loop0.pollMs,
3564
+ claimJitterMs: loop0.claimJitterMs,
3565
+ scanLimit: loop0.scanLimit
3566
+ }
3286
3567
  };
3287
3568
  registryReg = await registerInServicesRegistry(context, {
3288
3569
  queueName,
@@ -3295,6 +3576,7 @@ async function runTasksLoop(context, options) {
3295
3576
  enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
3296
3577
  metadata: options.runnerMetadata ?? defaultMeta
3297
3578
  });
3579
+ context.servicesRegistry = registryReg;
3298
3580
  runnerIdentity = {
3299
3581
  service_name: registryReg.serviceName,
3300
3582
  server_name: os3.hostname(),
@@ -3308,22 +3590,23 @@ async function runTasksLoop(context, options) {
3308
3590
  }
3309
3591
  try {
3310
3592
  while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
3311
- if (!runningStopControlPromise) {
3312
- const claimedStopTask = await claimNextRunnableTask(
3593
+ const { maxParallel, pollMs, claimJitterMs, scanLimit } = readLoopRuntime(context);
3594
+ if (!runningControlPromise) {
3595
+ const claimedControlTask = await claimNextRunnableTask(
3313
3596
  context,
3314
3597
  tasksTable,
3315
3598
  target,
3316
3599
  registry,
3317
3600
  10,
3318
- ["stopRunner", "stop"],
3601
+ controlLaneTaskNames(),
3319
3602
  runnerIdentity
3320
3603
  );
3321
- if (claimedStopTask) {
3322
- runningStopControlPromise = executeClaimedTask(
3604
+ if (claimedControlTask) {
3605
+ runningControlPromise = executeClaimedTask(
3323
3606
  context,
3324
3607
  tasksTable,
3325
3608
  historyTable,
3326
- claimedStopTask,
3609
+ claimedControlTask,
3327
3610
  registry,
3328
3611
  runningTaskInstances
3329
3612
  ).then(async (outcome) => {
@@ -3334,7 +3617,7 @@ async function runTasksLoop(context, options) {
3334
3617
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
3335
3618
  }
3336
3619
  }).finally(() => {
3337
- runningStopControlPromise = null;
3620
+ runningControlPromise = null;
3338
3621
  });
3339
3622
  }
3340
3623
  }
@@ -3365,8 +3648,8 @@ async function runTasksLoop(context, options) {
3365
3648
  runningPromises.add(p);
3366
3649
  }
3367
3650
  const wakePromises = [...runningPromises];
3368
- if (runningStopControlPromise) {
3369
- wakePromises.push(runningStopControlPromise);
3651
+ if (runningControlPromise) {
3652
+ wakePromises.push(runningControlPromise);
3370
3653
  }
3371
3654
  if (wakePromises.length === 0) {
3372
3655
  await sleepMs(pollMs);
@@ -3584,10 +3867,13 @@ var TasksManager = class _TasksManager {
3584
3867
  };
3585
3868
  export {
3586
3869
  AbstractTask,
3870
+ LOGGER_RUNTIME_KEYS,
3871
+ LOOP_RUNTIME_KEYS,
3587
3872
  SERVICE_TASK_NAMES,
3588
3873
  TaskGetLogs,
3589
3874
  TaskPing,
3590
3875
  TaskSampleProcess,
3876
+ TaskSetRuntimeParam,
3591
3877
  TaskShellCommand,
3592
3878
  TaskStopRunner,
3593
3879
  TaskSumAB,
@@ -3595,11 +3881,16 @@ export {
3595
3881
  TasksManager,
3596
3882
  TasksRegistry,
3597
3883
  appendTaskIpcLog,
3884
+ applyRuntimeParam,
3885
+ applyRuntimePatch,
3886
+ coerceRuntimeValue,
3887
+ controlLaneTaskNames,
3598
3888
  convertPattern,
3599
3889
  defaultTasksRegistry,
3600
3890
  enqueueStopTask,
3601
3891
  enqueueTask,
3602
3892
  ensureTaskTables,
3893
+ ensureTasksRuntime,
3603
3894
  flushTaskIpcLogs,
3604
3895
  ipcFileLogsTableNameForSourceResource,
3605
3896
  listServicesRegistry as listAliveRunnerHeartbeats,
@@ -3609,6 +3900,7 @@ export {
3609
3900
  nextTimeMatch,
3610
3901
  normalizeAllowedTasks,
3611
3902
  queueToTableNames,
3903
+ readLoopRuntime,
3612
3904
  readTaskIpcLogsSnapshot,
3613
3905
  registerInServicesRegistry,
3614
3906
  registerInServicesRegistry as registerRunnerHeartbeat,