@nmakarov/cli-toolkit 0.72.0 → 0.75.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
@@ -2048,6 +2048,8 @@ var AbstractTask = class _AbstractTask {
2048
2048
  constructor(context, task) {
2049
2049
  this.context = context;
2050
2050
  this.task = task;
2051
+ this._stopRequested = false;
2052
+ this._pauseRequested = false;
2051
2053
  }
2052
2054
  /**
2053
2055
  * Return a short reason string when the task should be deferred (e.g. "locked
@@ -2065,6 +2067,26 @@ var AbstractTask = class _AbstractTask {
2065
2067
  * @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
2066
2068
  */
2067
2069
  requestStop(_allowanceMs) {
2070
+ this._stopRequested = true;
2071
+ }
2072
+ /**
2073
+ * Cooperative pause signal (from `pauseTask` control-lane task). Long-running
2074
+ * tasks should finish the current unit of work, persist a checkpoint, and
2075
+ * return `{ success: true, results: { taskPaused: true, … } }` so the runner
2076
+ * keeps the row as `status=paused` instead of deleting it.
2077
+ *
2078
+ * @param {number} [_allowanceMs]
2079
+ */
2080
+ requestPause(_allowanceMs) {
2081
+ this._pauseRequested = true;
2082
+ }
2083
+ /** @returns {boolean} */
2084
+ isStopRequested() {
2085
+ return this._stopRequested === true;
2086
+ }
2087
+ /** @returns {boolean} */
2088
+ isPauseRequested() {
2089
+ return this._pauseRequested === true;
2068
2090
  }
2069
2091
  /**
2070
2092
  * Perform the task. Must be implemented by subclasses.
@@ -2771,6 +2793,8 @@ var CONTROL_LANE_TASK_NAMES = [
2771
2793
  "pause",
2772
2794
  "unpauseRunner",
2773
2795
  "unpause",
2796
+ "pauseTask",
2797
+ "resumeTask",
2774
2798
  "setRuntimeParam",
2775
2799
  "setRunnerParam"
2776
2800
  ];
@@ -2975,6 +2999,190 @@ var TaskUnpauseRunner = class extends AbstractTask {
2975
2999
  }
2976
3000
  };
2977
3001
 
3002
+ // src/tasks/coreTasks/TaskPauseTask.js
3003
+ function parseProgressObject(progress) {
3004
+ if (progress == null || progress === "") return {};
3005
+ if (typeof progress === "object" && !Array.isArray(progress)) {
3006
+ return { ...progress };
3007
+ }
3008
+ if (typeof progress === "string") {
3009
+ const t = progress.trim();
3010
+ if (t.startsWith("{") || t.startsWith("[")) {
3011
+ try {
3012
+ const parsed = JSON.parse(t);
3013
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return { ...parsed };
3014
+ } catch {
3015
+ }
3016
+ }
3017
+ return { message: progress };
3018
+ }
3019
+ return { message: String(progress) };
3020
+ }
3021
+ async function applyPauseTask(context, taskId) {
3022
+ const db = context.db;
3023
+ if (!db) throw new Error("pauseTask: context.db is required");
3024
+ const id = String(taskId ?? "").trim();
3025
+ if (!id) throw new ParamError('pauseTask: param "taskId" is required');
3026
+ const { tasksTable } = queueToTableNames(context.tasksQueueName ?? "tasks");
3027
+ const row = await db(tasksTable).where({ id }).first();
3028
+ if (!row) {
3029
+ return { success: false, results: { error: `Task ${id} not found`, taskId: id } };
3030
+ }
3031
+ if (row.status === "paused") {
3032
+ return {
3033
+ success: true,
3034
+ results: { taskId: id, status: "paused", message: "Task already paused" }
3035
+ };
3036
+ }
3037
+ if (row.status === "idle") {
3038
+ const progress = parseProgressObject(row.progress);
3039
+ progress.pauseRequested = true;
3040
+ progress.pausedAt = (/* @__PURE__ */ new Date()).toISOString();
3041
+ await db(tasksTable).where({ id }).update({
3042
+ status: "paused",
3043
+ status_changed_at: db.fn.now(),
3044
+ progress: toJsonColumn(progress)
3045
+ });
3046
+ context.logger?.warn?.(`[pauseTask] idle task ${id} (${row.name}) \u2192 paused`);
3047
+ return {
3048
+ success: true,
3049
+ results: { taskId: id, status: "paused", message: "Idle task paused" }
3050
+ };
3051
+ }
3052
+ if (row.status === "running") {
3053
+ const progress = parseProgressObject(row.progress);
3054
+ progress.pauseRequested = true;
3055
+ progress.pauseRequestedAt = (/* @__PURE__ */ new Date()).toISOString();
3056
+ await db(tasksTable).where({ id }).update({
3057
+ progress: toJsonColumn(progress)
3058
+ });
3059
+ const inst = context.runningTaskInstances?.get?.(id);
3060
+ if (inst && typeof inst.requestPause === "function") {
3061
+ await inst.requestPause();
3062
+ context.logger?.warn?.(
3063
+ `[pauseTask] signaled running task ${id} (${row.name}) to pause after current unit`
3064
+ );
3065
+ return {
3066
+ success: true,
3067
+ results: {
3068
+ taskId: id,
3069
+ status: "running",
3070
+ signaled: true,
3071
+ message: "Pause requested; task will finish current unit then park as paused"
3072
+ }
3073
+ };
3074
+ }
3075
+ context.logger?.warn?.(
3076
+ `[pauseTask] stamped pauseRequested on ${id} (${row.name}) \u2014 not running in this process`
3077
+ );
3078
+ return {
3079
+ success: true,
3080
+ results: {
3081
+ taskId: id,
3082
+ status: "running",
3083
+ signaled: false,
3084
+ message: "pauseRequested stamped on task row; target runner must observe it (enqueue pauseTask on that service)"
3085
+ }
3086
+ };
3087
+ }
3088
+ return {
3089
+ success: false,
3090
+ results: {
3091
+ error: `Cannot pause task in status "${row.status}"`,
3092
+ taskId: id,
3093
+ status: row.status
3094
+ }
3095
+ };
3096
+ }
3097
+ async function applyResumeTask(context, taskId) {
3098
+ const db = context.db;
3099
+ if (!db) throw new Error("resumeTask: context.db is required");
3100
+ const id = String(taskId ?? "").trim();
3101
+ if (!id) throw new ParamError('resumeTask: param "taskId" is required');
3102
+ const { tasksTable } = queueToTableNames(context.tasksQueueName ?? "tasks");
3103
+ const row = await db(tasksTable).where({ id }).first();
3104
+ if (!row) {
3105
+ return { success: false, results: { error: `Task ${id} not found`, taskId: id } };
3106
+ }
3107
+ if (row.status === "idle") {
3108
+ return {
3109
+ success: true,
3110
+ results: { taskId: id, status: "idle", message: "Task already idle (runnable)" }
3111
+ };
3112
+ }
3113
+ if (row.status !== "paused") {
3114
+ return {
3115
+ success: false,
3116
+ results: {
3117
+ error: `Cannot resume task in status "${row.status}" (expected paused)`,
3118
+ taskId: id,
3119
+ status: row.status
3120
+ }
3121
+ };
3122
+ }
3123
+ const progress = parseProgressObject(row.progress);
3124
+ delete progress.pauseRequested;
3125
+ delete progress.pauseRequestedAt;
3126
+ delete progress.pausedAt;
3127
+ progress.resumedAt = (/* @__PURE__ */ new Date()).toISOString();
3128
+ await db(tasksTable).where({ id }).update({
3129
+ status: "idle",
3130
+ status_changed_at: db.fn.now(),
3131
+ started_at: null,
3132
+ completed_at: null,
3133
+ success: null,
3134
+ progress: toJsonColumn(progress),
3135
+ // Clear claim binding so any matching worker can pick it up.
3136
+ service_name: null,
3137
+ server_name: null,
3138
+ instance_number: null,
3139
+ next_run_at: null
3140
+ });
3141
+ context.logger?.warn?.(`[resumeTask] paused task ${id} (${row.name}) \u2192 idle`);
3142
+ return {
3143
+ success: true,
3144
+ results: { taskId: id, status: "idle", message: "Task resumed (idle; will be claimed)" }
3145
+ };
3146
+ }
3147
+ var TaskPauseTask = class extends AbstractTask {
3148
+ static taskName = "pauseTask";
3149
+ static description = "Pause a specific task: finish current unit, persist status=paused";
3150
+ static defaultWaitForResult = true;
3151
+ static async resolveCustomParams(context, overrides = {}) {
3152
+ const merged = AbstractTask._mergeTypedParams(
3153
+ context,
3154
+ "task-pause-task",
3155
+ { taskId: "string" },
3156
+ overrides
3157
+ );
3158
+ const taskId = typeof merged.taskId === "string" ? merged.taskId.trim() : "";
3159
+ if (!taskId) throw new ParamError('pauseTask: param "taskId" is required');
3160
+ return { taskId };
3161
+ }
3162
+ async run() {
3163
+ return applyPauseTask(this.context, this.task?.params?.taskId);
3164
+ }
3165
+ };
3166
+ var TaskResumeTask = class extends AbstractTask {
3167
+ static taskName = "resumeTask";
3168
+ static description = "Resume a paused task (status paused \u2192 idle)";
3169
+ static defaultWaitForResult = true;
3170
+ static async resolveCustomParams(context, overrides = {}) {
3171
+ const merged = AbstractTask._mergeTypedParams(
3172
+ context,
3173
+ "task-resume-task",
3174
+ { taskId: "string" },
3175
+ overrides
3176
+ );
3177
+ const taskId = typeof merged.taskId === "string" ? merged.taskId.trim() : "";
3178
+ if (!taskId) throw new ParamError('resumeTask: param "taskId" is required');
3179
+ return { taskId };
3180
+ }
3181
+ async run() {
3182
+ return applyResumeTask(this.context, this.task?.params?.taskId);
3183
+ }
3184
+ };
3185
+
2978
3186
  // src/tasks/coreTasks/TaskGetLogs.js
