@nmakarov/cli-toolkit 0.18.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- 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 +1493 -516
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +1509 -531
- package/dist/cli-runner.js.map +1 -1
- package/dist/db.cjs +85 -157
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +84 -150
- 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 -33
- package/dist/http-client2.cjs.map +1 -1
- package/dist/http-client2.js +34 -30
- package/dist/http-client2.js.map +1 -1
- package/dist/index.cjs +2063 -658
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2063 -663
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +97 -69
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +112 -83
- 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 +22 -10
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +22 -7
- 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 +1640 -416
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +1614 -412
- 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 +36 -44
- package/scripts/ssm/parse-cli.js +35 -0
- package/scripts/ssm/ssm-admin.js +151 -0
- package/scripts/ssm/ssm-pull.js +147 -0
package/dist/cli-runner.cjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";
|
|
3
2
|
var __create = Object.create;
|
|
4
3
|
var __defProp = Object.defineProperty;
|
|
5
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -30,7 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
29
|
mod
|
|
31
30
|
));
|
|
32
31
|
|
|
33
|
-
// src/screen/components.
|
|
32
|
+
// src/screen/components.js
|
|
34
33
|
function getScreenWidth(maxWidth = null) {
|
|
35
34
|
const terminalWidth = process.stdout.columns || 80;
|
|
36
35
|
const availableWidth = Math.max(20, terminalWidth - 4);
|
|
@@ -104,14 +103,13 @@ function ScreenFooter({ lines, textStyle }) {
|
|
|
104
103
|
}
|
|
105
104
|
var import_react, import_ink;
|
|
106
105
|
var init_components = __esm({
|
|
107
|
-
"src/screen/components.
|
|
108
|
-
"use strict";
|
|
106
|
+
"src/screen/components.js"() {
|
|
109
107
|
import_react = require("react");
|
|
110
108
|
import_ink = require("ink");
|
|
111
109
|
}
|
|
112
110
|
});
|
|
113
111
|
|
|
114
|
-
// src/screen/list-components.
|
|
112
|
+
// src/screen/list-components.js
|
|
115
113
|
function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
|
|
116
114
|
const [, forceUpdate] = (0, import_react2.useState)({});
|
|
117
115
|
const termWidth = (process.stdout.columns || 80) - 8;
|
|
@@ -263,11 +261,13 @@ function MultiColumnListWithPreviewComponent({
|
|
|
263
261
|
...previewRows
|
|
264
262
|
);
|
|
265
263
|
}
|
|
266
|
-
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 }) {
|
|
267
265
|
const [, forceUpdate] = (0, import_react2.useState)({});
|
|
268
266
|
const [sortOrder, setSortOrder] = (0, import_react2.useState)("none");
|
|
269
267
|
const [scrollOffset, setScrollOffset] = (0, import_react2.useState)(0);
|
|
270
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;
|
|
271
271
|
const defaultGetTitle = (item) => {
|
|
272
272
|
return getTitle ? getTitle(item) : typeof item.value === "string" ? item.value : item.value?.title || item.name;
|
|
273
273
|
};
|
|
@@ -281,18 +281,24 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
281
281
|
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
282
282
|
}
|
|
283
283
|
}) : items;
|
|
284
|
+
const displayItemsRef = (0, import_react2.useRef)(displayItems);
|
|
285
|
+
displayItemsRef.current = displayItems;
|
|
284
286
|
const effectiveMaxHeight = maxHeight || displayItems.length;
|
|
285
|
-
const
|
|
287
|
+
const _canScroll = displayItems.length > effectiveMaxHeight;
|
|
286
288
|
const maxScrollOffset = Math.max(0, displayItems.length - effectiveMaxHeight);
|
|
287
289
|
const clampedScrollOffset = Math.min(Math.max(0, scrollOffset), maxScrollOffset);
|
|
288
290
|
const visibleItems = displayItems.slice(clampedScrollOffset, clampedScrollOffset + effectiveMaxHeight);
|
|
289
291
|
const canScrollUp = clampedScrollOffset > 0;
|
|
290
292
|
const canScrollDown = clampedScrollOffset < maxScrollOffset;
|
|
291
293
|
scrollStateRef.current = { scrollOffset, maxHeight: effectiveMaxHeight, totalItems: displayItems.length };
|
|
294
|
+
const onSelectionChangeRef = (0, import_react2.useRef)(onSelectionChange);
|
|
295
|
+
onSelectionChangeRef.current = onSelectionChange;
|
|
292
296
|
(0, import_react2.useEffect)(() => {
|
|
293
297
|
ctx.setAction("moveUp", () => {
|
|
294
298
|
const newIndex = Math.max(0, selectedIndexRef.current - 1);
|
|
295
299
|
selectedIndexRef.current = newIndex;
|
|
300
|
+
const list = displayItemsRef.current;
|
|
301
|
+
onSelectionChangeRef.current?.(newIndex, list[newIndex]);
|
|
296
302
|
const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
|
|
297
303
|
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
298
304
|
const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
|
|
@@ -302,18 +308,11 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
302
308
|
forceUpdate({});
|
|
303
309
|
});
|
|
304
310
|
ctx.setAction("moveDown", () => {
|
|
305
|
-
const currentItems =
|
|
306
|
-
|
|
307
|
-
const titleB = titleGetter(b).toLowerCase();
|
|
308
|
-
if (sortOrder === "asc") {
|
|
309
|
-
return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
|
|
310
|
-
} else {
|
|
311
|
-
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
312
|
-
}
|
|
313
|
-
}) : items;
|
|
314
|
-
const maxIndex = currentItems.length - 1;
|
|
311
|
+
const currentItems = displayItemsRef.current;
|
|
312
|
+
const maxIndex = Math.max(0, currentItems.length - 1);
|
|
315
313
|
const newIndex = Math.min(maxIndex, selectedIndexRef.current + 1);
|
|
316
314
|
selectedIndexRef.current = newIndex;
|
|
315
|
+
onSelectionChangeRef.current?.(newIndex, currentItems[newIndex]);
|
|
317
316
|
const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
|
|
318
317
|
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
319
318
|
const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
|
|
@@ -323,8 +322,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
323
322
|
forceUpdate({});
|
|
324
323
|
});
|
|
325
324
|
ctx.setAction("scrollUp", () => {
|
|
326
|
-
const { scrollOffset: currentScrollOffset
|
|
327
|
-
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
325
|
+
const { scrollOffset: currentScrollOffset } = scrollStateRef.current;
|
|
328
326
|
const newScrollOffset = Math.max(0, currentScrollOffset - 1);
|
|
329
327
|
setScrollOffset(newScrollOffset);
|
|
330
328
|
forceUpdate({});
|
|
@@ -339,9 +337,9 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
339
337
|
if (sortable) {
|
|
340
338
|
ctx.setAction("toggleSort", () => {
|
|
341
339
|
const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
|
|
342
|
-
const currentSelectedItem =
|
|
340
|
+
const currentSelectedItem = displayItemsRef.current[selectedIndexRef.current];
|
|
343
341
|
setSortOrder(nextSort);
|
|
344
|
-
const newSortedItems = nextSort !== "none" ? [...
|
|
342
|
+
const newSortedItems = nextSort !== "none" ? [...itemsRef.current].sort((a, b) => {
|
|
345
343
|
const titleA = titleGetter(a).toLowerCase();
|
|
346
344
|
const titleB = titleGetter(b).toLowerCase();
|
|
347
345
|
if (nextSort === "asc") {
|
|
@@ -349,7 +347,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
349
347
|
} else {
|
|
350
348
|
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
351
349
|
}
|
|
352
|
-
}) :
|
|
350
|
+
}) : itemsRef.current;
|
|
353
351
|
const newIndex = newSortedItems.findIndex((item) => item === currentSelectedItem);
|
|
354
352
|
if (newIndex !== -1) {
|
|
355
353
|
selectedIndexRef.current = newIndex;
|
|
@@ -487,8 +485,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
487
485
|
}
|
|
488
486
|
var import_react2, import_ink2, h2;
|
|
489
487
|
var init_list_components = __esm({
|
|
490
|
-
"src/screen/list-components.
|
|
491
|
-
"use strict";
|
|
488
|
+
"src/screen/list-components.js"() {
|
|
492
489
|
import_react2 = __toESM(require("react"), 1);
|
|
493
490
|
import_ink2 = require("ink");
|
|
494
491
|
init_components();
|
|
@@ -496,7 +493,7 @@ var init_list_components = __esm({
|
|
|
496
493
|
}
|
|
497
494
|
});
|
|
498
495
|
|
|
499
|
-
// src/screen/screens.
|
|
496
|
+
// src/screen/screens.js
|
|
500
497
|
function groupKeyBindings(bindings) {
|
|
501
498
|
const groups = {};
|
|
502
499
|
const enabledBindings = bindings.filter((b) => b.enabled !== false);
|
|
@@ -572,7 +569,7 @@ async function showScreen(config2) {
|
|
|
572
569
|
let renderResult = null;
|
|
573
570
|
let initialized = false;
|
|
574
571
|
const Screen = () => {
|
|
575
|
-
const [
|
|
572
|
+
const [, setUpdateCounter] = (0, import_react3.useState)(0);
|
|
576
573
|
if (!initialized) {
|
|
577
574
|
const defaultBindings = [
|
|
578
575
|
{ key: "escape", caption: "go back", action: "back", protected: true, order: 1 },
|
|
@@ -691,7 +688,7 @@ async function showScreen(config2) {
|
|
|
691
688
|
}
|
|
692
689
|
}
|
|
693
690
|
if (matchedBinding && actions[matchedBinding.action]) {
|
|
694
|
-
|
|
691
|
+
actions[matchedBinding.action]({
|
|
695
692
|
input,
|
|
696
693
|
key,
|
|
697
694
|
binding: matchedBinding
|
|
@@ -821,8 +818,7 @@ async function showMultiColumnListWithPreviewScreen(config2) {
|
|
|
821
818
|
}
|
|
822
819
|
var import_react3, import_ink3, showMenuScreen, showWordGridScreen;
|
|
823
820
|
var init_screens = __esm({
|
|
824
|
-
"src/screen/screens.
|
|
825
|
-
"use strict";
|
|
821
|
+
"src/screen/screens.js"() {
|
|
826
822
|
import_react3 = require("react");
|
|
827
823
|
import_ink3 = require("ink");
|
|
828
824
|
init_components();
|
|
@@ -832,7 +828,7 @@ var init_screens = __esm({
|
|
|
832
828
|
}
|
|
833
829
|
});
|
|
834
830
|
|
|
835
|
-
// src/screen/ui-elements.
|
|
831
|
+
// src/screen/ui-elements.js
|
|
836
832
|
function ListItem({
|
|
837
833
|
children,
|
|
838
834
|
isSelected = false,
|
|
@@ -857,7 +853,7 @@ function TextBlock({
|
|
|
857
853
|
color = "white",
|
|
858
854
|
dimmed = false,
|
|
859
855
|
bold = false,
|
|
860
|
-
maxWidth
|
|
856
|
+
maxWidth: _maxWidth
|
|
861
857
|
}) {
|
|
862
858
|
return (0, import_react4.createElement)(
|
|
863
859
|
import_ink4.Box,
|
|
@@ -897,7 +893,7 @@ function GridCell({
|
|
|
897
893
|
}, children)
|
|
898
894
|
);
|
|
899
895
|
}
|
|
900
|
-
function InputField({ prompt, value, onChange, onSubmit }) {
|
|
896
|
+
function InputField({ prompt, value, onChange: _onChange, onSubmit: _onSubmit }) {
|
|
901
897
|
return (0, import_react4.createElement)(
|
|
902
898
|
import_ink4.Box,
|
|
903
899
|
{ flexDirection: "column" },
|
|
@@ -911,33 +907,31 @@ function InputField({ prompt, value, onChange, onSubmit }) {
|
|
|
911
907
|
}
|
|
912
908
|
var import_react4, import_ink4;
|
|
913
909
|
var init_ui_elements = __esm({
|
|
914
|
-
"src/screen/ui-elements.
|
|
915
|
-
"use strict";
|
|
910
|
+
"src/screen/ui-elements.js"() {
|
|
916
911
|
import_react4 = require("react");
|
|
917
912
|
import_ink4 = require("ink");
|
|
918
913
|
}
|
|
919
914
|
});
|
|
920
915
|
|
|
921
|
-
// src/screen/utils.
|
|
916
|
+
// src/screen/utils.js
|
|
922
917
|
function buildBreadcrumb(parts) {
|
|
923
918
|
if (parts.length === 0) return "";
|
|
924
919
|
if (parts.length === 1) return parts[0];
|
|
925
920
|
return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
|
|
926
921
|
}
|
|
927
|
-
function buildDetailBreadcrumb(
|
|
928
|
-
if (
|
|
929
|
-
return suffix ? `\u2190 ${suffix}` :
|
|
922
|
+
function buildDetailBreadcrumb(path6, suffix = "") {
|
|
923
|
+
if (path6.length <= 1) {
|
|
924
|
+
return suffix ? `\u2190 ${suffix}` : path6[0] || "";
|
|
930
925
|
}
|
|
931
|
-
const breadcrumb = buildBreadcrumb(
|
|
926
|
+
const breadcrumb = buildBreadcrumb(path6);
|
|
932
927
|
return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
|
|
933
928
|
}
|
|
934
929
|
var init_utils = __esm({
|
|
935
|
-
"src/screen/utils.
|
|
936
|
-
"use strict";
|
|
930
|
+
"src/screen/utils.js"() {
|
|
937
931
|
}
|
|
938
932
|
});
|
|
939
933
|
|
|
940
|
-
// src/screen/footer-builder.
|
|
934
|
+
// src/screen/footer-builder.js
|
|
941
935
|
function buildFooter(config2 = {}) {
|
|
942
936
|
const {
|
|
943
937
|
navigation = null,
|
|
@@ -988,8 +982,7 @@ function organizeFooterMessages(messages) {
|
|
|
988
982
|
}
|
|
989
983
|
var FooterPresets;
|
|
990
984
|
var init_footer_builder = __esm({
|
|
991
|
-
"src/screen/footer-builder.
|
|
992
|
-
"use strict";
|
|
985
|
+
"src/screen/footer-builder.js"() {
|
|
993
986
|
FooterPresets = {
|
|
994
987
|
/**
|
|
995
988
|
* Menu screen footer
|
|
@@ -1048,7 +1041,7 @@ var init_footer_builder = __esm({
|
|
|
1048
1041
|
}
|
|
1049
1042
|
});
|
|
1050
1043
|
|
|
1051
|
-
// src/screen/index.
|
|
1044
|
+
// src/screen/index.js
|
|
1052
1045
|
var screen_exports = {};
|
|
1053
1046
|
__export(screen_exports, {
|
|
1054
1047
|
Box: () => import_ink5.Box,
|
|
@@ -1074,6 +1067,7 @@ __export(screen_exports, {
|
|
|
1074
1067
|
buildFooter: () => buildFooter,
|
|
1075
1068
|
h: () => import_react5.createElement,
|
|
1076
1069
|
load: () => load,
|
|
1070
|
+
memo: () => import_react5.memo,
|
|
1077
1071
|
organizeFooterMessages: () => organizeFooterMessages,
|
|
1078
1072
|
showListScreen: () => showListScreen,
|
|
1079
1073
|
showMenuScreen: () => showMenuScreen,
|
|
@@ -1084,6 +1078,7 @@ __export(screen_exports, {
|
|
|
1084
1078
|
useCallback: () => import_react5.useCallback,
|
|
1085
1079
|
useEffect: () => import_react5.useEffect,
|
|
1086
1080
|
useInput: () => import_ink5.useInput,
|
|
1081
|
+
useLayoutEffect: () => import_react5.useLayoutEffect,
|
|
1087
1082
|
useMemo: () => import_react5.useMemo,
|
|
1088
1083
|
useRef: () => import_react5.useRef,
|
|
1089
1084
|
useState: () => import_react5.useState
|
|
@@ -1099,8 +1094,7 @@ async function load() {
|
|
|
1099
1094
|
}
|
|
1100
1095
|
var import_react5, import_ink5, loadPromise;
|
|
1101
1096
|
var init_screen = __esm({
|
|
1102
|
-
"src/screen/index.
|
|
1103
|
-
"use strict";
|
|
1097
|
+
"src/screen/index.js"() {
|
|
1104
1098
|
import_react5 = __toESM(require("react"), 1);
|
|
1105
1099
|
import_ink5 = require("ink");
|
|
1106
1100
|
init_screens();
|
|
@@ -1117,11 +1111,11 @@ var init_screen = __esm({
|
|
|
1117
1111
|
}
|
|
1118
1112
|
});
|
|
1119
1113
|
|
|
1120
|
-
// src/scripts/cli-runner.
|
|
1121
|
-
var
|
|
1114
|
+
// src/scripts/cli-runner.js
|
|
1115
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
1122
1116
|
var import_node_url = require("url");
|
|
1123
1117
|
|
|
1124
|
-
// src/args/index.
|
|
1118
|
+
// src/args/index.js
|
|
1125
1119
|
var import_fs = require("fs");
|
|
1126
1120
|
var import_path = require("path");
|
|
1127
1121
|
var import_dotenv = require("dotenv");
|
|
@@ -1604,10 +1598,10 @@ var Args = class _Args {
|
|
|
1604
1598
|
}
|
|
1605
1599
|
};
|
|
1606
1600
|
|
|
1607
|
-
// src/params/index.
|
|
1601
|
+
// src/params/index.js
|
|
1608
1602
|
var import_joi = __toESM(require("joi"), 1);
|
|
1609
1603
|
|
|
1610
|
-
// src/errors.
|
|
1604
|
+
// src/errors.js
|
|
1611
1605
|
var FrameworkError = class extends Error {
|
|
1612
1606
|
constructor(message) {
|
|
1613
1607
|
super(message);
|
|
@@ -1633,7 +1627,7 @@ var FileDatabaseError = class extends FrameworkError {
|
|
|
1633
1627
|
}
|
|
1634
1628
|
};
|
|
1635
1629
|
|
|
1636
|
-
// src/params/custom-types.
|
|
1630
|
+
// src/params/custom-types.js
|
|
1637
1631
|
var joiEdateType = (value, helpers) => {
|
|
1638
1632
|
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(value)) {
|
|
1639
1633
|
const testDate = new Date(value);
|
|
@@ -1726,7 +1720,7 @@ function calculateTimeOffset(amount, unit, sign) {
|
|
|
1726
1720
|
}
|
|
1727
1721
|
return sign === "+" ? amount * multiplier : -amount * multiplier;
|
|
1728
1722
|
}
|
|
1729
|
-
var joiStringArrayType = (type) => (value,
|
|
1723
|
+
var joiStringArrayType = (type) => (value, _helpers) => {
|
|
1730
1724
|
if (value === void 0 || typeof value === "function") {
|
|
1731
1725
|
return [];
|
|
1732
1726
|
}
|
|
@@ -1752,7 +1746,7 @@ var joiStringArrayType = (type) => (value, helpers) => {
|
|
|
1752
1746
|
return arr;
|
|
1753
1747
|
};
|
|
1754
1748
|
|
|
1755
|
-
// src/params/index.
|
|
1749
|
+
// src/params/index.js
|
|
1756
1750
|
var Params = class _Params {
|
|
1757
1751
|
context;
|
|
1758
1752
|
// Partial context during initialization
|
|
@@ -1944,7 +1938,7 @@ var Params = class _Params {
|
|
|
1944
1938
|
throw new ParamError(`default value "${defValObj.value}" type mismatch`);
|
|
1945
1939
|
}
|
|
1946
1940
|
type = type.default(defValObj.value);
|
|
1947
|
-
} else if (str.match(
|
|
1941
|
+
} else if (str.match(/\s*required\s*/)) {
|
|
1948
1942
|
type = type.required();
|
|
1949
1943
|
} else {
|
|
1950
1944
|
type = type.optional();
|
|
@@ -2012,7 +2006,7 @@ var Params = class _Params {
|
|
|
2012
2006
|
definition = val;
|
|
2013
2007
|
val = val.value;
|
|
2014
2008
|
}
|
|
2015
|
-
|
|
2009
|
+
this.assignDefinition(key, definition);
|
|
2016
2010
|
if (!this.runAllRegisteredSetters(key, val)) {
|
|
2017
2011
|
this.params[key] = val;
|
|
2018
2012
|
}
|
|
@@ -2020,6 +2014,8 @@ var Params = class _Params {
|
|
|
2020
2014
|
/**
|
|
2021
2015
|
* Get all parameters from definitions (main script).
|
|
2022
2016
|
* Same as getAllForModule("script", defs). Processes left-to-right for cross-parameter references.
|
|
2017
|
+
* Libraries should use {@link getAllForModule} with an explicit module name (or {@link runWithModule}
|
|
2018
|
+
* around {@link get}) so --showUsedParams groups usage correctly.
|
|
2023
2019
|
*/
|
|
2024
2020
|
getAll(defs2) {
|
|
2025
2021
|
return this.getAllForModule("script", defs2);
|
|
@@ -2056,6 +2052,19 @@ var Params = class _Params {
|
|
|
2056
2052
|
this._currentModule = prev;
|
|
2057
2053
|
}
|
|
2058
2054
|
}
|
|
2055
|
+
/**
|
|
2056
|
+
* Run a callback with {@link _currentModule} set so single {@link get} calls are tracked
|
|
2057
|
+
* under the same module (for --showUsedParams / getFiguredByModule).
|
|
2058
|
+
*/
|
|
2059
|
+
runWithModule(moduleName, fn) {
|
|
2060
|
+
const prev = this._currentModule;
|
|
2061
|
+
this._currentModule = moduleName;
|
|
2062
|
+
try {
|
|
2063
|
+
return fn();
|
|
2064
|
+
} finally {
|
|
2065
|
+
this._currentModule = prev;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2059
2068
|
/**
|
|
2060
2069
|
* Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
|
|
2061
2070
|
*/
|
|
@@ -2069,9 +2078,9 @@ var Params = class _Params {
|
|
|
2069
2078
|
if (!parenMatch) continue;
|
|
2070
2079
|
const parts = parenMatch[1].split(":");
|
|
2071
2080
|
if (parts.length < 3) continue;
|
|
2072
|
-
const
|
|
2073
|
-
if (!
|
|
2074
|
-
const srcMatch =
|
|
2081
|
+
const path6 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
|
|
2082
|
+
if (!path6 || path6.includes(paramsIndexPath)) continue;
|
|
2083
|
+
const srcMatch = path6.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
|
|
2075
2084
|
if (srcMatch) return srcMatch[1];
|
|
2076
2085
|
}
|
|
2077
2086
|
return "script";
|
|
@@ -2116,11 +2125,11 @@ var Params = class _Params {
|
|
|
2116
2125
|
}
|
|
2117
2126
|
};
|
|
2118
2127
|
|
|
2119
|
-
// src/logger/index.
|
|
2128
|
+
// src/logger/index.js
|
|
2120
2129
|
var import_chalk = __toESM(require("chalk"), 1);
|
|
2121
2130
|
var import_util = __toESM(require("util"), 1);
|
|
2122
2131
|
|
|
2123
|
-
// src/logger/transports.
|
|
2132
|
+
// src/logger/transports.js
|
|
2124
2133
|
var ConsoleTransport = class {
|
|
2125
2134
|
write(payload) {
|
|
2126
2135
|
console.info(payload);
|
|
@@ -2140,7 +2149,7 @@ var ParentProcessTransport = class {
|
|
|
2140
2149
|
}
|
|
2141
2150
|
};
|
|
2142
2151
|
|
|
2143
|
-
// src/logger/index.
|
|
2152
|
+
// src/logger/index.js
|
|
2144
2153
|
var ALL_LEVELS = [
|
|
2145
2154
|
"silly",
|
|
2146
2155
|
"debug",
|
|
@@ -2213,6 +2222,7 @@ var Logger = class _Logger {
|
|
|
2213
2222
|
}
|
|
2214
2223
|
/**
|
|
2215
2224
|
* Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
|
|
2225
|
+
* Params are tracked under the `logger` module for --showUsedParams.
|
|
2216
2226
|
*/
|
|
2217
2227
|
static init(context, options) {
|
|
2218
2228
|
const paramDefs = {
|
|
@@ -2226,7 +2236,7 @@ var Logger = class _Logger {
|
|
|
2226
2236
|
progressWithTimes: "boolean default false",
|
|
2227
2237
|
progressThrottleMs: "number"
|
|
2228
2238
|
};
|
|
2229
|
-
const discovered = context.params.getAllForModule(paramDefs);
|
|
2239
|
+
const discovered = context.params.getAllForModule("logger", paramDefs);
|
|
2230
2240
|
const config2 = { ...discovered, ...options };
|
|
2231
2241
|
const logger = new _Logger(context, config2);
|
|
2232
2242
|
context.logger = logger;
|
|
@@ -2422,9 +2432,9 @@ var Logger = class _Logger {
|
|
|
2422
2432
|
}
|
|
2423
2433
|
};
|
|
2424
2434
|
|
|
2425
|
-
// src/init/index.
|
|
2435
|
+
// src/init/index.js
|
|
2426
2436
|
var import_events = require("events");
|
|
2427
|
-
function extractComponentOptions(opts,
|
|
2437
|
+
function extractComponentOptions(opts, _componentName) {
|
|
2428
2438
|
const reservedKeys = ["overrides", "defaults", "modules"];
|
|
2429
2439
|
const componentOptions = {};
|
|
2430
2440
|
for (const [key, value] of Object.entries(opts)) {
|
|
@@ -2491,6 +2501,19 @@ function printAllParameters(context) {
|
|
|
2491
2501
|
async function init(flow2, opts = {}) {
|
|
2492
2502
|
let stop = false;
|
|
2493
2503
|
let context = null;
|
|
2504
|
+
let cleanupRan = false;
|
|
2505
|
+
const runRegisteredCleanups = async (ctx) => {
|
|
2506
|
+
if (cleanupRan) return;
|
|
2507
|
+
cleanupRan = true;
|
|
2508
|
+
const fns = [...ctx.cleanupFunctions].reverse();
|
|
2509
|
+
for (const fn of fns) {
|
|
2510
|
+
try {
|
|
2511
|
+
await fn(ctx);
|
|
2512
|
+
} catch (error) {
|
|
2513
|
+
ctx.logger.warn("[cleanup] error in cleanup function:", error);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2494
2517
|
try {
|
|
2495
2518
|
try {
|
|
2496
2519
|
const screenModule = await Promise.resolve().then(() => (init_screen(), screen_exports));
|
|
@@ -2513,13 +2536,24 @@ async function init(flow2, opts = {}) {
|
|
|
2513
2536
|
printAllParameters(context);
|
|
2514
2537
|
process.exit(0);
|
|
2515
2538
|
}
|
|
2539
|
+
let sigintCount = 0;
|
|
2516
2540
|
process.on("SIGINT", async () => {
|
|
2517
|
-
if (
|
|
2518
|
-
|
|
2519
|
-
|
|
2541
|
+
if (!context) return;
|
|
2542
|
+
sigintCount += 1;
|
|
2543
|
+
if (sigintCount === 1) {
|
|
2544
|
+
stop = true;
|
|
2545
|
+
context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
|
|
2546
|
+
context.emitter.emit("stop", stopAllowance);
|
|
2547
|
+
return;
|
|
2520
2548
|
}
|
|
2549
|
+
context.logger.warn("[process] second SIGINT: running cleanup then exit");
|
|
2550
|
+
await runRegisteredCleanups(context);
|
|
2551
|
+
process.exit(2);
|
|
2552
|
+
});
|
|
2553
|
+
process.on("SIGTERM", () => {
|
|
2554
|
+
if (!context || stop) return;
|
|
2521
2555
|
stop = true;
|
|
2522
|
-
context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
|
|
2556
|
+
context.logger.info(`>> SIGTERM: emitting stop with allowance ${stopAllowance}`);
|
|
2523
2557
|
context.emitter.emit("stop", stopAllowance);
|
|
2524
2558
|
});
|
|
2525
2559
|
await flow2(context);
|
|
@@ -2544,33 +2578,62 @@ async function init(flow2, opts = {}) {
|
|
|
2544
2578
|
}
|
|
2545
2579
|
} finally {
|
|
2546
2580
|
if (context) {
|
|
2547
|
-
|
|
2548
|
-
try {
|
|
2549
|
-
await fn(context);
|
|
2550
|
-
} catch (error) {
|
|
2551
|
-
context.logger.warn("[cleanup] error in cleanup function:", error);
|
|
2552
|
-
}
|
|
2553
|
-
}
|
|
2581
|
+
await runRegisteredCleanups(context);
|
|
2554
2582
|
}
|
|
2555
2583
|
}
|
|
2556
2584
|
}
|
|
2557
2585
|
|
|
2558
|
-
// src/db/index.
|
|
2586
|
+
// src/db/index.js
|
|
2559
2587
|
var import_knex = __toESM(require("knex"), 1);
|
|
2588
|
+
var KNEX_DEFAULTS = {
|
|
2589
|
+
testConnection: true,
|
|
2590
|
+
pool: { min: 2, max: 10 },
|
|
2591
|
+
acquireConnectionTimeout: 1e4,
|
|
2592
|
+
ssl: { rejectUnauthorized: false }
|
|
2593
|
+
};
|
|
2560
2594
|
var Db = class {
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2595
|
+
static async init(context, options = {}) {
|
|
2596
|
+
const defs2 = {
|
|
2597
|
+
dbName: "string",
|
|
2598
|
+
dbConnectionString: "string",
|
|
2599
|
+
dbProfile: "boolean default false"
|
|
2600
|
+
};
|
|
2601
|
+
const discovered = context?.params?.getAllForModule?.("db", defs2) ?? {};
|
|
2602
|
+
const merged = { ...discovered, ...options };
|
|
2603
|
+
let { dbName, dbConnectionString } = merged;
|
|
2604
|
+
const { dbProfile } = merged;
|
|
2605
|
+
if (!dbName && !dbConnectionString) {
|
|
2606
|
+
dbName = "local";
|
|
2607
|
+
}
|
|
2608
|
+
if (dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
|
|
2609
|
+
dbConnectionString = dbName;
|
|
2610
|
+
dbName = void 0;
|
|
2611
|
+
}
|
|
2612
|
+
if (dbName && !dbConnectionString) {
|
|
2613
|
+
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
2614
|
+
dbConnectionString = await context.params.get(paramName, "string");
|
|
2615
|
+
if (!dbConnectionString) {
|
|
2616
|
+
throw new ParamError(
|
|
2617
|
+
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
2618
|
+
);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
const config2 = {
|
|
2622
|
+
...KNEX_DEFAULTS,
|
|
2623
|
+
connectionString: dbConnectionString,
|
|
2624
|
+
name: dbName || merged.name || "default",
|
|
2625
|
+
profile: !!dbProfile,
|
|
2626
|
+
logger: context.logger
|
|
2627
|
+
};
|
|
2628
|
+
return dbConnect(context, config2);
|
|
2629
|
+
}
|
|
2570
2630
|
constructor(config2) {
|
|
2571
|
-
if (!config2.connectionString) {
|
|
2631
|
+
if (!config2 || !config2.connectionString) {
|
|
2572
2632
|
throw new ParamError("Db: connectionString is required");
|
|
2573
2633
|
}
|
|
2634
|
+
this.knexInstance = null;
|
|
2635
|
+
this.isConnected = false;
|
|
2636
|
+
this.queriesLog = [];
|
|
2574
2637
|
this.config = {
|
|
2575
2638
|
testConnection: true,
|
|
2576
2639
|
profile: false,
|
|
@@ -2583,25 +2646,23 @@ var Db = class {
|
|
|
2583
2646
|
};
|
|
2584
2647
|
this.logger = this.config.logger;
|
|
2585
2648
|
const instance = this;
|
|
2586
|
-
const callableWrapper = function(
|
|
2649
|
+
const callableWrapper = function() {
|
|
2587
2650
|
throw new Error("This should never be called directly");
|
|
2588
2651
|
};
|
|
2589
2652
|
callableWrapper._instance = instance;
|
|
2590
2653
|
return new Proxy(callableWrapper, {
|
|
2591
|
-
|
|
2592
|
-
apply: (target, thisArg, argumentsList) => {
|
|
2654
|
+
apply: (target, _thisArg, argumentsList) => {
|
|
2593
2655
|
const inst = target._instance;
|
|
2594
2656
|
if (!inst.knexInstance) {
|
|
2595
2657
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
2596
2658
|
}
|
|
2597
2659
|
return inst.knexInstance(...argumentsList);
|
|
2598
2660
|
},
|
|
2599
|
-
// Intercept property access: db.schema, db.raw, etc.
|
|
2600
2661
|
get: (target, prop) => {
|
|
2601
2662
|
if (prop === "_instance") {
|
|
2602
2663
|
return target._instance;
|
|
2603
2664
|
}
|
|
2604
|
-
const
|
|
2665
|
+
const inst = target._instance;
|
|
2605
2666
|
const ownMethods = [
|
|
2606
2667
|
"connect",
|
|
2607
2668
|
"disconnect",
|
|
@@ -2614,26 +2675,26 @@ var Db = class {
|
|
|
2614
2675
|
"detectClient",
|
|
2615
2676
|
"attachProfiler"
|
|
2616
2677
|
];
|
|
2617
|
-
if (prop in
|
|
2618
|
-
const value =
|
|
2678
|
+
if (prop in inst) {
|
|
2679
|
+
const value = inst[prop];
|
|
2619
2680
|
if (typeof value === "function" && ownMethods.includes(prop)) {
|
|
2620
|
-
return value.bind(
|
|
2681
|
+
return value.bind(inst);
|
|
2621
2682
|
}
|
|
2622
2683
|
if (typeof value !== "function") {
|
|
2623
2684
|
return value;
|
|
2624
2685
|
}
|
|
2625
2686
|
}
|
|
2626
|
-
if (
|
|
2627
|
-
const knexProp =
|
|
2687
|
+
if (inst.knexInstance) {
|
|
2688
|
+
const knexProp = inst.knexInstance[prop];
|
|
2628
2689
|
if (typeof knexProp === "function") {
|
|
2629
|
-
return knexProp.bind(
|
|
2690
|
+
return knexProp.bind(inst.knexInstance);
|
|
2630
2691
|
}
|
|
2631
2692
|
return knexProp;
|
|
2632
2693
|
}
|
|
2633
|
-
if (prop in
|
|
2634
|
-
const method =
|
|
2694
|
+
if (prop in inst) {
|
|
2695
|
+
const method = inst[prop];
|
|
2635
2696
|
if (typeof method === "function") {
|
|
2636
|
-
return method.bind(
|
|
2697
|
+
return method.bind(inst);
|
|
2637
2698
|
}
|
|
2638
2699
|
return method;
|
|
2639
2700
|
}
|
|
@@ -2641,9 +2702,6 @@ var Db = class {
|
|
|
2641
2702
|
}
|
|
2642
2703
|
});
|
|
2643
2704
|
}
|
|
2644
|
-
/**
|
|
2645
|
-
* Detect database client type from connection string
|
|
2646
|
-
*/
|
|
2647
2705
|
detectClient(connectionString) {
|
|
2648
2706
|
if (connectionString.match(/^postgresql/)) {
|
|
2649
2707
|
return "pg";
|
|
@@ -2653,9 +2711,6 @@ var Db = class {
|
|
|
2653
2711
|
}
|
|
2654
2712
|
return null;
|
|
2655
2713
|
}
|
|
2656
|
-
/**
|
|
2657
|
-
* Connect to the database
|
|
2658
|
-
*/
|
|
2659
2714
|
async connect() {
|
|
2660
2715
|
if (this.isConnected && this.knexInstance) {
|
|
2661
2716
|
this.logger.warn?.("[Db] Already connected");
|
|
@@ -2664,14 +2719,13 @@ var Db = class {
|
|
|
2664
2719
|
const client = this.detectClient(this.config.connectionString);
|
|
2665
2720
|
if (!client) {
|
|
2666
2721
|
throw new ParamError(
|
|
2667
|
-
|
|
2722
|
+
"Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://"
|
|
2668
2723
|
);
|
|
2669
2724
|
}
|
|
2670
2725
|
try {
|
|
2671
2726
|
const connectionConfig = {
|
|
2672
2727
|
connectionString: this.config.connectionString,
|
|
2673
2728
|
family: 4
|
|
2674
|
-
// Force IPv4 only (disable IPv6)
|
|
2675
2729
|
};
|
|
2676
2730
|
this.knexInstance = (0, import_knex.default)({
|
|
2677
2731
|
client,
|
|
@@ -2687,7 +2741,9 @@ var Db = class {
|
|
|
2687
2741
|
await this.testConnection();
|
|
2688
2742
|
}
|
|
2689
2743
|
this.isConnected = true;
|
|
2690
|
-
this.logger.debug?.(
|
|
2744
|
+
this.logger.debug?.(
|
|
2745
|
+
`[Db] Connected to database "${this.config.name || this.config.connectionString}"`
|
|
2746
|
+
);
|
|
2691
2747
|
} catch (error) {
|
|
2692
2748
|
if (error instanceof ParamError) {
|
|
2693
2749
|
throw error;
|
|
@@ -2696,9 +2752,6 @@ var Db = class {
|
|
|
2696
2752
|
throw new ParamError(`Db: Connection failed - ${errorMsg}`);
|
|
2697
2753
|
}
|
|
2698
2754
|
}
|
|
2699
|
-
/**
|
|
2700
|
-
* Disconnect from the database
|
|
2701
|
-
*/
|
|
2702
2755
|
async disconnect() {
|
|
2703
2756
|
if (!this.knexInstance) {
|
|
2704
2757
|
return;
|
|
@@ -2708,16 +2761,15 @@ var Db = class {
|
|
|
2708
2761
|
this.knexInstance = null;
|
|
2709
2762
|
this.isConnected = false;
|
|
2710
2763
|
this.queriesLog = [];
|
|
2711
|
-
this.logger.debug?.(
|
|
2764
|
+
this.logger.debug?.(
|
|
2765
|
+
`[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`
|
|
2766
|
+
);
|
|
2712
2767
|
} catch (error) {
|
|
2713
2768
|
const errorMsg = this.getErrorMessage(error);
|
|
2714
2769
|
this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
|
|
2715
2770
|
throw error;
|
|
2716
2771
|
}
|
|
2717
2772
|
}
|
|
2718
|
-
/**
|
|
2719
|
-
* Extract error message from various error types
|
|
2720
|
-
*/
|
|
2721
2773
|
getErrorMessage(error) {
|
|
2722
2774
|
if (error instanceof AggregateError) {
|
|
2723
2775
|
const errors = error.errors || [];
|
|
@@ -2742,9 +2794,11 @@ var Db = class {
|
|
|
2742
2794
|
return `${code} (tried: ${addresses.join(", ")})`;
|
|
2743
2795
|
}
|
|
2744
2796
|
}
|
|
2745
|
-
const uniqueMessages = [
|
|
2746
|
-
|
|
2747
|
-
|
|
2797
|
+
const uniqueMessages = [
|
|
2798
|
+
...new Set(
|
|
2799
|
+
errors.map((e) => e instanceof Error ? e.message : String(e))
|
|
2800
|
+
)
|
|
2801
|
+
];
|
|
2748
2802
|
if (uniqueMessages.length === 1) {
|
|
2749
2803
|
return uniqueMessages[0];
|
|
2750
2804
|
}
|
|
@@ -2753,28 +2807,25 @@ var Db = class {
|
|
|
2753
2807
|
return error.message || "Multiple errors occurred";
|
|
2754
2808
|
}
|
|
2755
2809
|
if (error instanceof Error) {
|
|
2756
|
-
const
|
|
2757
|
-
if (
|
|
2758
|
-
return `${
|
|
2810
|
+
const code = error.code;
|
|
2811
|
+
if (code) {
|
|
2812
|
+
return `${code}: ${error.message || String(error)}`;
|
|
2759
2813
|
}
|
|
2760
2814
|
return error.message || String(error);
|
|
2761
2815
|
}
|
|
2762
2816
|
if (typeof error === "string") {
|
|
2763
2817
|
return error;
|
|
2764
2818
|
}
|
|
2765
|
-
if (error
|
|
2819
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
2766
2820
|
const msg = String(error.message);
|
|
2767
|
-
const
|
|
2768
|
-
if (
|
|
2769
|
-
return `${
|
|
2821
|
+
const code = error.code;
|
|
2822
|
+
if (code) {
|
|
2823
|
+
return `${code}: ${msg}`;
|
|
2770
2824
|
}
|
|
2771
2825
|
return msg;
|
|
2772
2826
|
}
|
|
2773
2827
|
return String(error) || "Unknown error";
|
|
2774
2828
|
}
|
|
2775
|
-
/**
|
|
2776
|
-
* Test database connection
|
|
2777
|
-
*/
|
|
2778
2829
|
async testConnection() {
|
|
2779
2830
|
if (!this.knexInstance) {
|
|
2780
2831
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -2790,9 +2841,6 @@ var Db = class {
|
|
|
2790
2841
|
throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
|
|
2791
2842
|
}
|
|
2792
2843
|
}
|
|
2793
|
-
/**
|
|
2794
|
-
* Attach query profiler to log all queries
|
|
2795
|
-
*/
|
|
2796
2844
|
attachProfiler() {
|
|
2797
2845
|
if (!this.knexInstance) {
|
|
2798
2846
|
return;
|
|
@@ -2802,7 +2850,7 @@ var Db = class {
|
|
|
2802
2850
|
this.knexInstance.on("query", (query) => {
|
|
2803
2851
|
query.__startTime = process.hrtime();
|
|
2804
2852
|
});
|
|
2805
|
-
this.knexInstance.on("query-response", (
|
|
2853
|
+
this.knexInstance.on("query-response", (_response, query) => {
|
|
2806
2854
|
const [seconds, nanoseconds] = process.hrtime(query.__startTime);
|
|
2807
2855
|
const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
|
|
2808
2856
|
const logEntry = {
|
|
@@ -2817,15 +2865,9 @@ var Db = class {
|
|
|
2817
2865
|
this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
|
|
2818
2866
|
});
|
|
2819
2867
|
}
|
|
2820
|
-
/**
|
|
2821
|
-
* Get query log (only available if profiling is enabled)
|
|
2822
|
-
*/
|
|
2823
2868
|
getQueryLog() {
|
|
2824
2869
|
return [...this.queriesLog];
|
|
2825
2870
|
}
|
|
2826
|
-
/**
|
|
2827
|
-
* Check if a table exists
|
|
2828
|
-
*/
|
|
2829
2871
|
async tableExists(tableName) {
|
|
2830
2872
|
if (!this.knexInstance) {
|
|
2831
2873
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -2837,65 +2879,28 @@ var Db = class {
|
|
|
2837
2879
|
throw error;
|
|
2838
2880
|
}
|
|
2839
2881
|
}
|
|
2840
|
-
/**
|
|
2841
|
-
* Get the underlying Knex instance (for advanced usage)
|
|
2842
|
-
*/
|
|
2843
2882
|
getKnex() {
|
|
2844
2883
|
if (!this.knexInstance) {
|
|
2845
2884
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
2846
2885
|
}
|
|
2847
2886
|
return this.knexInstance;
|
|
2848
2887
|
}
|
|
2849
|
-
/**
|
|
2850
|
-
* Get connection status
|
|
2851
|
-
*/
|
|
2852
2888
|
isConnectedToDb() {
|
|
2853
2889
|
return this.isConnected && this.knexInstance !== null;
|
|
2854
2890
|
}
|
|
2855
|
-
/**
|
|
2856
|
-
* Initialize Db with context (connects and registers disconnect cleanup).
|
|
2857
|
-
* Params are read via getAllForModule("db", defs). Same as dbInit(context, dbNameOrConnectionString).
|
|
2858
|
-
*/
|
|
2859
|
-
static async init(context, dbNameOrConnectionString) {
|
|
2860
|
-
return dbFindAndConnect(context, dbNameOrConnectionString);
|
|
2861
|
-
}
|
|
2862
2891
|
};
|
|
2863
2892
|
function capitalizeFirstLetter(str) {
|
|
2864
2893
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
2865
2894
|
}
|
|
2866
|
-
async function dbConnect(context,
|
|
2867
|
-
const defs2 = {
|
|
2868
|
-
testDbConnection: "boolean default true",
|
|
2869
|
-
name: "string",
|
|
2870
|
-
poolMin: "number default 2",
|
|
2871
|
-
poolMax: "number default 10",
|
|
2872
|
-
acquireConnectionTimeout: "number default 10000",
|
|
2873
|
-
sslRejectUnauthorized: "boolean default false"
|
|
2874
|
-
};
|
|
2875
|
-
const paramsConfig = context.params.getAllForModule(defs2);
|
|
2876
|
-
const config2 = {
|
|
2877
|
-
connectionString,
|
|
2878
|
-
name: paramsConfig.name || name || "default",
|
|
2879
|
-
testConnection: paramsConfig.testDbConnection,
|
|
2880
|
-
profile: dbProfile ?? false,
|
|
2881
|
-
pool: {
|
|
2882
|
-
min: paramsConfig.poolMin,
|
|
2883
|
-
max: paramsConfig.poolMax
|
|
2884
|
-
},
|
|
2885
|
-
acquireConnectionTimeout: paramsConfig.acquireConnectionTimeout,
|
|
2886
|
-
ssl: {
|
|
2887
|
-
rejectUnauthorized: paramsConfig.sslRejectUnauthorized
|
|
2888
|
-
},
|
|
2889
|
-
logger: context.logger
|
|
2890
|
-
};
|
|
2895
|
+
async function dbConnect(context, config2) {
|
|
2891
2896
|
try {
|
|
2892
2897
|
const db = new Db(config2);
|
|
2893
2898
|
context.registerCleanup(async () => {
|
|
2894
2899
|
await db.disconnect();
|
|
2895
|
-
context.logger.debug(`[Db] instance "${name
|
|
2900
|
+
context.logger.debug?.(`[Db] instance "${config2.name}" disconnected`);
|
|
2896
2901
|
});
|
|
2897
2902
|
await db.connect();
|
|
2898
|
-
context.logger.debug(`[Db] instance "${name
|
|
2903
|
+
context.logger.debug?.(`[Db] instance "${config2.name}" initialized`);
|
|
2899
2904
|
return db;
|
|
2900
2905
|
} catch (error) {
|
|
2901
2906
|
if (error instanceof ParamError) {
|
|
@@ -2905,48 +2910,11 @@ async function dbConnect(context, connectionString, name, dbProfile) {
|
|
|
2905
2910
|
throw new ParamError(`[Db] connect error: ${errorMsg}`);
|
|
2906
2911
|
}
|
|
2907
2912
|
}
|
|
2908
|
-
async function dbFindAndConnect(context, dbNameOrConnectionString) {
|
|
2909
|
-
let dbName;
|
|
2910
|
-
let dbConnectionString;
|
|
2911
|
-
let dbProfile;
|
|
2912
|
-
if (dbNameOrConnectionString) {
|
|
2913
|
-
if (dbNameOrConnectionString.match(/^(postgresql|mysql):\/\/[^\s]+:[^\s]+@[^\s]+:\d+\/[^\s]+$/)) {
|
|
2914
|
-
dbName = void 0;
|
|
2915
|
-
dbConnectionString = dbNameOrConnectionString;
|
|
2916
|
-
} else {
|
|
2917
|
-
dbName = dbNameOrConnectionString;
|
|
2918
|
-
}
|
|
2919
|
-
} else {
|
|
2920
|
-
const defs2 = {
|
|
2921
|
-
dbName: "string",
|
|
2922
|
-
dbConnectionString: "string",
|
|
2923
|
-
dbProfile: "boolean default false"
|
|
2924
|
-
};
|
|
2925
|
-
const paramsConfig = context.params.getAllForModule(defs2);
|
|
2926
|
-
dbName = paramsConfig.dbName;
|
|
2927
|
-
dbConnectionString = paramsConfig.dbConnectionString;
|
|
2928
|
-
dbProfile = paramsConfig.dbProfile;
|
|
2929
|
-
}
|
|
2930
|
-
if (!dbName && !dbConnectionString) {
|
|
2931
|
-
throw new ParamError("Db: either dbName or dbConnectionString must be specified");
|
|
2932
|
-
}
|
|
2933
|
-
if (dbName) {
|
|
2934
|
-
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
2935
|
-
dbConnectionString = await context.params.get(paramName, "string");
|
|
2936
|
-
if (!dbConnectionString) {
|
|
2937
|
-
throw new ParamError(
|
|
2938
|
-
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
2939
|
-
);
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
2942
|
-
const db = await dbConnect(context, dbConnectionString, dbName, dbProfile);
|
|
2943
|
-
return db;
|
|
2944
|
-
}
|
|
2945
|
-
async function dbInit(context, dbNameOrConnectionString) {
|
|
2946
|
-
return await dbFindAndConnect(context, dbNameOrConnectionString);
|
|
2947
|
-
}
|
|
2948
2913
|
|
|
2949
|
-
// src/
|
|
2914
|
+
// src/tasks/index.js
|
|
2915
|
+
var import_node_os3 = __toESM(require("os"), 1);
|
|
2916
|
+
|
|
2917
|
+
// src/utils/date-utils.js
|
|
2950
2918
|
function isTimestampFolder(folderName) {
|
|
2951
2919
|
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
|
|
2952
2920
|
if (!isoRegex.test(folderName)) {
|
|
@@ -2956,7 +2924,7 @@ function isTimestampFolder(folderName) {
|
|
|
2956
2924
|
return !isNaN(date.getTime()) && date.getTime() > 0;
|
|
2957
2925
|
}
|
|
2958
2926
|
|
|
2959
|
-
// src/utils/fs-utils.
|
|
2927
|
+
// src/utils/fs-utils.js
|
|
2960
2928
|
var import_fs2 = __toESM(require("fs"), 1);
|
|
2961
2929
|
var import_path2 = __toESM(require("path"), 1);
|
|
2962
2930
|
async function ensurePath(...pathParts) {
|
|
@@ -2980,7 +2948,7 @@ function getFileExtension(dataType) {
|
|
|
2980
2948
|
}
|
|
2981
2949
|
}
|
|
2982
2950
|
|
|
2983
|
-
// src/utils/os-utils.
|
|
2951
|
+
// src/utils/os-utils.js
|
|
2984
2952
|
var import_fs3 = __toESM(require("fs"), 1);
|
|
2985
2953
|
var import_path3 = __toESM(require("path"), 1);
|
|
2986
2954
|
var import_child_process = require("child_process");
|
|
@@ -3009,7 +2977,7 @@ function getFreeDiskSpace(targetPath) {
|
|
|
3009
2977
|
}
|
|
3010
2978
|
}
|
|
3011
2979
|
|
|
3012
|
-
// src/utils/format-utils.
|
|
2980
|
+
// src/utils/format-utils.js
|
|
3013
2981
|
function bytesToHumanReadable(bytes) {
|
|
3014
2982
|
if (bytes === 0) return "0 B";
|
|
3015
2983
|
const k = 1024;
|
|
@@ -3018,7 +2986,7 @@ function bytesToHumanReadable(bytes) {
|
|
|
3018
2986
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
3019
2987
|
}
|
|
3020
2988
|
|
|
3021
|
-
// src/utils/core-utils.
|
|
2989
|
+
// src/utils/core-utils.js
|
|
3022
2990
|
function sleepMs(ms) {
|
|
3023
2991
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3024
2992
|
}
|
|
@@ -3027,8 +2995,82 @@ function toJsonColumn(value) {
|
|
|
3027
2995
|
return JSON.stringify(value);
|
|
3028
2996
|
}
|
|
3029
2997
|
|
|
3030
|
-
// src/tasks/
|
|
2998
|
+
// src/tasks/servicesRegistry.js
|
|
2999
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
3000
|
+
|
|
3001
|
+
// src/tasks/taskUtils.js
|
|
3031
3002
|
var import_node_crypto = require("crypto");
|
|
3003
|
+
|
|
3004
|
+
// src/tasks/time-matcher.js
|
|
3005
|
+
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
3006
|
+
function resolveAsterisks(field, range) {
|
|
3007
|
+
return field.includes("*") ? field.replace("*", range) : field;
|
|
3008
|
+
}
|
|
3009
|
+
function resolveRanges(field) {
|
|
3010
|
+
const regex = /(\d+)-(\d+)/;
|
|
3011
|
+
let current = field;
|
|
3012
|
+
while (true) {
|
|
3013
|
+
const match = regex.exec(current);
|
|
3014
|
+
if (!match) break;
|
|
3015
|
+
const raw = match[0];
|
|
3016
|
+
let first = Number(match[1]);
|
|
3017
|
+
let last = Number(match[2]);
|
|
3018
|
+
if (last < first) {
|
|
3019
|
+
[first, last] = [last, first];
|
|
3020
|
+
}
|
|
3021
|
+
const values = [];
|
|
3022
|
+
for (let i = first; i <= last; i += 1) {
|
|
3023
|
+
values.push(i);
|
|
3024
|
+
}
|
|
3025
|
+
current = current.replace(raw, values.join(","));
|
|
3026
|
+
}
|
|
3027
|
+
return current;
|
|
3028
|
+
}
|
|
3029
|
+
function resolveSteps(field) {
|
|
3030
|
+
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
3031
|
+
if (!match) return field;
|
|
3032
|
+
const base = match[1];
|
|
3033
|
+
const step = Number(match[2]);
|
|
3034
|
+
if (!Number.isFinite(step) || step <= 0) return field;
|
|
3035
|
+
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
3036
|
+
}
|
|
3037
|
+
function convertPattern(pattern) {
|
|
3038
|
+
const parts = pattern.trim().split(/\s+/);
|
|
3039
|
+
if (parts.length !== 6) {
|
|
3040
|
+
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
3041
|
+
}
|
|
3042
|
+
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
3043
|
+
}
|
|
3044
|
+
function fieldMatches(field, value) {
|
|
3045
|
+
const allowed = field.split(",").map((v) => Number(v));
|
|
3046
|
+
return allowed.includes(value);
|
|
3047
|
+
}
|
|
3048
|
+
function matchesParsedPattern(parsed, date) {
|
|
3049
|
+
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());
|
|
3050
|
+
}
|
|
3051
|
+
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
3052
|
+
const parsed = convertPattern(pattern);
|
|
3053
|
+
return matchesParsedPattern(parsed, date);
|
|
3054
|
+
}
|
|
3055
|
+
var MS_PER_SECOND = 1e3;
|
|
3056
|
+
var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
|
|
3057
|
+
function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
|
|
3058
|
+
const parsed = convertPattern(pattern);
|
|
3059
|
+
let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
|
|
3060
|
+
const end = t + maxSearchMs;
|
|
3061
|
+
while (t <= end) {
|
|
3062
|
+
const date = new Date(t);
|
|
3063
|
+
if (matchesParsedPattern(parsed, date)) {
|
|
3064
|
+
return date;
|
|
3065
|
+
}
|
|
3066
|
+
t += MS_PER_SECOND;
|
|
3067
|
+
}
|
|
3068
|
+
throw new Error(
|
|
3069
|
+
`nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
|
|
3070
|
+
);
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// src/tasks/taskUtils.js
|
|
3032
3074
|
function getDb(context) {
|
|
3033
3075
|
const db = context.db;
|
|
3034
3076
|
if (!db) {
|
|
@@ -3036,89 +3078,84 @@ function getDb(context) {
|
|
|
3036
3078
|
}
|
|
3037
3079
|
return db;
|
|
3038
3080
|
}
|
|
3039
|
-
function queueToTableNames(
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3081
|
+
function queueToTableNames(queueName) {
|
|
3082
|
+
return {
|
|
3083
|
+
tasksTable: queueName,
|
|
3084
|
+
historyTable: `${queueName}_history`,
|
|
3085
|
+
registryTable: `${queueName}_services_registry`
|
|
3086
|
+
};
|
|
3087
|
+
}
|
|
3088
|
+
function defineTasksTable(t, db, tableNameForIndex) {
|
|
3089
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
3090
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
3091
|
+
t.timestamp("started_at");
|
|
3092
|
+
t.timestamp("completed_at");
|
|
3093
|
+
t.integer("priority").notNullable().defaultTo(50);
|
|
3094
|
+
t.text("schedule");
|
|
3095
|
+
t.timestamp("next_run_at").defaultTo(null);
|
|
3096
|
+
t.timestamp("past_due").defaultTo(null);
|
|
3097
|
+
t.text("name").notNullable();
|
|
3098
|
+
t.text("opid");
|
|
3099
|
+
t.json("params");
|
|
3100
|
+
t.text("service_group");
|
|
3101
|
+
t.integer("instance_number");
|
|
3102
|
+
t.text("service_name");
|
|
3103
|
+
t.text("server_name");
|
|
3104
|
+
t.text("status").notNullable().defaultTo("idle");
|
|
3105
|
+
t.timestamp("status_changed_at").defaultTo(null);
|
|
3106
|
+
t.text("progress");
|
|
3107
|
+
t.boolean("success");
|
|
3108
|
+
t.json("results");
|
|
3109
|
+
t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
|
|
3110
|
+
t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
|
|
3111
|
+
}
|
|
3112
|
+
function taskHistoryInsertFromQueueRow(row, overrides) {
|
|
3113
|
+
const { id, ...snapshot } = row;
|
|
3114
|
+
void id;
|
|
3043
3115
|
return {
|
|
3044
|
-
|
|
3045
|
-
|
|
3116
|
+
...snapshot,
|
|
3117
|
+
...overrides
|
|
3046
3118
|
};
|
|
3047
3119
|
}
|
|
3048
3120
|
async function ensureTaskTables(context, options = {}) {
|
|
3049
|
-
const
|
|
3121
|
+
const queueName = options.queueName ?? "tasks";
|
|
3050
3122
|
const recreate = options.recreate ?? false;
|
|
3051
3123
|
const db = getDb(context);
|
|
3052
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
3124
|
+
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
3053
3125
|
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
3054
3126
|
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
3127
|
+
const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
|
|
3055
3128
|
if (recreate) {
|
|
3056
3129
|
await db.schema.dropTableIfExists(historyTable);
|
|
3057
3130
|
await db.schema.dropTableIfExists(tasksTable);
|
|
3131
|
+
await db.schema.dropTableIfExists(registryTable);
|
|
3058
3132
|
}
|
|
3059
3133
|
if (needsTasks) {
|
|
3060
3134
|
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
3061
3135
|
await db.schema.createTable(tasksTable, (t) => {
|
|
3062
|
-
t
|
|
3063
|
-
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
3064
|
-
t.timestamp("started_at");
|
|
3065
|
-
t.timestamp("completed_at");
|
|
3066
|
-
t.integer("priority").notNullable().defaultTo(0);
|
|
3067
|
-
t.text("schedule");
|
|
3068
|
-
t.timestamp("past_due").defaultTo(null);
|
|
3069
|
-
t.text("target").notNullable();
|
|
3070
|
-
t.text("task").notNullable();
|
|
3071
|
-
t.json("params");
|
|
3072
|
-
t.text("opid");
|
|
3073
|
-
t.timestamp("paused_at").defaultTo(null);
|
|
3074
|
-
t.text("progress");
|
|
3075
|
-
t.boolean("success");
|
|
3076
|
-
t.json("results");
|
|
3077
|
-
});
|
|
3078
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
3079
|
-
t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
|
|
3080
|
-
t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
|
|
3081
|
-
t.index(["target", "task"], `${tasksTable}_target_task_idx`);
|
|
3082
|
-
});
|
|
3083
|
-
}
|
|
3084
|
-
const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
|
|
3085
|
-
if (!tasksHasOpid) {
|
|
3086
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
3087
|
-
t.text("opid");
|
|
3088
|
-
});
|
|
3089
|
-
}
|
|
3090
|
-
const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
|
|
3091
|
-
if (!tasksHasPausedAt) {
|
|
3092
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
3093
|
-
t.timestamp("paused_at").defaultTo(null);
|
|
3136
|
+
defineTasksTable(t, db, tasksTable);
|
|
3094
3137
|
});
|
|
3095
3138
|
}
|
|
3096
3139
|
if (needsHistory) {
|
|
3097
3140
|
await db.schema.createTable(historyTable, (t) => {
|
|
3098
|
-
t
|
|
3099
|
-
t.timestamp("created_at").notNullable();
|
|
3100
|
-
t.timestamp("started_at");
|
|
3101
|
-
t.timestamp("completed_at");
|
|
3102
|
-
t.integer("priority").notNullable().defaultTo(0);
|
|
3103
|
-
t.text("schedule");
|
|
3104
|
-
t.timestamp("past_due").defaultTo(null);
|
|
3105
|
-
t.text("target").notNullable();
|
|
3106
|
-
t.text("task").notNullable();
|
|
3107
|
-
t.json("params");
|
|
3108
|
-
t.text("opid");
|
|
3109
|
-
t.text("progress");
|
|
3110
|
-
t.boolean("success");
|
|
3111
|
-
t.json("results");
|
|
3112
|
-
});
|
|
3113
|
-
await db.schema.alterTable(historyTable, (t) => {
|
|
3114
|
-
t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
|
|
3115
|
-
t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
|
|
3141
|
+
defineTasksTable(t, db, historyTable);
|
|
3116
3142
|
});
|
|
3117
3143
|
}
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
t.text("
|
|
3144
|
+
if (needsRegistry) {
|
|
3145
|
+
await db.schema.createTable(registryTable, (t) => {
|
|
3146
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
3147
|
+
t.text("queue_name").notNullable();
|
|
3148
|
+
t.text("service_group").notNullable();
|
|
3149
|
+
t.integer("instance_number").notNullable().defaultTo(1);
|
|
3150
|
+
t.text("service_name").notNullable();
|
|
3151
|
+
t.text("server_name").notNullable();
|
|
3152
|
+
t.integer("pid");
|
|
3153
|
+
t.json("metadata");
|
|
3154
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
3155
|
+
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
3156
|
+
t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
|
|
3157
|
+
t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
|
|
3158
|
+
t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
|
|
3122
3159
|
});
|
|
3123
3160
|
}
|
|
3124
3161
|
}
|
|
@@ -3129,11 +3166,226 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
|
3129
3166
|
});
|
|
3130
3167
|
}
|
|
3131
3168
|
|
|
3132
|
-
// src/
|
|
3169
|
+
// src/tasks/servicesRegistry.js
|
|
3170
|
+
function getDb2(context) {
|
|
3171
|
+
const db = context.db;
|
|
3172
|
+
if (!db) {
|
|
3173
|
+
throw new Error("Services registry requires context.db");
|
|
3174
|
+
}
|
|
3175
|
+
return db;
|
|
3176
|
+
}
|
|
3177
|
+
var DEFAULT_GROUP_MAX_INSTANCES = {
|
|
3178
|
+
intake: 1,
|
|
3179
|
+
harvest: 1,
|
|
3180
|
+
harvester: 0,
|
|
3181
|
+
loader: 0,
|
|
3182
|
+
photos: 0,
|
|
3183
|
+
photosprocessor: 0,
|
|
3184
|
+
ingest: 0
|
|
3185
|
+
};
|
|
3186
|
+
function sanitizeNamePart(raw) {
|
|
3187
|
+
const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3188
|
+
return s.slice(0, 80) || "runner";
|
|
3189
|
+
}
|
|
3190
|
+
function resolveMaxInstances(serviceGroup, override) {
|
|
3191
|
+
if (override !== void 0 && Number.isFinite(override)) {
|
|
3192
|
+
return Math.max(0, Math.floor(Number(override)));
|
|
3193
|
+
}
|
|
3194
|
+
const g = serviceGroup.trim().toLowerCase();
|
|
3195
|
+
return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
|
|
3196
|
+
}
|
|
3197
|
+
async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
|
|
3198
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
3199
|
+
let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
|
|
3200
|
+
if (excludeRowId) {
|
|
3201
|
+
q = q.whereNot("id", excludeRowId);
|
|
3202
|
+
}
|
|
3203
|
+
const row = await q.count("id as count").first();
|
|
3204
|
+
return Number(row?.count ?? 0);
|
|
3205
|
+
}
|
|
3206
|
+
async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
|
|
3207
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
3208
|
+
const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
|
|
3209
|
+
const set = /* @__PURE__ */ new Set();
|
|
3210
|
+
for (const r of rows) {
|
|
3211
|
+
const n = Number(r.instance_number);
|
|
3212
|
+
if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
|
|
3213
|
+
}
|
|
3214
|
+
return set;
|
|
3215
|
+
}
|
|
3216
|
+
function isUniqueViolation(error) {
|
|
3217
|
+
const code = error?.code ?? error?.errno;
|
|
3218
|
+
return code === "23505" || String(error?.message || "").includes("duplicate key");
|
|
3219
|
+
}
|
|
3220
|
+
function buildMetadata(options) {
|
|
3221
|
+
const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
|
|
3222
|
+
if (options.target) {
|
|
3223
|
+
base.runnerTarget = options.target;
|
|
3224
|
+
}
|
|
3225
|
+
return toJsonColumn(Object.keys(base).length ? base : null);
|
|
3226
|
+
}
|
|
3227
|
+
function allocateInstanceNumber(occupied, explicit, maxSlots) {
|
|
3228
|
+
if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
|
|
3229
|
+
const e = Math.max(1, Math.floor(Number(explicit)));
|
|
3230
|
+
if (occupied.has(e)) {
|
|
3231
|
+
throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
|
|
3232
|
+
}
|
|
3233
|
+
if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
|
|
3234
|
+
throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
|
|
3235
|
+
}
|
|
3236
|
+
return e;
|
|
3237
|
+
}
|
|
3238
|
+
const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
|
|
3239
|
+
for (let n = 1; n <= cap; n++) {
|
|
3240
|
+
if (!occupied.has(n)) return n;
|
|
3241
|
+
}
|
|
3242
|
+
throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
|
|
3243
|
+
}
|
|
3244
|
+
function defaultServiceName(groupBase, hostBase, instanceNumber) {
|
|
3245
|
+
return `${groupBase}-${hostBase}-${instanceNumber}`;
|
|
3246
|
+
}
|
|
3247
|
+
async function registerInServicesRegistry(context, options) {
|
|
3248
|
+
const db = getDb2(context);
|
|
3249
|
+
const registryTable = queueToTableNames(options.queueName).registryTable;
|
|
3250
|
+
const serviceGroup = options.serviceGroup.trim();
|
|
3251
|
+
if (!serviceGroup) {
|
|
3252
|
+
throw new Error("registerInServicesRegistry: serviceGroup is required");
|
|
3253
|
+
}
|
|
3254
|
+
const serverName = import_node_os.default.hostname();
|
|
3255
|
+
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
3256
|
+
const meta = buildMetadata(options);
|
|
3257
|
+
const groupBase = sanitizeNamePart(serviceGroup);
|
|
3258
|
+
const hostBase = sanitizeNamePart(serverName);
|
|
3259
|
+
const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
|
|
3260
|
+
const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
|
|
3261
|
+
if (maxAllowed > 0 && aliveCount >= maxAllowed) {
|
|
3262
|
+
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
|
|
3263
|
+
if (options.enforceMaxInstances) {
|
|
3264
|
+
throw new Error(msg);
|
|
3265
|
+
}
|
|
3266
|
+
context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
|
|
3267
|
+
}
|
|
3268
|
+
const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
|
|
3269
|
+
const cutoff = new Date(Date.now() - options.staleMs);
|
|
3270
|
+
const MAX_ATTEMPTS = 8;
|
|
3271
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
3272
|
+
const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
|
|
3273
|
+
const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
|
|
3274
|
+
const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
|
|
3275
|
+
const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
3276
|
+
if (existing) {
|
|
3277
|
+
const lastSeen = new Date(existing.last_seen_at);
|
|
3278
|
+
const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
|
|
3279
|
+
if (isAlive) {
|
|
3280
|
+
if (options.serviceName?.trim()) {
|
|
3281
|
+
throw new Error(
|
|
3282
|
+
`[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
|
|
3283
|
+
);
|
|
3284
|
+
}
|
|
3285
|
+
context.logger.warn?.(
|
|
3286
|
+
`[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
|
|
3287
|
+
);
|
|
3288
|
+
if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
|
|
3289
|
+
throw new Error(
|
|
3290
|
+
`[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
|
|
3291
|
+
);
|
|
3292
|
+
}
|
|
3293
|
+
await new Promise((r) => setTimeout(r, 50 + attempt * 30));
|
|
3294
|
+
continue;
|
|
3295
|
+
}
|
|
3296
|
+
await db(registryTable).where({ id: existing.id }).update({
|
|
3297
|
+
server_name: serverName,
|
|
3298
|
+
pid,
|
|
3299
|
+
metadata: meta,
|
|
3300
|
+
service_group: serviceGroup,
|
|
3301
|
+
instance_number: instanceNumber,
|
|
3302
|
+
last_seen_at: db.fn.now()
|
|
3303
|
+
});
|
|
3304
|
+
const reg = {
|
|
3305
|
+
serviceName: serviceNameRaw,
|
|
3306
|
+
serviceGroup,
|
|
3307
|
+
queueName: options.queueName,
|
|
3308
|
+
target: options.target,
|
|
3309
|
+
rowId: String(existing.id),
|
|
3310
|
+
registryTable,
|
|
3311
|
+
instanceNumber
|
|
3312
|
+
};
|
|
3313
|
+
context.servicesRegistry = reg;
|
|
3314
|
+
context.runnerHeartbeat = reg;
|
|
3315
|
+
context.logger.info?.(
|
|
3316
|
+
`[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
3317
|
+
);
|
|
3318
|
+
return reg;
|
|
3319
|
+
}
|
|
3320
|
+
try {
|
|
3321
|
+
const rows = await db(registryTable).insert({
|
|
3322
|
+
queue_name: options.queueName,
|
|
3323
|
+
service_group: serviceGroup,
|
|
3324
|
+
instance_number: instanceNumber,
|
|
3325
|
+
service_name: serviceNameRaw,
|
|
3326
|
+
server_name: serverName,
|
|
3327
|
+
pid,
|
|
3328
|
+
metadata: meta,
|
|
3329
|
+
last_seen_at: db.fn.now(),
|
|
3330
|
+
created_at: db.fn.now()
|
|
3331
|
+
}).returning(["id", "service_name"]);
|
|
3332
|
+
const row = Array.isArray(rows) ? rows[0] : rows;
|
|
3333
|
+
let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
|
|
3334
|
+
if (!rowId) {
|
|
3335
|
+
const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
3336
|
+
rowId = again?.id != null ? String(again.id) : "";
|
|
3337
|
+
}
|
|
3338
|
+
if (!rowId) continue;
|
|
3339
|
+
const regNew = {
|
|
3340
|
+
serviceName: String(row?.service_name ?? serviceNameRaw),
|
|
3341
|
+
serviceGroup,
|
|
3342
|
+
queueName: options.queueName,
|
|
3343
|
+
target: options.target,
|
|
3344
|
+
rowId,
|
|
3345
|
+
registryTable,
|
|
3346
|
+
instanceNumber
|
|
3347
|
+
};
|
|
3348
|
+
context.servicesRegistry = regNew;
|
|
3349
|
+
context.runnerHeartbeat = regNew;
|
|
3350
|
+
context.logger.info?.(
|
|
3351
|
+
`[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
3352
|
+
);
|
|
3353
|
+
return regNew;
|
|
3354
|
+
} catch (error) {
|
|
3355
|
+
if (!isUniqueViolation(error)) {
|
|
3356
|
+
throw error;
|
|
3357
|
+
}
|
|
3358
|
+
context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
throw new Error(
|
|
3362
|
+
`[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
|
|
3363
|
+
);
|
|
3364
|
+
}
|
|
3365
|
+
async function touchServicesRegistry(context, registration) {
|
|
3366
|
+
const db = getDb2(context);
|
|
3367
|
+
const serverName = import_node_os.default.hostname();
|
|
3368
|
+
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
3369
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
3370
|
+
last_seen_at: db.fn.now(),
|
|
3371
|
+
server_name: serverName,
|
|
3372
|
+
pid
|
|
3373
|
+
});
|
|
3374
|
+
}
|
|
3375
|
+
async function unregisterServicesRegistry(context, registration) {
|
|
3376
|
+
const db = getDb2(context);
|
|
3377
|
+
await db(registration.registryTable).where({ id: registration.rowId }).delete();
|
|
3378
|
+
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
// src/tasks/taskLogs.js
|
|
3382
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
3383
|
+
|
|
3384
|
+
// src/filedatabase/index.js
|
|
3133
3385
|
var import_fs4 = __toESM(require("fs"), 1);
|
|
3134
3386
|
var import_path4 = __toESM(require("path"), 1);
|
|
3135
3387
|
|
|
3136
|
-
// src/filedatabase/serializers.
|
|
3388
|
+
// src/filedatabase/serializers.js
|
|
3137
3389
|
function detectDataType(data) {
|
|
3138
3390
|
if (Array.isArray(data)) {
|
|
3139
3391
|
return "json-array";
|
|
@@ -3165,7 +3417,7 @@ function deserializeData(rawData, dataType) {
|
|
|
3165
3417
|
}
|
|
3166
3418
|
}
|
|
3167
3419
|
|
|
3168
|
-
// src/filedatabase/index.
|
|
3420
|
+
// src/filedatabase/index.js
|
|
3169
3421
|
var FileDatabase = class _FileDatabase {
|
|
3170
3422
|
basePath;
|
|
3171
3423
|
namespace;
|
|
@@ -3251,7 +3503,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3251
3503
|
if (errors.length) {
|
|
3252
3504
|
throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
|
|
3253
3505
|
}
|
|
3254
|
-
|
|
3506
|
+
const parts = [this.basePath, this.namespace];
|
|
3255
3507
|
if (this.tableName) {
|
|
3256
3508
|
parts.push(...this.tableName.split("/"));
|
|
3257
3509
|
}
|
|
@@ -3744,14 +3996,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
3744
3996
|
* Prepare the instance for read or write operations
|
|
3745
3997
|
* This discovers state and sets up internal members based on mode and current data
|
|
3746
3998
|
*/
|
|
3747
|
-
async prepare(
|
|
3999
|
+
async prepare(options) {
|
|
4000
|
+
const { write, read, version, deferInitialVersion } = options;
|
|
3748
4001
|
if (write) {
|
|
3749
4002
|
if (this.versioned) {
|
|
3750
4003
|
if (this.currentVersion === null) {
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
4004
|
+
if (!deferInitialVersion) {
|
|
4005
|
+
await this.makeNewVersion();
|
|
4006
|
+
this.metadata = this.getDefaultMetadata();
|
|
4007
|
+
this.metadata.version = this.currentVersion;
|
|
4008
|
+
this.makeNewFile();
|
|
4009
|
+
}
|
|
3755
4010
|
} else {
|
|
3756
4011
|
if (!this.metadata.files.length) {
|
|
3757
4012
|
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
@@ -3861,7 +4116,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3861
4116
|
if (options.forceNewVersion && !this.versioned) {
|
|
3862
4117
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
3863
4118
|
}
|
|
3864
|
-
await this.prepare({ write: true });
|
|
4119
|
+
await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
|
|
3865
4120
|
const incomingDataType = detectDataType(data);
|
|
3866
4121
|
this.metadata.dataType = incomingDataType;
|
|
3867
4122
|
if (options.forceNewVersion) {
|
|
@@ -4155,7 +4410,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4155
4410
|
}
|
|
4156
4411
|
};
|
|
4157
4412
|
|
|
4158
|
-
// src/tasks/taskLogs.
|
|
4413
|
+
// src/tasks/taskLogs.js
|
|
4159
4414
|
function getLogsState(context) {
|
|
4160
4415
|
const holder = context;
|
|
4161
4416
|
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
@@ -4208,6 +4463,89 @@ function getLogsState(context) {
|
|
|
4208
4463
|
holder.__tasksLogsState = state;
|
|
4209
4464
|
return state;
|
|
4210
4465
|
}
|
|
4466
|
+
function ipcLogTargetKey(target) {
|
|
4467
|
+
const bp = target.basePath ?? "";
|
|
4468
|
+
const ns = target.namespace ?? "";
|
|
4469
|
+
return `${bp}::${ns}::${target.tableName}`;
|
|
4470
|
+
}
|
|
4471
|
+
function ipcFileLogsTableNameForSourceResource(source, resource) {
|
|
4472
|
+
const seg = (s) => {
|
|
4473
|
+
const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
4474
|
+
return t.length ? t : "x";
|
|
4475
|
+
};
|
|
4476
|
+
return `${seg(source)}/${seg(resource)}`;
|
|
4477
|
+
}
|
|
4478
|
+
async function readTaskIpcLogsSnapshot(context, options) {
|
|
4479
|
+
const holder = context;
|
|
4480
|
+
const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
|
|
4481
|
+
const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
|
|
4482
|
+
const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
|
|
4483
|
+
const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
|
|
4484
|
+
const fd = new FileDatabase({
|
|
4485
|
+
basePath,
|
|
4486
|
+
namespace,
|
|
4487
|
+
tableName,
|
|
4488
|
+
versioned: true,
|
|
4489
|
+
useMetadata: true,
|
|
4490
|
+
maxVersions: 30,
|
|
4491
|
+
pageSize: 2e3,
|
|
4492
|
+
logger: holder.logger
|
|
4493
|
+
});
|
|
4494
|
+
const versions = await fd.getVersions();
|
|
4495
|
+
if (versions.length === 0) {
|
|
4496
|
+
return { records: [], latestTs: null };
|
|
4497
|
+
}
|
|
4498
|
+
const latest = versions[versions.length - 1];
|
|
4499
|
+
const raw = await fd.read({ version: latest });
|
|
4500
|
+
const arr = Array.isArray(raw) ? raw : [];
|
|
4501
|
+
let filtered = arr;
|
|
4502
|
+
if (options.afterTs && String(options.afterTs).trim()) {
|
|
4503
|
+
const cut = String(options.afterTs).trim();
|
|
4504
|
+
filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
|
|
4505
|
+
}
|
|
4506
|
+
let latestTs = null;
|
|
4507
|
+
for (const r of filtered) {
|
|
4508
|
+
const ts = typeof r?.ts === "string" ? String(r.ts) : null;
|
|
4509
|
+
if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
|
|
4510
|
+
}
|
|
4511
|
+
const incremental = !!(options.afterTs && String(options.afterTs).trim());
|
|
4512
|
+
const maxReturn = incremental ? 1e4 : tail;
|
|
4513
|
+
const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
|
|
4514
|
+
return { records: sliced, latestTs };
|
|
4515
|
+
}
|
|
4516
|
+
function getLogsStateForTarget(context, target) {
|
|
4517
|
+
const holder = context;
|
|
4518
|
+
const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
|
|
4519
|
+
const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
4520
|
+
if (!enabled) return null;
|
|
4521
|
+
if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
|
|
4522
|
+
const map = holder.__tasksLogsTargetStates;
|
|
4523
|
+
const key = ipcLogTargetKey(target);
|
|
4524
|
+
if (map.has(key)) return map.get(key);
|
|
4525
|
+
const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
|
|
4526
|
+
const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
|
|
4527
|
+
const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
|
|
4528
|
+
const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
|
|
4529
|
+
const db = new FileDatabase({
|
|
4530
|
+
basePath,
|
|
4531
|
+
namespace,
|
|
4532
|
+
tableName: target.tableName,
|
|
4533
|
+
versioned: true,
|
|
4534
|
+
useMetadata: true,
|
|
4535
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4536
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4537
|
+
logger: holder.logger
|
|
4538
|
+
});
|
|
4539
|
+
const state = {
|
|
4540
|
+
db,
|
|
4541
|
+
errorDb: null,
|
|
4542
|
+
queue: Promise.resolve(),
|
|
4543
|
+
initialized: false,
|
|
4544
|
+
errorInitialized: false
|
|
4545
|
+
};
|
|
4546
|
+
map.set(key, state);
|
|
4547
|
+
return state;
|
|
4548
|
+
}
|
|
4211
4549
|
function isErrorPayload(payload) {
|
|
4212
4550
|
if (!payload) return false;
|
|
4213
4551
|
if (typeof payload === "object") {
|
|
@@ -4227,14 +4565,26 @@ function buildLogRecord(task, payload) {
|
|
|
4227
4565
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4228
4566
|
opid: task.opid ?? null,
|
|
4229
4567
|
taskId: task.id,
|
|
4230
|
-
taskName: task.
|
|
4231
|
-
target: task.
|
|
4568
|
+
taskName: task.name,
|
|
4569
|
+
target: task.service_group,
|
|
4232
4570
|
source: typeof params.source === "string" ? params.source : null,
|
|
4233
4571
|
resource: typeof params.resource === "string" ? params.resource : null,
|
|
4234
4572
|
payload
|
|
4235
4573
|
};
|
|
4236
4574
|
}
|
|
4237
|
-
function appendTaskIpcLog(context, task, payload) {
|
|
4575
|
+
function appendTaskIpcLog(context, task, payload, target) {
|
|
4576
|
+
if (target) {
|
|
4577
|
+
const state2 = getLogsStateForTarget(context, target);
|
|
4578
|
+
if (!state2?.db) return;
|
|
4579
|
+
const record2 = buildLogRecord(task, payload);
|
|
4580
|
+
state2.queue = state2.queue.then(async () => {
|
|
4581
|
+
await state2.db.write([record2], { forceNewVersion: !state2.initialized });
|
|
4582
|
+
state2.initialized = true;
|
|
4583
|
+
}).catch((error) => {
|
|
4584
|
+
context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
|
|
4585
|
+
});
|
|
4586
|
+
return;
|
|
4587
|
+
}
|
|
4238
4588
|
const state = getLogsState(context);
|
|
4239
4589
|
if (!state.db && !state.errorDb) return;
|
|
4240
4590
|
const record = buildLogRecord(task, payload);
|
|
@@ -4252,83 +4602,280 @@ function appendTaskIpcLog(context, task, payload) {
|
|
|
4252
4602
|
});
|
|
4253
4603
|
}
|
|
4254
4604
|
|
|
4255
|
-
// src/tasks/
|
|
4256
|
-
var
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
if (last < first) {
|
|
4270
|
-
[first, last] = [last, first];
|
|
4271
|
-
}
|
|
4272
|
-
const values = [];
|
|
4273
|
-
for (let i = first; i <= last; i += 1) {
|
|
4274
|
-
values.push(i);
|
|
4275
|
-
}
|
|
4276
|
-
current = current.replace(raw, values.join(","));
|
|
4277
|
-
}
|
|
4278
|
-
return current;
|
|
4279
|
-
}
|
|
4280
|
-
function resolveSteps(field) {
|
|
4281
|
-
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
4282
|
-
if (!match) return field;
|
|
4283
|
-
const base = match[1];
|
|
4284
|
-
const step = Number(match[2]);
|
|
4285
|
-
if (!Number.isFinite(step) || step <= 0) return field;
|
|
4286
|
-
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
4287
|
-
}
|
|
4288
|
-
function convertPattern(pattern) {
|
|
4289
|
-
const parts = pattern.trim().split(/\s+/);
|
|
4290
|
-
if (parts.length !== 6) {
|
|
4291
|
-
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
4292
|
-
}
|
|
4293
|
-
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
4294
|
-
}
|
|
4295
|
-
function fieldMatches(field, value) {
|
|
4296
|
-
const allowed = field.split(",").map((v) => Number(v));
|
|
4297
|
-
return allowed.includes(value);
|
|
4298
|
-
}
|
|
4299
|
-
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
4300
|
-
const parsed = convertPattern(pattern);
|
|
4301
|
-
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());
|
|
4302
|
-
}
|
|
4303
|
-
|
|
4304
|
-
// src/tasks/TaskMaster.ts
|
|
4305
|
-
var TaskMaster = class {
|
|
4306
|
-
context;
|
|
4307
|
-
task;
|
|
4605
|
+
// src/tasks/AbstractTask.js
|
|
4606
|
+
var AbstractTask = class _AbstractTask {
|
|
4607
|
+
/**
|
|
4608
|
+
* Whether `send-task` should wait for completion (and print a result
|
|
4609
|
+
* report) when no explicit `--wait` / `--noWait` flag is given. Defaults
|
|
4610
|
+
* to false; short-lived probe tasks (e.g. `ping`) override to true.
|
|
4611
|
+
*
|
|
4612
|
+
* @type {boolean}
|
|
4613
|
+
*/
|
|
4614
|
+
static defaultWaitForResult = false;
|
|
4615
|
+
/**
|
|
4616
|
+
* @param {object} context Runner context (db, logger, params, emitter...).
|
|
4617
|
+
* @param {object} task Task row as claimed from the queue.
|
|
4618
|
+
*/
|
|
4308
4619
|
constructor(context, task) {
|
|
4309
4620
|
this.context = context;
|
|
4310
4621
|
this.task = task;
|
|
4311
4622
|
}
|
|
4623
|
+
/**
|
|
4624
|
+
* Return a short reason string when the task should be deferred (e.g. "locked
|
|
4625
|
+
* by source"), or `false`/falsy when it is free to run. Default: always `false`.
|
|
4626
|
+
*
|
|
4627
|
+
* @returns {string | false | Promise<string | false>}
|
|
4628
|
+
*/
|
|
4312
4629
|
cantRunReason() {
|
|
4313
4630
|
return false;
|
|
4314
4631
|
}
|
|
4632
|
+
/**
|
|
4633
|
+
* Called by the runner when a stop has been requested. Subclasses running
|
|
4634
|
+
* long loops should flip a flag here and check it between iterations.
|
|
4635
|
+
*
|
|
4636
|
+
* @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
|
|
4637
|
+
*/
|
|
4315
4638
|
requestStop(_allowanceMs) {
|
|
4316
4639
|
}
|
|
4640
|
+
/**
|
|
4641
|
+
* Perform the task. Must be implemented by subclasses.
|
|
4642
|
+
*
|
|
4643
|
+
* @param {(progress: unknown) => Promise<void>} _reportProgress
|
|
4644
|
+
* Updates the DB `progress` column. Accepts any serializable value;
|
|
4645
|
+
* strings are stored verbatim, objects are JSON-stringified.
|
|
4646
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
4647
|
+
*/
|
|
4648
|
+
async run(_reportProgress) {
|
|
4649
|
+
throw new Error("AbstractTask.run must be implemented by subclass");
|
|
4650
|
+
}
|
|
4651
|
+
/**
|
|
4652
|
+
* Resolve a complete row payload for this task — envelope fields (queue,
|
|
4653
|
+
* priority, targeting, schedule…) plus the inner `params` blob produced by
|
|
4654
|
+
* {@link AbstractTask.resolveCustomParams}. Output shape matches
|
|
4655
|
+
* {@link enqueueTask}'s `options` argument, so the typical call is:
|
|
4656
|
+
*
|
|
4657
|
+
* const payload = await TaskClass.resolveParams(context, { name });
|
|
4658
|
+
* await enqueueTask(context, payload);
|
|
4659
|
+
*
|
|
4660
|
+
* Validation failures throw {@link ParamError} so the script aborts before
|
|
4661
|
+
* a malformed row hits the DB.
|
|
4662
|
+
*
|
|
4663
|
+
* @param {object} context
|
|
4664
|
+
* @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
|
|
4665
|
+
* Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
|
|
4666
|
+
* `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
|
|
4667
|
+
* `nextRunAt`, plus `params` (object — overlay onto inner blob).
|
|
4668
|
+
* @returns {Promise<object>}
|
|
4669
|
+
*/
|
|
4670
|
+
static async resolveParams(context, overrides = {}) {
|
|
4671
|
+
const main = _AbstractTask._resolveMainFields(context, overrides);
|
|
4672
|
+
const params = await this.resolveCustomParams(context, overrides);
|
|
4673
|
+
return { ...main, params };
|
|
4674
|
+
}
|
|
4675
|
+
/**
|
|
4676
|
+
* Resolve the inner JSON blob stored in the `params` column. Default
|
|
4677
|
+
* implementation passes through `--paramsJson` (parsed as a JSON object)
|
|
4678
|
+
* overlaid with `overrides.params` when supplied; returns `null` when
|
|
4679
|
+
* neither is provided.
|
|
4680
|
+
*
|
|
4681
|
+
* Subclasses with typed fields should override and call
|
|
4682
|
+
* {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
|
|
4683
|
+
* resolution, then validate and throw {@link ParamError} on bad input.
|
|
4684
|
+
*
|
|
4685
|
+
* @param {object} context
|
|
4686
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4687
|
+
* @returns {Promise<object|null>}
|
|
4688
|
+
*/
|
|
4689
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
4690
|
+
return _AbstractTask._defaultParamsBlob(context, overrides);
|
|
4691
|
+
}
|
|
4692
|
+
/**
|
|
4693
|
+
* Read main task envelope fields from `context.params` (CLI/env), with
|
|
4694
|
+
* any matching key on `overrides` taking precedence. Internal; called by
|
|
4695
|
+
* {@link AbstractTask.resolveParams}.
|
|
4696
|
+
*
|
|
4697
|
+
* @param {object} context
|
|
4698
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4699
|
+
* @returns {object}
|
|
4700
|
+
*/
|
|
4701
|
+
static _resolveMainFields(context, overrides = {}) {
|
|
4702
|
+
const defs2 = {
|
|
4703
|
+
queueName: "string default tasks",
|
|
4704
|
+
priority: "number default 50",
|
|
4705
|
+
serviceGroup: "string",
|
|
4706
|
+
serviceName: "string",
|
|
4707
|
+
instanceNumber: "number",
|
|
4708
|
+
serverName: "string",
|
|
4709
|
+
opid: "string",
|
|
4710
|
+
schedule: "string"
|
|
4711
|
+
};
|
|
4712
|
+
const cli = context.params.getAllForModule("task-envelope", defs2);
|
|
4713
|
+
const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
|
|
4714
|
+
if (!name) {
|
|
4715
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
4716
|
+
}
|
|
4717
|
+
let instanceNumber;
|
|
4718
|
+
const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
|
|
4719
|
+
if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
|
|
4720
|
+
const n = Number(rawInstance);
|
|
4721
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
4722
|
+
throw new ParamError("--instanceNumber must be a positive integer when set");
|
|
4723
|
+
}
|
|
4724
|
+
instanceNumber = n;
|
|
4725
|
+
} else {
|
|
4726
|
+
instanceNumber = null;
|
|
4727
|
+
}
|
|
4728
|
+
const priorityRaw = overrides.priority ?? cli.priority ?? 50;
|
|
4729
|
+
const priority = Number(priorityRaw);
|
|
4730
|
+
if (!Number.isFinite(priority)) {
|
|
4731
|
+
throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
|
|
4732
|
+
}
|
|
4733
|
+
return {
|
|
4734
|
+
name,
|
|
4735
|
+
queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
|
|
4736
|
+
priority,
|
|
4737
|
+
serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
|
|
4738
|
+
serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
|
|
4739
|
+
instanceNumber,
|
|
4740
|
+
serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
|
|
4741
|
+
opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
|
|
4742
|
+
schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
|
|
4743
|
+
nextRunAt: overrides.nextRunAt ?? null
|
|
4744
|
+
};
|
|
4745
|
+
}
|
|
4746
|
+
/**
|
|
4747
|
+
* Default inner-params resolver: parses `--paramsJson` (must be a JSON
|
|
4748
|
+
* object), then overlays `overrides.params` on top. Returns `null` when
|
|
4749
|
+
* neither is provided.
|
|
4750
|
+
*
|
|
4751
|
+
* @param {object} context
|
|
4752
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4753
|
+
* @returns {object|null}
|
|
4754
|
+
*/
|
|
4755
|
+
static _defaultParamsBlob(context, overrides = {}) {
|
|
4756
|
+
const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
|
|
4757
|
+
const fromJson = parseParamsJson(cli.paramsJson);
|
|
4758
|
+
const fromOverride = pickParamsObject(overrides);
|
|
4759
|
+
if (!fromJson && !fromOverride) return null;
|
|
4760
|
+
return { ...fromJson ?? {}, ...fromOverride ?? {} };
|
|
4761
|
+
}
|
|
4762
|
+
/**
|
|
4763
|
+
* Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
|
|
4764
|
+
* params (per `defs`) plus `--paramsJson` under a module namespace, then
|
|
4765
|
+
* merges them with explicit `overrides.params` in increasing priority:
|
|
4766
|
+
*
|
|
4767
|
+
* typed CLI flags → --paramsJson → overrides.params
|
|
4768
|
+
*
|
|
4769
|
+
* Undefined values are dropped so defaults declared in `defs` aren't
|
|
4770
|
+
* overwritten by missing-flag noise. Returns the merged object; the
|
|
4771
|
+
* caller is responsible for validation and throwing `ParamError`.
|
|
4772
|
+
*
|
|
4773
|
+
* @param {object} context
|
|
4774
|
+
* @param {string} moduleName Namespace for `--showUsedParams` grouping.
|
|
4775
|
+
* @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
|
|
4776
|
+
* @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
|
|
4777
|
+
* @returns {Record<string, unknown>}
|
|
4778
|
+
*/
|
|
4779
|
+
static _mergeTypedParams(context, moduleName, defs2, overrides = {}) {
|
|
4780
|
+
const fullDefs = { ...defs2, paramsJson: "string" };
|
|
4781
|
+
const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
|
|
4782
|
+
const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
|
|
4783
|
+
const fromCli = {};
|
|
4784
|
+
for (const [k, v] of Object.entries(cliRaw)) {
|
|
4785
|
+
if (k === "paramsJson") continue;
|
|
4786
|
+
if (v !== void 0 && v !== null) fromCli[k] = v;
|
|
4787
|
+
}
|
|
4788
|
+
const fromOverride = pickParamsObject(overrides) ?? {};
|
|
4789
|
+
return { ...fromCli, ...fromJson, ...fromOverride };
|
|
4790
|
+
}
|
|
4317
4791
|
};
|
|
4792
|
+
function emptyToUndef(s) {
|
|
4793
|
+
if (s === void 0 || s === null) return void 0;
|
|
4794
|
+
if (typeof s !== "string") return s;
|
|
4795
|
+
const t = s.trim();
|
|
4796
|
+
return t.length ? t : void 0;
|
|
4797
|
+
}
|
|
4798
|
+
function parseParamsJson(raw) {
|
|
4799
|
+
if (raw == null) return null;
|
|
4800
|
+
const t = String(raw).trim();
|
|
4801
|
+
if (!t) return null;
|
|
4802
|
+
let parsed;
|
|
4803
|
+
try {
|
|
4804
|
+
parsed = JSON.parse(t);
|
|
4805
|
+
} catch (e) {
|
|
4806
|
+
throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
|
|
4807
|
+
}
|
|
4808
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4809
|
+
throw new ParamError("--paramsJson must be a JSON object");
|
|
4810
|
+
}
|
|
4811
|
+
return parsed;
|
|
4812
|
+
}
|
|
4813
|
+
function pickParamsObject(overrides) {
|
|
4814
|
+
const p = overrides?.params;
|
|
4815
|
+
if (p && typeof p === "object" && !Array.isArray(p)) return p;
|
|
4816
|
+
return void 0;
|
|
4817
|
+
}
|
|
4318
4818
|
|
|
4319
|
-
// src/tasks/coreTasks/TaskPing.
|
|
4320
|
-
var TaskPing = class extends
|
|
4819
|
+
// src/tasks/coreTasks/TaskPing.js
|
|
4820
|
+
var TaskPing = class extends AbstractTask {
|
|
4821
|
+
/** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
|
|
4822
|
+
static defaultWaitForResult = true;
|
|
4823
|
+
/** Ping takes no params. */
|
|
4824
|
+
static async resolveCustomParams() {
|
|
4825
|
+
return null;
|
|
4826
|
+
}
|
|
4827
|
+
/**
|
|
4828
|
+
* @returns {Promise<{ success: true, results: "pong" }>}
|
|
4829
|
+
*/
|
|
4321
4830
|
async run() {
|
|
4322
4831
|
this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
|
|
4323
4832
|
return { success: true, results: "pong" };
|
|
4324
4833
|
}
|
|
4325
4834
|
};
|
|
4326
4835
|
|
|
4327
|
-
// src/tasks/coreTasks/TaskSampleProcess.
|
|
4328
|
-
var TaskSampleProcess = class extends
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4836
|
+
// src/tasks/coreTasks/TaskSampleProcess.js
|
|
4837
|
+
var TaskSampleProcess = class extends AbstractTask {
|
|
4838
|
+
/**
|
|
4839
|
+
* @param {object} context
|
|
4840
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4841
|
+
* @returns {Promise<{ total: number, delay: number, name?: string }>}
|
|
4842
|
+
*/
|
|
4843
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
4844
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
|
|
4845
|
+
total: "number default 10",
|
|
4846
|
+
delay: "number default 1000",
|
|
4847
|
+
name: "string"
|
|
4848
|
+
}, overrides);
|
|
4849
|
+
const total = Number(merged.total);
|
|
4850
|
+
const delay = Number(merged.delay);
|
|
4851
|
+
if (!Number.isInteger(total) || total <= 0) {
|
|
4852
|
+
throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
|
|
4853
|
+
}
|
|
4854
|
+
if (!Number.isInteger(delay) || delay < 0) {
|
|
4855
|
+
throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
|
|
4856
|
+
}
|
|
4857
|
+
const out = { total, delay };
|
|
4858
|
+
if (typeof merged.name === "string" && merged.name.trim()) {
|
|
4859
|
+
out.name = merged.name.trim();
|
|
4860
|
+
}
|
|
4861
|
+
return out;
|
|
4862
|
+
}
|
|
4863
|
+
/**
|
|
4864
|
+
* @param {object} context
|
|
4865
|
+
* @param {object} task
|
|
4866
|
+
*/
|
|
4867
|
+
constructor(context, task) {
|
|
4868
|
+
super(context, task);
|
|
4869
|
+
this.stopRequested = false;
|
|
4870
|
+
this.stopAllowanceMs = 0;
|
|
4871
|
+
this.stopDecisionLogged = false;
|
|
4872
|
+
}
|
|
4873
|
+
/**
|
|
4874
|
+
* Runner-facing stop signal. Records the allowance window so the main loop
|
|
4875
|
+
* can decide per-iteration whether to finish or abort early.
|
|
4876
|
+
*
|
|
4877
|
+
* @param {number} allowanceMs
|
|
4878
|
+
*/
|
|
4332
4879
|
requestStop(allowanceMs) {
|
|
4333
4880
|
this.stopRequested = true;
|
|
4334
4881
|
this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
|
|
@@ -4336,6 +4883,14 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4336
4883
|
`[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
|
|
4337
4884
|
);
|
|
4338
4885
|
}
|
|
4886
|
+
/**
|
|
4887
|
+
* Iterate `total` times, sleeping `delay` ms between ticks and reporting
|
|
4888
|
+
* progress every iteration. Validates params up front; invalid values short-
|
|
4889
|
+
* circuit to a structured failure without starting the loop.
|
|
4890
|
+
*
|
|
4891
|
+
* @param {(progress: object) => Promise<void>} reportProgress
|
|
4892
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
4893
|
+
*/
|
|
4339
4894
|
async run(reportProgress) {
|
|
4340
4895
|
const totalRaw = this.task?.params?.total ?? 10;
|
|
4341
4896
|
const delayRaw = this.task?.params?.delay ?? 1e3;
|
|
@@ -4417,7 +4972,7 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4417
4972
|
}
|
|
4418
4973
|
};
|
|
4419
4974
|
|
|
4420
|
-
// src/tasks/coreTasks/TaskShellCommand.
|
|
4975
|
+
// src/tasks/coreTasks/TaskShellCommand.js
|
|
4421
4976
|
var import_node_child_process = require("child_process");
|
|
4422
4977
|
function runShellCommand(command, cwd) {
|
|
4423
4978
|
return new Promise((resolve2, reject) => {
|
|
@@ -4447,7 +5002,27 @@ function runShellCommand(command, cwd) {
|
|
|
4447
5002
|
});
|
|
4448
5003
|
});
|
|
4449
5004
|
}
|
|
4450
|
-
var TaskShellCommand = class extends
|
|
5005
|
+
var TaskShellCommand = class extends AbstractTask {
|
|
5006
|
+
/**
|
|
5007
|
+
* @param {object} context
|
|
5008
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5009
|
+
* @returns {Promise<{ command: string, cwd?: string }>}
|
|
5010
|
+
*/
|
|
5011
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5012
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
|
|
5013
|
+
command: "string",
|
|
5014
|
+
cwd: "string"
|
|
5015
|
+
}, overrides);
|
|
5016
|
+
const command = typeof merged.command === "string" ? merged.command.trim() : "";
|
|
5017
|
+
if (!command) {
|
|
5018
|
+
throw new ParamError('shellCommand: param "command" must be a non-empty string');
|
|
5019
|
+
}
|
|
5020
|
+
const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
|
|
5021
|
+
return cwd ? { command, cwd } : { command };
|
|
5022
|
+
}
|
|
5023
|
+
/**
|
|
5024
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5025
|
+
*/
|
|
4451
5026
|
async run() {
|
|
4452
5027
|
const params = this.task?.params;
|
|
4453
5028
|
const commandRaw = typeof params === "string" ? params : params?.command;
|
|
@@ -4496,8 +5071,8 @@ var TaskShellCommand = class extends TaskMaster {
|
|
|
4496
5071
|
}
|
|
4497
5072
|
};
|
|
4498
5073
|
|
|
4499
|
-
// src/tasks/coreTasks/TaskSystemInfo.
|
|
4500
|
-
var
|
|
5074
|
+
// src/tasks/coreTasks/TaskSystemInfo.js
|
|
5075
|
+
var import_node_os2 = __toESM(require("os"), 1);
|
|
4501
5076
|
var import_promises = __toESM(require("fs/promises"), 1);
|
|
4502
5077
|
function toGb(valueBytes) {
|
|
4503
5078
|
return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
|
|
@@ -4516,13 +5091,22 @@ async function getDiskStats() {
|
|
|
4516
5091
|
free: toGb(free)
|
|
4517
5092
|
};
|
|
4518
5093
|
}
|
|
4519
|
-
var TaskSystemInfo = class extends
|
|
5094
|
+
var TaskSystemInfo = class extends AbstractTask {
|
|
5095
|
+
/** Same UX expectation as `ping` — short probe, print the result. */
|
|
5096
|
+
static defaultWaitForResult = true;
|
|
5097
|
+
/** systemInfo takes no params. */
|
|
5098
|
+
static async resolveCustomParams() {
|
|
5099
|
+
return null;
|
|
5100
|
+
}
|
|
5101
|
+
/**
|
|
5102
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5103
|
+
*/
|
|
4520
5104
|
async run() {
|
|
4521
5105
|
try {
|
|
4522
|
-
const totalMemory =
|
|
4523
|
-
const freeMemory =
|
|
5106
|
+
const totalMemory = import_node_os2.default.totalmem();
|
|
5107
|
+
const freeMemory = import_node_os2.default.freemem();
|
|
4524
5108
|
const usedMemory = totalMemory - freeMemory;
|
|
4525
|
-
const cpus =
|
|
5109
|
+
const cpus = import_node_os2.default.cpus();
|
|
4526
5110
|
const cpuUtilization = cpus.map((cpu) => {
|
|
4527
5111
|
const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
|
|
4528
5112
|
const usage = (total - cpu.times.idle) / total * 100;
|
|
@@ -4548,10 +5132,10 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
4548
5132
|
utilization: cpuUtilization
|
|
4549
5133
|
},
|
|
4550
5134
|
runtime: {
|
|
4551
|
-
platform:
|
|
4552
|
-
arch:
|
|
4553
|
-
uptimeSec:
|
|
4554
|
-
hostname:
|
|
5135
|
+
platform: import_node_os2.default.platform(),
|
|
5136
|
+
arch: import_node_os2.default.arch(),
|
|
5137
|
+
uptimeSec: import_node_os2.default.uptime(),
|
|
5138
|
+
hostname: import_node_os2.default.hostname()
|
|
4555
5139
|
}
|
|
4556
5140
|
};
|
|
4557
5141
|
this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
|
|
@@ -4568,8 +5152,31 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
4568
5152
|
}
|
|
4569
5153
|
};
|
|
4570
5154
|
|
|
4571
|
-
// src/tasks/coreTasks/TaskSumAB.
|
|
4572
|
-
var TaskSumAB = class extends
|
|
5155
|
+
// src/tasks/coreTasks/TaskSumAB.js
|
|
5156
|
+
var TaskSumAB = class extends AbstractTask {
|
|
5157
|
+
/** Short, deterministic — wait by default so callers see the sum. */
|
|
5158
|
+
static defaultWaitForResult = true;
|
|
5159
|
+
/**
|
|
5160
|
+
* @param {object} context
|
|
5161
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5162
|
+
* @returns {Promise<{ a: number, b: number }>}
|
|
5163
|
+
*/
|
|
5164
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5165
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
|
|
5166
|
+
a: "number",
|
|
5167
|
+
b: "number"
|
|
5168
|
+
}, overrides);
|
|
5169
|
+
if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
|
|
5170
|
+
throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
|
|
5171
|
+
}
|
|
5172
|
+
if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
|
|
5173
|
+
throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
|
|
5174
|
+
}
|
|
5175
|
+
return { a: merged.a, b: merged.b };
|
|
5176
|
+
}
|
|
5177
|
+
/**
|
|
5178
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5179
|
+
*/
|
|
4573
5180
|
async run() {
|
|
4574
5181
|
const a = this.task?.params?.a;
|
|
4575
5182
|
const b = this.task?.params?.b;
|
|
@@ -4600,8 +5207,46 @@ var TaskSumAB = class extends TaskMaster {
|
|
|
4600
5207
|
}
|
|
4601
5208
|
};
|
|
4602
5209
|
|
|
4603
|
-
// src/tasks/coreTasks/TaskStopRunner.
|
|
4604
|
-
var TaskStopRunner = class extends
|
|
5210
|
+
// src/tasks/coreTasks/TaskStopRunner.js
|
|
5211
|
+
var TaskStopRunner = class extends AbstractTask {
|
|
5212
|
+
/**
|
|
5213
|
+
* Stop tasks must target a concrete instance — without `serviceName` the
|
|
5214
|
+
* row would race against any worker on the queue. Layered on top of the
|
|
5215
|
+
* envelope built by {@link AbstractTask.resolveParams}.
|
|
5216
|
+
*
|
|
5217
|
+
* @param {object} context
|
|
5218
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5219
|
+
* @returns {Promise<object>}
|
|
5220
|
+
*/
|
|
5221
|
+
static async resolveParams(context, overrides = {}) {
|
|
5222
|
+
const main = await super.resolveParams(context, overrides);
|
|
5223
|
+
if (!main.serviceName) {
|
|
5224
|
+
throw new ParamError(
|
|
5225
|
+
"stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
|
|
5226
|
+
);
|
|
5227
|
+
}
|
|
5228
|
+
return main;
|
|
5229
|
+
}
|
|
5230
|
+
/**
|
|
5231
|
+
* @param {object} context
|
|
5232
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5233
|
+
* @returns {Promise<{ allowanceMs: number }>}
|
|
5234
|
+
*/
|
|
5235
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5236
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
|
|
5237
|
+
allowanceMs: "number default 5000"
|
|
5238
|
+
}, overrides);
|
|
5239
|
+
const allowanceMs = Number(merged.allowanceMs);
|
|
5240
|
+
if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
|
|
5241
|
+
throw new ParamError(
|
|
5242
|
+
`stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
|
|
5243
|
+
);
|
|
5244
|
+
}
|
|
5245
|
+
return { allowanceMs };
|
|
5246
|
+
}
|
|
5247
|
+
/**
|
|
5248
|
+
* @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
|
|
5249
|
+
*/
|
|
4605
5250
|
async run() {
|
|
4606
5251
|
const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
|
|
4607
5252
|
this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
|
|
@@ -4616,45 +5261,203 @@ var TaskStopRunner = class extends TaskMaster {
|
|
|
4616
5261
|
}
|
|
4617
5262
|
};
|
|
4618
5263
|
|
|
4619
|
-
// src/tasks/
|
|
5264
|
+
// src/tasks/coreTasks/TaskGetLogs.js
|
|
5265
|
+
var TaskGetLogs = class extends AbstractTask {
|
|
5266
|
+
/**
|
|
5267
|
+
* @param {object} context
|
|
5268
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5269
|
+
* @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
|
|
5270
|
+
*/
|
|
5271
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5272
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
|
|
5273
|
+
source: "string",
|
|
5274
|
+
resource: "string",
|
|
5275
|
+
tail: "number default 100",
|
|
5276
|
+
afterTs: "string"
|
|
5277
|
+
}, overrides);
|
|
5278
|
+
const source = typeof merged.source === "string" ? merged.source.trim() : "";
|
|
5279
|
+
const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
|
|
5280
|
+
if (!source) throw new ParamError('getLogs: param "source" is required');
|
|
5281
|
+
if (!resource) throw new ParamError('getLogs: param "resource" is required');
|
|
5282
|
+
let tail = Number(merged.tail);
|
|
5283
|
+
if (!Number.isFinite(tail) || tail < 1) tail = 100;
|
|
5284
|
+
tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
|
|
5285
|
+
const out = { source, resource, tail };
|
|
5286
|
+
if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
|
|
5287
|
+
out.afterTs = merged.afterTs.trim();
|
|
5288
|
+
}
|
|
5289
|
+
return out;
|
|
5290
|
+
}
|
|
5291
|
+
/**
|
|
5292
|
+
* @param {unknown} _reportProgress Unused (single-shot read, no progress events).
|
|
5293
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5294
|
+
*/
|
|
5295
|
+
async run(_reportProgress) {
|
|
5296
|
+
const p = this.task.params ?? {};
|
|
5297
|
+
const source = String(p.source ?? "").trim();
|
|
5298
|
+
const resource = String(p.resource ?? "").trim();
|
|
5299
|
+
const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
|
|
5300
|
+
const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
|
|
5301
|
+
if (!source || !resource) {
|
|
5302
|
+
return {
|
|
5303
|
+
success: false,
|
|
5304
|
+
results: { error: 'getLogs requires params "source" and "resource"' }
|
|
5305
|
+
};
|
|
5306
|
+
}
|
|
5307
|
+
try {
|
|
5308
|
+
const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
|
|
5309
|
+
source,
|
|
5310
|
+
resource,
|
|
5311
|
+
tail,
|
|
5312
|
+
afterTs
|
|
5313
|
+
});
|
|
5314
|
+
return {
|
|
5315
|
+
success: true,
|
|
5316
|
+
results: { records, latestTs, source, resource }
|
|
5317
|
+
};
|
|
5318
|
+
} catch (e) {
|
|
5319
|
+
return {
|
|
5320
|
+
success: false,
|
|
5321
|
+
results: { error: e?.message ?? String(e) }
|
|
5322
|
+
};
|
|
5323
|
+
}
|
|
5324
|
+
}
|
|
5325
|
+
};
|
|
5326
|
+
|
|
5327
|
+
// src/tasks/TasksRegistry.js
|
|
4620
5328
|
var TasksRegistry = class _TasksRegistry {
|
|
4621
|
-
|
|
5329
|
+
/**
|
|
5330
|
+
* @param {Record<string, Function>} [initial] Optional seed entries to copy in.
|
|
5331
|
+
*/
|
|
4622
5332
|
constructor(initial) {
|
|
5333
|
+
this.map = {};
|
|
4623
5334
|
if (initial) {
|
|
4624
5335
|
this.addMany(initial);
|
|
4625
5336
|
}
|
|
4626
5337
|
}
|
|
5338
|
+
/**
|
|
5339
|
+
* Build a registry pre-populated with every core task plus legacy aliases.
|
|
5340
|
+
* Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
|
|
5341
|
+
*
|
|
5342
|
+
* @returns {TasksRegistry}
|
|
5343
|
+
*/
|
|
4627
5344
|
static withCoreTasks() {
|
|
4628
|
-
return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
|
|
5345
|
+
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);
|
|
4629
5346
|
}
|
|
5347
|
+
/**
|
|
5348
|
+
* Register a single task class under a name. Overwrites any previous entry.
|
|
5349
|
+
*
|
|
5350
|
+
* @param {string} taskName
|
|
5351
|
+
* @param {Function} taskClass Subclass of `AbstractTask`.
|
|
5352
|
+
* @returns {this}
|
|
5353
|
+
*/
|
|
4630
5354
|
add(taskName, taskClass) {
|
|
4631
5355
|
this.map[taskName] = taskClass;
|
|
4632
5356
|
return this;
|
|
4633
5357
|
}
|
|
5358
|
+
/**
|
|
5359
|
+
* Bulk-register a name → class map. Later calls override earlier ones.
|
|
5360
|
+
*
|
|
5361
|
+
* @param {Record<string, Function>} entries
|
|
5362
|
+
* @returns {this}
|
|
5363
|
+
*/
|
|
4634
5364
|
addMany(entries) {
|
|
4635
5365
|
for (const [name, klass] of Object.entries(entries)) {
|
|
4636
5366
|
this.add(name, klass);
|
|
4637
5367
|
}
|
|
4638
5368
|
return this;
|
|
4639
5369
|
}
|
|
5370
|
+
/**
|
|
5371
|
+
* Look up a task class by name. Returns `undefined` when the name is unknown;
|
|
5372
|
+
* the runner treats that as "some other worker may handle this" and skips.
|
|
5373
|
+
*
|
|
5374
|
+
* @param {string} taskName
|
|
5375
|
+
* @returns {Function | undefined}
|
|
5376
|
+
*/
|
|
4640
5377
|
get(taskName) {
|
|
4641
5378
|
return this.map[taskName];
|
|
4642
5379
|
}
|
|
5380
|
+
/**
|
|
5381
|
+
* Strict variant of {@link get}: throws {@link ParamError} (with the list
|
|
5382
|
+
* of supported names) when `taskName` is unknown. Use from enqueuer code
|
|
5383
|
+
* paths where an unknown name is a hard CLI/programmer error.
|
|
5384
|
+
*
|
|
5385
|
+
* @param {string} taskName
|
|
5386
|
+
* @returns {Function}
|
|
5387
|
+
*/
|
|
5388
|
+
requireClass(taskName) {
|
|
5389
|
+
const TaskClass = taskName ? this.map[taskName] : void 0;
|
|
5390
|
+
if (!TaskClass) {
|
|
5391
|
+
const supported = this.listSupportedTasks().join(", ") || "(none)";
|
|
5392
|
+
throw new ParamError(
|
|
5393
|
+
`Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
|
|
5394
|
+
);
|
|
5395
|
+
}
|
|
5396
|
+
return TaskClass;
|
|
5397
|
+
}
|
|
5398
|
+
/**
|
|
5399
|
+
* Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
|
|
5400
|
+
* from `overrides` or `context.params`, look up the class, and delegate
|
|
5401
|
+
* to its static {@link AbstractTask.resolveParams} with `name` seeded into
|
|
5402
|
+
* the overrides. The returned object is shaped for {@link enqueueTask}.
|
|
5403
|
+
*
|
|
5404
|
+
* Validation failures (unknown task, missing required custom params, etc.)
|
|
5405
|
+
* surface as {@link ParamError} so the caller aborts cleanly before any
|
|
5406
|
+
* row is inserted.
|
|
5407
|
+
*
|
|
5408
|
+
* @param {object} context
|
|
5409
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5410
|
+
* @returns {Promise<object>}
|
|
5411
|
+
*/
|
|
5412
|
+
async resolveTaskParams(context, overrides = {}) {
|
|
5413
|
+
const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
|
|
5414
|
+
const fromCli = context.params.get("name", "string");
|
|
5415
|
+
const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
|
|
5416
|
+
const name = overrideName || cliName;
|
|
5417
|
+
if (!name) {
|
|
5418
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
5419
|
+
}
|
|
5420
|
+
const TaskClass = this.requireClass(name);
|
|
5421
|
+
return TaskClass.resolveParams(context, { ...overrides, name });
|
|
5422
|
+
}
|
|
5423
|
+
/**
|
|
5424
|
+
* Names of every registered task, sorted alphabetically (useful for CLI output
|
|
5425
|
+
* and allowlist sanity checks).
|
|
5426
|
+
*
|
|
5427
|
+
* @returns {string[]}
|
|
5428
|
+
*/
|
|
4643
5429
|
listSupportedTasks() {
|
|
4644
5430
|
return Object.keys(this.map).sort();
|
|
4645
5431
|
}
|
|
5432
|
+
/**
|
|
5433
|
+
* Shallow copy of the internal map, for handing to `addMany` on another registry
|
|
5434
|
+
* or for serialization.
|
|
5435
|
+
*
|
|
5436
|
+
* @returns {Record<string, Function>}
|
|
5437
|
+
*/
|
|
4646
5438
|
toObject() {
|
|
4647
5439
|
return { ...this.map };
|
|
4648
5440
|
}
|
|
4649
5441
|
};
|
|
4650
5442
|
|
|
4651
|
-
// src/tasks/
|
|
5443
|
+
// src/tasks/serviceTaskAllowlist.js
|
|
5444
|
+
function normalizeAllowedTasks(value) {
|
|
5445
|
+
if (!value) return void 0;
|
|
5446
|
+
if (Array.isArray(value)) {
|
|
5447
|
+
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
5448
|
+
return out2.length ? out2 : void 0;
|
|
5449
|
+
}
|
|
5450
|
+
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
5451
|
+
return out.length ? out : void 0;
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5454
|
+
// src/tasks/taskScriptRunner.js
|
|
4652
5455
|
var import_node_child_process2 = require("child_process");
|
|
4653
5456
|
|
|
4654
|
-
// src/tasks/index.
|
|
5457
|
+
// src/tasks/index.js
|
|
4655
5458
|
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
4656
5459
|
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
4657
|
-
function
|
|
5460
|
+
function getDb3(context) {
|
|
4658
5461
|
const db = context.db;
|
|
4659
5462
|
if (!db) {
|
|
4660
5463
|
throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
|
|
@@ -4666,15 +5469,6 @@ function normalizeRegistry(registry) {
|
|
|
4666
5469
|
if (registry instanceof TasksRegistry) return registry;
|
|
4667
5470
|
return new TasksRegistry().addMany(registry);
|
|
4668
5471
|
}
|
|
4669
|
-
function normalizeAllowedTasks(value) {
|
|
4670
|
-
if (!value) return void 0;
|
|
4671
|
-
if (Array.isArray(value)) {
|
|
4672
|
-
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
4673
|
-
return out2.length ? out2 : void 0;
|
|
4674
|
-
}
|
|
4675
|
-
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
4676
|
-
return out.length ? out : void 0;
|
|
4677
|
-
}
|
|
4678
5472
|
async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
|
|
4679
5473
|
context.logger.warn?.(`[tasks] signaling ${runningTaskInstances.size} running task(s) to stop`);
|
|
4680
5474
|
for (const [, taskInstance] of runningTaskInstances) {
|
|
@@ -4689,19 +5483,21 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
|
|
|
4689
5483
|
context.emitter.emit("stop", allowanceMs);
|
|
4690
5484
|
}
|
|
4691
5485
|
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
4692
|
-
const db =
|
|
4693
|
-
const taskName = row.
|
|
5486
|
+
const db = getDb3(context);
|
|
5487
|
+
const taskName = row.name;
|
|
4694
5488
|
const TaskClass = registry.get(taskName);
|
|
4695
|
-
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
4696
5489
|
if (!TaskClass) {
|
|
4697
5490
|
const err = { message: `Unknown task "${taskName}"` };
|
|
4698
|
-
await db(historyTable).insert(
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
5491
|
+
await db(historyTable).insert(
|
|
5492
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
5493
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5494
|
+
success: false,
|
|
5495
|
+
status: "failed",
|
|
5496
|
+
status_changed_at: db.fn.now(),
|
|
5497
|
+
params: toJsonColumn(row.params),
|
|
5498
|
+
results: toJsonColumn(err)
|
|
5499
|
+
})
|
|
5500
|
+
);
|
|
4705
5501
|
if (row.schedule) {
|
|
4706
5502
|
await db(tasksTable).where({ id: row.id }).update({
|
|
4707
5503
|
started_at: null,
|
|
@@ -4709,7 +5505,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4709
5505
|
success: false,
|
|
4710
5506
|
results: toJsonColumn(err),
|
|
4711
5507
|
past_due: null,
|
|
4712
|
-
|
|
5508
|
+
status: "paused",
|
|
5509
|
+
status_changed_at: db.fn.now(),
|
|
4713
5510
|
progress: LOCKED_BY_ERROR_MESSAGE
|
|
4714
5511
|
});
|
|
4715
5512
|
} else {
|
|
@@ -4736,13 +5533,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4736
5533
|
} finally {
|
|
4737
5534
|
runningTaskInstances.delete(row.id);
|
|
4738
5535
|
}
|
|
4739
|
-
await db(historyTable).insert(
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
5536
|
+
await db(historyTable).insert(
|
|
5537
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
5538
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5539
|
+
success,
|
|
5540
|
+
status: success ? "completed" : "failed",
|
|
5541
|
+
status_changed_at: db.fn.now(),
|
|
5542
|
+
params: toJsonColumn(row.params),
|
|
5543
|
+
results: toJsonColumn(results)
|
|
5544
|
+
})
|
|
5545
|
+
);
|
|
4746
5546
|
if (!success) {
|
|
4747
5547
|
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
4748
5548
|
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
@@ -4757,19 +5557,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4757
5557
|
const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
|
|
4758
5558
|
appendTaskIpcLog(context, row, {
|
|
4759
5559
|
level: "error",
|
|
4760
|
-
message: `[tasks] task failed: ${row.
|
|
5560
|
+
message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
|
|
4761
5561
|
details: results
|
|
4762
5562
|
});
|
|
4763
5563
|
}
|
|
4764
5564
|
if (row.schedule) {
|
|
4765
5565
|
if (success) {
|
|
5566
|
+
let nextRunAt = null;
|
|
5567
|
+
try {
|
|
5568
|
+
nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
|
|
5569
|
+
} catch (e) {
|
|
5570
|
+
context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
|
|
5571
|
+
}
|
|
4766
5572
|
await db(tasksTable).where({ id: row.id }).update({
|
|
4767
5573
|
started_at: null,
|
|
4768
5574
|
completed_at: /* @__PURE__ */ new Date(),
|
|
4769
5575
|
success,
|
|
4770
5576
|
results: toJsonColumn(results),
|
|
4771
5577
|
progress: null,
|
|
4772
|
-
past_due: null
|
|
5578
|
+
past_due: null,
|
|
5579
|
+
status: "idle",
|
|
5580
|
+
status_changed_at: db.fn.now(),
|
|
5581
|
+
next_run_at: nextRunAt,
|
|
5582
|
+
// Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
|
|
5583
|
+
service_name: null,
|
|
5584
|
+
server_name: null,
|
|
5585
|
+
instance_number: null
|
|
4773
5586
|
});
|
|
4774
5587
|
} else {
|
|
4775
5588
|
await db(tasksTable).where({ id: row.id }).update({
|
|
@@ -4777,7 +5590,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4777
5590
|
completed_at: /* @__PURE__ */ new Date(),
|
|
4778
5591
|
success,
|
|
4779
5592
|
results: toJsonColumn(results),
|
|
4780
|
-
|
|
5593
|
+
status: "paused",
|
|
5594
|
+
status_changed_at: db.fn.now(),
|
|
4781
5595
|
progress: LOCKED_BY_ERROR_MESSAGE,
|
|
4782
5596
|
past_due: null
|
|
4783
5597
|
});
|
|
@@ -4789,195 +5603,361 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4789
5603
|
const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
|
|
4790
5604
|
return { stopRunnerRequested, stopAllowanceMs };
|
|
4791
5605
|
}
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
5606
|
+
function shuffleTaskRowsInPlace(rows) {
|
|
5607
|
+
for (let i = rows.length - 1; i > 0; i--) {
|
|
5608
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
5609
|
+
const t = rows[i];
|
|
5610
|
+
rows[i] = rows[j];
|
|
5611
|
+
rows[j] = t;
|
|
5612
|
+
}
|
|
5613
|
+
}
|
|
5614
|
+
async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
|
|
5615
|
+
const db = getDb3(context);
|
|
5616
|
+
let query = db(tasksTable).where({ status: "idle" }).where(function() {
|
|
5617
|
+
this.whereNull("service_group").orWhere({ service_group: serviceGroup });
|
|
5618
|
+
}).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);
|
|
4795
5619
|
if (taskNames && taskNames.length > 0) {
|
|
4796
|
-
query = query.whereIn("
|
|
5620
|
+
query = query.whereIn("name", taskNames);
|
|
5621
|
+
}
|
|
5622
|
+
if (runnerIdentity) {
|
|
5623
|
+
query = query.where(function() {
|
|
5624
|
+
this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
|
|
5625
|
+
}).where(function() {
|
|
5626
|
+
this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
|
|
5627
|
+
}).where(function() {
|
|
5628
|
+
this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
|
|
5629
|
+
});
|
|
5630
|
+
} else {
|
|
5631
|
+
query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
|
|
4797
5632
|
}
|
|
4798
5633
|
const candidates = await query;
|
|
5634
|
+
shuffleTaskRowsInPlace(candidates);
|
|
4799
5635
|
for (const row of candidates) {
|
|
4800
5636
|
if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
|
|
4801
5637
|
continue;
|
|
4802
5638
|
}
|
|
4803
|
-
const TaskClass = registry.get(row.
|
|
4804
|
-
if (TaskClass) {
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
5639
|
+
const TaskClass = registry.get(row.name);
|
|
5640
|
+
if (!TaskClass) {
|
|
5641
|
+
continue;
|
|
5642
|
+
}
|
|
5643
|
+
const taskInstance = new TaskClass(context, row);
|
|
5644
|
+
const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
|
|
5645
|
+
if (reason) {
|
|
5646
|
+
if (!row.past_due) {
|
|
5647
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
5648
|
+
past_due: db.fn.now(),
|
|
5649
|
+
progress: String(reason)
|
|
5650
|
+
});
|
|
4815
5651
|
}
|
|
5652
|
+
continue;
|
|
4816
5653
|
}
|
|
4817
|
-
const
|
|
5654
|
+
const claimPatch = {
|
|
5655
|
+
started_at: db.fn.now(),
|
|
5656
|
+
status: "running",
|
|
5657
|
+
status_changed_at: db.fn.now()
|
|
5658
|
+
};
|
|
5659
|
+
if (runnerIdentity) {
|
|
5660
|
+
claimPatch.service_name = runnerIdentity.service_name;
|
|
5661
|
+
claimPatch.server_name = runnerIdentity.server_name;
|
|
5662
|
+
claimPatch.instance_number = runnerIdentity.instance_number;
|
|
5663
|
+
}
|
|
5664
|
+
const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
|
|
4818
5665
|
const claimed = Array.isArray(updated) ? updated[0] : null;
|
|
4819
5666
|
if (claimed) return claimed;
|
|
4820
5667
|
}
|
|
4821
5668
|
return null;
|
|
4822
5669
|
}
|
|
4823
5670
|
async function runTasksLoop(context, options) {
|
|
4824
|
-
const
|
|
5671
|
+
const queueName = options.queueName ?? "tasks";
|
|
4825
5672
|
const target = options.target;
|
|
4826
5673
|
const pollMs = options.pollMs ?? 1e3;
|
|
4827
|
-
const
|
|
5674
|
+
const claimJitterMs = options.claimJitterMs ?? 0;
|
|
5675
|
+
const maxParallel = options.maxParallel ?? 32;
|
|
4828
5676
|
const scanLimit = options.scanLimit ?? 100;
|
|
4829
5677
|
const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
4830
5678
|
const registry = normalizeRegistry(options.registry);
|
|
4831
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
5679
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
4832
5680
|
if (!target) throw new Error("runTasksLoop: target is required");
|
|
5681
|
+
context.tasksQueueName = queueName;
|
|
4833
5682
|
const runningPromises = /* @__PURE__ */ new Set();
|
|
4834
5683
|
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
4835
5684
|
let runningStopControlPromise = null;
|
|
4836
5685
|
let stopRequested = false;
|
|
4837
5686
|
let stopAllowanceMs = 5e3;
|
|
4838
|
-
context.
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
)
|
|
4849
|
-
|
|
4850
|
-
|
|
5687
|
+
context.tasksRunnerStop = false;
|
|
5688
|
+
let registryReg = null;
|
|
5689
|
+
let registryInterval = null;
|
|
5690
|
+
let runnerIdentity = null;
|
|
5691
|
+
const hbGroup = options.runnerServiceGroup?.trim();
|
|
5692
|
+
if (hbGroup) {
|
|
5693
|
+
const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
|
|
5694
|
+
const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
|
|
5695
|
+
const defaultMeta = {
|
|
5696
|
+
component: "tasks-runner",
|
|
5697
|
+
allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
|
|
5698
|
+
};
|
|
5699
|
+
registryReg = await registerInServicesRegistry(context, {
|
|
5700
|
+
queueName,
|
|
5701
|
+
target,
|
|
5702
|
+
serviceGroup: hbGroup,
|
|
5703
|
+
serviceName: options.runnerServiceName,
|
|
5704
|
+
instanceNumber: options.runnerInstanceNumber,
|
|
5705
|
+
staleMs,
|
|
5706
|
+
groupMaxInstances: options.runnerGroupMaxInstances,
|
|
5707
|
+
enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
|
|
5708
|
+
metadata: options.runnerMetadata ?? defaultMeta
|
|
5709
|
+
});
|
|
5710
|
+
runnerIdentity = {
|
|
5711
|
+
service_name: registryReg.serviceName,
|
|
5712
|
+
server_name: import_node_os3.default.hostname(),
|
|
5713
|
+
instance_number: registryReg.instanceNumber
|
|
5714
|
+
};
|
|
5715
|
+
registryInterval = setInterval(() => {
|
|
5716
|
+
void touchServicesRegistry(context, registryReg).catch((err) => {
|
|
5717
|
+
context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
|
|
5718
|
+
});
|
|
5719
|
+
}, hbIntervalMs);
|
|
5720
|
+
}
|
|
5721
|
+
try {
|
|
5722
|
+
while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
|
|
5723
|
+
if (!runningStopControlPromise) {
|
|
5724
|
+
const claimedStopTask = await claimNextRunnableTask(
|
|
5725
|
+
context,
|
|
5726
|
+
tasksTable,
|
|
5727
|
+
target,
|
|
5728
|
+
registry,
|
|
5729
|
+
10,
|
|
5730
|
+
["stopRunner", "stop"],
|
|
5731
|
+
runnerIdentity
|
|
5732
|
+
);
|
|
5733
|
+
if (claimedStopTask) {
|
|
5734
|
+
runningStopControlPromise = executeClaimedTask(
|
|
5735
|
+
context,
|
|
5736
|
+
tasksTable,
|
|
5737
|
+
historyTable,
|
|
5738
|
+
claimedStopTask,
|
|
5739
|
+
registry,
|
|
5740
|
+
runningTaskInstances
|
|
5741
|
+
).then(async (outcome) => {
|
|
5742
|
+
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5743
|
+
stopRequested = true;
|
|
5744
|
+
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5745
|
+
context.tasksRunnerStop = true;
|
|
5746
|
+
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5747
|
+
}
|
|
5748
|
+
}).finally(() => {
|
|
5749
|
+
runningStopControlPromise = null;
|
|
5750
|
+
});
|
|
5751
|
+
}
|
|
5752
|
+
}
|
|
5753
|
+
if (claimJitterMs > 0) {
|
|
5754
|
+
await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
|
|
5755
|
+
}
|
|
5756
|
+
while (runningPromises.size < maxParallel) {
|
|
5757
|
+
const claimed = await claimNextRunnableTask(
|
|
4851
5758
|
context,
|
|
4852
5759
|
tasksTable,
|
|
4853
|
-
|
|
4854
|
-
claimedStopTask,
|
|
5760
|
+
target,
|
|
4855
5761
|
registry,
|
|
4856
|
-
|
|
4857
|
-
|
|
5762
|
+
scanLimit,
|
|
5763
|
+
allowedTasks,
|
|
5764
|
+
runnerIdentity
|
|
5765
|
+
);
|
|
5766
|
+
if (!claimed) break;
|
|
5767
|
+
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
4858
5768
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
4859
5769
|
stopRequested = true;
|
|
4860
5770
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
4861
|
-
context.
|
|
5771
|
+
context.tasksRunnerStop = true;
|
|
4862
5772
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
4863
5773
|
}
|
|
4864
5774
|
}).finally(() => {
|
|
4865
|
-
|
|
5775
|
+
runningPromises.delete(p);
|
|
4866
5776
|
});
|
|
5777
|
+
runningPromises.add(p);
|
|
5778
|
+
}
|
|
5779
|
+
const wakePromises = [...runningPromises];
|
|
5780
|
+
if (runningStopControlPromise) {
|
|
5781
|
+
wakePromises.push(runningStopControlPromise);
|
|
5782
|
+
}
|
|
5783
|
+
if (wakePromises.length === 0) {
|
|
5784
|
+
await sleepMs(pollMs);
|
|
5785
|
+
} else {
|
|
5786
|
+
const safe = wakePromises.map((p) => p.catch(() => void 0));
|
|
5787
|
+
await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
|
|
4867
5788
|
}
|
|
4868
5789
|
}
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
context,
|
|
4872
|
-
tasksTable,
|
|
4873
|
-
target,
|
|
4874
|
-
registry,
|
|
4875
|
-
scanLimit,
|
|
4876
|
-
allowedTasks
|
|
4877
|
-
);
|
|
4878
|
-
if (!claimed) break;
|
|
4879
|
-
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
4880
|
-
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
4881
|
-
stopRequested = true;
|
|
4882
|
-
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
4883
|
-
context.__tasksRunnerStop = true;
|
|
4884
|
-
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
4885
|
-
}
|
|
4886
|
-
}).finally(() => {
|
|
4887
|
-
runningPromises.delete(p);
|
|
4888
|
-
});
|
|
4889
|
-
runningPromises.add(p);
|
|
5790
|
+
if (context.isStop() && !stopRequested) {
|
|
5791
|
+
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
4890
5792
|
}
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
5793
|
+
if (runningPromises.size > 0) {
|
|
5794
|
+
if (stopRequested) {
|
|
5795
|
+
await Promise.race([
|
|
5796
|
+
Promise.allSettled(Array.from(runningPromises)),
|
|
5797
|
+
sleepMs(stopAllowanceMs).then(() => {
|
|
5798
|
+
context.logger.warn?.(
|
|
5799
|
+
`[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
|
|
5800
|
+
);
|
|
5801
|
+
})
|
|
5802
|
+
]);
|
|
5803
|
+
} else {
|
|
5804
|
+
await Promise.allSettled(Array.from(runningPromises));
|
|
5805
|
+
}
|
|
5806
|
+
}
|
|
5807
|
+
} finally {
|
|
5808
|
+
if (registryInterval) {
|
|
5809
|
+
clearInterval(registryInterval);
|
|
5810
|
+
registryInterval = null;
|
|
5811
|
+
}
|
|
5812
|
+
if (registryReg) {
|
|
5813
|
+
await unregisterServicesRegistry(context, registryReg).catch((err) => {
|
|
5814
|
+
context.logger.warn?.(`[services-registry] unregister failed: ${err?.message ?? String(err)}`);
|
|
5815
|
+
});
|
|
5816
|
+
registryReg = null;
|
|
5817
|
+
delete context.servicesRegistry;
|
|
5818
|
+
delete context.runnerHeartbeat;
|
|
4908
5819
|
}
|
|
4909
5820
|
}
|
|
4910
5821
|
}
|
|
4911
5822
|
var TasksManager = class _TasksManager {
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
5823
|
+
/**
|
|
5824
|
+
* @param {object} context
|
|
5825
|
+
* @param {{
|
|
5826
|
+
* queueName?: string,
|
|
5827
|
+
* target?: string,
|
|
5828
|
+
* recreateTaskTables?: boolean,
|
|
5829
|
+
* pollMs?: number,
|
|
5830
|
+
* claimJitterMs?: number,
|
|
5831
|
+
* maxParallel?: number,
|
|
5832
|
+
* scanLimit?: number,
|
|
5833
|
+
* allowedTasks?: string | string[],
|
|
5834
|
+
* registry?: TasksRegistry | Record<string, Function>,
|
|
5835
|
+
* runnerServiceGroup?: string,
|
|
5836
|
+
* runnerServiceName?: string,
|
|
5837
|
+
* runnerInstanceNumber?: number,
|
|
5838
|
+
* runnerHeartbeatIntervalMs?: number,
|
|
5839
|
+
* runnerHeartbeatStaleMs?: number,
|
|
5840
|
+
* runnerGroupMaxInstances?: number,
|
|
5841
|
+
* runnerEnforceMaxInstances?: boolean,
|
|
5842
|
+
* runnerMetadata?: Record<string, unknown>,
|
|
5843
|
+
* }} [options]
|
|
5844
|
+
*/
|
|
4921
5845
|
constructor(context, options = {}) {
|
|
4922
5846
|
this.context = context;
|
|
4923
|
-
this.
|
|
5847
|
+
this.queueName = options.queueName ?? "tasks";
|
|
4924
5848
|
this.target = options.target ?? "localRunner";
|
|
4925
5849
|
this.recreateTaskTables = options.recreateTaskTables ?? false;
|
|
4926
5850
|
this.pollMs = options.pollMs ?? 1e3;
|
|
5851
|
+
this.claimJitterMs = options.claimJitterMs ?? 0;
|
|
4927
5852
|
this.maxParallel = options.maxParallel ?? 1;
|
|
4928
5853
|
this.scanLimit = options.scanLimit ?? 100;
|
|
4929
5854
|
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
4930
5855
|
this.registry = normalizeRegistry(options.registry);
|
|
5856
|
+
this.runnerServiceGroup = options.runnerServiceGroup;
|
|
5857
|
+
this.runnerServiceName = options.runnerServiceName;
|
|
5858
|
+
this.runnerInstanceNumber = options.runnerInstanceNumber;
|
|
5859
|
+
this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
|
|
5860
|
+
this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
|
|
5861
|
+
this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
|
|
5862
|
+
this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
|
|
5863
|
+
this.runnerMetadata = options.runnerMetadata;
|
|
4931
5864
|
}
|
|
5865
|
+
/**
|
|
5866
|
+
* Preferred factory: reads defaults from `context.params` (module namespace
|
|
5867
|
+
* `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
|
|
5868
|
+
* and inline options in one consistent resolver.
|
|
5869
|
+
*
|
|
5870
|
+
* @param {object} context
|
|
5871
|
+
* @param {ConstructorParameters<typeof TasksManager>[1]} [options]
|
|
5872
|
+
* @returns {TasksManager}
|
|
5873
|
+
*/
|
|
4932
5874
|
static init(context, options = {}) {
|
|
4933
5875
|
const defs2 = {
|
|
4934
5876
|
table: "string default tasks",
|
|
4935
5877
|
target: "string default localRunner",
|
|
4936
5878
|
recreateTaskTables: "boolean default false",
|
|
4937
5879
|
pollMs: "number default 1000",
|
|
5880
|
+
claimJitterMs: "number default 0",
|
|
4938
5881
|
maxParallel: "number default 1",
|
|
4939
5882
|
scanLimit: "number default 100",
|
|
4940
|
-
allowedTasks: "string"
|
|
5883
|
+
allowedTasks: "string",
|
|
5884
|
+
runnerServiceGroup: "string",
|
|
5885
|
+
runnerServiceName: "string",
|
|
5886
|
+
runnerInstanceNumber: "number",
|
|
5887
|
+
runnerHeartbeatIntervalMs: "number default 10000",
|
|
5888
|
+
runnerHeartbeatStaleMs: "number default 45000",
|
|
5889
|
+
runnerGroupMaxInstances: "number",
|
|
5890
|
+
runnerEnforceMaxInstances: "boolean default true"
|
|
4941
5891
|
};
|
|
4942
|
-
const discovered = context.params.getAllForModule(defs2);
|
|
5892
|
+
const discovered = context.params.getAllForModule("tasks", defs2);
|
|
4943
5893
|
const resolved = {
|
|
4944
|
-
|
|
5894
|
+
queueName: discovered.table,
|
|
4945
5895
|
target: discovered.target,
|
|
4946
5896
|
recreateTaskTables: discovered.recreateTaskTables,
|
|
4947
5897
|
pollMs: discovered.pollMs,
|
|
5898
|
+
claimJitterMs: discovered.claimJitterMs,
|
|
4948
5899
|
maxParallel: discovered.maxParallel,
|
|
4949
5900
|
scanLimit: discovered.scanLimit,
|
|
4950
5901
|
allowedTasks: discovered.allowedTasks,
|
|
5902
|
+
runnerServiceGroup: discovered.runnerServiceGroup,
|
|
5903
|
+
runnerServiceName: discovered.runnerServiceName,
|
|
5904
|
+
runnerInstanceNumber: discovered.runnerInstanceNumber,
|
|
5905
|
+
runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
|
|
5906
|
+
runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
|
|
5907
|
+
runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
|
|
5908
|
+
runnerEnforceMaxInstances: discovered.runnerEnforceMaxInstances,
|
|
4951
5909
|
...options
|
|
4952
5910
|
};
|
|
4953
5911
|
return new _TasksManager(context, resolved);
|
|
4954
5912
|
}
|
|
5913
|
+
/**
|
|
5914
|
+
* Idempotently ensure the three backing tables exist for this queue.
|
|
5915
|
+
*
|
|
5916
|
+
* @param {{ recreate?: boolean }} [options]
|
|
5917
|
+
* @returns {Promise<void>}
|
|
5918
|
+
*/
|
|
4955
5919
|
async ensureTaskTables(options = {}) {
|
|
4956
5920
|
await ensureTaskTables(this.context, {
|
|
4957
|
-
|
|
5921
|
+
queueName: this.queueName,
|
|
4958
5922
|
recreate: options.recreate ?? this.recreateTaskTables
|
|
4959
5923
|
});
|
|
4960
5924
|
}
|
|
5925
|
+
/**
|
|
5926
|
+
* Start the runner loop using this manager's resolved config. Per-call
|
|
5927
|
+
* options override the stored defaults, but `runnerMetadata` still falls
|
|
5928
|
+
* through when omitted.
|
|
5929
|
+
*
|
|
5930
|
+
* @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
|
|
5931
|
+
* @returns {Promise<void>}
|
|
5932
|
+
*/
|
|
4961
5933
|
async runTasksLoop(options = {}) {
|
|
4962
5934
|
await runTasksLoop(this.context, {
|
|
4963
|
-
|
|
5935
|
+
queueName: options.queueName ?? this.queueName,
|
|
4964
5936
|
target: options.target ?? this.target,
|
|
4965
5937
|
pollMs: options.pollMs ?? this.pollMs,
|
|
5938
|
+
claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
|
|
4966
5939
|
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
4967
5940
|
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
4968
5941
|
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
4969
|
-
registry: options.registry ?? this.registry
|
|
5942
|
+
registry: options.registry ?? this.registry,
|
|
5943
|
+
runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
|
|
5944
|
+
runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
|
|
5945
|
+
runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
|
|
5946
|
+
runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
|
|
5947
|
+
runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
|
|
5948
|
+
runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
|
|
5949
|
+
runnerEnforceMaxInstances: options.runnerEnforceMaxInstances ?? this.runnerEnforceMaxInstances,
|
|
5950
|
+
runnerMetadata: options.runnerMetadata ?? this.runnerMetadata
|
|
4970
5951
|
});
|
|
4971
5952
|
}
|
|
4972
5953
|
};
|
|
4973
5954
|
|
|
4974
|
-
// src/scripts/cli-runner.
|
|
5955
|
+
// src/scripts/cli-runner.js
|
|
4975
5956
|
var defs = {
|
|
4976
|
-
dbName: "string default local",
|
|
4977
5957
|
tasksModule: "string"
|
|
4978
5958
|
};
|
|
4979
5959
|
async function loadTasksModule(modulePath) {
|
|
4980
|
-
const absolute =
|
|
5960
|
+
const absolute = import_node_path2.default.isAbsolute(modulePath) ? modulePath : import_node_path2.default.resolve(process.cwd(), modulePath);
|
|
4981
5961
|
const imported = await import((0, import_node_url.pathToFileURL)(absolute).href);
|
|
4982
5962
|
if (!imported.tasksRegistry || typeof imported.tasksRegistry !== "object") {
|
|
4983
5963
|
throw new Error(`tasksModule "${modulePath}" must export "tasksRegistry" object`);
|
|
@@ -4985,11 +5965,8 @@ async function loadTasksModule(modulePath) {
|
|
|
4985
5965
|
return imported.tasksRegistry;
|
|
4986
5966
|
}
|
|
4987
5967
|
var flow = async (context) => {
|
|
4988
|
-
const {
|
|
4989
|
-
|
|
4990
|
-
tasksModule
|
|
4991
|
-
} = context.params.getAll(defs);
|
|
4992
|
-
const db = await dbInit(context, dbName);
|
|
5968
|
+
const { tasksModule } = context.params.getAll(defs);
|
|
5969
|
+
const db = await Db.init(context);
|
|
4993
5970
|
context.db = db;
|
|
4994
5971
|
const registry = new TasksRegistry().addMany(defaultTasksRegistry.toObject());
|
|
4995
5972
|
if (tasksModule) {
|