@nmakarov/cli-toolkit 0.21.0 → 0.25.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/args.cjs +1 -4
- package/dist/args.cjs.map +1 -1
- package/dist/args.js +1 -1
- package/dist/args.js.map +1 -1
- package/dist/cli-runner.cjs +1370 -623
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +1415 -667
- package/dist/cli-runner.js.map +1 -1
- package/dist/db.cjs +173 -158
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +172 -151
- package/dist/db.js.map +1 -1
- package/dist/errors.cjs +2 -2
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/filedatabase.cjs +19 -19
- package/dist/filedatabase.cjs.map +1 -1
- package/dist/filedatabase.js +19 -16
- package/dist/filedatabase.js.map +1 -1
- package/dist/http-client.cjs +9 -11
- package/dist/http-client.cjs.map +1 -1
- package/dist/http-client.js +10 -9
- package/dist/http-client.js.map +1 -1
- package/dist/http-client2.cjs +34 -37
- package/dist/http-client2.cjs.map +1 -1
- package/dist/http-client2.js +34 -34
- package/dist/http-client2.js.map +1 -1
- package/dist/index.cjs +1831 -713
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1837 -713
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +93 -68
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +108 -82
- package/dist/init.js.map +1 -1
- package/dist/logger.cjs +5 -5
- package/dist/logger.cjs.map +1 -1
- package/dist/logger.js +5 -4
- package/dist/logger.js.map +1 -1
- package/dist/mock-server.cjs +21 -33
- package/dist/mock-server.cjs.map +1 -1
- package/dist/mock-server.js +21 -28
- package/dist/mock-server.js.map +1 -1
- package/dist/params.cjs +18 -9
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +18 -6
- package/dist/params.js.map +1 -1
- package/dist/s3.cjs +286 -0
- package/dist/s3.cjs.map +1 -0
- package/dist/s3.js +273 -0
- package/dist/s3.js.map +1 -0
- package/dist/screen.cjs +34 -39
- package/dist/screen.cjs.map +1 -1
- package/dist/screen.js +48 -46
- package/dist/screen.js.map +1 -1
- package/dist/tasks.cjs +1354 -501
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +1369 -527
- package/dist/tasks.js.map +1 -1
- package/dist/utils.cjs +7 -8
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.js +6 -6
- package/dist/utils.js.map +1 -1
- package/package.json +32 -47
- package/scripts/ssm/{parse-cli.ts → parse-cli.js} +4 -4
- package/scripts/ssm/{ssm-admin.ts → ssm-admin.js} +12 -12
- package/scripts/ssm/{ssm-pull.ts → ssm-pull.js} +10 -13
package/dist/index.cjs
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
var __esmCache = {};
|
|
4
|
-
var __loadESMSync = function(moduleName) {
|
|
5
|
-
if (!__esmCache[moduleName]) {
|
|
6
|
-
throw new Error(`ESM module "${moduleName}" not loaded. Please call the load() function first: const toolkit = require("@nmakarov/cli-toolkit"); await toolkit.load();`);
|
|
7
|
-
}
|
|
8
|
-
return __esmCache[moduleName];
|
|
9
|
-
};
|
|
10
1
|
var __create = Object.create;
|
|
11
2
|
var __defProp = Object.defineProperty;
|
|
12
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -38,7 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
38
29
|
));
|
|
39
30
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
40
31
|
|
|
41
|
-
// src/screen/components.
|
|
32
|
+
// src/screen/components.js
|
|
42
33
|
function getScreenWidth(maxWidth = null) {
|
|
43
34
|
const terminalWidth = process.stdout.columns || 80;
|
|
44
35
|
const availableWidth = Math.max(20, terminalWidth - 4);
|
|
@@ -112,14 +103,13 @@ function ScreenFooter({ lines, textStyle }) {
|
|
|
112
103
|
}
|
|
113
104
|
var import_react, import_ink;
|
|
114
105
|
var init_components = __esm({
|
|
115
|
-
"src/screen/components.
|
|
116
|
-
"use strict";
|
|
106
|
+
"src/screen/components.js"() {
|
|
117
107
|
import_react = require("react");
|
|
118
108
|
import_ink = require("ink");
|
|
119
109
|
}
|
|
120
110
|
});
|
|
121
111
|
|
|
122
|
-
// src/screen/list-components.
|
|
112
|
+
// src/screen/list-components.js
|
|
123
113
|
function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
|
|
124
114
|
const [, forceUpdate] = (0, import_react2.useState)({});
|
|
125
115
|
const termWidth = (process.stdout.columns || 80) - 8;
|
|
@@ -271,11 +261,13 @@ function MultiColumnListWithPreviewComponent({
|
|
|
271
261
|
...previewRows
|
|
272
262
|
);
|
|
273
263
|
}
|
|
274
|
-
function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " " }) {
|
|
264
|
+
function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " ", onSelectionChange }) {
|
|
275
265
|
const [, forceUpdate] = (0, import_react2.useState)({});
|
|
276
266
|
const [sortOrder, setSortOrder] = (0, import_react2.useState)("none");
|
|
277
267
|
const [scrollOffset, setScrollOffset] = (0, import_react2.useState)(0);
|
|
278
268
|
const scrollStateRef = (0, import_react2.useRef)({ scrollOffset: 0, maxHeight: 0, totalItems: 0 });
|
|
269
|
+
const itemsRef = (0, import_react2.useRef)(items);
|
|
270
|
+
itemsRef.current = items;
|
|
279
271
|
const defaultGetTitle = (item) => {
|
|
280
272
|
return getTitle ? getTitle(item) : typeof item.value === "string" ? item.value : item.value?.title || item.name;
|
|
281
273
|
};
|
|
@@ -289,18 +281,24 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
289
281
|
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
290
282
|
}
|
|
291
283
|
}) : items;
|
|
284
|
+
const displayItemsRef = (0, import_react2.useRef)(displayItems);
|
|
285
|
+
displayItemsRef.current = displayItems;
|
|
292
286
|
const effectiveMaxHeight = maxHeight || displayItems.length;
|
|
293
|
-
const
|
|
287
|
+
const _canScroll = displayItems.length > effectiveMaxHeight;
|
|
294
288
|
const maxScrollOffset = Math.max(0, displayItems.length - effectiveMaxHeight);
|
|
295
289
|
const clampedScrollOffset = Math.min(Math.max(0, scrollOffset), maxScrollOffset);
|
|
296
290
|
const visibleItems = displayItems.slice(clampedScrollOffset, clampedScrollOffset + effectiveMaxHeight);
|
|
297
291
|
const canScrollUp = clampedScrollOffset > 0;
|
|
298
292
|
const canScrollDown = clampedScrollOffset < maxScrollOffset;
|
|
299
293
|
scrollStateRef.current = { scrollOffset, maxHeight: effectiveMaxHeight, totalItems: displayItems.length };
|
|
294
|
+
const onSelectionChangeRef = (0, import_react2.useRef)(onSelectionChange);
|
|
295
|
+
onSelectionChangeRef.current = onSelectionChange;
|
|
300
296
|
(0, import_react2.useEffect)(() => {
|
|
301
297
|
ctx.setAction("moveUp", () => {
|
|
302
298
|
const newIndex = Math.max(0, selectedIndexRef.current - 1);
|
|
303
299
|
selectedIndexRef.current = newIndex;
|
|
300
|
+
const list = displayItemsRef.current;
|
|
301
|
+
onSelectionChangeRef.current?.(newIndex, list[newIndex]);
|
|
304
302
|
const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
|
|
305
303
|
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
306
304
|
const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
|
|
@@ -310,18 +308,11 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
310
308
|
forceUpdate({});
|
|
311
309
|
});
|
|
312
310
|
ctx.setAction("moveDown", () => {
|
|
313
|
-
const currentItems =
|
|
314
|
-
|
|
315
|
-
const titleB = titleGetter(b).toLowerCase();
|
|
316
|
-
if (sortOrder === "asc") {
|
|
317
|
-
return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
|
|
318
|
-
} else {
|
|
319
|
-
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
320
|
-
}
|
|
321
|
-
}) : items;
|
|
322
|
-
const maxIndex = currentItems.length - 1;
|
|
311
|
+
const currentItems = displayItemsRef.current;
|
|
312
|
+
const maxIndex = Math.max(0, currentItems.length - 1);
|
|
323
313
|
const newIndex = Math.min(maxIndex, selectedIndexRef.current + 1);
|
|
324
314
|
selectedIndexRef.current = newIndex;
|
|
315
|
+
onSelectionChangeRef.current?.(newIndex, currentItems[newIndex]);
|
|
325
316
|
const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
|
|
326
317
|
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
327
318
|
const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
|
|
@@ -331,8 +322,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
331
322
|
forceUpdate({});
|
|
332
323
|
});
|
|
333
324
|
ctx.setAction("scrollUp", () => {
|
|
334
|
-
const { scrollOffset: currentScrollOffset
|
|
335
|
-
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
325
|
+
const { scrollOffset: currentScrollOffset } = scrollStateRef.current;
|
|
336
326
|
const newScrollOffset = Math.max(0, currentScrollOffset - 1);
|
|
337
327
|
setScrollOffset(newScrollOffset);
|
|
338
328
|
forceUpdate({});
|
|
@@ -347,9 +337,9 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
347
337
|
if (sortable) {
|
|
348
338
|
ctx.setAction("toggleSort", () => {
|
|
349
339
|
const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
|
|
350
|
-
const currentSelectedItem =
|
|
340
|
+
const currentSelectedItem = displayItemsRef.current[selectedIndexRef.current];
|
|
351
341
|
setSortOrder(nextSort);
|
|
352
|
-
const newSortedItems = nextSort !== "none" ? [...
|
|
342
|
+
const newSortedItems = nextSort !== "none" ? [...itemsRef.current].sort((a, b) => {
|
|
353
343
|
const titleA = titleGetter(a).toLowerCase();
|
|
354
344
|
const titleB = titleGetter(b).toLowerCase();
|
|
355
345
|
if (nextSort === "asc") {
|
|
@@ -357,7 +347,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
357
347
|
} else {
|
|
358
348
|
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
359
349
|
}
|
|
360
|
-
}) :
|
|
350
|
+
}) : itemsRef.current;
|
|
361
351
|
const newIndex = newSortedItems.findIndex((item) => item === currentSelectedItem);
|
|
362
352
|
if (newIndex !== -1) {
|
|
363
353
|
selectedIndexRef.current = newIndex;
|
|
@@ -495,8 +485,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
495
485
|
}
|
|
496
486
|
var import_react2, import_ink2, h2;
|
|
497
487
|
var init_list_components = __esm({
|
|
498
|
-
"src/screen/list-components.
|
|
499
|
-
"use strict";
|
|
488
|
+
"src/screen/list-components.js"() {
|
|
500
489
|
import_react2 = __toESM(require("react"), 1);
|
|
501
490
|
import_ink2 = require("ink");
|
|
502
491
|
init_components();
|
|
@@ -504,7 +493,7 @@ var init_list_components = __esm({
|
|
|
504
493
|
}
|
|
505
494
|
});
|
|
506
495
|
|
|
507
|
-
// src/screen/screens.
|
|
496
|
+
// src/screen/screens.js
|
|
508
497
|
function groupKeyBindings(bindings) {
|
|
509
498
|
const groups = {};
|
|
510
499
|
const enabledBindings = bindings.filter((b) => b.enabled !== false);
|
|
@@ -580,7 +569,7 @@ async function showScreen(config2) {
|
|
|
580
569
|
let renderResult = null;
|
|
581
570
|
let initialized = false;
|
|
582
571
|
const Screen = () => {
|
|
583
|
-
const [
|
|
572
|
+
const [, setUpdateCounter] = (0, import_react3.useState)(0);
|
|
584
573
|
if (!initialized) {
|
|
585
574
|
const defaultBindings = [
|
|
586
575
|
{ key: "escape", caption: "go back", action: "back", protected: true, order: 1 },
|
|
@@ -699,7 +688,7 @@ async function showScreen(config2) {
|
|
|
699
688
|
}
|
|
700
689
|
}
|
|
701
690
|
if (matchedBinding && actions[matchedBinding.action]) {
|
|
702
|
-
|
|
691
|
+
actions[matchedBinding.action]({
|
|
703
692
|
input,
|
|
704
693
|
key,
|
|
705
694
|
binding: matchedBinding
|
|
@@ -829,8 +818,7 @@ async function showMultiColumnListWithPreviewScreen(config2) {
|
|
|
829
818
|
}
|
|
830
819
|
var import_react3, import_ink3, showMenuScreen, showWordGridScreen;
|
|
831
820
|
var init_screens = __esm({
|
|
832
|
-
"src/screen/screens.
|
|
833
|
-
"use strict";
|
|
821
|
+
"src/screen/screens.js"() {
|
|
834
822
|
import_react3 = require("react");
|
|
835
823
|
import_ink3 = require("ink");
|
|
836
824
|
init_components();
|
|
@@ -840,7 +828,7 @@ var init_screens = __esm({
|
|
|
840
828
|
}
|
|
841
829
|
});
|
|
842
830
|
|
|
843
|
-
// src/screen/ui-elements.
|
|
831
|
+
// src/screen/ui-elements.js
|
|
844
832
|
function ListItem({
|
|
845
833
|
children,
|
|
846
834
|
isSelected = false,
|
|
@@ -865,7 +853,7 @@ function TextBlock({
|
|
|
865
853
|
color = "white",
|
|
866
854
|
dimmed = false,
|
|
867
855
|
bold = false,
|
|
868
|
-
maxWidth
|
|
856
|
+
maxWidth: _maxWidth
|
|
869
857
|
}) {
|
|
870
858
|
return (0, import_react4.createElement)(
|
|
871
859
|
import_ink4.Box,
|
|
@@ -905,7 +893,7 @@ function GridCell({
|
|
|
905
893
|
}, children)
|
|
906
894
|
);
|
|
907
895
|
}
|
|
908
|
-
function InputField({ prompt, value, onChange, onSubmit }) {
|
|
896
|
+
function InputField({ prompt, value, onChange: _onChange, onSubmit: _onSubmit }) {
|
|
909
897
|
return (0, import_react4.createElement)(
|
|
910
898
|
import_ink4.Box,
|
|
911
899
|
{ flexDirection: "column" },
|
|
@@ -919,14 +907,13 @@ function InputField({ prompt, value, onChange, onSubmit }) {
|
|
|
919
907
|
}
|
|
920
908
|
var import_react4, import_ink4;
|
|
921
909
|
var init_ui_elements = __esm({
|
|
922
|
-
"src/screen/ui-elements.
|
|
923
|
-
"use strict";
|
|
910
|
+
"src/screen/ui-elements.js"() {
|
|
924
911
|
import_react4 = require("react");
|
|
925
912
|
import_ink4 = require("ink");
|
|
926
913
|
}
|
|
927
914
|
});
|
|
928
915
|
|
|
929
|
-
// src/screen/utils.
|
|
916
|
+
// src/screen/utils.js
|
|
930
917
|
function buildBreadcrumb(parts) {
|
|
931
918
|
if (parts.length === 0) return "";
|
|
932
919
|
if (parts.length === 1) return parts[0];
|
|
@@ -940,12 +927,11 @@ function buildDetailBreadcrumb(path5, suffix = "") {
|
|
|
940
927
|
return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
|
|
941
928
|
}
|
|
942
929
|
var init_utils = __esm({
|
|
943
|
-
"src/screen/utils.
|
|
944
|
-
"use strict";
|
|
930
|
+
"src/screen/utils.js"() {
|
|
945
931
|
}
|
|
946
932
|
});
|
|
947
933
|
|
|
948
|
-
// src/screen/footer-builder.
|
|
934
|
+
// src/screen/footer-builder.js
|
|
949
935
|
function buildFooter(config2 = {}) {
|
|
950
936
|
const {
|
|
951
937
|
navigation = null,
|
|
@@ -996,8 +982,7 @@ function organizeFooterMessages(messages) {
|
|
|
996
982
|
}
|
|
997
983
|
var FooterPresets;
|
|
998
984
|
var init_footer_builder = __esm({
|
|
999
|
-
"src/screen/footer-builder.
|
|
1000
|
-
"use strict";
|
|
985
|
+
"src/screen/footer-builder.js"() {
|
|
1001
986
|
FooterPresets = {
|
|
1002
987
|
/**
|
|
1003
988
|
* Menu screen footer
|
|
@@ -1056,7 +1041,7 @@ var init_footer_builder = __esm({
|
|
|
1056
1041
|
}
|
|
1057
1042
|
});
|
|
1058
1043
|
|
|
1059
|
-
// src/screen/index.
|
|
1044
|
+
// src/screen/index.js
|
|
1060
1045
|
async function load() {
|
|
1061
1046
|
if (loadPromise) return loadPromise;
|
|
1062
1047
|
|
|
@@ -1065,8 +1050,7 @@ async function load() {
|
|
|
1065
1050
|
}
|
|
1066
1051
|
var import_react5, import_ink5, loadPromise;
|
|
1067
1052
|
var init_screen = __esm({
|
|
1068
|
-
"src/screen/index.
|
|
1069
|
-
"use strict";
|
|
1053
|
+
"src/screen/index.js"() {
|
|
1070
1054
|
import_react5 = __toESM(require("react"), 1);
|
|
1071
1055
|
import_ink5 = require("ink");
|
|
1072
1056
|
init_screens();
|
|
@@ -1083,9 +1067,10 @@ var init_screen = __esm({
|
|
|
1083
1067
|
}
|
|
1084
1068
|
});
|
|
1085
1069
|
|
|
1086
|
-
// src/index.
|
|
1070
|
+
// src/index.js
|
|
1087
1071
|
var src_exports = {};
|
|
1088
1072
|
__export(src_exports, {
|
|
1073
|
+
AbstractTask: () => AbstractTask,
|
|
1089
1074
|
Args: () => Args,
|
|
1090
1075
|
Box: () => import_ink5.Box,
|
|
1091
1076
|
Db: () => Db,
|
|
@@ -1101,13 +1086,15 @@ __export(src_exports, {
|
|
|
1101
1086
|
MultiColumnListWithPreviewComponent: () => MultiColumnListWithPreviewComponent,
|
|
1102
1087
|
Params: () => Params,
|
|
1103
1088
|
React: () => import_react5.default,
|
|
1089
|
+
S3: () => S3,
|
|
1090
|
+
SERVICE_TASK_NAMES: () => SERVICE_TASK_NAMES,
|
|
1104
1091
|
ScreenBody: () => ScreenBody,
|
|
1105
1092
|
ScreenContainer: () => ScreenContainer,
|
|
1106
1093
|
ScreenDivider: () => ScreenDivider,
|
|
1107
1094
|
ScreenFooter: () => ScreenFooter,
|
|
1108
1095
|
ScreenRow: () => ScreenRow,
|
|
1109
1096
|
ScreenTitle: () => ScreenTitle,
|
|
1110
|
-
|
|
1097
|
+
TaskGetLogs: () => TaskGetLogs,
|
|
1111
1098
|
TaskPing: () => TaskPing,
|
|
1112
1099
|
TaskSampleProcess: () => TaskSampleProcess,
|
|
1113
1100
|
TaskShellCommand: () => TaskShellCommand,
|
|
@@ -1122,17 +1109,17 @@ __export(src_exports, {
|
|
|
1122
1109
|
buildBreadcrumb: () => buildBreadcrumb,
|
|
1123
1110
|
buildDetailBreadcrumb: () => buildDetailBreadcrumb,
|
|
1124
1111
|
buildFooter: () => buildFooter,
|
|
1125
|
-
|
|
1126
|
-
dbFindAndConnect: () => dbFindAndConnect,
|
|
1127
|
-
dbInit: () => dbInit,
|
|
1112
|
+
convertPattern: () => convertPattern,
|
|
1128
1113
|
defaultFileSynopsisFunction: () => defaultFileSynopsisFunction,
|
|
1129
1114
|
defaultTasksRegistry: () => defaultTasksRegistry,
|
|
1130
1115
|
defaultVersionSynopsisFunction: () => defaultVersionSynopsisFunction,
|
|
1131
1116
|
enqueueStopTask: () => enqueueStopTask,
|
|
1132
1117
|
enqueueTask: () => enqueueTask,
|
|
1133
1118
|
ensureTaskTables: () => ensureTaskTables,
|
|
1119
|
+
flushTaskIpcLogs: () => flushTaskIpcLogs,
|
|
1134
1120
|
getArgsInstance: () => getArgsInstance,
|
|
1135
1121
|
h: () => import_react5.createElement,
|
|
1122
|
+
ipcFileLogsTableNameForSourceResource: () => ipcFileLogsTableNameForSourceResource,
|
|
1136
1123
|
joiEdateType: () => joiEdateType,
|
|
1137
1124
|
joiStringArrayType: () => joiStringArrayType,
|
|
1138
1125
|
listAliveRunnerHeartbeats: () => listServicesRegistry,
|
|
@@ -1140,14 +1127,22 @@ __export(src_exports, {
|
|
|
1140
1127
|
listSources: () => listSources,
|
|
1141
1128
|
listTables: () => listTables,
|
|
1142
1129
|
load: () => load,
|
|
1130
|
+
matchesParsedPattern: () => matchesParsedPattern,
|
|
1131
|
+
memo: () => import_react5.memo,
|
|
1132
|
+
mergeAllowedTasksWithServiceTasks: () => mergeAllowedTasksWithServiceTasks,
|
|
1133
|
+
nextTimeMatch: () => nextTimeMatch,
|
|
1134
|
+
normalizeAllowedTasks: () => normalizeAllowedTasks,
|
|
1143
1135
|
organizeFooterMessages: () => organizeFooterMessages,
|
|
1144
1136
|
queueToTableNames: () => queueToTableNames,
|
|
1137
|
+
readTaskIpcLogsSnapshot: () => readTaskIpcLogsSnapshot,
|
|
1145
1138
|
registerInServicesRegistry: () => registerInServicesRegistry,
|
|
1146
1139
|
registerRunnerHeartbeat: () => registerInServicesRegistry,
|
|
1140
|
+
resolveAsterisks: () => resolveAsterisks,
|
|
1141
|
+
resolveIpcFileLogsDir: () => resolveIpcFileLogsDir,
|
|
1142
|
+
resolveRanges: () => resolveRanges,
|
|
1143
|
+
resolveSteps: () => resolveSteps,
|
|
1147
1144
|
runNodeTaskScript: () => runNodeTaskScript,
|
|
1148
1145
|
runTasksLoop: () => runTasksLoop,
|
|
1149
|
-
runnerHeartbeatsTable: () => servicesRegistryTable,
|
|
1150
|
-
servicesRegistryTable: () => servicesRegistryTable,
|
|
1151
1146
|
setupContext: () => setupContext,
|
|
1152
1147
|
showListScreen: () => showListScreen,
|
|
1153
1148
|
showMenuScreen: () => showMenuScreen,
|
|
@@ -1155,6 +1150,8 @@ __export(src_exports, {
|
|
|
1155
1150
|
showMultiColumnListWithPreviewScreen: () => showMultiColumnListWithPreviewScreen,
|
|
1156
1151
|
showScreen: () => showScreen,
|
|
1157
1152
|
showWordGridScreen: () => showWordGridScreen,
|
|
1153
|
+
taskHistoryInsertFromQueueRow: () => taskHistoryInsertFromQueueRow,
|
|
1154
|
+
timeMatcher: () => timeMatcher,
|
|
1158
1155
|
touchRunnerHeartbeat: () => touchServicesRegistry,
|
|
1159
1156
|
touchServicesRegistry: () => touchServicesRegistry,
|
|
1160
1157
|
unregisterRunnerHeartbeat: () => unregisterServicesRegistry,
|
|
@@ -1164,6 +1161,7 @@ __export(src_exports, {
|
|
|
1164
1161
|
useCallback: () => import_react5.useCallback,
|
|
1165
1162
|
useEffect: () => import_react5.useEffect,
|
|
1166
1163
|
useInput: () => import_ink5.useInput,
|
|
1164
|
+
useLayoutEffect: () => import_react5.useLayoutEffect,
|
|
1167
1165
|
useMemo: () => import_react5.useMemo,
|
|
1168
1166
|
useRef: () => import_react5.useRef,
|
|
1169
1167
|
useState: () => import_react5.useState,
|
|
@@ -1171,7 +1169,7 @@ __export(src_exports, {
|
|
|
1171
1169
|
});
|
|
1172
1170
|
module.exports = __toCommonJS(src_exports);
|
|
1173
1171
|
|
|
1174
|
-
// src/args/index.
|
|
1172
|
+
// src/args/index.js
|
|
1175
1173
|
var import_fs = require("fs");
|
|
1176
1174
|
var import_path = require("path");
|
|
1177
1175
|
var import_dotenv = require("dotenv");
|
|
@@ -1658,10 +1656,10 @@ function getArgsInstance() {
|
|
|
1658
1656
|
return instance;
|
|
1659
1657
|
}
|
|
1660
1658
|
|
|
1661
|
-
// src/params/index.
|
|
1659
|
+
// src/params/index.js
|
|
1662
1660
|
var import_joi = __toESM(require("joi"), 1);
|
|
1663
1661
|
|
|
1664
|
-
// src/errors.
|
|
1662
|
+
// src/errors.js
|
|
1665
1663
|
var FrameworkError = class extends Error {
|
|
1666
1664
|
constructor(message) {
|
|
1667
1665
|
super(message);
|
|
@@ -1681,7 +1679,7 @@ var FileDatabaseError = class extends FrameworkError {
|
|
|
1681
1679
|
}
|
|
1682
1680
|
};
|
|
1683
1681
|
|
|
1684
|
-
// src/params/custom-types.
|
|
1682
|
+
// src/params/custom-types.js
|
|
1685
1683
|
var joiEdateType = (value, helpers) => {
|
|
1686
1684
|
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(value)) {
|
|
1687
1685
|
const testDate = new Date(value);
|
|
@@ -1774,7 +1772,7 @@ function calculateTimeOffset(amount, unit, sign) {
|
|
|
1774
1772
|
}
|
|
1775
1773
|
return sign === "+" ? amount * multiplier : -amount * multiplier;
|
|
1776
1774
|
}
|
|
1777
|
-
var joiStringArrayType = (type) => (value,
|
|
1775
|
+
var joiStringArrayType = (type) => (value, _helpers) => {
|
|
1778
1776
|
if (value === void 0 || typeof value === "function") {
|
|
1779
1777
|
return [];
|
|
1780
1778
|
}
|
|
@@ -1800,7 +1798,7 @@ var joiStringArrayType = (type) => (value, helpers) => {
|
|
|
1800
1798
|
return arr;
|
|
1801
1799
|
};
|
|
1802
1800
|
|
|
1803
|
-
// src/params/index.
|
|
1801
|
+
// src/params/index.js
|
|
1804
1802
|
var Params = class _Params {
|
|
1805
1803
|
context;
|
|
1806
1804
|
// Partial context during initialization
|
|
@@ -2060,7 +2058,7 @@ var Params = class _Params {
|
|
|
2060
2058
|
definition = val;
|
|
2061
2059
|
val = val.value;
|
|
2062
2060
|
}
|
|
2063
|
-
|
|
2061
|
+
this.assignDefinition(key, definition);
|
|
2064
2062
|
if (!this.runAllRegisteredSetters(key, val)) {
|
|
2065
2063
|
this.params[key] = val;
|
|
2066
2064
|
}
|
|
@@ -2119,6 +2117,18 @@ var Params = class _Params {
|
|
|
2119
2117
|
this._currentModule = prev;
|
|
2120
2118
|
}
|
|
2121
2119
|
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Async variant of {@link runWithModule} for modules that await params.get().
|
|
2122
|
+
*/
|
|
2123
|
+
async runWithModuleAsync(moduleName, fn) {
|
|
2124
|
+
const prev = this._currentModule;
|
|
2125
|
+
this._currentModule = moduleName;
|
|
2126
|
+
try {
|
|
2127
|
+
return await fn();
|
|
2128
|
+
} finally {
|
|
2129
|
+
this._currentModule = prev;
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2122
2132
|
/**
|
|
2123
2133
|
* Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
|
|
2124
2134
|
*/
|
|
@@ -2179,14 +2189,14 @@ var Params = class _Params {
|
|
|
2179
2189
|
}
|
|
2180
2190
|
};
|
|
2181
2191
|
|
|
2182
|
-
// src/
|
|
2192
|
+
// src/index.js
|
|
2183
2193
|
init_screen();
|
|
2184
2194
|
|
|
2185
|
-
// src/filedatabase/index.
|
|
2195
|
+
// src/filedatabase/index.js
|
|
2186
2196
|
var import_fs4 = __toESM(require("fs"), 1);
|
|
2187
2197
|
var import_path4 = __toESM(require("path"), 1);
|
|
2188
2198
|
|
|
2189
|
-
// src/utils/os-utils.
|
|
2199
|
+
// src/utils/os-utils.js
|
|
2190
2200
|
var import_fs2 = __toESM(require("fs"), 1);
|
|
2191
2201
|
var import_path2 = __toESM(require("path"), 1);
|
|
2192
2202
|
var import_child_process = require("child_process");
|
|
@@ -2215,7 +2225,7 @@ function getFreeDiskSpace(targetPath) {
|
|
|
2215
2225
|
}
|
|
2216
2226
|
}
|
|
2217
2227
|
|
|
2218
|
-
// src/utils/fs-utils.
|
|
2228
|
+
// src/utils/fs-utils.js
|
|
2219
2229
|
var import_fs3 = __toESM(require("fs"), 1);
|
|
2220
2230
|
var import_path3 = __toESM(require("path"), 1);
|
|
2221
2231
|
async function ensurePath(...pathParts) {
|
|
@@ -2239,7 +2249,7 @@ function getFileExtension(dataType) {
|
|
|
2239
2249
|
}
|
|
2240
2250
|
}
|
|
2241
2251
|
|
|
2242
|
-
// src/utils/format-utils.
|
|
2252
|
+
// src/utils/format-utils.js
|
|
2243
2253
|
function bytesToHumanReadable(bytes) {
|
|
2244
2254
|
if (bytes === 0) return "0 B";
|
|
2245
2255
|
const k = 1024;
|
|
@@ -2248,7 +2258,7 @@ function bytesToHumanReadable(bytes) {
|
|
|
2248
2258
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
2249
2259
|
}
|
|
2250
2260
|
|
|
2251
|
-
// src/utils/date-utils.
|
|
2261
|
+
// src/utils/date-utils.js
|
|
2252
2262
|
function isTimestampFolder(folderName) {
|
|
2253
2263
|
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
|
|
2254
2264
|
if (!isoRegex.test(folderName)) {
|
|
@@ -2258,7 +2268,7 @@ function isTimestampFolder(folderName) {
|
|
|
2258
2268
|
return !isNaN(date.getTime()) && date.getTime() > 0;
|
|
2259
2269
|
}
|
|
2260
2270
|
|
|
2261
|
-
// src/filedatabase/serializers.
|
|
2271
|
+
// src/filedatabase/serializers.js
|
|
2262
2272
|
function detectDataType(data) {
|
|
2263
2273
|
if (Array.isArray(data)) {
|
|
2264
2274
|
return "json-array";
|
|
@@ -2290,7 +2300,7 @@ function deserializeData(rawData, dataType) {
|
|
|
2290
2300
|
}
|
|
2291
2301
|
}
|
|
2292
2302
|
|
|
2293
|
-
// src/filedatabase/synopsis-functions.
|
|
2303
|
+
// src/filedatabase/synopsis-functions.js
|
|
2294
2304
|
function defaultFileSynopsisFunction(fileEntry, data) {
|
|
2295
2305
|
if (!Array.isArray(data) || data.length === 0) {
|
|
2296
2306
|
return { ...fileEntry };
|
|
@@ -2358,7 +2368,7 @@ function defaultVersionSynopsisFunction(metadata) {
|
|
|
2358
2368
|
return result;
|
|
2359
2369
|
}
|
|
2360
2370
|
|
|
2361
|
-
// src/filedatabase/index.
|
|
2371
|
+
// src/filedatabase/index.js
|
|
2362
2372
|
var FileDatabase = class _FileDatabase {
|
|
2363
2373
|
basePath;
|
|
2364
2374
|
namespace;
|
|
@@ -2444,7 +2454,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2444
2454
|
if (errors.length) {
|
|
2445
2455
|
throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
|
|
2446
2456
|
}
|
|
2447
|
-
|
|
2457
|
+
const parts = [this.basePath, this.namespace];
|
|
2448
2458
|
if (this.tableName) {
|
|
2449
2459
|
parts.push(...this.tableName.split("/"));
|
|
2450
2460
|
}
|
|
@@ -2937,14 +2947,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
2937
2947
|
* Prepare the instance for read or write operations
|
|
2938
2948
|
* This discovers state and sets up internal members based on mode and current data
|
|
2939
2949
|
*/
|
|
2940
|
-
async prepare(
|
|
2950
|
+
async prepare(options) {
|
|
2951
|
+
const { write, read, version, deferInitialVersion } = options;
|
|
2941
2952
|
if (write) {
|
|
2942
2953
|
if (this.versioned) {
|
|
2943
2954
|
if (this.currentVersion === null) {
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2955
|
+
if (!deferInitialVersion) {
|
|
2956
|
+
await this.makeNewVersion();
|
|
2957
|
+
this.metadata = this.getDefaultMetadata();
|
|
2958
|
+
this.metadata.version = this.currentVersion;
|
|
2959
|
+
this.makeNewFile();
|
|
2960
|
+
}
|
|
2948
2961
|
} else {
|
|
2949
2962
|
if (!this.metadata.files.length) {
|
|
2950
2963
|
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
@@ -3054,7 +3067,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3054
3067
|
if (options.forceNewVersion && !this.versioned) {
|
|
3055
3068
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
3056
3069
|
}
|
|
3057
|
-
await this.prepare({ write: true });
|
|
3070
|
+
await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
|
|
3058
3071
|
const incomingDataType = detectDataType(data);
|
|
3059
3072
|
this.metadata.dataType = incomingDataType;
|
|
3060
3073
|
if (options.forceNewVersion) {
|
|
@@ -3369,22 +3382,90 @@ function listSources(basePath) {
|
|
|
3369
3382
|
}
|
|
3370
3383
|
}
|
|
3371
3384
|
|
|
3372
|
-
// src/db/index.
|
|
3385
|
+
// src/db/index.js
|
|
3373
3386
|
var import_knex = __toESM(require("knex"), 1);
|
|
3387
|
+
var KNEX_DEFAULTS = {
|
|
3388
|
+
testConnection: true,
|
|
3389
|
+
pool: { min: 2, max: 10 },
|
|
3390
|
+
acquireConnectionTimeout: 1e4,
|
|
3391
|
+
ssl: { rejectUnauthorized: false }
|
|
3392
|
+
};
|
|
3374
3393
|
var Db = class {
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3394
|
+
static async init(context, options = {}) {
|
|
3395
|
+
const buildConfig = async () => {
|
|
3396
|
+
const defs = {
|
|
3397
|
+
dbName: "string",
|
|
3398
|
+
dbProfile: "boolean default false"
|
|
3399
|
+
};
|
|
3400
|
+
const discovered = context?.params?.getAllForModule?.("db", defs) ?? {};
|
|
3401
|
+
const merged = { ...discovered, ...options };
|
|
3402
|
+
let { dbName, dbProfile } = merged;
|
|
3403
|
+
let dbConnectionString = options.dbConnectionString ?? options.connectionString;
|
|
3404
|
+
let connectionParam = dbConnectionString ? "options" : null;
|
|
3405
|
+
if (!dbConnectionString) {
|
|
3406
|
+
const src = context?.args?.getSource?.("dbConnectionString");
|
|
3407
|
+
if (src === "cli" || src === "overrides" || src === "config") {
|
|
3408
|
+
dbConnectionString = await context.params.get("dbConnectionString", "string");
|
|
3409
|
+
connectionParam = "dbConnectionString";
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
if (!dbConnectionString && dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
|
|
3413
|
+
dbConnectionString = dbName;
|
|
3414
|
+
dbName = void 0;
|
|
3415
|
+
}
|
|
3416
|
+
if (!dbConnectionString && dbName) {
|
|
3417
|
+
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
3418
|
+
dbConnectionString = await context.params.get(paramName, "string");
|
|
3419
|
+
connectionParam = paramName;
|
|
3420
|
+
if (!dbConnectionString) {
|
|
3421
|
+
throw new ParamError(
|
|
3422
|
+
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
3423
|
+
);
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
if (!dbConnectionString) {
|
|
3427
|
+
dbConnectionString = await context.params.get("dbConnectionString", "string");
|
|
3428
|
+
if (dbConnectionString) {
|
|
3429
|
+
connectionParam = "dbConnectionString";
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
if (!dbConnectionString) {
|
|
3433
|
+
if (!dbName) {
|
|
3434
|
+
dbName = "local";
|
|
3435
|
+
}
|
|
3436
|
+
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
3437
|
+
dbConnectionString = await context.params.get(paramName, "string");
|
|
3438
|
+
connectionParam = paramName;
|
|
3439
|
+
if (!dbConnectionString) {
|
|
3440
|
+
throw new ParamError(
|
|
3441
|
+
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
3442
|
+
);
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
const displayName = resolveDbDisplayName(
|
|
3446
|
+
dbName,
|
|
3447
|
+
connectionParam,
|
|
3448
|
+
context?.args?.env,
|
|
3449
|
+
merged.name
|
|
3450
|
+
);
|
|
3451
|
+
return {
|
|
3452
|
+
...KNEX_DEFAULTS,
|
|
3453
|
+
connectionString: dbConnectionString,
|
|
3454
|
+
name: displayName,
|
|
3455
|
+
profile: !!dbProfile,
|
|
3456
|
+
logger: context.logger
|
|
3457
|
+
};
|
|
3458
|
+
};
|
|
3459
|
+
const config2 = context?.params?.runWithModuleAsync ? await context.params.runWithModuleAsync("db", buildConfig) : await buildConfig();
|
|
3460
|
+
return dbConnect(context, config2);
|
|
3461
|
+
}
|
|
3384
3462
|
constructor(config2) {
|
|
3385
|
-
if (!config2.connectionString) {
|
|
3463
|
+
if (!config2 || !config2.connectionString) {
|
|
3386
3464
|
throw new ParamError("Db: connectionString is required");
|
|
3387
3465
|
}
|
|
3466
|
+
this.knexInstance = null;
|
|
3467
|
+
this.isConnected = false;
|
|
3468
|
+
this.queriesLog = [];
|
|
3388
3469
|
this.config = {
|
|
3389
3470
|
testConnection: true,
|
|
3390
3471
|
profile: false,
|
|
@@ -3392,30 +3473,27 @@ var Db = class {
|
|
|
3392
3473
|
acquireConnectionTimeout: 1e4,
|
|
3393
3474
|
ssl: { rejectUnauthorized: false },
|
|
3394
3475
|
logger: console,
|
|
3395
|
-
name: "default",
|
|
3396
3476
|
...config2
|
|
3397
3477
|
};
|
|
3398
3478
|
this.logger = this.config.logger;
|
|
3399
3479
|
const instance2 = this;
|
|
3400
|
-
const callableWrapper = function(
|
|
3480
|
+
const callableWrapper = function() {
|
|
3401
3481
|
throw new Error("This should never be called directly");
|
|
3402
3482
|
};
|
|
3403
3483
|
callableWrapper._instance = instance2;
|
|
3404
3484
|
return new Proxy(callableWrapper, {
|
|
3405
|
-
|
|
3406
|
-
apply: (target, thisArg, argumentsList) => {
|
|
3485
|
+
apply: (target, _thisArg, argumentsList) => {
|
|
3407
3486
|
const inst = target._instance;
|
|
3408
3487
|
if (!inst.knexInstance) {
|
|
3409
3488
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
3410
3489
|
}
|
|
3411
3490
|
return inst.knexInstance(...argumentsList);
|
|
3412
3491
|
},
|
|
3413
|
-
// Intercept property access: db.schema, db.raw, etc.
|
|
3414
3492
|
get: (target, prop) => {
|
|
3415
3493
|
if (prop === "_instance") {
|
|
3416
3494
|
return target._instance;
|
|
3417
3495
|
}
|
|
3418
|
-
const
|
|
3496
|
+
const inst = target._instance;
|
|
3419
3497
|
const ownMethods = [
|
|
3420
3498
|
"connect",
|
|
3421
3499
|
"disconnect",
|
|
@@ -3428,26 +3506,26 @@ var Db = class {
|
|
|
3428
3506
|
"detectClient",
|
|
3429
3507
|
"attachProfiler"
|
|
3430
3508
|
];
|
|
3431
|
-
if (prop in
|
|
3432
|
-
const value =
|
|
3509
|
+
if (prop in inst) {
|
|
3510
|
+
const value = inst[prop];
|
|
3433
3511
|
if (typeof value === "function" && ownMethods.includes(prop)) {
|
|
3434
|
-
return value.bind(
|
|
3512
|
+
return value.bind(inst);
|
|
3435
3513
|
}
|
|
3436
3514
|
if (typeof value !== "function") {
|
|
3437
3515
|
return value;
|
|
3438
3516
|
}
|
|
3439
3517
|
}
|
|
3440
|
-
if (
|
|
3441
|
-
const knexProp =
|
|
3518
|
+
if (inst.knexInstance) {
|
|
3519
|
+
const knexProp = inst.knexInstance[prop];
|
|
3442
3520
|
if (typeof knexProp === "function") {
|
|
3443
|
-
return knexProp.bind(
|
|
3521
|
+
return knexProp.bind(inst.knexInstance);
|
|
3444
3522
|
}
|
|
3445
3523
|
return knexProp;
|
|
3446
3524
|
}
|
|
3447
|
-
if (prop in
|
|
3448
|
-
const method =
|
|
3525
|
+
if (prop in inst) {
|
|
3526
|
+
const method = inst[prop];
|
|
3449
3527
|
if (typeof method === "function") {
|
|
3450
|
-
return method.bind(
|
|
3528
|
+
return method.bind(inst);
|
|
3451
3529
|
}
|
|
3452
3530
|
return method;
|
|
3453
3531
|
}
|
|
@@ -3455,9 +3533,6 @@ var Db = class {
|
|
|
3455
3533
|
}
|
|
3456
3534
|
});
|
|
3457
3535
|
}
|
|
3458
|
-
/**
|
|
3459
|
-
* Detect database client type from connection string
|
|
3460
|
-
*/
|
|
3461
3536
|
detectClient(connectionString) {
|
|
3462
3537
|
if (connectionString.match(/^postgresql/)) {
|
|
3463
3538
|
return "pg";
|
|
@@ -3467,9 +3542,6 @@ var Db = class {
|
|
|
3467
3542
|
}
|
|
3468
3543
|
return null;
|
|
3469
3544
|
}
|
|
3470
|
-
/**
|
|
3471
|
-
* Connect to the database
|
|
3472
|
-
*/
|
|
3473
3545
|
async connect() {
|
|
3474
3546
|
if (this.isConnected && this.knexInstance) {
|
|
3475
3547
|
this.logger.warn?.("[Db] Already connected");
|
|
@@ -3478,14 +3550,13 @@ var Db = class {
|
|
|
3478
3550
|
const client = this.detectClient(this.config.connectionString);
|
|
3479
3551
|
if (!client) {
|
|
3480
3552
|
throw new ParamError(
|
|
3481
|
-
|
|
3553
|
+
"Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://"
|
|
3482
3554
|
);
|
|
3483
3555
|
}
|
|
3484
3556
|
try {
|
|
3485
3557
|
const connectionConfig = {
|
|
3486
3558
|
connectionString: this.config.connectionString,
|
|
3487
3559
|
family: 4
|
|
3488
|
-
// Force IPv4 only (disable IPv6)
|
|
3489
3560
|
};
|
|
3490
3561
|
this.knexInstance = (0, import_knex.default)({
|
|
3491
3562
|
client,
|
|
@@ -3501,7 +3572,7 @@ var Db = class {
|
|
|
3501
3572
|
await this.testConnection();
|
|
3502
3573
|
}
|
|
3503
3574
|
this.isConnected = true;
|
|
3504
|
-
this.logger.debug?.(
|
|
3575
|
+
this.logger.debug?.(formatDbConnectMessage(this.config.name, this.config.connectionString));
|
|
3505
3576
|
} catch (error) {
|
|
3506
3577
|
if (error instanceof ParamError) {
|
|
3507
3578
|
throw error;
|
|
@@ -3510,9 +3581,6 @@ var Db = class {
|
|
|
3510
3581
|
throw new ParamError(`Db: Connection failed - ${errorMsg}`);
|
|
3511
3582
|
}
|
|
3512
3583
|
}
|
|
3513
|
-
/**
|
|
3514
|
-
* Disconnect from the database
|
|
3515
|
-
*/
|
|
3516
3584
|
async disconnect() {
|
|
3517
3585
|
if (!this.knexInstance) {
|
|
3518
3586
|
return;
|
|
@@ -3522,16 +3590,13 @@ var Db = class {
|
|
|
3522
3590
|
this.knexInstance = null;
|
|
3523
3591
|
this.isConnected = false;
|
|
3524
3592
|
this.queriesLog = [];
|
|
3525
|
-
this.logger.debug?.(
|
|
3593
|
+
this.logger.debug?.(formatDbDisconnectMessage(this.config.name, this.config.connectionString));
|
|
3526
3594
|
} catch (error) {
|
|
3527
3595
|
const errorMsg = this.getErrorMessage(error);
|
|
3528
3596
|
this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
|
|
3529
3597
|
throw error;
|
|
3530
3598
|
}
|
|
3531
3599
|
}
|
|
3532
|
-
/**
|
|
3533
|
-
* Extract error message from various error types
|
|
3534
|
-
*/
|
|
3535
3600
|
getErrorMessage(error) {
|
|
3536
3601
|
if (error instanceof AggregateError) {
|
|
3537
3602
|
const errors = error.errors || [];
|
|
@@ -3556,9 +3621,11 @@ var Db = class {
|
|
|
3556
3621
|
return `${code} (tried: ${addresses.join(", ")})`;
|
|
3557
3622
|
}
|
|
3558
3623
|
}
|
|
3559
|
-
const uniqueMessages = [
|
|
3560
|
-
|
|
3561
|
-
|
|
3624
|
+
const uniqueMessages = [
|
|
3625
|
+
...new Set(
|
|
3626
|
+
errors.map((e) => e instanceof Error ? e.message : String(e))
|
|
3627
|
+
)
|
|
3628
|
+
];
|
|
3562
3629
|
if (uniqueMessages.length === 1) {
|
|
3563
3630
|
return uniqueMessages[0];
|
|
3564
3631
|
}
|
|
@@ -3567,28 +3634,25 @@ var Db = class {
|
|
|
3567
3634
|
return error.message || "Multiple errors occurred";
|
|
3568
3635
|
}
|
|
3569
3636
|
if (error instanceof Error) {
|
|
3570
|
-
const
|
|
3571
|
-
if (
|
|
3572
|
-
return `${
|
|
3637
|
+
const code = error.code;
|
|
3638
|
+
if (code) {
|
|
3639
|
+
return `${code}: ${error.message || String(error)}`;
|
|
3573
3640
|
}
|
|
3574
3641
|
return error.message || String(error);
|
|
3575
3642
|
}
|
|
3576
3643
|
if (typeof error === "string") {
|
|
3577
3644
|
return error;
|
|
3578
3645
|
}
|
|
3579
|
-
if (error
|
|
3646
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
3580
3647
|
const msg = String(error.message);
|
|
3581
|
-
const
|
|
3582
|
-
if (
|
|
3583
|
-
return `${
|
|
3648
|
+
const code = error.code;
|
|
3649
|
+
if (code) {
|
|
3650
|
+
return `${code}: ${msg}`;
|
|
3584
3651
|
}
|
|
3585
3652
|
return msg;
|
|
3586
3653
|
}
|
|
3587
3654
|
return String(error) || "Unknown error";
|
|
3588
3655
|
}
|
|
3589
|
-
/**
|
|
3590
|
-
* Test database connection
|
|
3591
|
-
*/
|
|
3592
3656
|
async testConnection() {
|
|
3593
3657
|
if (!this.knexInstance) {
|
|
3594
3658
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -3604,9 +3668,6 @@ var Db = class {
|
|
|
3604
3668
|
throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
|
|
3605
3669
|
}
|
|
3606
3670
|
}
|
|
3607
|
-
/**
|
|
3608
|
-
* Attach query profiler to log all queries
|
|
3609
|
-
*/
|
|
3610
3671
|
attachProfiler() {
|
|
3611
3672
|
if (!this.knexInstance) {
|
|
3612
3673
|
return;
|
|
@@ -3616,7 +3677,7 @@ var Db = class {
|
|
|
3616
3677
|
this.knexInstance.on("query", (query) => {
|
|
3617
3678
|
query.__startTime = process.hrtime();
|
|
3618
3679
|
});
|
|
3619
|
-
this.knexInstance.on("query-response", (
|
|
3680
|
+
this.knexInstance.on("query-response", (_response, query) => {
|
|
3620
3681
|
const [seconds, nanoseconds] = process.hrtime(query.__startTime);
|
|
3621
3682
|
const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
|
|
3622
3683
|
const logEntry = {
|
|
@@ -3631,15 +3692,9 @@ var Db = class {
|
|
|
3631
3692
|
this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
|
|
3632
3693
|
});
|
|
3633
3694
|
}
|
|
3634
|
-
/**
|
|
3635
|
-
* Get query log (only available if profiling is enabled)
|
|
3636
|
-
*/
|
|
3637
3695
|
getQueryLog() {
|
|
3638
3696
|
return [...this.queriesLog];
|
|
3639
3697
|
}
|
|
3640
|
-
/**
|
|
3641
|
-
* Check if a table exists
|
|
3642
|
-
*/
|
|
3643
3698
|
async tableExists(tableName) {
|
|
3644
3699
|
if (!this.knexInstance) {
|
|
3645
3700
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -3651,65 +3706,87 @@ var Db = class {
|
|
|
3651
3706
|
throw error;
|
|
3652
3707
|
}
|
|
3653
3708
|
}
|
|
3654
|
-
/**
|
|
3655
|
-
* Get the underlying Knex instance (for advanced usage)
|
|
3656
|
-
*/
|
|
3657
3709
|
getKnex() {
|
|
3658
3710
|
if (!this.knexInstance) {
|
|
3659
3711
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
3660
3712
|
}
|
|
3661
3713
|
return this.knexInstance;
|
|
3662
3714
|
}
|
|
3663
|
-
/**
|
|
3664
|
-
* Get connection status
|
|
3665
|
-
*/
|
|
3666
3715
|
isConnectedToDb() {
|
|
3667
3716
|
return this.isConnected && this.knexInstance !== null;
|
|
3668
3717
|
}
|
|
3669
|
-
/**
|
|
3670
|
-
* Initialize Db with context (connects and registers disconnect cleanup).
|
|
3671
|
-
* Params are read via getAllForModule("db", defs). Same as dbInit(context, dbNameOrConnectionString).
|
|
3672
|
-
*/
|
|
3673
|
-
static async init(context, dbNameOrConnectionString) {
|
|
3674
|
-
return dbFindAndConnect(context, dbNameOrConnectionString);
|
|
3675
|
-
}
|
|
3676
3718
|
};
|
|
3677
3719
|
function capitalizeFirstLetter(str) {
|
|
3678
3720
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
3679
3721
|
}
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
}
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3722
|
+
function resolveDbDisplayName(dbName, connectionParam, argsEnv, mergedName) {
|
|
3723
|
+
if (dbName) {
|
|
3724
|
+
return dbName;
|
|
3725
|
+
}
|
|
3726
|
+
if (mergedName) {
|
|
3727
|
+
return mergedName;
|
|
3728
|
+
}
|
|
3729
|
+
if (connectionParam?.startsWith("dbConnectionString") && connectionParam.length > "dbConnectionString".length) {
|
|
3730
|
+
return connectionParam.slice("dbConnectionString".length).toLowerCase();
|
|
3731
|
+
}
|
|
3732
|
+
if (connectionParam === "dbConnectionString" && argsEnv) {
|
|
3733
|
+
return argsEnv;
|
|
3734
|
+
}
|
|
3735
|
+
return void 0;
|
|
3736
|
+
}
|
|
3737
|
+
function formatDbConnectMessage(name, connectionString) {
|
|
3738
|
+
const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
|
|
3739
|
+
if (name) {
|
|
3740
|
+
return `[Db] Connected to database "${name}"${endpointSuffix}`;
|
|
3741
|
+
}
|
|
3742
|
+
return `[Db] Connected${endpointSuffix}`;
|
|
3743
|
+
}
|
|
3744
|
+
function formatDbDisconnectMessage(name, connectionString) {
|
|
3745
|
+
const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
|
|
3746
|
+
if (name) {
|
|
3747
|
+
return `[Db] Disconnected from database "${name}"${endpointSuffix}`;
|
|
3748
|
+
}
|
|
3749
|
+
return `[Db] Disconnected${endpointSuffix}`;
|
|
3750
|
+
}
|
|
3751
|
+
function formatDbInstanceMessage(action, name) {
|
|
3752
|
+
if (name) {
|
|
3753
|
+
return `[Db] instance "${name}" ${action}`;
|
|
3754
|
+
}
|
|
3755
|
+
return `[Db] instance ${action}`;
|
|
3756
|
+
}
|
|
3757
|
+
function formatConnectionEndpointSuffix(connectionString) {
|
|
3758
|
+
const endpoint = formatConnectionEndpoint(connectionString);
|
|
3759
|
+
return endpoint ? ` (${endpoint})` : "";
|
|
3760
|
+
}
|
|
3761
|
+
function formatConnectionEndpoint(connectionString) {
|
|
3762
|
+
try {
|
|
3763
|
+
const url = new URL(connectionString);
|
|
3764
|
+
const host = url.hostname;
|
|
3765
|
+
if (!host) {
|
|
3766
|
+
return null;
|
|
3767
|
+
}
|
|
3768
|
+
let port = url.port;
|
|
3769
|
+
if (!port) {
|
|
3770
|
+
if (url.protocol === "postgresql:") {
|
|
3771
|
+
port = "5432";
|
|
3772
|
+
} else if (url.protocol === "mysql:") {
|
|
3773
|
+
port = "3306";
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
return port ? `${host}:${port}` : host;
|
|
3777
|
+
} catch {
|
|
3778
|
+
return null;
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
async function dbConnect(context, config2) {
|
|
3705
3782
|
try {
|
|
3706
3783
|
const db = new Db(config2);
|
|
3707
3784
|
context.registerCleanup(async () => {
|
|
3708
3785
|
await db.disconnect();
|
|
3709
|
-
context.logger.debug(
|
|
3786
|
+
context.logger.debug?.(formatDbInstanceMessage("disconnected", config2.name));
|
|
3710
3787
|
});
|
|
3711
3788
|
await db.connect();
|
|
3712
|
-
context.logger.debug(
|
|
3789
|
+
context.logger.debug?.(formatDbInstanceMessage("initialized", config2.name));
|
|
3713
3790
|
return db;
|
|
3714
3791
|
} catch (error) {
|
|
3715
3792
|
if (error instanceof ParamError) {
|
|
@@ -3719,52 +3796,255 @@ async function dbConnect(context, connectionString, name, dbProfile) {
|
|
|
3719
3796
|
throw new ParamError(`[Db] connect error: ${errorMsg}`);
|
|
3720
3797
|
}
|
|
3721
3798
|
}
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3799
|
+
|
|
3800
|
+
// src/s3/index.js
|
|
3801
|
+
var import_client_s3 = require("@aws-sdk/client-s3");
|
|
3802
|
+
var DEFAULT_PROFILE = "local";
|
|
3803
|
+
function capitalize(str) {
|
|
3804
|
+
if (!str) return str;
|
|
3805
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
3806
|
+
}
|
|
3807
|
+
var S3 = class _S3 {
|
|
3808
|
+
/**
|
|
3809
|
+
* Build an S3 instance. Reads bucket profile from --bucket (default "local"),
|
|
3810
|
+
* then resolves per-profile params, builds the SDK client, and returns the
|
|
3811
|
+
* instance. Optionally pings the bucket once to verify reachability.
|
|
3812
|
+
*/
|
|
3813
|
+
static async init(context, options = {}) {
|
|
3814
|
+
const profileDef = { bucket: "string" };
|
|
3815
|
+
const discovered = context?.params?.getAllForModule?.("s3", profileDef) ?? {};
|
|
3816
|
+
const profile = options.bucket ?? discovered.bucket ?? DEFAULT_PROFILE;
|
|
3817
|
+
const cap = capitalize(profile);
|
|
3818
|
+
const config2 = {
|
|
3819
|
+
profile,
|
|
3820
|
+
bucketName: options.bucketName ?? await context.params.get(`s3Bucket${cap}`, "string"),
|
|
3821
|
+
region: options.region ?? await context.params.get(`s3Region${cap}`, "string default us-east-1"),
|
|
3822
|
+
endpoint: options.endpoint ?? await context.params.get(`s3Endpoint${cap}`, "string"),
|
|
3823
|
+
forcePathStyle: options.forcePathStyle ?? await context.params.get(`s3ForcePathStyle${cap}`, "boolean default false"),
|
|
3824
|
+
accessKeyId: options.accessKeyId ?? await context.params.get(`s3AccessKeyId${cap}`, "string"),
|
|
3825
|
+
secretAccessKey: options.secretAccessKey ?? await context.params.get(`s3SecretAccessKey${cap}`, "string")
|
|
3738
3826
|
};
|
|
3739
|
-
|
|
3740
|
-
dbName = paramsConfig.dbName;
|
|
3741
|
-
dbConnectionString = paramsConfig.dbConnectionString;
|
|
3742
|
-
dbProfile = paramsConfig.dbProfile;
|
|
3743
|
-
}
|
|
3744
|
-
if (!dbName && !dbConnectionString) {
|
|
3745
|
-
throw new ParamError("Db: either dbName or dbConnectionString must be specified");
|
|
3746
|
-
}
|
|
3747
|
-
if (dbName) {
|
|
3748
|
-
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
3749
|
-
dbConnectionString = await context.params.get(paramName, "string");
|
|
3750
|
-
if (!dbConnectionString) {
|
|
3827
|
+
if (!config2.bucketName) {
|
|
3751
3828
|
throw new ParamError(
|
|
3752
|
-
`
|
|
3829
|
+
`S3: bucket name not configured for profile "${profile}" (set s3Bucket${cap} or S3_BUCKET_${profile.toUpperCase()})`
|
|
3753
3830
|
);
|
|
3754
3831
|
}
|
|
3832
|
+
const s3 = new _S3(context, config2);
|
|
3833
|
+
if (options.testBucket !== false) {
|
|
3834
|
+
try {
|
|
3835
|
+
await s3.bucketExists();
|
|
3836
|
+
context.logger?.debug?.(
|
|
3837
|
+
`[S3] profile="${profile}" bucket="${config2.bucketName}" reachable`
|
|
3838
|
+
);
|
|
3839
|
+
} catch (err) {
|
|
3840
|
+
context.logger?.warn?.(
|
|
3841
|
+
`[S3] profile="${profile}" bucket="${config2.bucketName}" reachability test failed: ${err?.message ?? err}`
|
|
3842
|
+
);
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
return s3;
|
|
3846
|
+
}
|
|
3847
|
+
constructor(context, config2) {
|
|
3848
|
+
this.logger = context?.logger ?? console;
|
|
3849
|
+
this.profile = config2.profile;
|
|
3850
|
+
this.bucketName = config2.bucketName;
|
|
3851
|
+
this.region = config2.region;
|
|
3852
|
+
this.endpoint = config2.endpoint || null;
|
|
3853
|
+
const clientConfig = { region: config2.region };
|
|
3854
|
+
if (config2.endpoint) clientConfig.endpoint = config2.endpoint;
|
|
3855
|
+
if (config2.forcePathStyle) clientConfig.forcePathStyle = true;
|
|
3856
|
+
if (config2.accessKeyId && config2.secretAccessKey) {
|
|
3857
|
+
clientConfig.credentials = {
|
|
3858
|
+
accessKeyId: config2.accessKeyId,
|
|
3859
|
+
secretAccessKey: config2.secretAccessKey
|
|
3860
|
+
};
|
|
3861
|
+
}
|
|
3862
|
+
this.client = new import_client_s3.S3Client(clientConfig);
|
|
3755
3863
|
}
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3864
|
+
// ── info ────────────────────────────────────────────────────────────────
|
|
3865
|
+
getBucketName() {
|
|
3866
|
+
return this.bucketName;
|
|
3867
|
+
}
|
|
3868
|
+
getProfile() {
|
|
3869
|
+
return this.profile;
|
|
3870
|
+
}
|
|
3871
|
+
getRegion() {
|
|
3872
|
+
return this.region;
|
|
3873
|
+
}
|
|
3874
|
+
getEndpoint() {
|
|
3875
|
+
return this.endpoint;
|
|
3876
|
+
}
|
|
3877
|
+
// ── reachability ────────────────────────────────────────────────────────
|
|
3878
|
+
async bucketExists() {
|
|
3879
|
+
await this.client.send(new import_client_s3.HeadBucketCommand({ Bucket: this.bucketName }));
|
|
3880
|
+
return true;
|
|
3881
|
+
}
|
|
3882
|
+
// ── HEAD / GET ──────────────────────────────────────────────────────────
|
|
3883
|
+
/** Returns null on 404; never throws for "missing". Other errors throw. */
|
|
3884
|
+
async headObject(key) {
|
|
3885
|
+
try {
|
|
3886
|
+
const out = await this.client.send(new import_client_s3.HeadObjectCommand({
|
|
3887
|
+
Bucket: this.bucketName,
|
|
3888
|
+
Key: key
|
|
3889
|
+
}));
|
|
3890
|
+
return {
|
|
3891
|
+
etag: out.ETag,
|
|
3892
|
+
size: out.ContentLength,
|
|
3893
|
+
contentType: out.ContentType,
|
|
3894
|
+
lastModified: out.LastModified,
|
|
3895
|
+
metadata: out.Metadata,
|
|
3896
|
+
storageClass: out.StorageClass
|
|
3897
|
+
};
|
|
3898
|
+
} catch (err) {
|
|
3899
|
+
if (this._isNotFound(err)) return null;
|
|
3900
|
+
throw err;
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
/** Returns { body: Readable, contentType, contentLength, etag, ... } or null on 404. */
|
|
3904
|
+
async getObject(key) {
|
|
3905
|
+
try {
|
|
3906
|
+
const out = await this.client.send(new import_client_s3.GetObjectCommand({
|
|
3907
|
+
Bucket: this.bucketName,
|
|
3908
|
+
Key: key
|
|
3909
|
+
}));
|
|
3910
|
+
return {
|
|
3911
|
+
body: out.Body,
|
|
3912
|
+
contentType: out.ContentType,
|
|
3913
|
+
contentLength: out.ContentLength,
|
|
3914
|
+
etag: out.ETag,
|
|
3915
|
+
lastModified: out.LastModified,
|
|
3916
|
+
metadata: out.Metadata
|
|
3917
|
+
};
|
|
3918
|
+
} catch (err) {
|
|
3919
|
+
if (this._isNotFound(err)) return null;
|
|
3920
|
+
throw err;
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
/** Buffers the whole object. Use only for small objects (manifests, JSON). */
|
|
3924
|
+
async getObjectBytes(key) {
|
|
3925
|
+
const obj = await this.getObject(key);
|
|
3926
|
+
if (!obj) return null;
|
|
3927
|
+
const chunks = [];
|
|
3928
|
+
for await (const chunk of obj.body) chunks.push(chunk);
|
|
3929
|
+
return { ...obj, body: Buffer.concat(chunks) };
|
|
3930
|
+
}
|
|
3931
|
+
/** Convenience for JSON manifests. Returns parsed object or null on 404. */
|
|
3932
|
+
async getJson(key) {
|
|
3933
|
+
const obj = await this.getObjectBytes(key);
|
|
3934
|
+
if (!obj) return null;
|
|
3935
|
+
return JSON.parse(obj.body.toString("utf8"));
|
|
3936
|
+
}
|
|
3937
|
+
// ── PUT ─────────────────────────────────────────────────────────────────
|
|
3938
|
+
async putObject({ key, body, contentType, contentLength, tags, metadata }) {
|
|
3939
|
+
const cmd = new import_client_s3.PutObjectCommand({
|
|
3940
|
+
Bucket: this.bucketName,
|
|
3941
|
+
Key: key,
|
|
3942
|
+
Body: body,
|
|
3943
|
+
...contentType && { ContentType: contentType },
|
|
3944
|
+
...contentLength != null && { ContentLength: contentLength },
|
|
3945
|
+
...metadata && { Metadata: metadata },
|
|
3946
|
+
...tags && { Tagging: this._tagsToQuery(tags) }
|
|
3947
|
+
});
|
|
3948
|
+
return this.client.send(cmd);
|
|
3949
|
+
}
|
|
3950
|
+
/** Convenience for JSON manifests. */
|
|
3951
|
+
async putJson(key, value, opts = {}) {
|
|
3952
|
+
const json = JSON.stringify(value, null, opts.pretty ? 2 : 0);
|
|
3953
|
+
const body = Buffer.from(json, "utf8");
|
|
3954
|
+
return this.putObject({
|
|
3955
|
+
key,
|
|
3956
|
+
body,
|
|
3957
|
+
contentType: "application/json",
|
|
3958
|
+
contentLength: body.length,
|
|
3959
|
+
tags: opts.tags,
|
|
3960
|
+
metadata: opts.metadata
|
|
3961
|
+
});
|
|
3962
|
+
}
|
|
3963
|
+
// ── DELETE ──────────────────────────────────────────────────────────────
|
|
3964
|
+
async deleteObject(key) {
|
|
3965
|
+
return this.client.send(new import_client_s3.DeleteObjectCommand({
|
|
3966
|
+
Bucket: this.bucketName,
|
|
3967
|
+
Key: key
|
|
3968
|
+
}));
|
|
3969
|
+
}
|
|
3970
|
+
// ── COPY (for migration: legacy → new bucket, or intra-bucket "rename") ─
|
|
3971
|
+
async copyObject({ sourceBucket, sourceKey, key, contentType, metadata, tags }) {
|
|
3972
|
+
const src = sourceBucket || this.bucketName;
|
|
3973
|
+
const cmd = new import_client_s3.CopyObjectCommand({
|
|
3974
|
+
Bucket: this.bucketName,
|
|
3975
|
+
Key: key,
|
|
3976
|
+
CopySource: encodeURIComponent(`${src}/${sourceKey}`),
|
|
3977
|
+
...contentType && {
|
|
3978
|
+
ContentType: contentType,
|
|
3979
|
+
MetadataDirective: "REPLACE"
|
|
3980
|
+
},
|
|
3981
|
+
...metadata && {
|
|
3982
|
+
Metadata: metadata,
|
|
3983
|
+
MetadataDirective: "REPLACE"
|
|
3984
|
+
},
|
|
3985
|
+
...tags && {
|
|
3986
|
+
Tagging: this._tagsToQuery(tags),
|
|
3987
|
+
TaggingDirective: "REPLACE"
|
|
3988
|
+
}
|
|
3989
|
+
});
|
|
3990
|
+
return this.client.send(cmd);
|
|
3991
|
+
}
|
|
3992
|
+
// ── LIST ────────────────────────────────────────────────────────────────
|
|
3993
|
+
async listObjects(prefix, { keysOnly = false, maxKeys = 1e3, continuationToken } = {}) {
|
|
3994
|
+
const out = await this.client.send(new import_client_s3.ListObjectsV2Command({
|
|
3995
|
+
Bucket: this.bucketName,
|
|
3996
|
+
Prefix: prefix,
|
|
3997
|
+
MaxKeys: maxKeys,
|
|
3998
|
+
ContinuationToken: continuationToken
|
|
3999
|
+
}));
|
|
4000
|
+
const items = (out.Contents ?? []).map((o) => ({
|
|
4001
|
+
key: o.Key,
|
|
4002
|
+
size: o.Size,
|
|
4003
|
+
etag: o.ETag,
|
|
4004
|
+
lastModified: o.LastModified,
|
|
4005
|
+
storageClass: o.StorageClass
|
|
4006
|
+
}));
|
|
4007
|
+
return {
|
|
4008
|
+
items: keysOnly ? items.map((i) => i.key) : items,
|
|
4009
|
+
isTruncated: !!out.IsTruncated,
|
|
4010
|
+
nextContinuationToken: out.NextContinuationToken
|
|
4011
|
+
};
|
|
4012
|
+
}
|
|
4013
|
+
// ── TAGS (used for lifecycle rules, e.g. status=closed → Glacier IR) ────
|
|
4014
|
+
async putObjectTagging(key, tags) {
|
|
4015
|
+
return this.client.send(new import_client_s3.PutObjectTaggingCommand({
|
|
4016
|
+
Bucket: this.bucketName,
|
|
4017
|
+
Key: key,
|
|
4018
|
+
Tagging: { TagSet: this._tagsToTagSet(tags) }
|
|
4019
|
+
}));
|
|
4020
|
+
}
|
|
4021
|
+
async getObjectTagging(key) {
|
|
4022
|
+
const out = await this.client.send(new import_client_s3.GetObjectTaggingCommand({
|
|
4023
|
+
Bucket: this.bucketName,
|
|
4024
|
+
Key: key
|
|
4025
|
+
}));
|
|
4026
|
+
const tags = {};
|
|
4027
|
+
for (const t of out.TagSet ?? []) tags[t.Key] = t.Value;
|
|
4028
|
+
return tags;
|
|
4029
|
+
}
|
|
4030
|
+
// ── internals ───────────────────────────────────────────────────────────
|
|
4031
|
+
_isNotFound(err) {
|
|
4032
|
+
const status = err?.$metadata?.httpStatusCode;
|
|
4033
|
+
return status === 404 || err?.name === "NotFound" || err?.name === "NoSuchKey" || err?.Code === "NoSuchKey";
|
|
4034
|
+
}
|
|
4035
|
+
_tagsToQuery(tags) {
|
|
4036
|
+
return Object.entries(tags).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
|
|
4037
|
+
}
|
|
4038
|
+
_tagsToTagSet(tags) {
|
|
4039
|
+
return Object.entries(tags).map(([Key, Value]) => ({ Key, Value: String(Value) }));
|
|
4040
|
+
}
|
|
4041
|
+
};
|
|
3762
4042
|
|
|
3763
|
-
// src/logger/index.
|
|
4043
|
+
// src/logger/index.js
|
|
3764
4044
|
var import_chalk = __toESM(require("chalk"), 1);
|
|
3765
4045
|
var import_util = __toESM(require("util"), 1);
|
|
3766
4046
|
|
|
3767
|
-
// src/logger/transports.
|
|
4047
|
+
// src/logger/transports.js
|
|
3768
4048
|
var ConsoleTransport = class {
|
|
3769
4049
|
write(payload) {
|
|
3770
4050
|
console.info(payload);
|
|
@@ -3784,7 +4064,7 @@ var ParentProcessTransport = class {
|
|
|
3784
4064
|
}
|
|
3785
4065
|
};
|
|
3786
4066
|
|
|
3787
|
-
// src/logger/index.
|
|
4067
|
+
// src/logger/index.js
|
|
3788
4068
|
var ALL_LEVELS = [
|
|
3789
4069
|
"silly",
|
|
3790
4070
|
"debug",
|
|
@@ -3857,6 +4137,7 @@ var Logger = class _Logger {
|
|
|
3857
4137
|
}
|
|
3858
4138
|
/**
|
|
3859
4139
|
* Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
|
|
4140
|
+
* Params are tracked under the `logger` module for --showUsedParams.
|
|
3860
4141
|
*/
|
|
3861
4142
|
static init(context, options) {
|
|
3862
4143
|
const paramDefs = {
|
|
@@ -3870,7 +4151,7 @@ var Logger = class _Logger {
|
|
|
3870
4151
|
progressWithTimes: "boolean default false",
|
|
3871
4152
|
progressThrottleMs: "number"
|
|
3872
4153
|
};
|
|
3873
|
-
const discovered = context.params.getAllForModule(paramDefs);
|
|
4154
|
+
const discovered = context.params.getAllForModule("logger", paramDefs);
|
|
3874
4155
|
const config2 = { ...discovered, ...options };
|
|
3875
4156
|
const logger = new _Logger(context, config2);
|
|
3876
4157
|
context.logger = logger;
|
|
@@ -4066,9 +4347,9 @@ var Logger = class _Logger {
|
|
|
4066
4347
|
}
|
|
4067
4348
|
};
|
|
4068
4349
|
|
|
4069
|
-
// src/init/index.
|
|
4350
|
+
// src/init/index.js
|
|
4070
4351
|
var import_events = require("events");
|
|
4071
|
-
function extractComponentOptions(opts,
|
|
4352
|
+
function extractComponentOptions(opts, _componentName) {
|
|
4072
4353
|
const reservedKeys = ["overrides", "defaults", "modules"];
|
|
4073
4354
|
const componentOptions = {};
|
|
4074
4355
|
for (const [key, value] of Object.entries(opts)) {
|
|
@@ -4113,7 +4394,10 @@ function setupContext(opts = {}) {
|
|
|
4113
4394
|
return setup(opts);
|
|
4114
4395
|
}
|
|
4115
4396
|
|
|
4116
|
-
// src/
|
|
4397
|
+
// src/tasks/index.js
|
|
4398
|
+
var import_node_os3 = __toESM(require("os"), 1);
|
|
4399
|
+
|
|
4400
|
+
// src/utils/core-utils.js
|
|
4117
4401
|
function sleepMs(ms) {
|
|
4118
4402
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
4119
4403
|
}
|
|
@@ -4122,14 +4406,82 @@ function toJsonColumn(value) {
|
|
|
4122
4406
|
return JSON.stringify(value);
|
|
4123
4407
|
}
|
|
4124
4408
|
|
|
4125
|
-
// src/tasks/servicesRegistry.
|
|
4126
|
-
var import_node_crypto2 = require("crypto");
|
|
4127
|
-
var import_promises = require("fs/promises");
|
|
4409
|
+
// src/tasks/servicesRegistry.js
|
|
4128
4410
|
var import_node_os = __toESM(require("os"), 1);
|
|
4129
|
-
var import_node_path = __toESM(require("path"), 1);
|
|
4130
4411
|
|
|
4131
|
-
// src/tasks/taskUtils.
|
|
4412
|
+
// src/tasks/taskUtils.js
|
|
4132
4413
|
var import_node_crypto = require("crypto");
|
|
4414
|
+
|
|
4415
|
+
// src/tasks/time-matcher.js
|
|
4416
|
+
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
4417
|
+
function resolveAsterisks(field, range) {
|
|
4418
|
+
return field.includes("*") ? field.replace("*", range) : field;
|
|
4419
|
+
}
|
|
4420
|
+
function resolveRanges(field) {
|
|
4421
|
+
const regex = /(\d+)-(\d+)/;
|
|
4422
|
+
let current = field;
|
|
4423
|
+
while (true) {
|
|
4424
|
+
const match = regex.exec(current);
|
|
4425
|
+
if (!match) break;
|
|
4426
|
+
const raw = match[0];
|
|
4427
|
+
let first = Number(match[1]);
|
|
4428
|
+
let last = Number(match[2]);
|
|
4429
|
+
if (last < first) {
|
|
4430
|
+
[first, last] = [last, first];
|
|
4431
|
+
}
|
|
4432
|
+
const values = [];
|
|
4433
|
+
for (let i = first; i <= last; i += 1) {
|
|
4434
|
+
values.push(i);
|
|
4435
|
+
}
|
|
4436
|
+
current = current.replace(raw, values.join(","));
|
|
4437
|
+
}
|
|
4438
|
+
return current;
|
|
4439
|
+
}
|
|
4440
|
+
function resolveSteps(field) {
|
|
4441
|
+
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
4442
|
+
if (!match) return field;
|
|
4443
|
+
const base = match[1];
|
|
4444
|
+
const step = Number(match[2]);
|
|
4445
|
+
if (!Number.isFinite(step) || step <= 0) return field;
|
|
4446
|
+
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
4447
|
+
}
|
|
4448
|
+
function convertPattern(pattern) {
|
|
4449
|
+
const parts = pattern.trim().split(/\s+/);
|
|
4450
|
+
if (parts.length !== 6) {
|
|
4451
|
+
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
4452
|
+
}
|
|
4453
|
+
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
4454
|
+
}
|
|
4455
|
+
function fieldMatches(field, value) {
|
|
4456
|
+
const allowed = field.split(",").map((v) => Number(v));
|
|
4457
|
+
return allowed.includes(value);
|
|
4458
|
+
}
|
|
4459
|
+
function matchesParsedPattern(parsed, date) {
|
|
4460
|
+
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());
|
|
4461
|
+
}
|
|
4462
|
+
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
4463
|
+
const parsed = convertPattern(pattern);
|
|
4464
|
+
return matchesParsedPattern(parsed, date);
|
|
4465
|
+
}
|
|
4466
|
+
var MS_PER_SECOND = 1e3;
|
|
4467
|
+
var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
|
|
4468
|
+
function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
|
|
4469
|
+
const parsed = convertPattern(pattern);
|
|
4470
|
+
let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
|
|
4471
|
+
const end = t + maxSearchMs;
|
|
4472
|
+
while (t <= end) {
|
|
4473
|
+
const date = new Date(t);
|
|
4474
|
+
if (matchesParsedPattern(parsed, date)) {
|
|
4475
|
+
return date;
|
|
4476
|
+
}
|
|
4477
|
+
t += MS_PER_SECOND;
|
|
4478
|
+
}
|
|
4479
|
+
throw new Error(
|
|
4480
|
+
`nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
|
|
4481
|
+
);
|
|
4482
|
+
}
|
|
4483
|
+
|
|
4484
|
+
// src/tasks/taskUtils.js
|
|
4133
4485
|
function getDb(context) {
|
|
4134
4486
|
const db = context.db;
|
|
4135
4487
|
if (!db) {
|
|
@@ -4137,113 +4489,117 @@ function getDb(context) {
|
|
|
4137
4489
|
}
|
|
4138
4490
|
return db;
|
|
4139
4491
|
}
|
|
4140
|
-
function queueToTableNames(
|
|
4141
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
|
|
4142
|
-
throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
|
|
4143
|
-
}
|
|
4492
|
+
function queueToTableNames(queueName) {
|
|
4144
4493
|
return {
|
|
4145
|
-
tasksTable:
|
|
4146
|
-
historyTable: `${
|
|
4494
|
+
tasksTable: queueName,
|
|
4495
|
+
historyTable: `${queueName}_history`,
|
|
4496
|
+
registryTable: `${queueName}_services_registry`
|
|
4147
4497
|
};
|
|
4148
4498
|
}
|
|
4149
|
-
function
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4499
|
+
function defineTasksTable(t, db, tableNameForIndex) {
|
|
4500
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
4501
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
4502
|
+
t.timestamp("started_at");
|
|
4503
|
+
t.timestamp("completed_at");
|
|
4504
|
+
t.integer("priority").notNullable().defaultTo(50);
|
|
4505
|
+
t.text("schedule");
|
|
4506
|
+
t.timestamp("next_run_at").defaultTo(null);
|
|
4507
|
+
t.timestamp("past_due").defaultTo(null);
|
|
4508
|
+
t.text("name").notNullable();
|
|
4509
|
+
t.text("opid");
|
|
4510
|
+
t.json("params");
|
|
4511
|
+
t.text("service_group");
|
|
4512
|
+
t.integer("instance_number");
|
|
4513
|
+
t.text("service_name");
|
|
4514
|
+
t.text("server_name");
|
|
4515
|
+
t.text("status").notNullable().defaultTo("idle");
|
|
4516
|
+
t.timestamp("status_changed_at").defaultTo(null);
|
|
4517
|
+
t.text("progress");
|
|
4518
|
+
t.boolean("success");
|
|
4519
|
+
t.json("results");
|
|
4520
|
+
t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
|
|
4521
|
+
t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
|
|
4522
|
+
}
|
|
4523
|
+
function taskHistoryInsertFromQueueRow(row, overrides) {
|
|
4524
|
+
const { id, ...snapshot } = row;
|
|
4525
|
+
void id;
|
|
4526
|
+
return {
|
|
4527
|
+
...snapshot,
|
|
4528
|
+
...overrides
|
|
4529
|
+
};
|
|
4154
4530
|
}
|
|
4155
4531
|
async function ensureTaskTables(context, options = {}) {
|
|
4156
|
-
const
|
|
4532
|
+
const queueName = options.queueName ?? "tasks";
|
|
4157
4533
|
const recreate = options.recreate ?? false;
|
|
4158
4534
|
const db = getDb(context);
|
|
4159
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
4535
|
+
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
4160
4536
|
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
4161
4537
|
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
4538
|
+
const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
|
|
4162
4539
|
if (recreate) {
|
|
4163
4540
|
await db.schema.dropTableIfExists(historyTable);
|
|
4164
4541
|
await db.schema.dropTableIfExists(tasksTable);
|
|
4542
|
+
await db.schema.dropTableIfExists(registryTable);
|
|
4165
4543
|
}
|
|
4166
4544
|
if (needsTasks) {
|
|
4167
4545
|
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
4168
4546
|
await db.schema.createTable(tasksTable, (t) => {
|
|
4169
|
-
t
|
|
4170
|
-
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
4171
|
-
t.timestamp("started_at");
|
|
4172
|
-
t.timestamp("completed_at");
|
|
4173
|
-
t.integer("priority").notNullable().defaultTo(0);
|
|
4174
|
-
t.text("schedule");
|
|
4175
|
-
t.timestamp("past_due").defaultTo(null);
|
|
4176
|
-
t.text("target").notNullable();
|
|
4177
|
-
t.text("task").notNullable();
|
|
4178
|
-
t.json("params");
|
|
4179
|
-
t.text("opid");
|
|
4180
|
-
t.timestamp("paused_at").defaultTo(null);
|
|
4181
|
-
t.text("progress");
|
|
4182
|
-
t.boolean("success");
|
|
4183
|
-
t.json("results");
|
|
4184
|
-
});
|
|
4185
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
4186
|
-
t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
|
|
4187
|
-
t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
|
|
4188
|
-
t.index(["target", "task"], `${tasksTable}_target_task_idx`);
|
|
4547
|
+
defineTasksTable(t, db, tasksTable);
|
|
4189
4548
|
});
|
|
4190
4549
|
}
|
|
4191
4550
|
if (needsHistory) {
|
|
4192
4551
|
await db.schema.createTable(historyTable, (t) => {
|
|
4193
|
-
t
|
|
4194
|
-
t.timestamp("created_at").notNullable();
|
|
4195
|
-
t.timestamp("started_at");
|
|
4196
|
-
t.timestamp("completed_at");
|
|
4197
|
-
t.integer("priority").notNullable().defaultTo(0);
|
|
4198
|
-
t.text("schedule");
|
|
4199
|
-
t.timestamp("past_due").defaultTo(null);
|
|
4200
|
-
t.text("target").notNullable();
|
|
4201
|
-
t.text("task").notNullable();
|
|
4202
|
-
t.json("params");
|
|
4203
|
-
t.text("opid");
|
|
4204
|
-
t.text("progress");
|
|
4205
|
-
t.boolean("success");
|
|
4206
|
-
t.json("results");
|
|
4207
|
-
});
|
|
4208
|
-
await db.schema.alterTable(historyTable, (t) => {
|
|
4209
|
-
t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
|
|
4210
|
-
t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
|
|
4552
|
+
defineTasksTable(t, db, historyTable);
|
|
4211
4553
|
});
|
|
4212
4554
|
}
|
|
4213
|
-
const registryTable = servicesRegistryTable(queue);
|
|
4214
|
-
const needsRegistry = !await db.tableExists(registryTable);
|
|
4215
4555
|
if (needsRegistry) {
|
|
4216
4556
|
await db.schema.createTable(registryTable, (t) => {
|
|
4217
4557
|
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
4218
|
-
t.
|
|
4219
|
-
t.text("queue").notNullable();
|
|
4558
|
+
t.text("queue_name").notNullable();
|
|
4220
4559
|
t.text("service_group").notNullable();
|
|
4560
|
+
t.integer("instance_number").notNullable().defaultTo(1);
|
|
4221
4561
|
t.text("service_name").notNullable();
|
|
4222
|
-
t.text("
|
|
4223
|
-
t.text("hostname");
|
|
4562
|
+
t.text("server_name").notNullable();
|
|
4224
4563
|
t.integer("pid");
|
|
4225
4564
|
t.json("metadata");
|
|
4226
4565
|
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
4227
4566
|
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
4228
|
-
t.unique(["
|
|
4229
|
-
t.index(["
|
|
4230
|
-
t.index(["
|
|
4567
|
+
t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
|
|
4568
|
+
t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
|
|
4569
|
+
t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
|
|
4231
4570
|
});
|
|
4232
4571
|
}
|
|
4233
4572
|
}
|
|
4234
4573
|
async function enqueueTask(context, options) {
|
|
4235
4574
|
const db = getDb(context);
|
|
4236
|
-
const
|
|
4237
|
-
const { tasksTable } = queueToTableNames(
|
|
4575
|
+
const queueName = options.queueName ?? "tasks";
|
|
4576
|
+
const { tasksTable } = queueToTableNames(queueName);
|
|
4238
4577
|
const id = (0, import_node_crypto.randomUUID)();
|
|
4578
|
+
const name = options.name ?? options.task;
|
|
4579
|
+
if (!name) {
|
|
4580
|
+
throw new Error("enqueueTask: name (or task) is required");
|
|
4581
|
+
}
|
|
4582
|
+
const schedule = options.schedule?.trim() ? options.schedule : null;
|
|
4583
|
+
let nextRunAt = null;
|
|
4584
|
+
if (options.nextRunAt !== void 0) {
|
|
4585
|
+
nextRunAt = options.nextRunAt == null ? null : new Date(options.nextRunAt);
|
|
4586
|
+
} else if (schedule) {
|
|
4587
|
+
nextRunAt = nextTimeMatch(schedule, /* @__PURE__ */ new Date());
|
|
4588
|
+
}
|
|
4239
4589
|
await db(tasksTable).insert({
|
|
4240
4590
|
id,
|
|
4241
|
-
|
|
4242
|
-
task: options.task,
|
|
4591
|
+
name,
|
|
4243
4592
|
params: toJsonColumn(options.params ?? null),
|
|
4244
4593
|
opid: options.opid ?? null,
|
|
4245
|
-
priority: options.priority ??
|
|
4246
|
-
schedule
|
|
4594
|
+
priority: options.priority ?? 50,
|
|
4595
|
+
schedule,
|
|
4596
|
+
next_run_at: nextRunAt,
|
|
4597
|
+
service_group: options.serviceGroup ?? null,
|
|
4598
|
+
instance_number: options.instanceNumber ?? null,
|
|
4599
|
+
service_name: options.serviceName ?? null,
|
|
4600
|
+
server_name: options.serverName ?? null,
|
|
4601
|
+
status: "idle",
|
|
4602
|
+
status_changed_at: db.fn.now()
|
|
4247
4603
|
});
|
|
4248
4604
|
return id;
|
|
4249
4605
|
}
|
|
@@ -4254,7 +4610,7 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
|
4254
4610
|
});
|
|
4255
4611
|
}
|
|
4256
4612
|
|
|
4257
|
-
// src/tasks/servicesRegistry.
|
|
4613
|
+
// src/tasks/servicesRegistry.js
|
|
4258
4614
|
function getDb2(context) {
|
|
4259
4615
|
const db = context.db;
|
|
4260
4616
|
if (!db) {
|
|
@@ -4268,7 +4624,7 @@ function parseMetadataColumn(value) {
|
|
|
4268
4624
|
if (typeof value === "string") {
|
|
4269
4625
|
try {
|
|
4270
4626
|
const p = JSON.parse(value);
|
|
4271
|
-
return p && typeof p === "object" && !Array.isArray(
|
|
4627
|
+
return p && typeof p === "object" && !Array.isArray(value) ? p : {};
|
|
4272
4628
|
} catch {
|
|
4273
4629
|
return {};
|
|
4274
4630
|
}
|
|
@@ -4278,6 +4634,7 @@ function parseMetadataColumn(value) {
|
|
|
4278
4634
|
var DEFAULT_GROUP_MAX_INSTANCES = {
|
|
4279
4635
|
intake: 1,
|
|
4280
4636
|
harvest: 1,
|
|
4637
|
+
harvester: 0,
|
|
4281
4638
|
loader: 0,
|
|
4282
4639
|
photos: 0,
|
|
4283
4640
|
photosprocessor: 0,
|
|
@@ -4287,25 +4644,6 @@ function sanitizeNamePart(raw) {
|
|
|
4287
4644
|
const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
4288
4645
|
return s.slice(0, 80) || "runner";
|
|
4289
4646
|
}
|
|
4290
|
-
function identityFilePath(identityDir, queue, serviceGroup) {
|
|
4291
|
-
const safeQ = sanitizeNamePart(queue);
|
|
4292
|
-
const safeG = sanitizeNamePart(serviceGroup);
|
|
4293
|
-
return import_node_path.default.join(identityDir, `${safeQ}_${safeG}.json`);
|
|
4294
|
-
}
|
|
4295
|
-
async function readIdentityFile(filePath) {
|
|
4296
|
-
try {
|
|
4297
|
-
const text = await (0, import_promises.readFile)(filePath, "utf8");
|
|
4298
|
-
const parsed = JSON.parse(text);
|
|
4299
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
4300
|
-
} catch {
|
|
4301
|
-
return {};
|
|
4302
|
-
}
|
|
4303
|
-
}
|
|
4304
|
-
async function writeIdentityFile(filePath, data) {
|
|
4305
|
-
await (0, import_promises.mkdir)(import_node_path.default.dirname(filePath), { recursive: true });
|
|
4306
|
-
await (0, import_promises.writeFile)(filePath, `${JSON.stringify(data, null, 2)}
|
|
4307
|
-
`, "utf8");
|
|
4308
|
-
}
|
|
4309
4647
|
function resolveMaxInstances(serviceGroup, override) {
|
|
4310
4648
|
if (override !== void 0 && Number.isFinite(override)) {
|
|
4311
4649
|
return Math.max(0, Math.floor(Number(override)));
|
|
@@ -4313,179 +4651,190 @@ function resolveMaxInstances(serviceGroup, override) {
|
|
|
4313
4651
|
const g = serviceGroup.trim().toLowerCase();
|
|
4314
4652
|
return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
|
|
4315
4653
|
}
|
|
4316
|
-
async function countAliveInGroup(db, registryTable,
|
|
4654
|
+
async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
|
|
4317
4655
|
const cutoff = new Date(Date.now() - staleMs);
|
|
4318
|
-
let q = db(registryTable).where({
|
|
4319
|
-
if (
|
|
4320
|
-
q = q.whereNot("
|
|
4656
|
+
let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
|
|
4657
|
+
if (excludeRowId) {
|
|
4658
|
+
q = q.whereNot("id", excludeRowId);
|
|
4321
4659
|
}
|
|
4322
4660
|
const row = await q.count("id as count").first();
|
|
4323
4661
|
return Number(row?.count ?? 0);
|
|
4324
4662
|
}
|
|
4663
|
+
async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
|
|
4664
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
4665
|
+
const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
|
|
4666
|
+
const set = /* @__PURE__ */ new Set();
|
|
4667
|
+
for (const r of rows) {
|
|
4668
|
+
const n = Number(r.instance_number);
|
|
4669
|
+
if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
|
|
4670
|
+
}
|
|
4671
|
+
return set;
|
|
4672
|
+
}
|
|
4325
4673
|
function isUniqueViolation(error) {
|
|
4326
4674
|
const code = error?.code ?? error?.errno;
|
|
4327
4675
|
return code === "23505" || String(error?.message || "").includes("duplicate key");
|
|
4328
4676
|
}
|
|
4677
|
+
function buildMetadata(options) {
|
|
4678
|
+
const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
|
|
4679
|
+
if (options.target) {
|
|
4680
|
+
base.runnerTarget = options.target;
|
|
4681
|
+
}
|
|
4682
|
+
return toJsonColumn(Object.keys(base).length ? base : null);
|
|
4683
|
+
}
|
|
4684
|
+
function allocateInstanceNumber(occupied, explicit, maxSlots) {
|
|
4685
|
+
if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
|
|
4686
|
+
const e = Math.max(1, Math.floor(Number(explicit)));
|
|
4687
|
+
if (occupied.has(e)) {
|
|
4688
|
+
throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
|
|
4689
|
+
}
|
|
4690
|
+
if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
|
|
4691
|
+
throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
|
|
4692
|
+
}
|
|
4693
|
+
return e;
|
|
4694
|
+
}
|
|
4695
|
+
const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
|
|
4696
|
+
for (let n = 1; n <= cap; n++) {
|
|
4697
|
+
if (!occupied.has(n)) return n;
|
|
4698
|
+
}
|
|
4699
|
+
throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
|
|
4700
|
+
}
|
|
4701
|
+
function defaultServiceName(groupBase, hostBase, instanceNumber) {
|
|
4702
|
+
return `${groupBase}-${hostBase}-${instanceNumber}`;
|
|
4703
|
+
}
|
|
4329
4704
|
async function registerInServicesRegistry(context, options) {
|
|
4330
4705
|
const db = getDb2(context);
|
|
4331
|
-
const registryTable =
|
|
4706
|
+
const registryTable = queueToTableNames(options.queueName).registryTable;
|
|
4332
4707
|
const serviceGroup = options.serviceGroup.trim();
|
|
4333
4708
|
if (!serviceGroup) {
|
|
4334
4709
|
throw new Error("registerInServicesRegistry: serviceGroup is required");
|
|
4335
4710
|
}
|
|
4336
|
-
const
|
|
4337
|
-
let identity = await readIdentityFile(identityPath);
|
|
4338
|
-
let instanceId = typeof identity.instanceId === "string" && identity.instanceId.trim() ? identity.instanceId.trim() : (0, import_node_crypto2.randomUUID)();
|
|
4339
|
-
identity.instanceId = instanceId;
|
|
4340
|
-
await writeIdentityFile(identityPath, identity);
|
|
4341
|
-
const hostname = import_node_os.default.hostname();
|
|
4711
|
+
const serverName = import_node_os.default.hostname();
|
|
4342
4712
|
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
4343
|
-
const meta =
|
|
4344
|
-
const
|
|
4345
|
-
|
|
4346
|
-
await db(registryTable).where({ instance_id: instanceId }).update({
|
|
4347
|
-
target: options.target,
|
|
4348
|
-
hostname,
|
|
4349
|
-
pid,
|
|
4350
|
-
metadata: meta,
|
|
4351
|
-
last_seen_at: db.fn.now()
|
|
4352
|
-
});
|
|
4353
|
-
const serviceName = String(existing.service_name);
|
|
4354
|
-
identity.serviceName = serviceName;
|
|
4355
|
-
await writeIdentityFile(identityPath, identity);
|
|
4356
|
-
const reg = {
|
|
4357
|
-
instanceId,
|
|
4358
|
-
serviceName,
|
|
4359
|
-
serviceGroup,
|
|
4360
|
-
queue: options.queue,
|
|
4361
|
-
target: options.target,
|
|
4362
|
-
rowId: String(existing.id)
|
|
4363
|
-
};
|
|
4364
|
-
context.servicesRegistry = reg;
|
|
4365
|
-
context.runnerHeartbeat = reg;
|
|
4366
|
-
context.logger.info?.(
|
|
4367
|
-
`[services-registry] resumed instance_id=${instanceId} name=${serviceName} group=${serviceGroup} queue=${options.queue}`
|
|
4368
|
-
);
|
|
4369
|
-
return {
|
|
4370
|
-
instanceId,
|
|
4371
|
-
serviceName,
|
|
4372
|
-
serviceGroup,
|
|
4373
|
-
queue: options.queue,
|
|
4374
|
-
target: options.target,
|
|
4375
|
-
rowId: String(existing.id),
|
|
4376
|
-
registryTable
|
|
4377
|
-
};
|
|
4378
|
-
}
|
|
4713
|
+
const meta = buildMetadata(options);
|
|
4714
|
+
const groupBase = sanitizeNamePart(serviceGroup);
|
|
4715
|
+
const hostBase = sanitizeNamePart(serverName);
|
|
4379
4716
|
const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
|
|
4380
|
-
const
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
options.queue,
|
|
4384
|
-
serviceGroup,
|
|
4385
|
-
options.staleMs,
|
|
4386
|
-
instanceId
|
|
4387
|
-
);
|
|
4388
|
-
if (maxAllowed > 0 && aliveOthers >= maxAllowed) {
|
|
4389
|
-
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveOthers} alive (max ${maxAllowed}, queue=${options.queue}).`;
|
|
4717
|
+
const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
|
|
4718
|
+
if (maxAllowed > 0 && aliveCount >= maxAllowed) {
|
|
4719
|
+
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
|
|
4390
4720
|
if (options.enforceMaxInstances) {
|
|
4391
4721
|
throw new Error(msg);
|
|
4392
4722
|
}
|
|
4393
4723
|
context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
|
|
4394
4724
|
}
|
|
4395
|
-
const
|
|
4396
|
-
const
|
|
4397
|
-
const
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4725
|
+
const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
|
|
4726
|
+
const cutoff = new Date(Date.now() - options.staleMs);
|
|
4727
|
+
const MAX_ATTEMPTS = 8;
|
|
4728
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
4729
|
+
const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
|
|
4730
|
+
const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
|
|
4731
|
+
const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
|
|
4732
|
+
const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
4733
|
+
if (existing) {
|
|
4734
|
+
const lastSeen = new Date(existing.last_seen_at);
|
|
4735
|
+
const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
|
|
4736
|
+
if (isAlive) {
|
|
4737
|
+
if (options.serviceName?.trim()) {
|
|
4738
|
+
throw new Error(
|
|
4739
|
+
`[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
|
|
4740
|
+
);
|
|
4741
|
+
}
|
|
4742
|
+
context.logger.warn?.(
|
|
4743
|
+
`[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
|
|
4744
|
+
);
|
|
4745
|
+
if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
|
|
4746
|
+
throw new Error(
|
|
4747
|
+
`[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
|
|
4748
|
+
);
|
|
4749
|
+
}
|
|
4750
|
+
await new Promise((r) => setTimeout(r, 50 + attempt * 30));
|
|
4751
|
+
continue;
|
|
4752
|
+
}
|
|
4753
|
+
await db(registryTable).where({ id: existing.id }).update({
|
|
4754
|
+
server_name: serverName,
|
|
4755
|
+
pid,
|
|
4756
|
+
metadata: meta,
|
|
4757
|
+
service_group: serviceGroup,
|
|
4758
|
+
instance_number: instanceNumber,
|
|
4759
|
+
last_seen_at: db.fn.now()
|
|
4760
|
+
});
|
|
4761
|
+
const reg = {
|
|
4762
|
+
serviceName: serviceNameRaw,
|
|
4763
|
+
serviceGroup,
|
|
4764
|
+
queueName: options.queueName,
|
|
4765
|
+
target: options.target,
|
|
4766
|
+
rowId: String(existing.id),
|
|
4767
|
+
registryTable,
|
|
4768
|
+
instanceNumber
|
|
4769
|
+
};
|
|
4770
|
+
context.servicesRegistry = reg;
|
|
4771
|
+
context.runnerHeartbeat = reg;
|
|
4772
|
+
context.logger.info?.(
|
|
4773
|
+
`[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
4774
|
+
);
|
|
4775
|
+
return reg;
|
|
4776
|
+
}
|
|
4420
4777
|
try {
|
|
4421
4778
|
const rows = await db(registryTable).insert({
|
|
4422
|
-
|
|
4423
|
-
queue: options.queue,
|
|
4779
|
+
queue_name: options.queueName,
|
|
4424
4780
|
service_group: serviceGroup,
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4781
|
+
instance_number: instanceNumber,
|
|
4782
|
+
service_name: serviceNameRaw,
|
|
4783
|
+
server_name: serverName,
|
|
4428
4784
|
pid,
|
|
4429
4785
|
metadata: meta,
|
|
4430
|
-
last_seen_at: db.fn.now()
|
|
4786
|
+
last_seen_at: db.fn.now(),
|
|
4787
|
+
created_at: db.fn.now()
|
|
4431
4788
|
}).returning(["id", "service_name"]);
|
|
4432
4789
|
const row = Array.isArray(rows) ? rows[0] : rows;
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4790
|
+
let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
|
|
4791
|
+
if (!rowId) {
|
|
4792
|
+
const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
4793
|
+
rowId = again?.id != null ? String(again.id) : "";
|
|
4436
4794
|
}
|
|
4795
|
+
if (!rowId) continue;
|
|
4796
|
+
const regNew = {
|
|
4797
|
+
serviceName: String(row?.service_name ?? serviceNameRaw),
|
|
4798
|
+
serviceGroup,
|
|
4799
|
+
queueName: options.queueName,
|
|
4800
|
+
target: options.target,
|
|
4801
|
+
rowId,
|
|
4802
|
+
registryTable,
|
|
4803
|
+
instanceNumber
|
|
4804
|
+
};
|
|
4805
|
+
context.servicesRegistry = regNew;
|
|
4806
|
+
context.runnerHeartbeat = regNew;
|
|
4807
|
+
context.logger.info?.(
|
|
4808
|
+
`[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
4809
|
+
);
|
|
4810
|
+
return regNew;
|
|
4437
4811
|
} catch (error) {
|
|
4438
4812
|
if (!isUniqueViolation(error)) {
|
|
4439
4813
|
throw error;
|
|
4440
4814
|
}
|
|
4815
|
+
context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
|
|
4441
4816
|
}
|
|
4442
4817
|
}
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
`[services-registry] could not allocate a unique service_name for group=${serviceGroup} queue=${options.queue} (too many collisions).`
|
|
4446
|
-
);
|
|
4447
|
-
}
|
|
4448
|
-
identity.serviceName = inserted.service_name;
|
|
4449
|
-
await writeIdentityFile(identityPath, identity);
|
|
4450
|
-
const regNew = {
|
|
4451
|
-
instanceId,
|
|
4452
|
-
serviceName: inserted.service_name,
|
|
4453
|
-
serviceGroup,
|
|
4454
|
-
queue: options.queue,
|
|
4455
|
-
target: options.target,
|
|
4456
|
-
rowId: inserted.id
|
|
4457
|
-
};
|
|
4458
|
-
context.servicesRegistry = regNew;
|
|
4459
|
-
context.runnerHeartbeat = regNew;
|
|
4460
|
-
context.logger.info?.(
|
|
4461
|
-
`[services-registry] registered instance_id=${instanceId} name=${inserted.service_name} group=${serviceGroup} queue=${options.queue} target=${options.target}`
|
|
4818
|
+
throw new Error(
|
|
4819
|
+
`[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
|
|
4462
4820
|
);
|
|
4463
|
-
return {
|
|
4464
|
-
instanceId,
|
|
4465
|
-
serviceName: inserted.service_name,
|
|
4466
|
-
serviceGroup,
|
|
4467
|
-
queue: options.queue,
|
|
4468
|
-
target: options.target,
|
|
4469
|
-
rowId: inserted.id,
|
|
4470
|
-
registryTable
|
|
4471
|
-
};
|
|
4472
4821
|
}
|
|
4473
4822
|
async function touchServicesRegistry(context, registration) {
|
|
4474
4823
|
const db = getDb2(context);
|
|
4475
|
-
const
|
|
4824
|
+
const serverName = import_node_os.default.hostname();
|
|
4476
4825
|
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
4477
|
-
await db(registration.registryTable).where({
|
|
4826
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
4478
4827
|
last_seen_at: db.fn.now(),
|
|
4479
|
-
|
|
4828
|
+
server_name: serverName,
|
|
4480
4829
|
pid
|
|
4481
4830
|
});
|
|
4482
4831
|
}
|
|
4483
4832
|
async function updateServicesRegistryMetadata(context, registration, patch) {
|
|
4484
4833
|
const db = getDb2(context);
|
|
4485
|
-
const row = await db(registration.registryTable).where({
|
|
4834
|
+
const row = await db(registration.registryTable).where({ id: registration.rowId }).first();
|
|
4486
4835
|
const prev = parseMetadataColumn(row?.metadata);
|
|
4487
4836
|
const merged = { ...prev, ...patch };
|
|
4488
|
-
await db(registration.registryTable).where({
|
|
4837
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
4489
4838
|
metadata: toJsonColumn(merged),
|
|
4490
4839
|
last_seen_at: db.fn.now()
|
|
4491
4840
|
});
|
|
@@ -4493,14 +4842,14 @@ async function updateServicesRegistryMetadata(context, registration, patch) {
|
|
|
4493
4842
|
}
|
|
4494
4843
|
async function unregisterServicesRegistry(context, registration) {
|
|
4495
4844
|
const db = getDb2(context);
|
|
4496
|
-
await db(registration.registryTable).where({
|
|
4497
|
-
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName}
|
|
4845
|
+
await db(registration.registryTable).where({ id: registration.rowId }).delete();
|
|
4846
|
+
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
|
|
4498
4847
|
}
|
|
4499
|
-
async function listServicesRegistry(context, options = {
|
|
4848
|
+
async function listServicesRegistry(context, options = { queueName: "tasks" }) {
|
|
4500
4849
|
const db = getDb2(context);
|
|
4501
4850
|
const staleMs = options.staleMs ?? 6e4;
|
|
4502
4851
|
const cutoff = new Date(Date.now() - staleMs);
|
|
4503
|
-
const table =
|
|
4852
|
+
const table = queueToTableNames(options.queueName).registryTable;
|
|
4504
4853
|
let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
|
|
4505
4854
|
if (options.serviceGroup?.trim()) {
|
|
4506
4855
|
q = q.where({ service_group: options.serviceGroup.trim() });
|
|
@@ -4508,7 +4857,8 @@ async function listServicesRegistry(context, options = { queue: "tasks" }) {
|
|
|
4508
4857
|
return await q;
|
|
4509
4858
|
}
|
|
4510
4859
|
|
|
4511
|
-
// src/tasks/taskLogs.
|
|
4860
|
+
// src/tasks/taskLogs.js
|
|
4861
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
4512
4862
|
function getLogsState(context) {
|
|
4513
4863
|
const holder = context;
|
|
4514
4864
|
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
@@ -4561,6 +4911,96 @@ function getLogsState(context) {
|
|
|
4561
4911
|
holder.__tasksLogsState = state;
|
|
4562
4912
|
return state;
|
|
4563
4913
|
}
|
|
4914
|
+
function ipcLogTargetKey(target) {
|
|
4915
|
+
const bp = target.basePath ?? "";
|
|
4916
|
+
const ns = target.namespace ?? "";
|
|
4917
|
+
return `${bp}::${ns}::${target.tableName}`;
|
|
4918
|
+
}
|
|
4919
|
+
function ipcFileLogsTableNameForSourceResource(source, resource) {
|
|
4920
|
+
const seg = (s) => {
|
|
4921
|
+
const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
4922
|
+
return t.length ? t : "x";
|
|
4923
|
+
};
|
|
4924
|
+
return `${seg(source)}/${seg(resource)}`;
|
|
4925
|
+
}
|
|
4926
|
+
async function readTaskIpcLogsSnapshot(context, options) {
|
|
4927
|
+
const holder = context;
|
|
4928
|
+
const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
|
|
4929
|
+
const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
|
|
4930
|
+
const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
|
|
4931
|
+
const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
|
|
4932
|
+
const fd = new FileDatabase({
|
|
4933
|
+
basePath,
|
|
4934
|
+
namespace,
|
|
4935
|
+
tableName,
|
|
4936
|
+
versioned: true,
|
|
4937
|
+
useMetadata: true,
|
|
4938
|
+
maxVersions: 30,
|
|
4939
|
+
pageSize: 2e3,
|
|
4940
|
+
logger: holder.logger
|
|
4941
|
+
});
|
|
4942
|
+
const versions = await fd.getVersions();
|
|
4943
|
+
if (versions.length === 0) {
|
|
4944
|
+
return { records: [], latestTs: null };
|
|
4945
|
+
}
|
|
4946
|
+
const latest = versions[versions.length - 1];
|
|
4947
|
+
const raw = await fd.read({ version: latest });
|
|
4948
|
+
const arr = Array.isArray(raw) ? raw : [];
|
|
4949
|
+
let filtered = arr;
|
|
4950
|
+
if (options.afterTs && String(options.afterTs).trim()) {
|
|
4951
|
+
const cut = String(options.afterTs).trim();
|
|
4952
|
+
filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
|
|
4953
|
+
}
|
|
4954
|
+
let latestTs = null;
|
|
4955
|
+
for (const r of filtered) {
|
|
4956
|
+
const ts = typeof r?.ts === "string" ? String(r.ts) : null;
|
|
4957
|
+
if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
|
|
4958
|
+
}
|
|
4959
|
+
const incremental = !!(options.afterTs && String(options.afterTs).trim());
|
|
4960
|
+
const maxReturn = incremental ? 1e4 : tail;
|
|
4961
|
+
const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
|
|
4962
|
+
return { records: sliced, latestTs };
|
|
4963
|
+
}
|
|
4964
|
+
function resolveIpcFileLogsDir(context, target) {
|
|
4965
|
+
const holder = context;
|
|
4966
|
+
const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
|
|
4967
|
+
const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
|
|
4968
|
+
const segments = target.tableName.split("/").filter(Boolean);
|
|
4969
|
+
return import_node_path.default.resolve(basePath, namespace, ...segments);
|
|
4970
|
+
}
|
|
4971
|
+
function getLogsStateForTarget(context, target) {
|
|
4972
|
+
const holder = context;
|
|
4973
|
+
const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
|
|
4974
|
+
const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
4975
|
+
if (!enabled) return null;
|
|
4976
|
+
if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
|
|
4977
|
+
const map = holder.__tasksLogsTargetStates;
|
|
4978
|
+
const key = ipcLogTargetKey(target);
|
|
4979
|
+
if (map.has(key)) return map.get(key);
|
|
4980
|
+
const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
|
|
4981
|
+
const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
|
|
4982
|
+
const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
|
|
4983
|
+
const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
|
|
4984
|
+
const db = new FileDatabase({
|
|
4985
|
+
basePath,
|
|
4986
|
+
namespace,
|
|
4987
|
+
tableName: target.tableName,
|
|
4988
|
+
versioned: true,
|
|
4989
|
+
useMetadata: true,
|
|
4990
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4991
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4992
|
+
logger: holder.logger
|
|
4993
|
+
});
|
|
4994
|
+
const state = {
|
|
4995
|
+
db,
|
|
4996
|
+
errorDb: null,
|
|
4997
|
+
queue: Promise.resolve(),
|
|
4998
|
+
initialized: false,
|
|
4999
|
+
errorInitialized: false
|
|
5000
|
+
};
|
|
5001
|
+
map.set(key, state);
|
|
5002
|
+
return state;
|
|
5003
|
+
}
|
|
4564
5004
|
function isErrorPayload(payload) {
|
|
4565
5005
|
if (!payload) return false;
|
|
4566
5006
|
if (typeof payload === "object") {
|
|
@@ -4580,14 +5020,26 @@ function buildLogRecord(task, payload) {
|
|
|
4580
5020
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4581
5021
|
opid: task.opid ?? null,
|
|
4582
5022
|
taskId: task.id,
|
|
4583
|
-
taskName: task.
|
|
4584
|
-
target: task.
|
|
5023
|
+
taskName: task.name,
|
|
5024
|
+
target: task.service_group,
|
|
4585
5025
|
source: typeof params.source === "string" ? params.source : null,
|
|
4586
5026
|
resource: typeof params.resource === "string" ? params.resource : null,
|
|
4587
5027
|
payload
|
|
4588
5028
|
};
|
|
4589
5029
|
}
|
|
4590
|
-
function appendTaskIpcLog(context, task, payload) {
|
|
5030
|
+
function appendTaskIpcLog(context, task, payload, target) {
|
|
5031
|
+
if (target) {
|
|
5032
|
+
const state2 = getLogsStateForTarget(context, target);
|
|
5033
|
+
if (!state2?.db) return;
|
|
5034
|
+
const record2 = buildLogRecord(task, payload);
|
|
5035
|
+
state2.queue = state2.queue.then(async () => {
|
|
5036
|
+
await state2.db.write([record2], { forceNewVersion: !state2.initialized });
|
|
5037
|
+
state2.initialized = true;
|
|
5038
|
+
}).catch((error) => {
|
|
5039
|
+
context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
|
|
5040
|
+
});
|
|
5041
|
+
return;
|
|
5042
|
+
}
|
|
4591
5043
|
const state = getLogsState(context);
|
|
4592
5044
|
if (!state.db && !state.errorDb) return;
|
|
4593
5045
|
const record = buildLogRecord(task, payload);
|
|
@@ -4604,84 +5056,293 @@ function appendTaskIpcLog(context, task, payload) {
|
|
|
4604
5056
|
context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
|
|
4605
5057
|
});
|
|
4606
5058
|
}
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
let current = field;
|
|
4616
|
-
while (true) {
|
|
4617
|
-
const match = regex.exec(current);
|
|
4618
|
-
if (!match) break;
|
|
4619
|
-
const raw = match[0];
|
|
4620
|
-
let first = Number(match[1]);
|
|
4621
|
-
let last = Number(match[2]);
|
|
4622
|
-
if (last < first) {
|
|
4623
|
-
[first, last] = [last, first];
|
|
4624
|
-
}
|
|
4625
|
-
const values = [];
|
|
4626
|
-
for (let i = first; i <= last; i += 1) {
|
|
4627
|
-
values.push(i);
|
|
5059
|
+
async function flushTaskIpcLogs(context) {
|
|
5060
|
+
const holder = context;
|
|
5061
|
+
const promises = [];
|
|
5062
|
+
if (holder.__tasksLogsState?.queue) promises.push(holder.__tasksLogsState.queue);
|
|
5063
|
+
const map = holder.__tasksLogsTargetStates;
|
|
5064
|
+
if (map) {
|
|
5065
|
+
for (const s of map.values()) {
|
|
5066
|
+
if (s.queue) promises.push(s.queue);
|
|
4628
5067
|
}
|
|
4629
|
-
current = current.replace(raw, values.join(","));
|
|
4630
5068
|
}
|
|
4631
|
-
|
|
4632
|
-
}
|
|
4633
|
-
function resolveSteps(field) {
|
|
4634
|
-
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
4635
|
-
if (!match) return field;
|
|
4636
|
-
const base = match[1];
|
|
4637
|
-
const step = Number(match[2]);
|
|
4638
|
-
if (!Number.isFinite(step) || step <= 0) return field;
|
|
4639
|
-
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
4640
|
-
}
|
|
4641
|
-
function convertPattern(pattern) {
|
|
4642
|
-
const parts = pattern.trim().split(/\s+/);
|
|
4643
|
-
if (parts.length !== 6) {
|
|
4644
|
-
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
4645
|
-
}
|
|
4646
|
-
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
4647
|
-
}
|
|
4648
|
-
function fieldMatches(field, value) {
|
|
4649
|
-
const allowed = field.split(",").map((v) => Number(v));
|
|
4650
|
-
return allowed.includes(value);
|
|
4651
|
-
}
|
|
4652
|
-
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
4653
|
-
const parsed = convertPattern(pattern);
|
|
4654
|
-
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());
|
|
5069
|
+
await Promise.all(promises);
|
|
4655
5070
|
}
|
|
4656
5071
|
|
|
4657
|
-
// src/tasks/
|
|
4658
|
-
var
|
|
4659
|
-
|
|
4660
|
-
|
|
5072
|
+
// src/tasks/AbstractTask.js
|
|
5073
|
+
var AbstractTask = class _AbstractTask {
|
|
5074
|
+
/**
|
|
5075
|
+
* Whether `send-task` should wait for completion (and print a result
|
|
5076
|
+
* report) when no explicit `--wait` / `--noWait` flag is given. Defaults
|
|
5077
|
+
* to false; short-lived probe tasks (e.g. `ping`) override to true.
|
|
5078
|
+
*
|
|
5079
|
+
* @type {boolean}
|
|
5080
|
+
*/
|
|
5081
|
+
static defaultWaitForResult = false;
|
|
5082
|
+
/**
|
|
5083
|
+
* @param {object} context Runner context (db, logger, params, emitter...).
|
|
5084
|
+
* @param {object} task Task row as claimed from the queue.
|
|
5085
|
+
*/
|
|
4661
5086
|
constructor(context, task) {
|
|
4662
5087
|
this.context = context;
|
|
4663
5088
|
this.task = task;
|
|
4664
5089
|
}
|
|
5090
|
+
/**
|
|
5091
|
+
* Return a short reason string when the task should be deferred (e.g. "locked
|
|
5092
|
+
* by source"), or `false`/falsy when it is free to run. Default: always `false`.
|
|
5093
|
+
*
|
|
5094
|
+
* @returns {string | false | Promise<string | false>}
|
|
5095
|
+
*/
|
|
4665
5096
|
cantRunReason() {
|
|
4666
5097
|
return false;
|
|
4667
5098
|
}
|
|
5099
|
+
/**
|
|
5100
|
+
* Called by the runner when a stop has been requested. Subclasses running
|
|
5101
|
+
* long loops should flip a flag here and check it between iterations.
|
|
5102
|
+
*
|
|
5103
|
+
* @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
|
|
5104
|
+
*/
|
|
4668
5105
|
requestStop(_allowanceMs) {
|
|
4669
5106
|
}
|
|
5107
|
+
/**
|
|
5108
|
+
* Perform the task. Must be implemented by subclasses.
|
|
5109
|
+
*
|
|
5110
|
+
* @param {(progress: unknown) => Promise<void>} _reportProgress
|
|
5111
|
+
* Updates the DB `progress` column. Accepts any serializable value;
|
|
5112
|
+
* strings are stored verbatim, objects are JSON-stringified.
|
|
5113
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5114
|
+
*/
|
|
5115
|
+
async run(_reportProgress) {
|
|
5116
|
+
throw new Error("AbstractTask.run must be implemented by subclass");
|
|
5117
|
+
}
|
|
5118
|
+
/**
|
|
5119
|
+
* Resolve a complete row payload for this task — envelope fields (queue,
|
|
5120
|
+
* priority, targeting, schedule…) plus the inner `params` blob produced by
|
|
5121
|
+
* {@link AbstractTask.resolveCustomParams}. Output shape matches
|
|
5122
|
+
* {@link enqueueTask}'s `options` argument, so the typical call is:
|
|
5123
|
+
*
|
|
5124
|
+
* const payload = await TaskClass.resolveParams(context, { name });
|
|
5125
|
+
* await enqueueTask(context, payload);
|
|
5126
|
+
*
|
|
5127
|
+
* Validation failures throw {@link ParamError} so the script aborts before
|
|
5128
|
+
* a malformed row hits the DB.
|
|
5129
|
+
*
|
|
5130
|
+
* @param {object} context
|
|
5131
|
+
* @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
|
|
5132
|
+
* Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
|
|
5133
|
+
* `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
|
|
5134
|
+
* `nextRunAt`, plus `params` (object — overlay onto inner blob).
|
|
5135
|
+
* @returns {Promise<object>}
|
|
5136
|
+
*/
|
|
5137
|
+
static async resolveParams(context, overrides = {}) {
|
|
5138
|
+
const main = _AbstractTask._resolveMainFields(context, overrides);
|
|
5139
|
+
const params = await this.resolveCustomParams(context, overrides);
|
|
5140
|
+
return { ...main, params };
|
|
5141
|
+
}
|
|
5142
|
+
/**
|
|
5143
|
+
* Resolve the inner JSON blob stored in the `params` column. Default
|
|
5144
|
+
* implementation passes through `--paramsJson` (parsed as a JSON object)
|
|
5145
|
+
* overlaid with `overrides.params` when supplied; returns `null` when
|
|
5146
|
+
* neither is provided.
|
|
5147
|
+
*
|
|
5148
|
+
* Subclasses with typed fields should override and call
|
|
5149
|
+
* {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
|
|
5150
|
+
* resolution, then validate and throw {@link ParamError} on bad input.
|
|
5151
|
+
*
|
|
5152
|
+
* @param {object} context
|
|
5153
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5154
|
+
* @returns {Promise<object|null>}
|
|
5155
|
+
*/
|
|
5156
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5157
|
+
return _AbstractTask._defaultParamsBlob(context, overrides);
|
|
5158
|
+
}
|
|
5159
|
+
/**
|
|
5160
|
+
* Read main task envelope fields from `context.params` (CLI/env), with
|
|
5161
|
+
* any matching key on `overrides` taking precedence. Internal; called by
|
|
5162
|
+
* {@link AbstractTask.resolveParams}.
|
|
5163
|
+
*
|
|
5164
|
+
* @param {object} context
|
|
5165
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5166
|
+
* @returns {object}
|
|
5167
|
+
*/
|
|
5168
|
+
static _resolveMainFields(context, overrides = {}) {
|
|
5169
|
+
const defs = {
|
|
5170
|
+
queueName: "string default tasks",
|
|
5171
|
+
priority: "number default 50",
|
|
5172
|
+
serviceGroup: "string",
|
|
5173
|
+
serviceName: "string",
|
|
5174
|
+
instanceNumber: "number",
|
|
5175
|
+
serverName: "string",
|
|
5176
|
+
opid: "string",
|
|
5177
|
+
schedule: "string"
|
|
5178
|
+
};
|
|
5179
|
+
const cli = context.params.getAllForModule("task-envelope", defs);
|
|
5180
|
+
const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
|
|
5181
|
+
if (!name) {
|
|
5182
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
5183
|
+
}
|
|
5184
|
+
let instanceNumber;
|
|
5185
|
+
const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
|
|
5186
|
+
if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
|
|
5187
|
+
const n = Number(rawInstance);
|
|
5188
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
5189
|
+
throw new ParamError("--instanceNumber must be a positive integer when set");
|
|
5190
|
+
}
|
|
5191
|
+
instanceNumber = n;
|
|
5192
|
+
} else {
|
|
5193
|
+
instanceNumber = null;
|
|
5194
|
+
}
|
|
5195
|
+
const priorityRaw = overrides.priority ?? cli.priority ?? 50;
|
|
5196
|
+
const priority = Number(priorityRaw);
|
|
5197
|
+
if (!Number.isFinite(priority)) {
|
|
5198
|
+
throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
|
|
5199
|
+
}
|
|
5200
|
+
return {
|
|
5201
|
+
name,
|
|
5202
|
+
queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
|
|
5203
|
+
priority,
|
|
5204
|
+
serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
|
|
5205
|
+
serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
|
|
5206
|
+
instanceNumber,
|
|
5207
|
+
serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
|
|
5208
|
+
opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
|
|
5209
|
+
schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
|
|
5210
|
+
nextRunAt: overrides.nextRunAt ?? null
|
|
5211
|
+
};
|
|
5212
|
+
}
|
|
5213
|
+
/**
|
|
5214
|
+
* Default inner-params resolver: parses `--paramsJson` (must be a JSON
|
|
5215
|
+
* object), then overlays `overrides.params` on top. Returns `null` when
|
|
5216
|
+
* neither is provided.
|
|
5217
|
+
*
|
|
5218
|
+
* @param {object} context
|
|
5219
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5220
|
+
* @returns {object|null}
|
|
5221
|
+
*/
|
|
5222
|
+
static _defaultParamsBlob(context, overrides = {}) {
|
|
5223
|
+
const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
|
|
5224
|
+
const fromJson = parseParamsJson(cli.paramsJson);
|
|
5225
|
+
const fromOverride = pickParamsObject(overrides);
|
|
5226
|
+
if (!fromJson && !fromOverride) return null;
|
|
5227
|
+
return { ...fromJson ?? {}, ...fromOverride ?? {} };
|
|
5228
|
+
}
|
|
5229
|
+
/**
|
|
5230
|
+
* Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
|
|
5231
|
+
* params (per `defs`) plus `--paramsJson` under a module namespace, then
|
|
5232
|
+
* merges them with explicit `overrides.params` in increasing priority:
|
|
5233
|
+
*
|
|
5234
|
+
* typed CLI flags → --paramsJson → overrides.params
|
|
5235
|
+
*
|
|
5236
|
+
* Undefined values are dropped so defaults declared in `defs` aren't
|
|
5237
|
+
* overwritten by missing-flag noise. Returns the merged object; the
|
|
5238
|
+
* caller is responsible for validation and throwing `ParamError`.
|
|
5239
|
+
*
|
|
5240
|
+
* @param {object} context
|
|
5241
|
+
* @param {string} moduleName Namespace for `--showUsedParams` grouping.
|
|
5242
|
+
* @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
|
|
5243
|
+
* @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
|
|
5244
|
+
* @returns {Record<string, unknown>}
|
|
5245
|
+
*/
|
|
5246
|
+
static _mergeTypedParams(context, moduleName, defs, overrides = {}) {
|
|
5247
|
+
const fullDefs = { ...defs, paramsJson: "string" };
|
|
5248
|
+
const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
|
|
5249
|
+
const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
|
|
5250
|
+
const fromCli = {};
|
|
5251
|
+
for (const [k, v] of Object.entries(cliRaw)) {
|
|
5252
|
+
if (k === "paramsJson") continue;
|
|
5253
|
+
if (v !== void 0 && v !== null) fromCli[k] = v;
|
|
5254
|
+
}
|
|
5255
|
+
const fromOverride = pickParamsObject(overrides) ?? {};
|
|
5256
|
+
return { ...fromCli, ...fromJson, ...fromOverride };
|
|
5257
|
+
}
|
|
4670
5258
|
};
|
|
5259
|
+
function emptyToUndef(s) {
|
|
5260
|
+
if (s === void 0 || s === null) return void 0;
|
|
5261
|
+
if (typeof s !== "string") return s;
|
|
5262
|
+
const t = s.trim();
|
|
5263
|
+
return t.length ? t : void 0;
|
|
5264
|
+
}
|
|
5265
|
+
function parseParamsJson(raw) {
|
|
5266
|
+
if (raw == null) return null;
|
|
5267
|
+
const t = String(raw).trim();
|
|
5268
|
+
if (!t) return null;
|
|
5269
|
+
let parsed;
|
|
5270
|
+
try {
|
|
5271
|
+
parsed = JSON.parse(t);
|
|
5272
|
+
} catch (e) {
|
|
5273
|
+
throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
|
|
5274
|
+
}
|
|
5275
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5276
|
+
throw new ParamError("--paramsJson must be a JSON object");
|
|
5277
|
+
}
|
|
5278
|
+
return parsed;
|
|
5279
|
+
}
|
|
5280
|
+
function pickParamsObject(overrides) {
|
|
5281
|
+
const p = overrides?.params;
|
|
5282
|
+
if (p && typeof p === "object" && !Array.isArray(p)) return p;
|
|
5283
|
+
return void 0;
|
|
5284
|
+
}
|
|
4671
5285
|
|
|
4672
|
-
// src/tasks/coreTasks/TaskPing.
|
|
4673
|
-
var TaskPing = class extends
|
|
5286
|
+
// src/tasks/coreTasks/TaskPing.js
|
|
5287
|
+
var TaskPing = class extends AbstractTask {
|
|
5288
|
+
/** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
|
|
5289
|
+
static defaultWaitForResult = true;
|
|
5290
|
+
/** Ping takes no params. */
|
|
5291
|
+
static async resolveCustomParams() {
|
|
5292
|
+
return null;
|
|
5293
|
+
}
|
|
5294
|
+
/**
|
|
5295
|
+
* @returns {Promise<{ success: true, results: "pong" }>}
|
|
5296
|
+
*/
|
|
4674
5297
|
async run() {
|
|
4675
5298
|
this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
|
|
4676
5299
|
return { success: true, results: "pong" };
|
|
4677
5300
|
}
|
|
4678
5301
|
};
|
|
4679
5302
|
|
|
4680
|
-
// src/tasks/coreTasks/TaskSampleProcess.
|
|
4681
|
-
var TaskSampleProcess = class extends
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
5303
|
+
// src/tasks/coreTasks/TaskSampleProcess.js
|
|
5304
|
+
var TaskSampleProcess = class extends AbstractTask {
|
|
5305
|
+
/**
|
|
5306
|
+
* @param {object} context
|
|
5307
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5308
|
+
* @returns {Promise<{ total: number, delay: number, name?: string }>}
|
|
5309
|
+
*/
|
|
5310
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5311
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
|
|
5312
|
+
total: "number default 10",
|
|
5313
|
+
delay: "number default 1000",
|
|
5314
|
+
name: "string"
|
|
5315
|
+
}, overrides);
|
|
5316
|
+
const total = Number(merged.total);
|
|
5317
|
+
const delay = Number(merged.delay);
|
|
5318
|
+
if (!Number.isInteger(total) || total <= 0) {
|
|
5319
|
+
throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
|
|
5320
|
+
}
|
|
5321
|
+
if (!Number.isInteger(delay) || delay < 0) {
|
|
5322
|
+
throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
|
|
5323
|
+
}
|
|
5324
|
+
const out = { total, delay };
|
|
5325
|
+
if (typeof merged.name === "string" && merged.name.trim()) {
|
|
5326
|
+
out.name = merged.name.trim();
|
|
5327
|
+
}
|
|
5328
|
+
return out;
|
|
5329
|
+
}
|
|
5330
|
+
/**
|
|
5331
|
+
* @param {object} context
|
|
5332
|
+
* @param {object} task
|
|
5333
|
+
*/
|
|
5334
|
+
constructor(context, task) {
|
|
5335
|
+
super(context, task);
|
|
5336
|
+
this.stopRequested = false;
|
|
5337
|
+
this.stopAllowanceMs = 0;
|
|
5338
|
+
this.stopDecisionLogged = false;
|
|
5339
|
+
}
|
|
5340
|
+
/**
|
|
5341
|
+
* Runner-facing stop signal. Records the allowance window so the main loop
|
|
5342
|
+
* can decide per-iteration whether to finish or abort early.
|
|
5343
|
+
*
|
|
5344
|
+
* @param {number} allowanceMs
|
|
5345
|
+
*/
|
|
4685
5346
|
requestStop(allowanceMs) {
|
|
4686
5347
|
this.stopRequested = true;
|
|
4687
5348
|
this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
|
|
@@ -4689,6 +5350,14 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4689
5350
|
`[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
|
|
4690
5351
|
);
|
|
4691
5352
|
}
|
|
5353
|
+
/**
|
|
5354
|
+
* Iterate `total` times, sleeping `delay` ms between ticks and reporting
|
|
5355
|
+
* progress every iteration. Validates params up front; invalid values short-
|
|
5356
|
+
* circuit to a structured failure without starting the loop.
|
|
5357
|
+
*
|
|
5358
|
+
* @param {(progress: object) => Promise<void>} reportProgress
|
|
5359
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5360
|
+
*/
|
|
4692
5361
|
async run(reportProgress) {
|
|
4693
5362
|
const totalRaw = this.task?.params?.total ?? 10;
|
|
4694
5363
|
const delayRaw = this.task?.params?.delay ?? 1e3;
|
|
@@ -4770,7 +5439,7 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4770
5439
|
}
|
|
4771
5440
|
};
|
|
4772
5441
|
|
|
4773
|
-
// src/tasks/coreTasks/TaskShellCommand.
|
|
5442
|
+
// src/tasks/coreTasks/TaskShellCommand.js
|
|
4774
5443
|
var import_node_child_process = require("child_process");
|
|
4775
5444
|
function runShellCommand(command, cwd) {
|
|
4776
5445
|
return new Promise((resolve2, reject) => {
|
|
@@ -4800,7 +5469,27 @@ function runShellCommand(command, cwd) {
|
|
|
4800
5469
|
});
|
|
4801
5470
|
});
|
|
4802
5471
|
}
|
|
4803
|
-
var TaskShellCommand = class extends
|
|
5472
|
+
var TaskShellCommand = class extends AbstractTask {
|
|
5473
|
+
/**
|
|
5474
|
+
* @param {object} context
|
|
5475
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5476
|
+
* @returns {Promise<{ command: string, cwd?: string }>}
|
|
5477
|
+
*/
|
|
5478
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5479
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
|
|
5480
|
+
command: "string",
|
|
5481
|
+
cwd: "string"
|
|
5482
|
+
}, overrides);
|
|
5483
|
+
const command = typeof merged.command === "string" ? merged.command.trim() : "";
|
|
5484
|
+
if (!command) {
|
|
5485
|
+
throw new ParamError('shellCommand: param "command" must be a non-empty string');
|
|
5486
|
+
}
|
|
5487
|
+
const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
|
|
5488
|
+
return cwd ? { command, cwd } : { command };
|
|
5489
|
+
}
|
|
5490
|
+
/**
|
|
5491
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5492
|
+
*/
|
|
4804
5493
|
async run() {
|
|
4805
5494
|
const params = this.task?.params;
|
|
4806
5495
|
const commandRaw = typeof params === "string" ? params : params?.command;
|
|
@@ -4849,9 +5538,9 @@ var TaskShellCommand = class extends TaskMaster {
|
|
|
4849
5538
|
}
|
|
4850
5539
|
};
|
|
4851
5540
|
|
|
4852
|
-
// src/tasks/coreTasks/TaskSystemInfo.
|
|
5541
|
+
// src/tasks/coreTasks/TaskSystemInfo.js
|
|
4853
5542
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
4854
|
-
var
|
|
5543
|
+
var import_promises = __toESM(require("fs/promises"), 1);
|
|
4855
5544
|
function toGb(valueBytes) {
|
|
4856
5545
|
return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
|
|
4857
5546
|
}
|
|
@@ -4859,7 +5548,7 @@ function toMb(valueBytes) {
|
|
|
4859
5548
|
return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
|
|
4860
5549
|
}
|
|
4861
5550
|
async function getDiskStats() {
|
|
4862
|
-
const stats = await
|
|
5551
|
+
const stats = await import_promises.default.statfs("/");
|
|
4863
5552
|
const total = Number(stats.bsize) * Number(stats.blocks);
|
|
4864
5553
|
const free = Number(stats.bsize) * Number(stats.bavail);
|
|
4865
5554
|
const used = total - free;
|
|
@@ -4869,7 +5558,16 @@ async function getDiskStats() {
|
|
|
4869
5558
|
free: toGb(free)
|
|
4870
5559
|
};
|
|
4871
5560
|
}
|
|
4872
|
-
var TaskSystemInfo = class extends
|
|
5561
|
+
var TaskSystemInfo = class extends AbstractTask {
|
|
5562
|
+
/** Same UX expectation as `ping` — short probe, print the result. */
|
|
5563
|
+
static defaultWaitForResult = true;
|
|
5564
|
+
/** systemInfo takes no params. */
|
|
5565
|
+
static async resolveCustomParams() {
|
|
5566
|
+
return null;
|
|
5567
|
+
}
|
|
5568
|
+
/**
|
|
5569
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5570
|
+
*/
|
|
4873
5571
|
async run() {
|
|
4874
5572
|
try {
|
|
4875
5573
|
const totalMemory = import_node_os2.default.totalmem();
|
|
@@ -4921,8 +5619,31 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
4921
5619
|
}
|
|
4922
5620
|
};
|
|
4923
5621
|
|
|
4924
|
-
// src/tasks/coreTasks/TaskSumAB.
|
|
4925
|
-
var TaskSumAB = class extends
|
|
5622
|
+
// src/tasks/coreTasks/TaskSumAB.js
|
|
5623
|
+
var TaskSumAB = class extends AbstractTask {
|
|
5624
|
+
/** Short, deterministic — wait by default so callers see the sum. */
|
|
5625
|
+
static defaultWaitForResult = true;
|
|
5626
|
+
/**
|
|
5627
|
+
* @param {object} context
|
|
5628
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5629
|
+
* @returns {Promise<{ a: number, b: number }>}
|
|
5630
|
+
*/
|
|
5631
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5632
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
|
|
5633
|
+
a: "number",
|
|
5634
|
+
b: "number"
|
|
5635
|
+
}, overrides);
|
|
5636
|
+
if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
|
|
5637
|
+
throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
|
|
5638
|
+
}
|
|
5639
|
+
if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
|
|
5640
|
+
throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
|
|
5641
|
+
}
|
|
5642
|
+
return { a: merged.a, b: merged.b };
|
|
5643
|
+
}
|
|
5644
|
+
/**
|
|
5645
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5646
|
+
*/
|
|
4926
5647
|
async run() {
|
|
4927
5648
|
const a = this.task?.params?.a;
|
|
4928
5649
|
const b = this.task?.params?.b;
|
|
@@ -4953,8 +5674,46 @@ var TaskSumAB = class extends TaskMaster {
|
|
|
4953
5674
|
}
|
|
4954
5675
|
};
|
|
4955
5676
|
|
|
4956
|
-
// src/tasks/coreTasks/TaskStopRunner.
|
|
4957
|
-
var TaskStopRunner = class extends
|
|
5677
|
+
// src/tasks/coreTasks/TaskStopRunner.js
|
|
5678
|
+
var TaskStopRunner = class extends AbstractTask {
|
|
5679
|
+
/**
|
|
5680
|
+
* Stop tasks must target a concrete instance — without `serviceName` the
|
|
5681
|
+
* row would race against any worker on the queue. Layered on top of the
|
|
5682
|
+
* envelope built by {@link AbstractTask.resolveParams}.
|
|
5683
|
+
*
|
|
5684
|
+
* @param {object} context
|
|
5685
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5686
|
+
* @returns {Promise<object>}
|
|
5687
|
+
*/
|
|
5688
|
+
static async resolveParams(context, overrides = {}) {
|
|
5689
|
+
const main = await super.resolveParams(context, overrides);
|
|
5690
|
+
if (!main.serviceName) {
|
|
5691
|
+
throw new ParamError(
|
|
5692
|
+
"stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
|
|
5693
|
+
);
|
|
5694
|
+
}
|
|
5695
|
+
return main;
|
|
5696
|
+
}
|
|
5697
|
+
/**
|
|
5698
|
+
* @param {object} context
|
|
5699
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5700
|
+
* @returns {Promise<{ allowanceMs: number }>}
|
|
5701
|
+
*/
|
|
5702
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5703
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
|
|
5704
|
+
allowanceMs: "number default 5000"
|
|
5705
|
+
}, overrides);
|
|
5706
|
+
const allowanceMs = Number(merged.allowanceMs);
|
|
5707
|
+
if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
|
|
5708
|
+
throw new ParamError(
|
|
5709
|
+
`stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
|
|
5710
|
+
);
|
|
5711
|
+
}
|
|
5712
|
+
return { allowanceMs };
|
|
5713
|
+
}
|
|
5714
|
+
/**
|
|
5715
|
+
* @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
|
|
5716
|
+
*/
|
|
4958
5717
|
async run() {
|
|
4959
5718
|
const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
|
|
4960
5719
|
this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
|
|
@@ -4969,172 +5728,396 @@ var TaskStopRunner = class extends TaskMaster {
|
|
|
4969
5728
|
}
|
|
4970
5729
|
};
|
|
4971
5730
|
|
|
4972
|
-
// src/tasks/
|
|
5731
|
+
// src/tasks/coreTasks/TaskGetLogs.js
|
|
5732
|
+
var TaskGetLogs = class extends AbstractTask {
|
|
5733
|
+
/**
|
|
5734
|
+
* @param {object} context
|
|
5735
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5736
|
+
* @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
|
|
5737
|
+
*/
|
|
5738
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5739
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
|
|
5740
|
+
source: "string",
|
|
5741
|
+
resource: "string",
|
|
5742
|
+
tail: "number default 100",
|
|
5743
|
+
afterTs: "string"
|
|
5744
|
+
}, overrides);
|
|
5745
|
+
const source = typeof merged.source === "string" ? merged.source.trim() : "";
|
|
5746
|
+
const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
|
|
5747
|
+
if (!source) throw new ParamError('getLogs: param "source" is required');
|
|
5748
|
+
if (!resource) throw new ParamError('getLogs: param "resource" is required');
|
|
5749
|
+
let tail = Number(merged.tail);
|
|
5750
|
+
if (!Number.isFinite(tail) || tail < 1) tail = 100;
|
|
5751
|
+
tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
|
|
5752
|
+
const out = { source, resource, tail };
|
|
5753
|
+
if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
|
|
5754
|
+
out.afterTs = merged.afterTs.trim();
|
|
5755
|
+
}
|
|
5756
|
+
return out;
|
|
5757
|
+
}
|
|
5758
|
+
/**
|
|
5759
|
+
* @param {unknown} _reportProgress Unused (single-shot read, no progress events).
|
|
5760
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5761
|
+
*/
|
|
5762
|
+
async run(_reportProgress) {
|
|
5763
|
+
const p = this.task.params ?? {};
|
|
5764
|
+
const source = String(p.source ?? "").trim();
|
|
5765
|
+
const resource = String(p.resource ?? "").trim();
|
|
5766
|
+
const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
|
|
5767
|
+
const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
|
|
5768
|
+
if (!source || !resource) {
|
|
5769
|
+
return {
|
|
5770
|
+
success: false,
|
|
5771
|
+
results: { error: 'getLogs requires params "source" and "resource"' }
|
|
5772
|
+
};
|
|
5773
|
+
}
|
|
5774
|
+
try {
|
|
5775
|
+
const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
|
|
5776
|
+
source,
|
|
5777
|
+
resource,
|
|
5778
|
+
tail,
|
|
5779
|
+
afterTs
|
|
5780
|
+
});
|
|
5781
|
+
return {
|
|
5782
|
+
success: true,
|
|
5783
|
+
results: { records, latestTs, source, resource }
|
|
5784
|
+
};
|
|
5785
|
+
} catch (e) {
|
|
5786
|
+
return {
|
|
5787
|
+
success: false,
|
|
5788
|
+
results: { error: e?.message ?? String(e) }
|
|
5789
|
+
};
|
|
5790
|
+
}
|
|
5791
|
+
}
|
|
5792
|
+
};
|
|
5793
|
+
|
|
5794
|
+
// src/tasks/TasksRegistry.js
|
|
4973
5795
|
var TasksRegistry = class _TasksRegistry {
|
|
4974
|
-
|
|
5796
|
+
/**
|
|
5797
|
+
* @param {Record<string, Function>} [initial] Optional seed entries to copy in.
|
|
5798
|
+
*/
|
|
4975
5799
|
constructor(initial) {
|
|
5800
|
+
this.map = {};
|
|
4976
5801
|
if (initial) {
|
|
4977
5802
|
this.addMany(initial);
|
|
4978
5803
|
}
|
|
4979
5804
|
}
|
|
5805
|
+
/**
|
|
5806
|
+
* Build a registry pre-populated with every core task plus legacy aliases.
|
|
5807
|
+
* Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
|
|
5808
|
+
*
|
|
5809
|
+
* @returns {TasksRegistry}
|
|
5810
|
+
*/
|
|
4980
5811
|
static withCoreTasks() {
|
|
4981
|
-
return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
|
|
5812
|
+
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);
|
|
4982
5813
|
}
|
|
5814
|
+
/**
|
|
5815
|
+
* Register a single task class under a name. Overwrites any previous entry.
|
|
5816
|
+
*
|
|
5817
|
+
* @param {string} taskName
|
|
5818
|
+
* @param {Function} taskClass Subclass of `AbstractTask`.
|
|
5819
|
+
* @returns {this}
|
|
5820
|
+
*/
|
|
4983
5821
|
add(taskName, taskClass) {
|
|
4984
5822
|
this.map[taskName] = taskClass;
|
|
4985
5823
|
return this;
|
|
4986
5824
|
}
|
|
5825
|
+
/**
|
|
5826
|
+
* Bulk-register a name → class map. Later calls override earlier ones.
|
|
5827
|
+
*
|
|
5828
|
+
* @param {Record<string, Function>} entries
|
|
5829
|
+
* @returns {this}
|
|
5830
|
+
*/
|
|
4987
5831
|
addMany(entries) {
|
|
4988
5832
|
for (const [name, klass] of Object.entries(entries)) {
|
|
4989
5833
|
this.add(name, klass);
|
|
4990
5834
|
}
|
|
4991
5835
|
return this;
|
|
4992
5836
|
}
|
|
5837
|
+
/**
|
|
5838
|
+
* Look up a task class by name. Returns `undefined` when the name is unknown;
|
|
5839
|
+
* the runner treats that as "some other worker may handle this" and skips.
|
|
5840
|
+
*
|
|
5841
|
+
* @param {string} taskName
|
|
5842
|
+
* @returns {Function | undefined}
|
|
5843
|
+
*/
|
|
4993
5844
|
get(taskName) {
|
|
4994
5845
|
return this.map[taskName];
|
|
4995
5846
|
}
|
|
5847
|
+
/**
|
|
5848
|
+
* Strict variant of {@link get}: throws {@link ParamError} (with the list
|
|
5849
|
+
* of supported names) when `taskName` is unknown. Use from enqueuer code
|
|
5850
|
+
* paths where an unknown name is a hard CLI/programmer error.
|
|
5851
|
+
*
|
|
5852
|
+
* @param {string} taskName
|
|
5853
|
+
* @returns {Function}
|
|
5854
|
+
*/
|
|
5855
|
+
requireClass(taskName) {
|
|
5856
|
+
const TaskClass = taskName ? this.map[taskName] : void 0;
|
|
5857
|
+
if (!TaskClass) {
|
|
5858
|
+
const supported = this.listSupportedTasks().join(", ") || "(none)";
|
|
5859
|
+
throw new ParamError(
|
|
5860
|
+
`Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
|
|
5861
|
+
);
|
|
5862
|
+
}
|
|
5863
|
+
return TaskClass;
|
|
5864
|
+
}
|
|
5865
|
+
/**
|
|
5866
|
+
* Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
|
|
5867
|
+
* from `overrides` or `context.params`, look up the class, and delegate
|
|
5868
|
+
* to its static {@link AbstractTask.resolveParams} with `name` seeded into
|
|
5869
|
+
* the overrides. The returned object is shaped for {@link enqueueTask}.
|
|
5870
|
+
*
|
|
5871
|
+
* Validation failures (unknown task, missing required custom params, etc.)
|
|
5872
|
+
* surface as {@link ParamError} so the caller aborts cleanly before any
|
|
5873
|
+
* row is inserted.
|
|
5874
|
+
*
|
|
5875
|
+
* @param {object} context
|
|
5876
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5877
|
+
* @returns {Promise<object>}
|
|
5878
|
+
*/
|
|
5879
|
+
async resolveTaskParams(context, overrides = {}) {
|
|
5880
|
+
const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
|
|
5881
|
+
const fromCli = context.params.get("name", "string");
|
|
5882
|
+
const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
|
|
5883
|
+
const name = overrideName || cliName;
|
|
5884
|
+
if (!name) {
|
|
5885
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
5886
|
+
}
|
|
5887
|
+
const TaskClass = this.requireClass(name);
|
|
5888
|
+
return TaskClass.resolveParams(context, { ...overrides, name });
|
|
5889
|
+
}
|
|
5890
|
+
/**
|
|
5891
|
+
* Names of every registered task, sorted alphabetically (useful for CLI output
|
|
5892
|
+
* and allowlist sanity checks).
|
|
5893
|
+
*
|
|
5894
|
+
* @returns {string[]}
|
|
5895
|
+
*/
|
|
4996
5896
|
listSupportedTasks() {
|
|
4997
5897
|
return Object.keys(this.map).sort();
|
|
4998
5898
|
}
|
|
5899
|
+
/**
|
|
5900
|
+
* Shallow copy of the internal map, for handing to `addMany` on another registry
|
|
5901
|
+
* or for serialization.
|
|
5902
|
+
*
|
|
5903
|
+
* @returns {Record<string, Function>}
|
|
5904
|
+
*/
|
|
4999
5905
|
toObject() {
|
|
5000
5906
|
return { ...this.map };
|
|
5001
5907
|
}
|
|
5002
5908
|
};
|
|
5003
5909
|
|
|
5004
|
-
// src/tasks/
|
|
5910
|
+
// src/tasks/serviceTaskAllowlist.js
|
|
5911
|
+
var SERVICE_TASK_NAMES = [
|
|
5912
|
+
"ping",
|
|
5913
|
+
"stop",
|
|
5914
|
+
"stopRunner",
|
|
5915
|
+
"shellCommand",
|
|
5916
|
+
"systemInfo",
|
|
5917
|
+
"info",
|
|
5918
|
+
"getLogs"
|
|
5919
|
+
];
|
|
5920
|
+
function normalizeAllowedTasks(value) {
|
|
5921
|
+
if (!value) return void 0;
|
|
5922
|
+
if (Array.isArray(value)) {
|
|
5923
|
+
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
5924
|
+
return out2.length ? out2 : void 0;
|
|
5925
|
+
}
|
|
5926
|
+
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
5927
|
+
return out.length ? out : void 0;
|
|
5928
|
+
}
|
|
5929
|
+
function mergeAllowedTasksWithServiceTasks(names) {
|
|
5930
|
+
const set = /* @__PURE__ */ new Set([...SERVICE_TASK_NAMES, ...names ?? []]);
|
|
5931
|
+
return Array.from(set).sort();
|
|
5932
|
+
}
|
|
5933
|
+
|
|
5934
|
+
// src/tasks/taskScriptRunner.js
|
|
5005
5935
|
var import_node_child_process2 = require("child_process");
|
|
5936
|
+
var MAX_PROGRESS_TEXT_LEN = 4e3;
|
|
5006
5937
|
function toCliArgs(args = []) {
|
|
5007
5938
|
return args.filter((a) => typeof a === "string" && a.length > 0);
|
|
5008
5939
|
}
|
|
5009
5940
|
function formatChildLogPrefix(task) {
|
|
5010
|
-
return `${task.
|
|
5941
|
+
return `${task.name}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
|
|
5011
5942
|
}
|
|
5012
|
-
|
|
5013
|
-
|
|
5943
|
+
function isProgressPayload(payload) {
|
|
5944
|
+
if (!payload || typeof payload !== "object") return false;
|
|
5945
|
+
if (payload.level !== "progress") return false;
|
|
5946
|
+
const count = Number(payload.count);
|
|
5947
|
+
const total = Number(payload.total);
|
|
5948
|
+
return Number.isFinite(count) && Number.isFinite(total) && total > 0;
|
|
5949
|
+
}
|
|
5950
|
+
function formatProgressText(payload, fallbackPrefix) {
|
|
5951
|
+
const pfx = payload.prefix ? `${payload.prefix} ` : fallbackPrefix ? `${fallbackPrefix} ` : "";
|
|
5952
|
+
const label = typeof payload.message === "string" && payload.message ? `${payload.message} ` : "";
|
|
5953
|
+
return `${pfx}${label}${payload.count}/${payload.total}`;
|
|
5954
|
+
}
|
|
5955
|
+
function forwardChildLogToParent(context, prefix, message) {
|
|
5956
|
+
if (!message || typeof message !== "object") return;
|
|
5957
|
+
const text = typeof message.message === "string" ? message.message : null;
|
|
5958
|
+
if (!text) return;
|
|
5959
|
+
const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
|
|
5960
|
+
const line = `[child:${prefix}] ${text}`;
|
|
5961
|
+
const logger = context.logger;
|
|
5962
|
+
switch (level) {
|
|
5963
|
+
case "error":
|
|
5964
|
+
case "fatal":
|
|
5965
|
+
logger.error?.(line);
|
|
5966
|
+
return;
|
|
5967
|
+
case "warn":
|
|
5968
|
+
case "warning":
|
|
5969
|
+
logger.warn?.(line);
|
|
5970
|
+
return;
|
|
5971
|
+
case "debug":
|
|
5972
|
+
logger.debug?.(line);
|
|
5973
|
+
return;
|
|
5974
|
+
case "info":
|
|
5975
|
+
default:
|
|
5976
|
+
logger.info?.(line);
|
|
5977
|
+
}
|
|
5978
|
+
}
|
|
5979
|
+
function buildNodeArgs(scriptPath, cliArgs) {
|
|
5014
5980
|
const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
|
|
5015
5981
|
const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
}
|
|
5029
|
-
}
|
|
5982
|
+
return hasTsRuntimeInParent ? [...inheritedExecArgs, scriptPath, ...cliArgs] : ["--import", "tsx", scriptPath, ...cliArgs];
|
|
5983
|
+
}
|
|
5984
|
+
function resolveTasksTableName(context) {
|
|
5985
|
+
return context.tasksQueueName || context.params?.get?.("table") || "tasks";
|
|
5986
|
+
}
|
|
5987
|
+
function announceIpcFileLogsTarget(context, options) {
|
|
5988
|
+
if (!options.ipcFileLogs) return;
|
|
5989
|
+
const logsDir = resolveIpcFileLogsDir(context, options.ipcFileLogs);
|
|
5990
|
+
const enabledRaw = context.params?.get?.("tasksLogsEnabled");
|
|
5991
|
+
const logsEnabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
5992
|
+
context.logger.info?.(
|
|
5993
|
+
`[tasks] IPC file logs: ${logsDir}` + (logsEnabled ? "" : " (tasksLogsEnabled=false; not persisted)")
|
|
5030
5994
|
);
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
let
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
|
|
5044
|
-
}).catch((error) => {
|
|
5045
|
-
context.logger.warn?.(
|
|
5046
|
-
`[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
5047
|
-
);
|
|
5048
|
-
});
|
|
5049
|
-
if (options.onProgress) {
|
|
5050
|
-
progressCallbackChain = progressCallbackChain.then(async () => {
|
|
5051
|
-
await options.onProgress?.(text.slice(0, 4e3));
|
|
5052
|
-
}).catch((error) => {
|
|
5053
|
-
context.logger.warn?.(
|
|
5054
|
-
`[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
5055
|
-
);
|
|
5995
|
+
}
|
|
5996
|
+
function createSerializedQueue() {
|
|
5997
|
+
let chain = Promise.resolve();
|
|
5998
|
+
return {
|
|
5999
|
+
push(fn) {
|
|
6000
|
+
chain = chain.then(fn, () => {
|
|
6001
|
+
}).catch(() => {
|
|
6002
|
+
});
|
|
6003
|
+
return chain;
|
|
6004
|
+
},
|
|
6005
|
+
drain() {
|
|
6006
|
+
return chain.catch(() => {
|
|
5056
6007
|
});
|
|
5057
6008
|
}
|
|
5058
6009
|
};
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
6010
|
+
}
|
|
6011
|
+
async function runNodeTaskScript(context, options) {
|
|
6012
|
+
const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
|
|
6013
|
+
const nodeArgs = buildNodeArgs(options.scriptPath, cliArgs);
|
|
6014
|
+
const child = (0, import_node_child_process2.spawn)(process.execPath, nodeArgs, {
|
|
6015
|
+
cwd: options.cwd || process.cwd(),
|
|
6016
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
6017
|
+
env: {
|
|
6018
|
+
...process.env,
|
|
6019
|
+
TASK_ID: options.task.id,
|
|
6020
|
+
TASK_NAME: options.task.name,
|
|
6021
|
+
TASK_OPID: options.task.opid || ""
|
|
6022
|
+
}
|
|
6023
|
+
});
|
|
6024
|
+
announceIpcFileLogsTarget(context, options);
|
|
6025
|
+
const prefix = formatChildLogPrefix(options.task);
|
|
6026
|
+
const tasksTable = resolveTasksTableName(context);
|
|
6027
|
+
const progressQueue = createSerializedQueue();
|
|
6028
|
+
const state = {
|
|
6029
|
+
stdout: "",
|
|
6030
|
+
stderr: "",
|
|
6031
|
+
workerResult: null,
|
|
6032
|
+
hadErrorMessage: false
|
|
6033
|
+
};
|
|
6034
|
+
const writeProgress = (text) => {
|
|
6035
|
+
const trimmed = typeof text === "string" ? text.slice(0, MAX_PROGRESS_TEXT_LEN) : "";
|
|
6036
|
+
if (!trimmed) return;
|
|
6037
|
+
progressQueue.push(async () => {
|
|
6038
|
+
const db = context.db;
|
|
6039
|
+
if (db) {
|
|
6040
|
+
try {
|
|
6041
|
+
await db(tasksTable).where({ id: options.task.id }).update({ progress: trimmed });
|
|
6042
|
+
} catch (error) {
|
|
6043
|
+
context.logger.warn?.(
|
|
6044
|
+
`[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
6045
|
+
);
|
|
6046
|
+
}
|
|
6047
|
+
}
|
|
6048
|
+
if (options.onProgress) {
|
|
6049
|
+
try {
|
|
6050
|
+
await options.onProgress(trimmed);
|
|
6051
|
+
} catch (error) {
|
|
6052
|
+
context.logger.warn?.(
|
|
6053
|
+
`[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
6054
|
+
);
|
|
6055
|
+
}
|
|
6056
|
+
}
|
|
6057
|
+
});
|
|
5072
6058
|
};
|
|
5073
|
-
child.stdout
|
|
6059
|
+
child.stdout?.on("data", (chunk) => {
|
|
5074
6060
|
const text = String(chunk);
|
|
5075
|
-
stdout += text;
|
|
6061
|
+
state.stdout += text;
|
|
5076
6062
|
if (text.trim()) {
|
|
5077
6063
|
context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
5078
|
-
updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
|
|
5079
6064
|
}
|
|
5080
6065
|
});
|
|
5081
|
-
child.stderr
|
|
6066
|
+
child.stderr?.on("data", (chunk) => {
|
|
5082
6067
|
const text = String(chunk);
|
|
5083
|
-
stderr += text;
|
|
6068
|
+
state.stderr += text;
|
|
5084
6069
|
if (text.trim()) {
|
|
5085
6070
|
context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
5086
6071
|
}
|
|
5087
6072
|
});
|
|
5088
6073
|
child.on("message", (message) => {
|
|
5089
6074
|
if (message && typeof message === "object" && "__taskWorkerResult" in message) {
|
|
5090
|
-
workerResult = message.__taskWorkerResult;
|
|
6075
|
+
state.workerResult = message.__taskWorkerResult;
|
|
5091
6076
|
return;
|
|
5092
6077
|
}
|
|
5093
6078
|
if (message && typeof message === "object") {
|
|
5094
6079
|
const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
|
|
5095
6080
|
if (level === "error" || level === "fatal") {
|
|
5096
|
-
hadErrorMessage = true;
|
|
6081
|
+
state.hadErrorMessage = true;
|
|
5097
6082
|
}
|
|
5098
6083
|
}
|
|
5099
|
-
appendTaskIpcLog(context, options.task, message);
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
const countNum = Number(String(message.count).trim());
|
|
5105
|
-
const totalNum = Number(message.total);
|
|
5106
|
-
if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
|
|
5107
|
-
context.logger.progress(message.message || "progress", {
|
|
5108
|
-
prefix: message.prefix || prefix,
|
|
5109
|
-
count: countNum,
|
|
5110
|
-
total: totalNum
|
|
5111
|
-
});
|
|
5112
|
-
} else {
|
|
5113
|
-
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
5114
|
-
}
|
|
5115
|
-
} else {
|
|
5116
|
-
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
5117
|
-
}
|
|
6084
|
+
appendTaskIpcLog(context, options.task, message, options.ipcFileLogs);
|
|
6085
|
+
try {
|
|
6086
|
+
options.onChildIpcMessage?.(message);
|
|
6087
|
+
} catch (e) {
|
|
6088
|
+
context.logger.warn?.(`[tasks] onChildIpcMessage failed: ${e?.message ?? String(e)}`);
|
|
5118
6089
|
}
|
|
6090
|
+
if (isProgressPayload(message)) {
|
|
6091
|
+
context.logger.progress(message.message || "progress", {
|
|
6092
|
+
prefix: message.prefix || prefix,
|
|
6093
|
+
count: Number(message.count),
|
|
6094
|
+
total: Number(message.total)
|
|
6095
|
+
});
|
|
6096
|
+
writeProgress(formatProgressText(message, prefix));
|
|
6097
|
+
return;
|
|
6098
|
+
}
|
|
6099
|
+
forwardChildLogToParent(context, prefix, message);
|
|
5119
6100
|
});
|
|
5120
6101
|
return await new Promise((resolve2, reject) => {
|
|
5121
6102
|
child.on("error", (error) => reject(error));
|
|
5122
6103
|
child.on("close", (exitCode, signal) => {
|
|
5123
|
-
|
|
6104
|
+
void (async () => {
|
|
6105
|
+
await flushTaskIpcLogs(context);
|
|
6106
|
+
await progressQueue.drain();
|
|
5124
6107
|
resolve2({
|
|
5125
6108
|
exitCode,
|
|
5126
6109
|
signal,
|
|
5127
|
-
stdout: stdout.trim(),
|
|
5128
|
-
stderr: stderr.trim(),
|
|
5129
|
-
workerResult,
|
|
5130
|
-
hadErrorMessage
|
|
6110
|
+
stdout: state.stdout.trim(),
|
|
6111
|
+
stderr: state.stderr.trim(),
|
|
6112
|
+
workerResult: state.workerResult,
|
|
6113
|
+
hadErrorMessage: state.hadErrorMessage
|
|
5131
6114
|
});
|
|
5132
|
-
});
|
|
6115
|
+
})();
|
|
5133
6116
|
});
|
|
5134
6117
|
});
|
|
5135
6118
|
}
|
|
5136
6119
|
|
|
5137
|
-
// src/tasks/index.
|
|
6120
|
+
// src/tasks/index.js
|
|
5138
6121
|
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
5139
6122
|
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
5140
6123
|
function getDb3(context) {
|
|
@@ -5149,22 +6132,13 @@ function normalizeRegistry(registry) {
|
|
|
5149
6132
|
if (registry instanceof TasksRegistry) return registry;
|
|
5150
6133
|
return new TasksRegistry().addMany(registry);
|
|
5151
6134
|
}
|
|
5152
|
-
function
|
|
5153
|
-
if (!value) return void 0;
|
|
5154
|
-
if (Array.isArray(value)) {
|
|
5155
|
-
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
5156
|
-
return out2.length ? out2 : void 0;
|
|
5157
|
-
}
|
|
5158
|
-
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
5159
|
-
return out.length ? out : void 0;
|
|
5160
|
-
}
|
|
5161
|
-
async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
|
|
6135
|
+
async function enqueueStopTask(context, serviceGroup, queueName = "tasks", allowanceMs = 5e3) {
|
|
5162
6136
|
return enqueueTask(context, {
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
task: "stopRunner",
|
|
6137
|
+
queueName,
|
|
6138
|
+
name: "stopRunner",
|
|
5166
6139
|
params: { allowanceMs },
|
|
5167
|
-
priority:
|
|
6140
|
+
priority: 0,
|
|
6141
|
+
serviceGroup
|
|
5168
6142
|
});
|
|
5169
6143
|
}
|
|
5170
6144
|
async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
|
|
@@ -5182,18 +6156,20 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
|
|
|
5182
6156
|
}
|
|
5183
6157
|
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
5184
6158
|
const db = getDb3(context);
|
|
5185
|
-
const taskName = row.
|
|
6159
|
+
const taskName = row.name;
|
|
5186
6160
|
const TaskClass = registry.get(taskName);
|
|
5187
|
-
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
5188
6161
|
if (!TaskClass) {
|
|
5189
6162
|
const err = { message: `Unknown task "${taskName}"` };
|
|
5190
|
-
await db(historyTable).insert(
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
6163
|
+
await db(historyTable).insert(
|
|
6164
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
6165
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
6166
|
+
success: false,
|
|
6167
|
+
status: "failed",
|
|
6168
|
+
status_changed_at: db.fn.now(),
|
|
6169
|
+
params: toJsonColumn(row.params),
|
|
6170
|
+
results: toJsonColumn(err)
|
|
6171
|
+
})
|
|
6172
|
+
);
|
|
5197
6173
|
if (row.schedule) {
|
|
5198
6174
|
await db(tasksTable).where({ id: row.id }).update({
|
|
5199
6175
|
started_at: null,
|
|
@@ -5201,7 +6177,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5201
6177
|
success: false,
|
|
5202
6178
|
results: toJsonColumn(err),
|
|
5203
6179
|
past_due: null,
|
|
5204
|
-
|
|
6180
|
+
status: "paused",
|
|
6181
|
+
status_changed_at: db.fn.now(),
|
|
5205
6182
|
progress: LOCKED_BY_ERROR_MESSAGE
|
|
5206
6183
|
});
|
|
5207
6184
|
} else {
|
|
@@ -5228,13 +6205,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5228
6205
|
} finally {
|
|
5229
6206
|
runningTaskInstances.delete(row.id);
|
|
5230
6207
|
}
|
|
5231
|
-
await db(historyTable).insert(
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
6208
|
+
await db(historyTable).insert(
|
|
6209
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
6210
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
6211
|
+
success,
|
|
6212
|
+
status: success ? "completed" : "failed",
|
|
6213
|
+
status_changed_at: db.fn.now(),
|
|
6214
|
+
params: toJsonColumn(row.params),
|
|
6215
|
+
results: toJsonColumn(results)
|
|
6216
|
+
})
|
|
6217
|
+
);
|
|
5238
6218
|
if (!success) {
|
|
5239
6219
|
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
5240
6220
|
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
@@ -5249,19 +6229,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5249
6229
|
const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
|
|
5250
6230
|
appendTaskIpcLog(context, row, {
|
|
5251
6231
|
level: "error",
|
|
5252
|
-
message: `[tasks] task failed: ${row.
|
|
6232
|
+
message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
|
|
5253
6233
|
details: results
|
|
5254
6234
|
});
|
|
5255
6235
|
}
|
|
5256
6236
|
if (row.schedule) {
|
|
5257
6237
|
if (success) {
|
|
6238
|
+
let nextRunAt = null;
|
|
6239
|
+
try {
|
|
6240
|
+
nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
|
|
6241
|
+
} catch (e) {
|
|
6242
|
+
context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
|
|
6243
|
+
}
|
|
5258
6244
|
await db(tasksTable).where({ id: row.id }).update({
|
|
5259
6245
|
started_at: null,
|
|
5260
6246
|
completed_at: /* @__PURE__ */ new Date(),
|
|
5261
6247
|
success,
|
|
5262
6248
|
results: toJsonColumn(results),
|
|
5263
6249
|
progress: null,
|
|
5264
|
-
past_due: null
|
|
6250
|
+
past_due: null,
|
|
6251
|
+
status: "idle",
|
|
6252
|
+
status_changed_at: db.fn.now(),
|
|
6253
|
+
next_run_at: nextRunAt,
|
|
6254
|
+
// Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
|
|
6255
|
+
service_name: null,
|
|
6256
|
+
server_name: null,
|
|
6257
|
+
instance_number: null
|
|
5265
6258
|
});
|
|
5266
6259
|
} else {
|
|
5267
6260
|
await db(tasksTable).where({ id: row.id }).update({
|
|
@@ -5269,7 +6262,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5269
6262
|
completed_at: /* @__PURE__ */ new Date(),
|
|
5270
6263
|
success,
|
|
5271
6264
|
results: toJsonColumn(results),
|
|
5272
|
-
|
|
6265
|
+
status: "paused",
|
|
6266
|
+
status_changed_at: db.fn.now(),
|
|
5273
6267
|
progress: LOCKED_BY_ERROR_MESSAGE,
|
|
5274
6268
|
past_due: null
|
|
5275
6269
|
});
|
|
@@ -5281,58 +6275,93 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5281
6275
|
const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
|
|
5282
6276
|
return { stopRunnerRequested, stopAllowanceMs };
|
|
5283
6277
|
}
|
|
5284
|
-
|
|
6278
|
+
function shuffleTaskRowsInPlace(rows) {
|
|
6279
|
+
for (let i = rows.length - 1; i > 0; i--) {
|
|
6280
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
6281
|
+
const t = rows[i];
|
|
6282
|
+
rows[i] = rows[j];
|
|
6283
|
+
rows[j] = t;
|
|
6284
|
+
}
|
|
6285
|
+
}
|
|
6286
|
+
async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
|
|
5285
6287
|
const db = getDb3(context);
|
|
5286
|
-
let query = db(tasksTable).
|
|
6288
|
+
let query = db(tasksTable).where({ status: "idle" }).where(function() {
|
|
6289
|
+
this.whereNull("service_group").orWhere({ service_group: serviceGroup });
|
|
6290
|
+
}).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);
|
|
5287
6291
|
if (taskNames && taskNames.length > 0) {
|
|
5288
|
-
query = query.whereIn("
|
|
6292
|
+
query = query.whereIn("name", taskNames);
|
|
6293
|
+
}
|
|
6294
|
+
if (runnerIdentity) {
|
|
6295
|
+
query = query.where(function() {
|
|
6296
|
+
this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
|
|
6297
|
+
}).where(function() {
|
|
6298
|
+
this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
|
|
6299
|
+
}).where(function() {
|
|
6300
|
+
this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
|
|
6301
|
+
});
|
|
6302
|
+
} else {
|
|
6303
|
+
query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
|
|
5289
6304
|
}
|
|
5290
6305
|
const candidates = await query;
|
|
6306
|
+
shuffleTaskRowsInPlace(candidates);
|
|
5291
6307
|
for (const row of candidates) {
|
|
5292
6308
|
if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
|
|
5293
6309
|
continue;
|
|
5294
6310
|
}
|
|
5295
|
-
const TaskClass = registry.get(row.
|
|
5296
|
-
if (TaskClass) {
|
|
5297
|
-
|
|
5298
|
-
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
6311
|
+
const TaskClass = registry.get(row.name);
|
|
6312
|
+
if (!TaskClass) {
|
|
6313
|
+
continue;
|
|
6314
|
+
}
|
|
6315
|
+
const taskInstance = new TaskClass(context, row);
|
|
6316
|
+
const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
|
|
6317
|
+
if (reason) {
|
|
6318
|
+
if (!row.past_due) {
|
|
6319
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
6320
|
+
past_due: db.fn.now(),
|
|
6321
|
+
progress: String(reason)
|
|
6322
|
+
});
|
|
5307
6323
|
}
|
|
6324
|
+
continue;
|
|
6325
|
+
}
|
|
6326
|
+
const claimPatch = {
|
|
6327
|
+
started_at: db.fn.now(),
|
|
6328
|
+
status: "running",
|
|
6329
|
+
status_changed_at: db.fn.now()
|
|
6330
|
+
};
|
|
6331
|
+
if (runnerIdentity) {
|
|
6332
|
+
claimPatch.service_name = runnerIdentity.service_name;
|
|
6333
|
+
claimPatch.server_name = runnerIdentity.server_name;
|
|
6334
|
+
claimPatch.instance_number = runnerIdentity.instance_number;
|
|
5308
6335
|
}
|
|
5309
|
-
const updated = await db(tasksTable).where({ id: row.id
|
|
6336
|
+
const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
|
|
5310
6337
|
const claimed = Array.isArray(updated) ? updated[0] : null;
|
|
5311
6338
|
if (claimed) return claimed;
|
|
5312
6339
|
}
|
|
5313
6340
|
return null;
|
|
5314
6341
|
}
|
|
5315
6342
|
async function runTasksLoop(context, options) {
|
|
5316
|
-
const
|
|
6343
|
+
const queueName = options.queueName ?? "tasks";
|
|
5317
6344
|
const target = options.target;
|
|
5318
6345
|
const pollMs = options.pollMs ?? 1e3;
|
|
5319
|
-
const
|
|
6346
|
+
const claimJitterMs = options.claimJitterMs ?? 0;
|
|
6347
|
+
const maxParallel = options.maxParallel ?? 32;
|
|
5320
6348
|
const scanLimit = options.scanLimit ?? 100;
|
|
5321
6349
|
const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5322
6350
|
const registry = normalizeRegistry(options.registry);
|
|
5323
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
6351
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
5324
6352
|
if (!target) throw new Error("runTasksLoop: target is required");
|
|
6353
|
+
context.tasksQueueName = queueName;
|
|
5325
6354
|
const runningPromises = /* @__PURE__ */ new Set();
|
|
5326
6355
|
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
5327
6356
|
let runningStopControlPromise = null;
|
|
5328
6357
|
let stopRequested = false;
|
|
5329
6358
|
let stopAllowanceMs = 5e3;
|
|
5330
|
-
context.
|
|
6359
|
+
context.tasksRunnerStop = false;
|
|
5331
6360
|
let registryReg = null;
|
|
5332
6361
|
let registryInterval = null;
|
|
6362
|
+
let runnerIdentity = null;
|
|
5333
6363
|
const hbGroup = options.runnerServiceGroup?.trim();
|
|
5334
6364
|
if (hbGroup) {
|
|
5335
|
-
const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
|
|
5336
6365
|
const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
|
|
5337
6366
|
const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
|
|
5338
6367
|
const defaultMeta = {
|
|
@@ -5340,16 +6369,21 @@ async function runTasksLoop(context, options) {
|
|
|
5340
6369
|
allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
|
|
5341
6370
|
};
|
|
5342
6371
|
registryReg = await registerInServicesRegistry(context, {
|
|
5343
|
-
|
|
6372
|
+
queueName,
|
|
5344
6373
|
target,
|
|
5345
6374
|
serviceGroup: hbGroup,
|
|
5346
6375
|
serviceName: options.runnerServiceName,
|
|
5347
|
-
|
|
6376
|
+
instanceNumber: options.runnerInstanceNumber,
|
|
5348
6377
|
staleMs,
|
|
5349
6378
|
groupMaxInstances: options.runnerGroupMaxInstances,
|
|
5350
6379
|
enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
|
|
5351
6380
|
metadata: options.runnerMetadata ?? defaultMeta
|
|
5352
6381
|
});
|
|
6382
|
+
runnerIdentity = {
|
|
6383
|
+
service_name: registryReg.serviceName,
|
|
6384
|
+
server_name: import_node_os3.default.hostname(),
|
|
6385
|
+
instance_number: registryReg.instanceNumber
|
|
6386
|
+
};
|
|
5353
6387
|
registryInterval = setInterval(() => {
|
|
5354
6388
|
void touchServicesRegistry(context, registryReg).catch((err) => {
|
|
5355
6389
|
context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
|
|
@@ -5357,7 +6391,7 @@ async function runTasksLoop(context, options) {
|
|
|
5357
6391
|
}, hbIntervalMs);
|
|
5358
6392
|
}
|
|
5359
6393
|
try {
|
|
5360
|
-
while (!context.isStop() && !stopRequested &&
|
|
6394
|
+
while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
|
|
5361
6395
|
if (!runningStopControlPromise) {
|
|
5362
6396
|
const claimedStopTask = await claimNextRunnableTask(
|
|
5363
6397
|
context,
|
|
@@ -5365,7 +6399,8 @@ async function runTasksLoop(context, options) {
|
|
|
5365
6399
|
target,
|
|
5366
6400
|
registry,
|
|
5367
6401
|
10,
|
|
5368
|
-
["stopRunner", "stop"]
|
|
6402
|
+
["stopRunner", "stop"],
|
|
6403
|
+
runnerIdentity
|
|
5369
6404
|
);
|
|
5370
6405
|
if (claimedStopTask) {
|
|
5371
6406
|
runningStopControlPromise = executeClaimedTask(
|
|
@@ -5379,7 +6414,7 @@ async function runTasksLoop(context, options) {
|
|
|
5379
6414
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5380
6415
|
stopRequested = true;
|
|
5381
6416
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5382
|
-
context.
|
|
6417
|
+
context.tasksRunnerStop = true;
|
|
5383
6418
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5384
6419
|
}
|
|
5385
6420
|
}).finally(() => {
|
|
@@ -5387,6 +6422,9 @@ async function runTasksLoop(context, options) {
|
|
|
5387
6422
|
});
|
|
5388
6423
|
}
|
|
5389
6424
|
}
|
|
6425
|
+
if (claimJitterMs > 0) {
|
|
6426
|
+
await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
|
|
6427
|
+
}
|
|
5390
6428
|
while (runningPromises.size < maxParallel) {
|
|
5391
6429
|
const claimed = await claimNextRunnableTask(
|
|
5392
6430
|
context,
|
|
@@ -5394,14 +6432,15 @@ async function runTasksLoop(context, options) {
|
|
|
5394
6432
|
target,
|
|
5395
6433
|
registry,
|
|
5396
6434
|
scanLimit,
|
|
5397
|
-
allowedTasks
|
|
6435
|
+
allowedTasks,
|
|
6436
|
+
runnerIdentity
|
|
5398
6437
|
);
|
|
5399
6438
|
if (!claimed) break;
|
|
5400
6439
|
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
5401
6440
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5402
6441
|
stopRequested = true;
|
|
5403
6442
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5404
|
-
context.
|
|
6443
|
+
context.tasksRunnerStop = true;
|
|
5405
6444
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5406
6445
|
}
|
|
5407
6446
|
}).finally(() => {
|
|
@@ -5409,7 +6448,16 @@ async function runTasksLoop(context, options) {
|
|
|
5409
6448
|
});
|
|
5410
6449
|
runningPromises.add(p);
|
|
5411
6450
|
}
|
|
5412
|
-
|
|
6451
|
+
const wakePromises = [...runningPromises];
|
|
6452
|
+
if (runningStopControlPromise) {
|
|
6453
|
+
wakePromises.push(runningStopControlPromise);
|
|
6454
|
+
}
|
|
6455
|
+
if (wakePromises.length === 0) {
|
|
6456
|
+
await sleepMs(pollMs);
|
|
6457
|
+
} else {
|
|
6458
|
+
const safe = wakePromises.map((p) => p.catch(() => void 0));
|
|
6459
|
+
await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
|
|
6460
|
+
}
|
|
5413
6461
|
}
|
|
5414
6462
|
if (context.isStop() && !stopRequested) {
|
|
5415
6463
|
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
@@ -5445,89 +6493,130 @@ async function runTasksLoop(context, options) {
|
|
|
5445
6493
|
}
|
|
5446
6494
|
async function waitForTaskResult(context, taskId, options = {}) {
|
|
5447
6495
|
const db = getDb3(context);
|
|
5448
|
-
const
|
|
6496
|
+
const queueName = options.queueName ?? "tasks";
|
|
5449
6497
|
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
5450
6498
|
const pollMs = options.pollMs ?? 500;
|
|
5451
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
6499
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
5452
6500
|
const deadline = Date.now() + timeoutMs;
|
|
6501
|
+
const waitStartedAt = /* @__PURE__ */ new Date();
|
|
6502
|
+
let cachedNameOpid = null;
|
|
6503
|
+
async function historySinceWait(name, opid) {
|
|
6504
|
+
let q = db(historyTable).where({ name }).where("completed_at", ">=", waitStartedAt);
|
|
6505
|
+
if (opid == null || opid === "") {
|
|
6506
|
+
q = q.whereNull("opid");
|
|
6507
|
+
} else {
|
|
6508
|
+
q = q.where({ opid });
|
|
6509
|
+
}
|
|
6510
|
+
return await q.orderBy("completed_at", "desc").first();
|
|
6511
|
+
}
|
|
5453
6512
|
while (Date.now() <= deadline) {
|
|
5454
|
-
const
|
|
5455
|
-
if (
|
|
6513
|
+
const legacy = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
|
|
6514
|
+
if (legacy) {
|
|
6515
|
+
return legacy;
|
|
6516
|
+
}
|
|
5456
6517
|
const pending = await db(tasksTable).where({ id: taskId }).first();
|
|
5457
|
-
if (
|
|
5458
|
-
|
|
5459
|
-
|
|
6518
|
+
if (pending) {
|
|
6519
|
+
cachedNameOpid = { name: pending.name, opid: pending.opid };
|
|
6520
|
+
const done = await historySinceWait(pending.name, pending.opid);
|
|
6521
|
+
if (done) {
|
|
6522
|
+
return done;
|
|
6523
|
+
}
|
|
6524
|
+
} else if (cachedNameOpid) {
|
|
6525
|
+
const done = await historySinceWait(cachedNameOpid.name, cachedNameOpid.opid);
|
|
6526
|
+
if (done) {
|
|
6527
|
+
return done;
|
|
6528
|
+
}
|
|
6529
|
+
return null;
|
|
6530
|
+
} else {
|
|
6531
|
+
return null;
|
|
5460
6532
|
}
|
|
5461
6533
|
await sleepMs(pollMs);
|
|
5462
6534
|
}
|
|
5463
6535
|
return null;
|
|
5464
6536
|
}
|
|
5465
6537
|
var TasksManager = class _TasksManager {
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
6538
|
+
/**
|
|
6539
|
+
* @param {object} context
|
|
6540
|
+
* @param {{
|
|
6541
|
+
* queueName?: string,
|
|
6542
|
+
* target?: string,
|
|
6543
|
+
* recreateTaskTables?: boolean,
|
|
6544
|
+
* pollMs?: number,
|
|
6545
|
+
* claimJitterMs?: number,
|
|
6546
|
+
* maxParallel?: number,
|
|
6547
|
+
* scanLimit?: number,
|
|
6548
|
+
* allowedTasks?: string | string[],
|
|
6549
|
+
* registry?: TasksRegistry | Record<string, Function>,
|
|
6550
|
+
* runnerServiceGroup?: string,
|
|
6551
|
+
* runnerServiceName?: string,
|
|
6552
|
+
* runnerInstanceNumber?: number,
|
|
6553
|
+
* runnerHeartbeatIntervalMs?: number,
|
|
6554
|
+
* runnerHeartbeatStaleMs?: number,
|
|
6555
|
+
* runnerGroupMaxInstances?: number,
|
|
6556
|
+
* runnerEnforceMaxInstances?: boolean,
|
|
6557
|
+
* runnerMetadata?: Record<string, unknown>,
|
|
6558
|
+
* }} [options]
|
|
6559
|
+
*/
|
|
5483
6560
|
constructor(context, options = {}) {
|
|
5484
6561
|
this.context = context;
|
|
5485
|
-
this.
|
|
6562
|
+
this.queueName = options.queueName ?? "tasks";
|
|
5486
6563
|
this.target = options.target ?? "localRunner";
|
|
5487
6564
|
this.recreateTaskTables = options.recreateTaskTables ?? false;
|
|
5488
6565
|
this.pollMs = options.pollMs ?? 1e3;
|
|
6566
|
+
this.claimJitterMs = options.claimJitterMs ?? 0;
|
|
5489
6567
|
this.maxParallel = options.maxParallel ?? 1;
|
|
5490
6568
|
this.scanLimit = options.scanLimit ?? 100;
|
|
5491
6569
|
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5492
6570
|
this.registry = normalizeRegistry(options.registry);
|
|
5493
6571
|
this.runnerServiceGroup = options.runnerServiceGroup;
|
|
5494
6572
|
this.runnerServiceName = options.runnerServiceName;
|
|
5495
|
-
this.
|
|
6573
|
+
this.runnerInstanceNumber = options.runnerInstanceNumber;
|
|
5496
6574
|
this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
|
|
5497
6575
|
this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
|
|
5498
6576
|
this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
|
|
5499
6577
|
this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
|
|
5500
6578
|
this.runnerMetadata = options.runnerMetadata;
|
|
5501
6579
|
}
|
|
6580
|
+
/**
|
|
6581
|
+
* Preferred factory: reads defaults from `context.params` (module namespace
|
|
6582
|
+
* `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
|
|
6583
|
+
* and inline options in one consistent resolver.
|
|
6584
|
+
*
|
|
6585
|
+
* @param {object} context
|
|
6586
|
+
* @param {ConstructorParameters<typeof TasksManager>[1]} [options]
|
|
6587
|
+
* @returns {TasksManager}
|
|
6588
|
+
*/
|
|
5502
6589
|
static init(context, options = {}) {
|
|
5503
6590
|
const defs = {
|
|
5504
6591
|
table: "string default tasks",
|
|
5505
6592
|
target: "string default localRunner",
|
|
5506
6593
|
recreateTaskTables: "boolean default false",
|
|
5507
6594
|
pollMs: "number default 1000",
|
|
6595
|
+
claimJitterMs: "number default 0",
|
|
5508
6596
|
maxParallel: "number default 1",
|
|
5509
6597
|
scanLimit: "number default 100",
|
|
5510
6598
|
allowedTasks: "string",
|
|
5511
6599
|
runnerServiceGroup: "string",
|
|
5512
6600
|
runnerServiceName: "string",
|
|
5513
|
-
|
|
6601
|
+
runnerInstanceNumber: "number",
|
|
5514
6602
|
runnerHeartbeatIntervalMs: "number default 10000",
|
|
5515
6603
|
runnerHeartbeatStaleMs: "number default 45000",
|
|
5516
6604
|
runnerGroupMaxInstances: "number",
|
|
5517
6605
|
runnerEnforceMaxInstances: "boolean default true"
|
|
5518
6606
|
};
|
|
5519
|
-
const discovered = context.params.getAllForModule(defs);
|
|
6607
|
+
const discovered = context.params.getAllForModule("tasks", defs);
|
|
5520
6608
|
const resolved = {
|
|
5521
|
-
|
|
6609
|
+
queueName: discovered.table,
|
|
5522
6610
|
target: discovered.target,
|
|
5523
6611
|
recreateTaskTables: discovered.recreateTaskTables,
|
|
5524
6612
|
pollMs: discovered.pollMs,
|
|
6613
|
+
claimJitterMs: discovered.claimJitterMs,
|
|
5525
6614
|
maxParallel: discovered.maxParallel,
|
|
5526
6615
|
scanLimit: discovered.scanLimit,
|
|
5527
6616
|
allowedTasks: discovered.allowedTasks,
|
|
5528
6617
|
runnerServiceGroup: discovered.runnerServiceGroup,
|
|
5529
6618
|
runnerServiceName: discovered.runnerServiceName,
|
|
5530
|
-
|
|
6619
|
+
runnerInstanceNumber: discovered.runnerInstanceNumber,
|
|
5531
6620
|
runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
|
|
5532
6621
|
runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
|
|
5533
6622
|
runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
|
|
@@ -5536,24 +6625,39 @@ var TasksManager = class _TasksManager {
|
|
|
5536
6625
|
};
|
|
5537
6626
|
return new _TasksManager(context, resolved);
|
|
5538
6627
|
}
|
|
6628
|
+
/**
|
|
6629
|
+
* Idempotently ensure the three backing tables exist for this queue.
|
|
6630
|
+
*
|
|
6631
|
+
* @param {{ recreate?: boolean }} [options]
|
|
6632
|
+
* @returns {Promise<void>}
|
|
6633
|
+
*/
|
|
5539
6634
|
async ensureTaskTables(options = {}) {
|
|
5540
6635
|
await ensureTaskTables(this.context, {
|
|
5541
|
-
|
|
6636
|
+
queueName: this.queueName,
|
|
5542
6637
|
recreate: options.recreate ?? this.recreateTaskTables
|
|
5543
6638
|
});
|
|
5544
6639
|
}
|
|
6640
|
+
/**
|
|
6641
|
+
* Start the runner loop using this manager's resolved config. Per-call
|
|
6642
|
+
* options override the stored defaults, but `runnerMetadata` still falls
|
|
6643
|
+
* through when omitted.
|
|
6644
|
+
*
|
|
6645
|
+
* @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
|
|
6646
|
+
* @returns {Promise<void>}
|
|
6647
|
+
*/
|
|
5545
6648
|
async runTasksLoop(options = {}) {
|
|
5546
6649
|
await runTasksLoop(this.context, {
|
|
5547
|
-
|
|
6650
|
+
queueName: options.queueName ?? this.queueName,
|
|
5548
6651
|
target: options.target ?? this.target,
|
|
5549
6652
|
pollMs: options.pollMs ?? this.pollMs,
|
|
6653
|
+
claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
|
|
5550
6654
|
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
5551
6655
|
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
5552
6656
|
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
5553
6657
|
registry: options.registry ?? this.registry,
|
|
5554
6658
|
runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
|
|
5555
6659
|
runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
|
|
5556
|
-
|
|
6660
|
+
runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
|
|
5557
6661
|
runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
|
|
5558
6662
|
runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
|
|
5559
6663
|
runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
|
|
@@ -5564,6 +6668,7 @@ var TasksManager = class _TasksManager {
|
|
|
5564
6668
|
};
|
|
5565
6669
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5566
6670
|
0 && (module.exports = {
|
|
6671
|
+
AbstractTask,
|
|
5567
6672
|
Args,
|
|
5568
6673
|
Box,
|
|
5569
6674
|
Db,
|
|
@@ -5579,13 +6684,15 @@ var TasksManager = class _TasksManager {
|
|
|
5579
6684
|
MultiColumnListWithPreviewComponent,
|
|
5580
6685
|
Params,
|
|
5581
6686
|
React,
|
|
6687
|
+
S3,
|
|
6688
|
+
SERVICE_TASK_NAMES,
|
|
5582
6689
|
ScreenBody,
|
|
5583
6690
|
ScreenContainer,
|
|
5584
6691
|
ScreenDivider,
|
|
5585
6692
|
ScreenFooter,
|
|
5586
6693
|
ScreenRow,
|
|
5587
6694
|
ScreenTitle,
|
|
5588
|
-
|
|
6695
|
+
TaskGetLogs,
|
|
5589
6696
|
TaskPing,
|
|
5590
6697
|
TaskSampleProcess,
|
|
5591
6698
|
TaskShellCommand,
|
|
@@ -5600,17 +6707,17 @@ var TasksManager = class _TasksManager {
|
|
|
5600
6707
|
buildBreadcrumb,
|
|
5601
6708
|
buildDetailBreadcrumb,
|
|
5602
6709
|
buildFooter,
|
|
5603
|
-
|
|
5604
|
-
dbFindAndConnect,
|
|
5605
|
-
dbInit,
|
|
6710
|
+
convertPattern,
|
|
5606
6711
|
defaultFileSynopsisFunction,
|
|
5607
6712
|
defaultTasksRegistry,
|
|
5608
6713
|
defaultVersionSynopsisFunction,
|
|
5609
6714
|
enqueueStopTask,
|
|
5610
6715
|
enqueueTask,
|
|
5611
6716
|
ensureTaskTables,
|
|
6717
|
+
flushTaskIpcLogs,
|
|
5612
6718
|
getArgsInstance,
|
|
5613
6719
|
h,
|
|
6720
|
+
ipcFileLogsTableNameForSourceResource,
|
|
5614
6721
|
joiEdateType,
|
|
5615
6722
|
joiStringArrayType,
|
|
5616
6723
|
listAliveRunnerHeartbeats,
|
|
@@ -5618,14 +6725,22 @@ var TasksManager = class _TasksManager {
|
|
|
5618
6725
|
listSources,
|
|
5619
6726
|
listTables,
|
|
5620
6727
|
load,
|
|
6728
|
+
matchesParsedPattern,
|
|
6729
|
+
memo,
|
|
6730
|
+
mergeAllowedTasksWithServiceTasks,
|
|
6731
|
+
nextTimeMatch,
|
|
6732
|
+
normalizeAllowedTasks,
|
|
5621
6733
|
organizeFooterMessages,
|
|
5622
6734
|
queueToTableNames,
|
|
6735
|
+
readTaskIpcLogsSnapshot,
|
|
5623
6736
|
registerInServicesRegistry,
|
|
5624
6737
|
registerRunnerHeartbeat,
|
|
6738
|
+
resolveAsterisks,
|
|
6739
|
+
resolveIpcFileLogsDir,
|
|
6740
|
+
resolveRanges,
|
|
6741
|
+
resolveSteps,
|
|
5625
6742
|
runNodeTaskScript,
|
|
5626
6743
|
runTasksLoop,
|
|
5627
|
-
runnerHeartbeatsTable,
|
|
5628
|
-
servicesRegistryTable,
|
|
5629
6744
|
setupContext,
|
|
5630
6745
|
showListScreen,
|
|
5631
6746
|
showMenuScreen,
|
|
@@ -5633,6 +6748,8 @@ var TasksManager = class _TasksManager {
|
|
|
5633
6748
|
showMultiColumnListWithPreviewScreen,
|
|
5634
6749
|
showScreen,
|
|
5635
6750
|
showWordGridScreen,
|
|
6751
|
+
taskHistoryInsertFromQueueRow,
|
|
6752
|
+
timeMatcher,
|
|
5636
6753
|
touchRunnerHeartbeat,
|
|
5637
6754
|
touchServicesRegistry,
|
|
5638
6755
|
unregisterRunnerHeartbeat,
|
|
@@ -5642,6 +6759,7 @@ var TasksManager = class _TasksManager {
|
|
|
5642
6759
|
useCallback,
|
|
5643
6760
|
useEffect,
|
|
5644
6761
|
useInput,
|
|
6762
|
+
useLayoutEffect,
|
|
5645
6763
|
useMemo,
|
|
5646
6764
|
useRef,
|
|
5647
6765
|
useState,
|