2979
3187
  var TaskGetLogs = class extends AbstractTask {
2980
3188
  /**
@@ -3193,7 +3401,7 @@ var TasksRegistry = class _TasksRegistry {
3193
3401
  * @returns {TasksRegistry}
3194
3402
  */
3195
3403
  static withCoreTasks() {
3196
- 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("pauseRunner", TaskPauseRunner).add("pause", TaskPauseRunner).add("unpauseRunner", TaskUnpauseRunner).add("unpause", TaskUnpauseRunner).add("getLogs", TaskGetLogs).add("setRuntimeParam", TaskSetRuntimeParam).add("setRunnerParam", TaskSetRuntimeParam);
3404
+ 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("pauseRunner", TaskPauseRunner).add("pause", TaskPauseRunner).add("unpauseRunner", TaskUnpauseRunner).add("unpause", TaskUnpauseRunner).add("pauseTask", TaskPauseTask).add("resumeTask", TaskResumeTask).add("getLogs", TaskGetLogs).add("setRuntimeParam", TaskSetRuntimeParam).add("setRunnerParam", TaskSetRuntimeParam);
3197
3405
  }
3198
3406
  /**
3199
3407
  * Register a single task class under a name. Overwrites any previous entry.
@@ -3300,6 +3508,8 @@ var SERVICE_TASK_NAMES = [
3300
3508
  "pauseRunner",
3301
3509
  "unpause",
3302
3510
  "unpauseRunner",
3511
+ "pauseTask",
3512
+ "resumeTask",
3303
3513
  "shellCommand",
3304
3514
  "systemInfo",
3305
3515
  "info",
@@ -3595,11 +3805,12 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
3595
3805
  } finally {
3596
3806
  runningTaskInstances.delete(row.id);
3597
3807
  }
3808
+ const taskPaused = success && results && typeof results === "object" && results.taskPaused === true;
3598
3809
  await db(historyTable).insert(
3599
3810
  taskHistoryInsertFromQueueRow(row, {
3600
3811
  completed_at: /* @__PURE__ */ new Date(),
3601
3812
  success,
3602
- status: success ? "completed" : "failed",
3813
+ status: taskPaused ? "paused" : success ? "completed" : "failed",
3603
3814
  status_changed_at: db.fn.now(),
3604
3815
  params: toJsonColumn(row.params),
3605
3816
  results: toJsonColumn(results)
@@ -3622,7 +3833,24 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
3622
3833
  details: results
3623
3834
  });
3624
3835
  }
3625
- if (row.schedule) {
3836
+ if (taskPaused) {
3837
+ const checkpointParams = results.checkpointParams && typeof results.checkpointParams === "object" ? results.checkpointParams : row.params;
3838
+ const progressPayload = results.progress != null ? results.progress : { paused: true, pausedAt: (/* @__PURE__ */ new Date()).toISOString(), message: "paused" };
3839
+ await db(tasksTable).where({ id: row.id }).update({
3840
+ started_at: null,
3841
+ completed_at: /* @__PURE__ */ new Date(),
3842
+ success: true,
3843
+ results: toJsonColumn(results),
3844
+ params: toJsonColumn(checkpointParams),
3845
+ progress: toJsonColumn(progressPayload),
3846
+ status: "paused",
3847
+ status_changed_at: db.fn.now(),
3848
+ past_due: null,
3849
+ service_name: null,
3850
+ server_name: null,
3851
+ instance_number: null
3852
+ });
3853
+ } else if (row.schedule) {
3626
3854
  if (success) {
3627
3855
  let nextRunAt = null;
3628
3856
  try {
@@ -3750,6 +3978,7 @@ async function runTasksLoop(context, options) {
3750
3978
  }
3751
3979
  const runningPromises = /* @__PURE__ */ new Set();
3752
3980
  const runningTaskInstances = /* @__PURE__ */ new Map();
3981
+ context.runningTaskInstances = runningTaskInstances;
3753
3982
  let runningControlPromise = null;
3754
3983
  let stopRequested = false;
3755
3984
  let stopAllowanceMs = Number(context.stopAllowanceMs) > 0 ? Number(context.stopAllowanceMs) : 6e4;
@@ -4109,7 +4338,9 @@ export {
4109
4338
  SERVICE_TASK_NAMES,
4110
4339
  TaskGetLogs,
4111
4340
  TaskPauseRunner,
4341
+ TaskPauseTask,
4112
4342
  TaskPing,
4343
+ TaskResumeTask,
4113
4344
  TaskSampleProcess,
4114
4345
  TaskSetRuntimeParam,
4115
4346
  TaskShellCommand,
@@ -4120,6 +4351,8 @@ export {
4120
4351
  TasksManager,
4121
4352
  TasksRegistry,
4122
4353
  appendTaskIpcLog,
4354
+ applyPauseTask,
4355
+ applyResumeTask,
4123
4356
  applyRunnerPaused,
4124
4357
  applyRuntimeParam,
4125
4358
  applyRuntimePatch,