@nmakarov/cli-toolkit 0.21.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/args.cjs +1 -4
- package/dist/args.cjs.map +1 -1
- package/dist/args.js +1 -1
- package/dist/args.js.map +1 -1
- package/dist/cli-runner.cjs +1370 -623
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +1415 -667
- package/dist/cli-runner.js.map +1 -1
- package/dist/db.cjs +173 -158
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +172 -151
- package/dist/db.js.map +1 -1
- package/dist/errors.cjs +2 -2
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/filedatabase.cjs +19 -19
- package/dist/filedatabase.cjs.map +1 -1
- package/dist/filedatabase.js +19 -16
- package/dist/filedatabase.js.map +1 -1
- package/dist/http-client.cjs +9 -11
- package/dist/http-client.cjs.map +1 -1
- package/dist/http-client.js +10 -9
- package/dist/http-client.js.map +1 -1
- package/dist/http-client2.cjs +34 -37
- package/dist/http-client2.cjs.map +1 -1
- package/dist/http-client2.js +34 -34
- package/dist/http-client2.js.map +1 -1
- package/dist/index.cjs +1831 -713
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1837 -713
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +93 -68
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +108 -82
- package/dist/init.js.map +1 -1
- package/dist/logger.cjs +5 -5
- package/dist/logger.cjs.map +1 -1
- package/dist/logger.js +5 -4
- package/dist/logger.js.map +1 -1
- package/dist/mock-server.cjs +21 -33
- package/dist/mock-server.cjs.map +1 -1
- package/dist/mock-server.js +21 -28
- package/dist/mock-server.js.map +1 -1
- package/dist/params.cjs +18 -9
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +18 -6
- package/dist/params.js.map +1 -1
- package/dist/s3.cjs +286 -0
- package/dist/s3.cjs.map +1 -0
- package/dist/s3.js +273 -0
- package/dist/s3.js.map +1 -0
- package/dist/screen.cjs +34 -39
- package/dist/screen.cjs.map +1 -1
- package/dist/screen.js +48 -46
- package/dist/screen.js.map +1 -1
- package/dist/tasks.cjs +1354 -501
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +1369 -527
- package/dist/tasks.js.map +1 -1
- package/dist/utils.cjs +7 -8
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.js +6 -6
- package/dist/utils.js.map +1 -1
- package/package.json +32 -47
- package/scripts/ssm/{parse-cli.ts → parse-cli.js} +4 -4
- package/scripts/ssm/{ssm-admin.ts → ssm-admin.js} +12 -12
- package/scripts/ssm/{ssm-pull.ts → ssm-pull.js} +10 -13
package/dist/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,14 +907,13 @@ 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];
|
|
@@ -932,12 +927,11 @@ function buildDetailBreadcrumb(path6, suffix = "") {
|
|
|
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.
|
|
1114
|
+
// src/scripts/cli-runner.js
|
|
1121
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
|
|
@@ -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
|
}
|
|
@@ -2071,6 +2065,18 @@ var Params = class _Params {
|
|
|
2071
2065
|
this._currentModule = prev;
|
|
2072
2066
|
}
|
|
2073
2067
|
}
|
|
2068
|
+
/**
|
|
2069
|
+
* Async variant of {@link runWithModule} for modules that await params.get().
|
|
2070
|
+
*/
|
|
2071
|
+
async runWithModuleAsync(moduleName, fn) {
|
|
2072
|
+
const prev = this._currentModule;
|
|
2073
|
+
this._currentModule = moduleName;
|
|
2074
|
+
try {
|
|
2075
|
+
return await fn();
|
|
2076
|
+
} finally {
|
|
2077
|
+
this._currentModule = prev;
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2074
2080
|
/**
|
|
2075
2081
|
* Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
|
|
2076
2082
|
*/
|
|
@@ -2131,11 +2137,11 @@ var Params = class _Params {
|
|
|
2131
2137
|
}
|
|
2132
2138
|
};
|
|
2133
2139
|
|
|
2134
|
-
// src/logger/index.
|
|
2140
|
+
// src/logger/index.js
|
|
2135
2141
|
var import_chalk = __toESM(require("chalk"), 1);
|
|
2136
2142
|
var import_util = __toESM(require("util"), 1);
|
|
2137
2143
|
|
|
2138
|
-
// src/logger/transports.
|
|
2144
|
+
// src/logger/transports.js
|
|
2139
2145
|
var ConsoleTransport = class {
|
|
2140
2146
|
write(payload) {
|
|
2141
2147
|
console.info(payload);
|
|
@@ -2155,7 +2161,7 @@ var ParentProcessTransport = class {
|
|
|
2155
2161
|
}
|
|
2156
2162
|
};
|
|
2157
2163
|
|
|
2158
|
-
// src/logger/index.
|
|
2164
|
+
// src/logger/index.js
|
|
2159
2165
|
var ALL_LEVELS = [
|
|
2160
2166
|
"silly",
|
|
2161
2167
|
"debug",
|
|
@@ -2228,6 +2234,7 @@ var Logger = class _Logger {
|
|
|
2228
2234
|
}
|
|
2229
2235
|
/**
|
|
2230
2236
|
* Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
|
|
2237
|
+
* Params are tracked under the `logger` module for --showUsedParams.
|
|
2231
2238
|
*/
|
|
2232
2239
|
static init(context, options) {
|
|
2233
2240
|
const paramDefs = {
|
|
@@ -2241,7 +2248,7 @@ var Logger = class _Logger {
|
|
|
2241
2248
|
progressWithTimes: "boolean default false",
|
|
2242
2249
|
progressThrottleMs: "number"
|
|
2243
2250
|
};
|
|
2244
|
-
const discovered = context.params.getAllForModule(paramDefs);
|
|
2251
|
+
const discovered = context.params.getAllForModule("logger", paramDefs);
|
|
2245
2252
|
const config2 = { ...discovered, ...options };
|
|
2246
2253
|
const logger = new _Logger(context, config2);
|
|
2247
2254
|
context.logger = logger;
|
|
@@ -2437,9 +2444,9 @@ var Logger = class _Logger {
|
|
|
2437
2444
|
}
|
|
2438
2445
|
};
|
|
2439
2446
|
|
|
2440
|
-
// src/init/index.
|
|
2447
|
+
// src/init/index.js
|
|
2441
2448
|
var import_events = require("events");
|
|
2442
|
-
function extractComponentOptions(opts,
|
|
2449
|
+
function extractComponentOptions(opts, _componentName) {
|
|
2443
2450
|
const reservedKeys = ["overrides", "defaults", "modules"];
|
|
2444
2451
|
const componentOptions = {};
|
|
2445
2452
|
for (const [key, value] of Object.entries(opts)) {
|
|
@@ -2506,6 +2513,19 @@ function printAllParameters(context) {
|
|
|
2506
2513
|
async function init(flow2, opts = {}) {
|
|
2507
2514
|
let stop = false;
|
|
2508
2515
|
let context = null;
|
|
2516
|
+
let cleanupRan = false;
|
|
2517
|
+
const runRegisteredCleanups = async (ctx) => {
|
|
2518
|
+
if (cleanupRan) return;
|
|
2519
|
+
cleanupRan = true;
|
|
2520
|
+
const fns = [...ctx.cleanupFunctions].reverse();
|
|
2521
|
+
for (const fn of fns) {
|
|
2522
|
+
try {
|
|
2523
|
+
await fn(ctx);
|
|
2524
|
+
} catch (error) {
|
|
2525
|
+
ctx.logger.warn("[cleanup] error in cleanup function:", error);
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
};
|
|
2509
2529
|
try {
|
|
2510
2530
|
try {
|
|
2511
2531
|
const screenModule = await Promise.resolve().then(() => (init_screen(), screen_exports));
|
|
@@ -2528,13 +2548,24 @@ async function init(flow2, opts = {}) {
|
|
|
2528
2548
|
printAllParameters(context);
|
|
2529
2549
|
process.exit(0);
|
|
2530
2550
|
}
|
|
2551
|
+
let sigintCount = 0;
|
|
2531
2552
|
process.on("SIGINT", async () => {
|
|
2532
|
-
if (
|
|
2533
|
-
|
|
2534
|
-
|
|
2553
|
+
if (!context) return;
|
|
2554
|
+
sigintCount += 1;
|
|
2555
|
+
if (sigintCount === 1) {
|
|
2556
|
+
stop = true;
|
|
2557
|
+
context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
|
|
2558
|
+
context.emitter.emit("stop", stopAllowance);
|
|
2559
|
+
return;
|
|
2535
2560
|
}
|
|
2561
|
+
context.logger.warn("[process] second SIGINT: running cleanup then exit");
|
|
2562
|
+
await runRegisteredCleanups(context);
|
|
2563
|
+
process.exit(2);
|
|
2564
|
+
});
|
|
2565
|
+
process.on("SIGTERM", () => {
|
|
2566
|
+
if (!context || stop) return;
|
|
2536
2567
|
stop = true;
|
|
2537
|
-
context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
|
|
2568
|
+
context.logger.info(`>> SIGTERM: emitting stop with allowance ${stopAllowance}`);
|
|
2538
2569
|
context.emitter.emit("stop", stopAllowance);
|
|
2539
2570
|
});
|
|
2540
2571
|
await flow2(context);
|
|
@@ -2559,33 +2590,95 @@ async function init(flow2, opts = {}) {
|
|
|
2559
2590
|
}
|
|
2560
2591
|
} finally {
|
|
2561
2592
|
if (context) {
|
|
2562
|
-
|
|
2563
|
-
try {
|
|
2564
|
-
await fn(context);
|
|
2565
|
-
} catch (error) {
|
|
2566
|
-
context.logger.warn("[cleanup] error in cleanup function:", error);
|
|
2567
|
-
}
|
|
2568
|
-
}
|
|
2593
|
+
await runRegisteredCleanups(context);
|
|
2569
2594
|
}
|
|
2570
2595
|
}
|
|
2571
2596
|
}
|
|
2572
2597
|
|
|
2573
|
-
// src/db/index.
|
|
2598
|
+
// src/db/index.js
|
|
2574
2599
|
var import_knex = __toESM(require("knex"), 1);
|
|
2600
|
+
var KNEX_DEFAULTS = {
|
|
2601
|
+
testConnection: true,
|
|
2602
|
+
pool: { min: 2, max: 10 },
|
|
2603
|
+
acquireConnectionTimeout: 1e4,
|
|
2604
|
+
ssl: { rejectUnauthorized: false }
|
|
2605
|
+
};
|
|
2575
2606
|
var Db = class {
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2607
|
+
static async init(context, options = {}) {
|
|
2608
|
+
const buildConfig = async () => {
|
|
2609
|
+
const defs2 = {
|
|
2610
|
+
dbName: "string",
|
|
2611
|
+
dbProfile: "boolean default false"
|
|
2612
|
+
};
|
|
2613
|
+
const discovered = context?.params?.getAllForModule?.("db", defs2) ?? {};
|
|
2614
|
+
const merged = { ...discovered, ...options };
|
|
2615
|
+
let { dbName, dbProfile } = merged;
|
|
2616
|
+
let dbConnectionString = options.dbConnectionString ?? options.connectionString;
|
|
2617
|
+
let connectionParam = dbConnectionString ? "options" : null;
|
|
2618
|
+
if (!dbConnectionString) {
|
|
2619
|
+
const src = context?.args?.getSource?.("dbConnectionString");
|
|
2620
|
+
if (src === "cli" || src === "overrides" || src === "config") {
|
|
2621
|
+
dbConnectionString = await context.params.get("dbConnectionString", "string");
|
|
2622
|
+
connectionParam = "dbConnectionString";
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
if (!dbConnectionString && dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
|
|
2626
|
+
dbConnectionString = dbName;
|
|
2627
|
+
dbName = void 0;
|
|
2628
|
+
}
|
|
2629
|
+
if (!dbConnectionString && dbName) {
|
|
2630
|
+
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
2631
|
+
dbConnectionString = await context.params.get(paramName, "string");
|
|
2632
|
+
connectionParam = paramName;
|
|
2633
|
+
if (!dbConnectionString) {
|
|
2634
|
+
throw new ParamError(
|
|
2635
|
+
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
2636
|
+
);
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
if (!dbConnectionString) {
|
|
2640
|
+
dbConnectionString = await context.params.get("dbConnectionString", "string");
|
|
2641
|
+
if (dbConnectionString) {
|
|
2642
|
+
connectionParam = "dbConnectionString";
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
if (!dbConnectionString) {
|
|
2646
|
+
if (!dbName) {
|
|
2647
|
+
dbName = "local";
|
|
2648
|
+
}
|
|
2649
|
+
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
2650
|
+
dbConnectionString = await context.params.get(paramName, "string");
|
|
2651
|
+
connectionParam = paramName;
|
|
2652
|
+
if (!dbConnectionString) {
|
|
2653
|
+
throw new ParamError(
|
|
2654
|
+
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
2655
|
+
);
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
const displayName = resolveDbDisplayName(
|
|
2659
|
+
dbName,
|
|
2660
|
+
connectionParam,
|
|
2661
|
+
context?.args?.env,
|
|
2662
|
+
merged.name
|
|
2663
|
+
);
|
|
2664
|
+
return {
|
|
2665
|
+
...KNEX_DEFAULTS,
|
|
2666
|
+
connectionString: dbConnectionString,
|
|
2667
|
+
name: displayName,
|
|
2668
|
+
profile: !!dbProfile,
|
|
2669
|
+
logger: context.logger
|
|
2670
|
+
};
|
|
2671
|
+
};
|
|
2672
|
+
const config2 = context?.params?.runWithModuleAsync ? await context.params.runWithModuleAsync("db", buildConfig) : await buildConfig();
|
|
2673
|
+
return dbConnect(context, config2);
|
|
2674
|
+
}
|
|
2585
2675
|
constructor(config2) {
|
|
2586
|
-
if (!config2.connectionString) {
|
|
2676
|
+
if (!config2 || !config2.connectionString) {
|
|
2587
2677
|
throw new ParamError("Db: connectionString is required");
|
|
2588
2678
|
}
|
|
2679
|
+
this.knexInstance = null;
|
|
2680
|
+
this.isConnected = false;
|
|
2681
|
+
this.queriesLog = [];
|
|
2589
2682
|
this.config = {
|
|
2590
2683
|
testConnection: true,
|
|
2591
2684
|
profile: false,
|
|
@@ -2593,30 +2686,27 @@ var Db = class {
|
|
|
2593
2686
|
acquireConnectionTimeout: 1e4,
|
|
2594
2687
|
ssl: { rejectUnauthorized: false },
|
|
2595
2688
|
logger: console,
|
|
2596
|
-
name: "default",
|
|
2597
2689
|
...config2
|
|
2598
2690
|
};
|
|
2599
2691
|
this.logger = this.config.logger;
|
|
2600
2692
|
const instance = this;
|
|
2601
|
-
const callableWrapper = function(
|
|
2693
|
+
const callableWrapper = function() {
|
|
2602
2694
|
throw new Error("This should never be called directly");
|
|
2603
2695
|
};
|
|
2604
2696
|
callableWrapper._instance = instance;
|
|
2605
2697
|
return new Proxy(callableWrapper, {
|
|
2606
|
-
|
|
2607
|
-
apply: (target, thisArg, argumentsList) => {
|
|
2698
|
+
apply: (target, _thisArg, argumentsList) => {
|
|
2608
2699
|
const inst = target._instance;
|
|
2609
2700
|
if (!inst.knexInstance) {
|
|
2610
2701
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
2611
2702
|
}
|
|
2612
2703
|
return inst.knexInstance(...argumentsList);
|
|
2613
2704
|
},
|
|
2614
|
-
// Intercept property access: db.schema, db.raw, etc.
|
|
2615
2705
|
get: (target, prop) => {
|
|
2616
2706
|
if (prop === "_instance") {
|
|
2617
2707
|
return target._instance;
|
|
2618
2708
|
}
|
|
2619
|
-
const
|
|
2709
|
+
const inst = target._instance;
|
|
2620
2710
|
const ownMethods = [
|
|
2621
2711
|
"connect",
|
|
2622
2712
|
"disconnect",
|
|
@@ -2629,26 +2719,26 @@ var Db = class {
|
|
|
2629
2719
|
"detectClient",
|
|
2630
2720
|
"attachProfiler"
|
|
2631
2721
|
];
|
|
2632
|
-
if (prop in
|
|
2633
|
-
const value =
|
|
2722
|
+
if (prop in inst) {
|
|
2723
|
+
const value = inst[prop];
|
|
2634
2724
|
if (typeof value === "function" && ownMethods.includes(prop)) {
|
|
2635
|
-
return value.bind(
|
|
2725
|
+
return value.bind(inst);
|
|
2636
2726
|
}
|
|
2637
2727
|
if (typeof value !== "function") {
|
|
2638
2728
|
return value;
|
|
2639
2729
|
}
|
|
2640
2730
|
}
|
|
2641
|
-
if (
|
|
2642
|
-
const knexProp =
|
|
2731
|
+
if (inst.knexInstance) {
|
|
2732
|
+
const knexProp = inst.knexInstance[prop];
|
|
2643
2733
|
if (typeof knexProp === "function") {
|
|
2644
|
-
return knexProp.bind(
|
|
2734
|
+
return knexProp.bind(inst.knexInstance);
|
|
2645
2735
|
}
|
|
2646
2736
|
return knexProp;
|
|
2647
2737
|
}
|
|
2648
|
-
if (prop in
|
|
2649
|
-
const method =
|
|
2738
|
+
if (prop in inst) {
|
|
2739
|
+
const method = inst[prop];
|
|
2650
2740
|
if (typeof method === "function") {
|
|
2651
|
-
return method.bind(
|
|
2741
|
+
return method.bind(inst);
|
|
2652
2742
|
}
|
|
2653
2743
|
return method;
|
|
2654
2744
|
}
|
|
@@ -2656,9 +2746,6 @@ var Db = class {
|
|
|
2656
2746
|
}
|
|
2657
2747
|
});
|
|
2658
2748
|
}
|
|
2659
|
-
/**
|
|
2660
|
-
* Detect database client type from connection string
|
|
2661
|
-
*/
|
|
2662
2749
|
detectClient(connectionString) {
|
|
2663
2750
|
if (connectionString.match(/^postgresql/)) {
|
|
2664
2751
|
return "pg";
|
|
@@ -2668,9 +2755,6 @@ var Db = class {
|
|
|
2668
2755
|
}
|
|
2669
2756
|
return null;
|
|
2670
2757
|
}
|
|
2671
|
-
/**
|
|
2672
|
-
* Connect to the database
|
|
2673
|
-
*/
|
|
2674
2758
|
async connect() {
|
|
2675
2759
|
if (this.isConnected && this.knexInstance) {
|
|
2676
2760
|
this.logger.warn?.("[Db] Already connected");
|
|
@@ -2679,14 +2763,13 @@ var Db = class {
|
|
|
2679
2763
|
const client = this.detectClient(this.config.connectionString);
|
|
2680
2764
|
if (!client) {
|
|
2681
2765
|
throw new ParamError(
|
|
2682
|
-
|
|
2766
|
+
"Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://"
|
|
2683
2767
|
);
|
|
2684
2768
|
}
|
|
2685
2769
|
try {
|
|
2686
2770
|
const connectionConfig = {
|
|
2687
2771
|
connectionString: this.config.connectionString,
|
|
2688
2772
|
family: 4
|
|
2689
|
-
// Force IPv4 only (disable IPv6)
|
|
2690
2773
|
};
|
|
2691
2774
|
this.knexInstance = (0, import_knex.default)({
|
|
2692
2775
|
client,
|
|
@@ -2702,7 +2785,7 @@ var Db = class {
|
|
|
2702
2785
|
await this.testConnection();
|
|
2703
2786
|
}
|
|
2704
2787
|
this.isConnected = true;
|
|
2705
|
-
this.logger.debug?.(
|
|
2788
|
+
this.logger.debug?.(formatDbConnectMessage(this.config.name, this.config.connectionString));
|
|
2706
2789
|
} catch (error) {
|
|
2707
2790
|
if (error instanceof ParamError) {
|
|
2708
2791
|
throw error;
|
|
@@ -2711,9 +2794,6 @@ var Db = class {
|
|
|
2711
2794
|
throw new ParamError(`Db: Connection failed - ${errorMsg}`);
|
|
2712
2795
|
}
|
|
2713
2796
|
}
|
|
2714
|
-
/**
|
|
2715
|
-
* Disconnect from the database
|
|
2716
|
-
*/
|
|
2717
2797
|
async disconnect() {
|
|
2718
2798
|
if (!this.knexInstance) {
|
|
2719
2799
|
return;
|
|
@@ -2723,16 +2803,13 @@ var Db = class {
|
|
|
2723
2803
|
this.knexInstance = null;
|
|
2724
2804
|
this.isConnected = false;
|
|
2725
2805
|
this.queriesLog = [];
|
|
2726
|
-
this.logger.debug?.(
|
|
2806
|
+
this.logger.debug?.(formatDbDisconnectMessage(this.config.name, this.config.connectionString));
|
|
2727
2807
|
} catch (error) {
|
|
2728
2808
|
const errorMsg = this.getErrorMessage(error);
|
|
2729
2809
|
this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
|
|
2730
2810
|
throw error;
|
|
2731
2811
|
}
|
|
2732
2812
|
}
|
|
2733
|
-
/**
|
|
2734
|
-
* Extract error message from various error types
|
|
2735
|
-
*/
|
|
2736
2813
|
getErrorMessage(error) {
|
|
2737
2814
|
if (error instanceof AggregateError) {
|
|
2738
2815
|
const errors = error.errors || [];
|
|
@@ -2757,9 +2834,11 @@ var Db = class {
|
|
|
2757
2834
|
return `${code} (tried: ${addresses.join(", ")})`;
|
|
2758
2835
|
}
|
|
2759
2836
|
}
|
|
2760
|
-
const uniqueMessages = [
|
|
2761
|
-
|
|
2762
|
-
|
|
2837
|
+
const uniqueMessages = [
|
|
2838
|
+
...new Set(
|
|
2839
|
+
errors.map((e) => e instanceof Error ? e.message : String(e))
|
|
2840
|
+
)
|
|
2841
|
+
];
|
|
2763
2842
|
if (uniqueMessages.length === 1) {
|
|
2764
2843
|
return uniqueMessages[0];
|
|
2765
2844
|
}
|
|
@@ -2768,28 +2847,25 @@ var Db = class {
|
|
|
2768
2847
|
return error.message || "Multiple errors occurred";
|
|
2769
2848
|
}
|
|
2770
2849
|
if (error instanceof Error) {
|
|
2771
|
-
const
|
|
2772
|
-
if (
|
|
2773
|
-
return `${
|
|
2850
|
+
const code = error.code;
|
|
2851
|
+
if (code) {
|
|
2852
|
+
return `${code}: ${error.message || String(error)}`;
|
|
2774
2853
|
}
|
|
2775
2854
|
return error.message || String(error);
|
|
2776
2855
|
}
|
|
2777
2856
|
if (typeof error === "string") {
|
|
2778
2857
|
return error;
|
|
2779
2858
|
}
|
|
2780
|
-
if (error
|
|
2859
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
2781
2860
|
const msg = String(error.message);
|
|
2782
|
-
const
|
|
2783
|
-
if (
|
|
2784
|
-
return `${
|
|
2861
|
+
const code = error.code;
|
|
2862
|
+
if (code) {
|
|
2863
|
+
return `${code}: ${msg}`;
|
|
2785
2864
|
}
|
|
2786
2865
|
return msg;
|
|
2787
2866
|
}
|
|
2788
2867
|
return String(error) || "Unknown error";
|
|
2789
2868
|
}
|
|
2790
|
-
/**
|
|
2791
|
-
* Test database connection
|
|
2792
|
-
*/
|
|
2793
2869
|
async testConnection() {
|
|
2794
2870
|
if (!this.knexInstance) {
|
|
2795
2871
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -2805,9 +2881,6 @@ var Db = class {
|
|
|
2805
2881
|
throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
|
|
2806
2882
|
}
|
|
2807
2883
|
}
|
|
2808
|
-
/**
|
|
2809
|
-
* Attach query profiler to log all queries
|
|
2810
|
-
*/
|
|
2811
2884
|
attachProfiler() {
|
|
2812
2885
|
if (!this.knexInstance) {
|
|
2813
2886
|
return;
|
|
@@ -2817,7 +2890,7 @@ var Db = class {
|
|
|
2817
2890
|
this.knexInstance.on("query", (query) => {
|
|
2818
2891
|
query.__startTime = process.hrtime();
|
|
2819
2892
|
});
|
|
2820
|
-
this.knexInstance.on("query-response", (
|
|
2893
|
+
this.knexInstance.on("query-response", (_response, query) => {
|
|
2821
2894
|
const [seconds, nanoseconds] = process.hrtime(query.__startTime);
|
|
2822
2895
|
const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
|
|
2823
2896
|
const logEntry = {
|
|
@@ -2832,15 +2905,9 @@ var Db = class {
|
|
|
2832
2905
|
this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
|
|
2833
2906
|
});
|
|
2834
2907
|
}
|
|
2835
|
-
/**
|
|
2836
|
-
* Get query log (only available if profiling is enabled)
|
|
2837
|
-
*/
|
|
2838
2908
|
getQueryLog() {
|
|
2839
2909
|
return [...this.queriesLog];
|
|
2840
2910
|
}
|
|
2841
|
-
/**
|
|
2842
|
-
* Check if a table exists
|
|
2843
|
-
*/
|
|
2844
2911
|
async tableExists(tableName) {
|
|
2845
2912
|
if (!this.knexInstance) {
|
|
2846
2913
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -2852,65 +2919,87 @@ var Db = class {
|
|
|
2852
2919
|
throw error;
|
|
2853
2920
|
}
|
|
2854
2921
|
}
|
|
2855
|
-
/**
|
|
2856
|
-
* Get the underlying Knex instance (for advanced usage)
|
|
2857
|
-
*/
|
|
2858
2922
|
getKnex() {
|
|
2859
2923
|
if (!this.knexInstance) {
|
|
2860
2924
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
2861
2925
|
}
|
|
2862
2926
|
return this.knexInstance;
|
|
2863
2927
|
}
|
|
2864
|
-
/**
|
|
2865
|
-
* Get connection status
|
|
2866
|
-
*/
|
|
2867
2928
|
isConnectedToDb() {
|
|
2868
2929
|
return this.isConnected && this.knexInstance !== null;
|
|
2869
2930
|
}
|
|
2870
|
-
/**
|
|
2871
|
-
* Initialize Db with context (connects and registers disconnect cleanup).
|
|
2872
|
-
* Params are read via getAllForModule("db", defs). Same as dbInit(context, dbNameOrConnectionString).
|
|
2873
|
-
*/
|
|
2874
|
-
static async init(context, dbNameOrConnectionString) {
|
|
2875
|
-
return dbFindAndConnect(context, dbNameOrConnectionString);
|
|
2876
|
-
}
|
|
2877
2931
|
};
|
|
2878
2932
|
function capitalizeFirstLetter(str) {
|
|
2879
2933
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
2880
2934
|
}
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
}
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2935
|
+
function resolveDbDisplayName(dbName, connectionParam, argsEnv, mergedName) {
|
|
2936
|
+
if (dbName) {
|
|
2937
|
+
return dbName;
|
|
2938
|
+
}
|
|
2939
|
+
if (mergedName) {
|
|
2940
|
+
return mergedName;
|
|
2941
|
+
}
|
|
2942
|
+
if (connectionParam?.startsWith("dbConnectionString") && connectionParam.length > "dbConnectionString".length) {
|
|
2943
|
+
return connectionParam.slice("dbConnectionString".length).toLowerCase();
|
|
2944
|
+
}
|
|
2945
|
+
if (connectionParam === "dbConnectionString" && argsEnv) {
|
|
2946
|
+
return argsEnv;
|
|
2947
|
+
}
|
|
2948
|
+
return void 0;
|
|
2949
|
+
}
|
|
2950
|
+
function formatDbConnectMessage(name, connectionString) {
|
|
2951
|
+
const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
|
|
2952
|
+
if (name) {
|
|
2953
|
+
return `[Db] Connected to database "${name}"${endpointSuffix}`;
|
|
2954
|
+
}
|
|
2955
|
+
return `[Db] Connected${endpointSuffix}`;
|
|
2956
|
+
}
|
|
2957
|
+
function formatDbDisconnectMessage(name, connectionString) {
|
|
2958
|
+
const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
|
|
2959
|
+
if (name) {
|
|
2960
|
+
return `[Db] Disconnected from database "${name}"${endpointSuffix}`;
|
|
2961
|
+
}
|
|
2962
|
+
return `[Db] Disconnected${endpointSuffix}`;
|
|
2963
|
+
}
|
|
2964
|
+
function formatDbInstanceMessage(action, name) {
|
|
2965
|
+
if (name) {
|
|
2966
|
+
return `[Db] instance "${name}" ${action}`;
|
|
2967
|
+
}
|
|
2968
|
+
return `[Db] instance ${action}`;
|
|
2969
|
+
}
|
|
2970
|
+
function formatConnectionEndpointSuffix(connectionString) {
|
|
2971
|
+
const endpoint = formatConnectionEndpoint(connectionString);
|
|
2972
|
+
return endpoint ? ` (${endpoint})` : "";
|
|
2973
|
+
}
|
|
2974
|
+
function formatConnectionEndpoint(connectionString) {
|
|
2975
|
+
try {
|
|
2976
|
+
const url = new URL(connectionString);
|
|
2977
|
+
const host = url.hostname;
|
|
2978
|
+
if (!host) {
|
|
2979
|
+
return null;
|
|
2980
|
+
}
|
|
2981
|
+
let port = url.port;
|
|
2982
|
+
if (!port) {
|
|
2983
|
+
if (url.protocol === "postgresql:") {
|
|
2984
|
+
port = "5432";
|
|
2985
|
+
} else if (url.protocol === "mysql:") {
|
|
2986
|
+
port = "3306";
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
return port ? `${host}:${port}` : host;
|
|
2990
|
+
} catch {
|
|
2991
|
+
return null;
|
|
2992
|
+
}
|
|
2993
|
+
}
|
|
2994
|
+
async function dbConnect(context, config2) {
|
|
2906
2995
|
try {
|
|
2907
2996
|
const db = new Db(config2);
|
|
2908
2997
|
context.registerCleanup(async () => {
|
|
2909
2998
|
await db.disconnect();
|
|
2910
|
-
context.logger.debug(
|
|
2999
|
+
context.logger.debug?.(formatDbInstanceMessage("disconnected", config2.name));
|
|
2911
3000
|
});
|
|
2912
3001
|
await db.connect();
|
|
2913
|
-
context.logger.debug(
|
|
3002
|
+
context.logger.debug?.(formatDbInstanceMessage("initialized", config2.name));
|
|
2914
3003
|
return db;
|
|
2915
3004
|
} catch (error) {
|
|
2916
3005
|
if (error instanceof ParamError) {
|
|
@@ -2920,48 +3009,11 @@ async function dbConnect(context, connectionString, name, dbProfile) {
|
|
|
2920
3009
|
throw new ParamError(`[Db] connect error: ${errorMsg}`);
|
|
2921
3010
|
}
|
|
2922
3011
|
}
|
|
2923
|
-
async function dbFindAndConnect(context, dbNameOrConnectionString) {
|
|
2924
|
-
let dbName;
|
|
2925
|
-
let dbConnectionString;
|
|
2926
|
-
let dbProfile;
|
|
2927
|
-
if (dbNameOrConnectionString) {
|
|
2928
|
-
if (dbNameOrConnectionString.match(/^(postgresql|mysql):\/\/[^\s]+:[^\s]+@[^\s]+:\d+\/[^\s]+$/)) {
|
|
2929
|
-
dbName = void 0;
|
|
2930
|
-
dbConnectionString = dbNameOrConnectionString;
|
|
2931
|
-
} else {
|
|
2932
|
-
dbName = dbNameOrConnectionString;
|
|
2933
|
-
}
|
|
2934
|
-
} else {
|
|
2935
|
-
const defs2 = {
|
|
2936
|
-
dbName: "string",
|
|
2937
|
-
dbConnectionString: "string",
|
|
2938
|
-
dbProfile: "boolean default false"
|
|
2939
|
-
};
|
|
2940
|
-
const paramsConfig = context.params.getAll(defs2);
|
|
2941
|
-
dbName = paramsConfig.dbName;
|
|
2942
|
-
dbConnectionString = paramsConfig.dbConnectionString;
|
|
2943
|
-
dbProfile = paramsConfig.dbProfile;
|
|
2944
|
-
}
|
|
2945
|
-
if (!dbName && !dbConnectionString) {
|
|
2946
|
-
throw new ParamError("Db: either dbName or dbConnectionString must be specified");
|
|
2947
|
-
}
|
|
2948
|
-
if (dbName) {
|
|
2949
|
-
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
2950
|
-
dbConnectionString = await context.params.get(paramName, "string");
|
|
2951
|
-
if (!dbConnectionString) {
|
|
2952
|
-
throw new ParamError(
|
|
2953
|
-
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
2954
|
-
);
|
|
2955
|
-
}
|
|
2956
|
-
}
|
|
2957
|
-
const db = await dbConnect(context, dbConnectionString, dbName, dbProfile);
|
|
2958
|
-
return db;
|
|
2959
|
-
}
|
|
2960
|
-
async function dbInit(context, dbNameOrConnectionString) {
|
|
2961
|
-
return await dbFindAndConnect(context, dbNameOrConnectionString);
|
|
2962
|
-
}
|
|
2963
3012
|
|
|
2964
|
-
// src/
|
|
3013
|
+
// src/tasks/index.js
|
|
3014
|
+
var import_node_os3 = __toESM(require("os"), 1);
|
|
3015
|
+
|
|
3016
|
+
// src/utils/date-utils.js
|
|
2965
3017
|
function isTimestampFolder(folderName) {
|
|
2966
3018
|
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
|
|
2967
3019
|
if (!isoRegex.test(folderName)) {
|
|
@@ -2971,7 +3023,7 @@ function isTimestampFolder(folderName) {
|
|
|
2971
3023
|
return !isNaN(date.getTime()) && date.getTime() > 0;
|
|
2972
3024
|
}
|
|
2973
3025
|
|
|
2974
|
-
// src/utils/fs-utils.
|
|
3026
|
+
// src/utils/fs-utils.js
|
|
2975
3027
|
var import_fs2 = __toESM(require("fs"), 1);
|
|
2976
3028
|
var import_path2 = __toESM(require("path"), 1);
|
|
2977
3029
|
async function ensurePath(...pathParts) {
|
|
@@ -2995,7 +3047,7 @@ function getFileExtension(dataType) {
|
|
|
2995
3047
|
}
|
|
2996
3048
|
}
|
|
2997
3049
|
|
|
2998
|
-
// src/utils/os-utils.
|
|
3050
|
+
// src/utils/os-utils.js
|
|
2999
3051
|
var import_fs3 = __toESM(require("fs"), 1);
|
|
3000
3052
|
var import_path3 = __toESM(require("path"), 1);
|
|
3001
3053
|
var import_child_process = require("child_process");
|
|
@@ -3024,7 +3076,7 @@ function getFreeDiskSpace(targetPath) {
|
|
|
3024
3076
|
}
|
|
3025
3077
|
}
|
|
3026
3078
|
|
|
3027
|
-
// src/utils/format-utils.
|
|
3079
|
+
// src/utils/format-utils.js
|
|
3028
3080
|
function bytesToHumanReadable(bytes) {
|
|
3029
3081
|
if (bytes === 0) return "0 B";
|
|
3030
3082
|
const k = 1024;
|
|
@@ -3033,7 +3085,7 @@ function bytesToHumanReadable(bytes) {
|
|
|
3033
3085
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
3034
3086
|
}
|
|
3035
3087
|
|
|
3036
|
-
// src/utils/core-utils.
|
|
3088
|
+
// src/utils/core-utils.js
|
|
3037
3089
|
function sleepMs(ms) {
|
|
3038
3090
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3039
3091
|
}
|
|
@@ -3042,14 +3094,82 @@ function toJsonColumn(value) {
|
|
|
3042
3094
|
return JSON.stringify(value);
|
|
3043
3095
|
}
|
|
3044
3096
|
|
|
3045
|
-
// src/tasks/servicesRegistry.
|
|
3046
|
-
var import_node_crypto2 = require("crypto");
|
|
3047
|
-
var import_promises = require("fs/promises");
|
|
3097
|
+
// src/tasks/servicesRegistry.js
|
|
3048
3098
|
var import_node_os = __toESM(require("os"), 1);
|
|
3049
|
-
var import_node_path = __toESM(require("path"), 1);
|
|
3050
3099
|
|
|
3051
|
-
// src/tasks/taskUtils.
|
|
3100
|
+
// src/tasks/taskUtils.js
|
|
3052
3101
|
var import_node_crypto = require("crypto");
|
|
3102
|
+
|
|
3103
|
+
// src/tasks/time-matcher.js
|
|
3104
|
+
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
3105
|
+
function resolveAsterisks(field, range) {
|
|
3106
|
+
return field.includes("*") ? field.replace("*", range) : field;
|
|
3107
|
+
}
|
|
3108
|
+
function resolveRanges(field) {
|
|
3109
|
+
const regex = /(\d+)-(\d+)/;
|
|
3110
|
+
let current = field;
|
|
3111
|
+
while (true) {
|
|
3112
|
+
const match = regex.exec(current);
|
|
3113
|
+
if (!match) break;
|
|
3114
|
+
const raw = match[0];
|
|
3115
|
+
let first = Number(match[1]);
|
|
3116
|
+
let last = Number(match[2]);
|
|
3117
|
+
if (last < first) {
|
|
3118
|
+
[first, last] = [last, first];
|
|
3119
|
+
}
|
|
3120
|
+
const values = [];
|
|
3121
|
+
for (let i = first; i <= last; i += 1) {
|
|
3122
|
+
values.push(i);
|
|
3123
|
+
}
|
|
3124
|
+
current = current.replace(raw, values.join(","));
|
|
3125
|
+
}
|
|
3126
|
+
return current;
|
|
3127
|
+
}
|
|
3128
|
+
function resolveSteps(field) {
|
|
3129
|
+
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
3130
|
+
if (!match) return field;
|
|
3131
|
+
const base = match[1];
|
|
3132
|
+
const step = Number(match[2]);
|
|
3133
|
+
if (!Number.isFinite(step) || step <= 0) return field;
|
|
3134
|
+
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
3135
|
+
}
|
|
3136
|
+
function convertPattern(pattern) {
|
|
3137
|
+
const parts = pattern.trim().split(/\s+/);
|
|
3138
|
+
if (parts.length !== 6) {
|
|
3139
|
+
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
3140
|
+
}
|
|
3141
|
+
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
3142
|
+
}
|
|
3143
|
+
function fieldMatches(field, value) {
|
|
3144
|
+
const allowed = field.split(",").map((v) => Number(v));
|
|
3145
|
+
return allowed.includes(value);
|
|
3146
|
+
}
|
|
3147
|
+
function matchesParsedPattern(parsed, date) {
|
|
3148
|
+
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());
|
|
3149
|
+
}
|
|
3150
|
+
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
3151
|
+
const parsed = convertPattern(pattern);
|
|
3152
|
+
return matchesParsedPattern(parsed, date);
|
|
3153
|
+
}
|
|
3154
|
+
var MS_PER_SECOND = 1e3;
|
|
3155
|
+
var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
|
|
3156
|
+
function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
|
|
3157
|
+
const parsed = convertPattern(pattern);
|
|
3158
|
+
let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
|
|
3159
|
+
const end = t + maxSearchMs;
|
|
3160
|
+
while (t <= end) {
|
|
3161
|
+
const date = new Date(t);
|
|
3162
|
+
if (matchesParsedPattern(parsed, date)) {
|
|
3163
|
+
return date;
|
|
3164
|
+
}
|
|
3165
|
+
t += MS_PER_SECOND;
|
|
3166
|
+
}
|
|
3167
|
+
throw new Error(
|
|
3168
|
+
`nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
|
|
3169
|
+
);
|
|
3170
|
+
}
|
|
3171
|
+
|
|
3172
|
+
// src/tasks/taskUtils.js
|
|
3053
3173
|
function getDb(context) {
|
|
3054
3174
|
const db = context.db;
|
|
3055
3175
|
if (!db) {
|
|
@@ -3057,97 +3177,84 @@ function getDb(context) {
|
|
|
3057
3177
|
}
|
|
3058
3178
|
return db;
|
|
3059
3179
|
}
|
|
3060
|
-
function queueToTableNames(
|
|
3061
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
|
|
3062
|
-
throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
|
|
3063
|
-
}
|
|
3180
|
+
function queueToTableNames(queueName) {
|
|
3064
3181
|
return {
|
|
3065
|
-
tasksTable:
|
|
3066
|
-
historyTable: `${
|
|
3182
|
+
tasksTable: queueName,
|
|
3183
|
+
historyTable: `${queueName}_history`,
|
|
3184
|
+
registryTable: `${queueName}_services_registry`
|
|
3067
3185
|
};
|
|
3068
3186
|
}
|
|
3069
|
-
function
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3187
|
+
function defineTasksTable(t, db, tableNameForIndex) {
|
|
3188
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
3189
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
3190
|
+
t.timestamp("started_at");
|
|
3191
|
+
t.timestamp("completed_at");
|
|
3192
|
+
t.integer("priority").notNullable().defaultTo(50);
|
|
3193
|
+
t.text("schedule");
|
|
3194
|
+
t.timestamp("next_run_at").defaultTo(null);
|
|
3195
|
+
t.timestamp("past_due").defaultTo(null);
|
|
3196
|
+
t.text("name").notNullable();
|
|
3197
|
+
t.text("opid");
|
|
3198
|
+
t.json("params");
|
|
3199
|
+
t.text("service_group");
|
|
3200
|
+
t.integer("instance_number");
|
|
3201
|
+
t.text("service_name");
|
|
3202
|
+
t.text("server_name");
|
|
3203
|
+
t.text("status").notNullable().defaultTo("idle");
|
|
3204
|
+
t.timestamp("status_changed_at").defaultTo(null);
|
|
3205
|
+
t.text("progress");
|
|
3206
|
+
t.boolean("success");
|
|
3207
|
+
t.json("results");
|
|
3208
|
+
t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
|
|
3209
|
+
t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
|
|
3210
|
+
}
|
|
3211
|
+
function taskHistoryInsertFromQueueRow(row, overrides) {
|
|
3212
|
+
const { id, ...snapshot } = row;
|
|
3213
|
+
void id;
|
|
3214
|
+
return {
|
|
3215
|
+
...snapshot,
|
|
3216
|
+
...overrides
|
|
3217
|
+
};
|
|
3074
3218
|
}
|
|
3075
3219
|
async function ensureTaskTables(context, options = {}) {
|
|
3076
|
-
const
|
|
3220
|
+
const queueName = options.queueName ?? "tasks";
|
|
3077
3221
|
const recreate = options.recreate ?? false;
|
|
3078
3222
|
const db = getDb(context);
|
|
3079
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
3223
|
+
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
3080
3224
|
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
3081
3225
|
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
3226
|
+
const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
|
|
3082
3227
|
if (recreate) {
|
|
3083
3228
|
await db.schema.dropTableIfExists(historyTable);
|
|
3084
3229
|
await db.schema.dropTableIfExists(tasksTable);
|
|
3230
|
+
await db.schema.dropTableIfExists(registryTable);
|
|
3085
3231
|
}
|
|
3086
3232
|
if (needsTasks) {
|
|
3087
3233
|
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
3088
3234
|
await db.schema.createTable(tasksTable, (t) => {
|
|
3089
|
-
t
|
|
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(0);
|
|
3094
|
-
t.text("schedule");
|
|
3095
|
-
t.timestamp("past_due").defaultTo(null);
|
|
3096
|
-
t.text("target").notNullable();
|
|
3097
|
-
t.text("task").notNullable();
|
|
3098
|
-
t.json("params");
|
|
3099
|
-
t.text("opid");
|
|
3100
|
-
t.timestamp("paused_at").defaultTo(null);
|
|
3101
|
-
t.text("progress");
|
|
3102
|
-
t.boolean("success");
|
|
3103
|
-
t.json("results");
|
|
3104
|
-
});
|
|
3105
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
3106
|
-
t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
|
|
3107
|
-
t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
|
|
3108
|
-
t.index(["target", "task"], `${tasksTable}_target_task_idx`);
|
|
3235
|
+
defineTasksTable(t, db, tasksTable);
|
|
3109
3236
|
});
|
|
3110
3237
|
}
|
|
3111
3238
|
if (needsHistory) {
|
|
3112
3239
|
await db.schema.createTable(historyTable, (t) => {
|
|
3113
|
-
t
|
|
3114
|
-
t.timestamp("created_at").notNullable();
|
|
3115
|
-
t.timestamp("started_at");
|
|
3116
|
-
t.timestamp("completed_at");
|
|
3117
|
-
t.integer("priority").notNullable().defaultTo(0);
|
|
3118
|
-
t.text("schedule");
|
|
3119
|
-
t.timestamp("past_due").defaultTo(null);
|
|
3120
|
-
t.text("target").notNullable();
|
|
3121
|
-
t.text("task").notNullable();
|
|
3122
|
-
t.json("params");
|
|
3123
|
-
t.text("opid");
|
|
3124
|
-
t.text("progress");
|
|
3125
|
-
t.boolean("success");
|
|
3126
|
-
t.json("results");
|
|
3127
|
-
});
|
|
3128
|
-
await db.schema.alterTable(historyTable, (t) => {
|
|
3129
|
-
t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
|
|
3130
|
-
t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
|
|
3240
|
+
defineTasksTable(t, db, historyTable);
|
|
3131
3241
|
});
|
|
3132
3242
|
}
|
|
3133
|
-
const registryTable = servicesRegistryTable(queue);
|
|
3134
|
-
const needsRegistry = !await db.tableExists(registryTable);
|
|
3135
3243
|
if (needsRegistry) {
|
|
3136
3244
|
await db.schema.createTable(registryTable, (t) => {
|
|
3137
3245
|
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
3138
|
-
t.
|
|
3139
|
-
t.text("queue").notNullable();
|
|
3246
|
+
t.text("queue_name").notNullable();
|
|
3140
3247
|
t.text("service_group").notNullable();
|
|
3248
|
+
t.integer("instance_number").notNullable().defaultTo(1);
|
|
3141
3249
|
t.text("service_name").notNullable();
|
|
3142
|
-
t.text("
|
|
3143
|
-
t.text("hostname");
|
|
3250
|
+
t.text("server_name").notNullable();
|
|
3144
3251
|
t.integer("pid");
|
|
3145
3252
|
t.json("metadata");
|
|
3146
3253
|
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
3147
3254
|
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
3148
|
-
t.unique(["
|
|
3149
|
-
t.index(["
|
|
3150
|
-
t.index(["
|
|
3255
|
+
t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
|
|
3256
|
+
t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
|
|
3257
|
+
t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
|
|
3151
3258
|
});
|
|
3152
3259
|
}
|
|
3153
3260
|
}
|
|
@@ -3158,7 +3265,7 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
|
3158
3265
|
});
|
|
3159
3266
|
}
|
|
3160
3267
|
|
|
3161
|
-
// src/tasks/servicesRegistry.
|
|
3268
|
+
// src/tasks/servicesRegistry.js
|
|
3162
3269
|
function getDb2(context) {
|
|
3163
3270
|
const db = context.db;
|
|
3164
3271
|
if (!db) {
|
|
@@ -3169,6 +3276,7 @@ function getDb2(context) {
|
|
|
3169
3276
|
var DEFAULT_GROUP_MAX_INSTANCES = {
|
|
3170
3277
|
intake: 1,
|
|
3171
3278
|
harvest: 1,
|
|
3279
|
+
harvester: 0,
|
|
3172
3280
|
loader: 0,
|
|
3173
3281
|
photos: 0,
|
|
3174
3282
|
photosprocessor: 0,
|
|
@@ -3178,25 +3286,6 @@ function sanitizeNamePart(raw) {
|
|
|
3178
3286
|
const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3179
3287
|
return s.slice(0, 80) || "runner";
|
|
3180
3288
|
}
|
|
3181
|
-
function identityFilePath(identityDir, queue, serviceGroup) {
|
|
3182
|
-
const safeQ = sanitizeNamePart(queue);
|
|
3183
|
-
const safeG = sanitizeNamePart(serviceGroup);
|
|
3184
|
-
return import_node_path.default.join(identityDir, `${safeQ}_${safeG}.json`);
|
|
3185
|
-
}
|
|
3186
|
-
async function readIdentityFile(filePath) {
|
|
3187
|
-
try {
|
|
3188
|
-
const text = await (0, import_promises.readFile)(filePath, "utf8");
|
|
3189
|
-
const parsed = JSON.parse(text);
|
|
3190
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
3191
|
-
} catch {
|
|
3192
|
-
return {};
|
|
3193
|
-
}
|
|
3194
|
-
}
|
|
3195
|
-
async function writeIdentityFile(filePath, data) {
|
|
3196
|
-
await (0, import_promises.mkdir)(import_node_path.default.dirname(filePath), { recursive: true });
|
|
3197
|
-
await (0, import_promises.writeFile)(filePath, `${JSON.stringify(data, null, 2)}
|
|
3198
|
-
`, "utf8");
|
|
3199
|
-
}
|
|
3200
3289
|
function resolveMaxInstances(serviceGroup, override) {
|
|
3201
3290
|
if (override !== void 0 && Number.isFinite(override)) {
|
|
3202
3291
|
return Math.max(0, Math.floor(Number(override)));
|
|
@@ -3204,184 +3293,198 @@ function resolveMaxInstances(serviceGroup, override) {
|
|
|
3204
3293
|
const g = serviceGroup.trim().toLowerCase();
|
|
3205
3294
|
return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
|
|
3206
3295
|
}
|
|
3207
|
-
async function countAliveInGroup(db, registryTable,
|
|
3296
|
+
async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
|
|
3208
3297
|
const cutoff = new Date(Date.now() - staleMs);
|
|
3209
|
-
let q = db(registryTable).where({
|
|
3210
|
-
if (
|
|
3211
|
-
q = q.whereNot("
|
|
3298
|
+
let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
|
|
3299
|
+
if (excludeRowId) {
|
|
3300
|
+
q = q.whereNot("id", excludeRowId);
|
|
3212
3301
|
}
|
|
3213
3302
|
const row = await q.count("id as count").first();
|
|
3214
3303
|
return Number(row?.count ?? 0);
|
|
3215
3304
|
}
|
|
3305
|
+
async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
|
|
3306
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
3307
|
+
const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
|
|
3308
|
+
const set = /* @__PURE__ */ new Set();
|
|
3309
|
+
for (const r of rows) {
|
|
3310
|
+
const n = Number(r.instance_number);
|
|
3311
|
+
if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
|
|
3312
|
+
}
|
|
3313
|
+
return set;
|
|
3314
|
+
}
|
|
3216
3315
|
function isUniqueViolation(error) {
|
|
3217
3316
|
const code = error?.code ?? error?.errno;
|
|
3218
3317
|
return code === "23505" || String(error?.message || "").includes("duplicate key");
|
|
3219
3318
|
}
|
|
3319
|
+
function buildMetadata(options) {
|
|
3320
|
+
const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
|
|
3321
|
+
if (options.target) {
|
|
3322
|
+
base.runnerTarget = options.target;
|
|
3323
|
+
}
|
|
3324
|
+
return toJsonColumn(Object.keys(base).length ? base : null);
|
|
3325
|
+
}
|
|
3326
|
+
function allocateInstanceNumber(occupied, explicit, maxSlots) {
|
|
3327
|
+
if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
|
|
3328
|
+
const e = Math.max(1, Math.floor(Number(explicit)));
|
|
3329
|
+
if (occupied.has(e)) {
|
|
3330
|
+
throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
|
|
3331
|
+
}
|
|
3332
|
+
if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
|
|
3333
|
+
throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
|
|
3334
|
+
}
|
|
3335
|
+
return e;
|
|
3336
|
+
}
|
|
3337
|
+
const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
|
|
3338
|
+
for (let n = 1; n <= cap; n++) {
|
|
3339
|
+
if (!occupied.has(n)) return n;
|
|
3340
|
+
}
|
|
3341
|
+
throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
|
|
3342
|
+
}
|
|
3343
|
+
function defaultServiceName(groupBase, hostBase, instanceNumber) {
|
|
3344
|
+
return `${groupBase}-${hostBase}-${instanceNumber}`;
|
|
3345
|
+
}
|
|
3220
3346
|
async function registerInServicesRegistry(context, options) {
|
|
3221
3347
|
const db = getDb2(context);
|
|
3222
|
-
const registryTable =
|
|
3348
|
+
const registryTable = queueToTableNames(options.queueName).registryTable;
|
|
3223
3349
|
const serviceGroup = options.serviceGroup.trim();
|
|
3224
3350
|
if (!serviceGroup) {
|
|
3225
3351
|
throw new Error("registerInServicesRegistry: serviceGroup is required");
|
|
3226
3352
|
}
|
|
3227
|
-
const
|
|
3228
|
-
let identity = await readIdentityFile(identityPath);
|
|
3229
|
-
let instanceId = typeof identity.instanceId === "string" && identity.instanceId.trim() ? identity.instanceId.trim() : (0, import_node_crypto2.randomUUID)();
|
|
3230
|
-
identity.instanceId = instanceId;
|
|
3231
|
-
await writeIdentityFile(identityPath, identity);
|
|
3232
|
-
const hostname = import_node_os.default.hostname();
|
|
3353
|
+
const serverName = import_node_os.default.hostname();
|
|
3233
3354
|
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
3234
|
-
const meta =
|
|
3235
|
-
const
|
|
3236
|
-
|
|
3237
|
-
await db(registryTable).where({ instance_id: instanceId }).update({
|
|
3238
|
-
target: options.target,
|
|
3239
|
-
hostname,
|
|
3240
|
-
pid,
|
|
3241
|
-
metadata: meta,
|
|
3242
|
-
last_seen_at: db.fn.now()
|
|
3243
|
-
});
|
|
3244
|
-
const serviceName = String(existing.service_name);
|
|
3245
|
-
identity.serviceName = serviceName;
|
|
3246
|
-
await writeIdentityFile(identityPath, identity);
|
|
3247
|
-
const reg = {
|
|
3248
|
-
instanceId,
|
|
3249
|
-
serviceName,
|
|
3250
|
-
serviceGroup,
|
|
3251
|
-
queue: options.queue,
|
|
3252
|
-
target: options.target,
|
|
3253
|
-
rowId: String(existing.id)
|
|
3254
|
-
};
|
|
3255
|
-
context.servicesRegistry = reg;
|
|
3256
|
-
context.runnerHeartbeat = reg;
|
|
3257
|
-
context.logger.info?.(
|
|
3258
|
-
`[services-registry] resumed instance_id=${instanceId} name=${serviceName} group=${serviceGroup} queue=${options.queue}`
|
|
3259
|
-
);
|
|
3260
|
-
return {
|
|
3261
|
-
instanceId,
|
|
3262
|
-
serviceName,
|
|
3263
|
-
serviceGroup,
|
|
3264
|
-
queue: options.queue,
|
|
3265
|
-
target: options.target,
|
|
3266
|
-
rowId: String(existing.id),
|
|
3267
|
-
registryTable
|
|
3268
|
-
};
|
|
3269
|
-
}
|
|
3355
|
+
const meta = buildMetadata(options);
|
|
3356
|
+
const groupBase = sanitizeNamePart(serviceGroup);
|
|
3357
|
+
const hostBase = sanitizeNamePart(serverName);
|
|
3270
3358
|
const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
|
|
3271
|
-
const
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
options.queue,
|
|
3275
|
-
serviceGroup,
|
|
3276
|
-
options.staleMs,
|
|
3277
|
-
instanceId
|
|
3278
|
-
);
|
|
3279
|
-
if (maxAllowed > 0 && aliveOthers >= maxAllowed) {
|
|
3280
|
-
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveOthers} alive (max ${maxAllowed}, queue=${options.queue}).`;
|
|
3359
|
+
const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
|
|
3360
|
+
if (maxAllowed > 0 && aliveCount >= maxAllowed) {
|
|
3361
|
+
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
|
|
3281
3362
|
if (options.enforceMaxInstances) {
|
|
3282
3363
|
throw new Error(msg);
|
|
3283
3364
|
}
|
|
3284
3365
|
context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
|
|
3285
3366
|
}
|
|
3286
|
-
const
|
|
3287
|
-
const
|
|
3288
|
-
const
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3367
|
+
const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
|
|
3368
|
+
const cutoff = new Date(Date.now() - options.staleMs);
|
|
3369
|
+
const MAX_ATTEMPTS = 8;
|
|
3370
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
3371
|
+
const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
|
|
3372
|
+
const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
|
|
3373
|
+
const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
|
|
3374
|
+
const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
3375
|
+
if (existing) {
|
|
3376
|
+
const lastSeen = new Date(existing.last_seen_at);
|
|
3377
|
+
const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
|
|
3378
|
+
if (isAlive) {
|
|
3379
|
+
if (options.serviceName?.trim()) {
|
|
3380
|
+
throw new Error(
|
|
3381
|
+
`[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
context.logger.warn?.(
|
|
3385
|
+
`[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
|
|
3386
|
+
);
|
|
3387
|
+
if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
|
|
3388
|
+
throw new Error(
|
|
3389
|
+
`[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
|
|
3390
|
+
);
|
|
3391
|
+
}
|
|
3392
|
+
await new Promise((r) => setTimeout(r, 50 + attempt * 30));
|
|
3393
|
+
continue;
|
|
3306
3394
|
}
|
|
3395
|
+
await db(registryTable).where({ id: existing.id }).update({
|
|
3396
|
+
server_name: serverName,
|
|
3397
|
+
pid,
|
|
3398
|
+
metadata: meta,
|
|
3399
|
+
service_group: serviceGroup,
|
|
3400
|
+
instance_number: instanceNumber,
|
|
3401
|
+
last_seen_at: db.fn.now()
|
|
3402
|
+
});
|
|
3403
|
+
const reg = {
|
|
3404
|
+
serviceName: serviceNameRaw,
|
|
3405
|
+
serviceGroup,
|
|
3406
|
+
queueName: options.queueName,
|
|
3407
|
+
target: options.target,
|
|
3408
|
+
rowId: String(existing.id),
|
|
3409
|
+
registryTable,
|
|
3410
|
+
instanceNumber
|
|
3411
|
+
};
|
|
3412
|
+
context.servicesRegistry = reg;
|
|
3413
|
+
context.runnerHeartbeat = reg;
|
|
3414
|
+
context.logger.info?.(
|
|
3415
|
+
`[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
3416
|
+
);
|
|
3417
|
+
return reg;
|
|
3307
3418
|
}
|
|
3308
|
-
}
|
|
3309
|
-
let inserted;
|
|
3310
|
-
for (const candidate of eachServiceNameCandidate(baseCandidates)) {
|
|
3311
3419
|
try {
|
|
3312
3420
|
const rows = await db(registryTable).insert({
|
|
3313
|
-
|
|
3314
|
-
queue: options.queue,
|
|
3421
|
+
queue_name: options.queueName,
|
|
3315
3422
|
service_group: serviceGroup,
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3423
|
+
instance_number: instanceNumber,
|
|
3424
|
+
service_name: serviceNameRaw,
|
|
3425
|
+
server_name: serverName,
|
|
3319
3426
|
pid,
|
|
3320
3427
|
metadata: meta,
|
|
3321
|
-
last_seen_at: db.fn.now()
|
|
3428
|
+
last_seen_at: db.fn.now(),
|
|
3429
|
+
created_at: db.fn.now()
|
|
3322
3430
|
}).returning(["id", "service_name"]);
|
|
3323
3431
|
const row = Array.isArray(rows) ? rows[0] : rows;
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3432
|
+
let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
|
|
3433
|
+
if (!rowId) {
|
|
3434
|
+
const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
3435
|
+
rowId = again?.id != null ? String(again.id) : "";
|
|
3327
3436
|
}
|
|
3437
|
+
if (!rowId) continue;
|
|
3438
|
+
const regNew = {
|
|
3439
|
+
serviceName: String(row?.service_name ?? serviceNameRaw),
|
|
3440
|
+
serviceGroup,
|
|
3441
|
+
queueName: options.queueName,
|
|
3442
|
+
target: options.target,
|
|
3443
|
+
rowId,
|
|
3444
|
+
registryTable,
|
|
3445
|
+
instanceNumber
|
|
3446
|
+
};
|
|
3447
|
+
context.servicesRegistry = regNew;
|
|
3448
|
+
context.runnerHeartbeat = regNew;
|
|
3449
|
+
context.logger.info?.(
|
|
3450
|
+
`[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
3451
|
+
);
|
|
3452
|
+
return regNew;
|
|
3328
3453
|
} catch (error) {
|
|
3329
3454
|
if (!isUniqueViolation(error)) {
|
|
3330
3455
|
throw error;
|
|
3331
3456
|
}
|
|
3457
|
+
context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
|
|
3332
3458
|
}
|
|
3333
3459
|
}
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
`[services-registry] could not allocate a unique service_name for group=${serviceGroup} queue=${options.queue} (too many collisions).`
|
|
3337
|
-
);
|
|
3338
|
-
}
|
|
3339
|
-
identity.serviceName = inserted.service_name;
|
|
3340
|
-
await writeIdentityFile(identityPath, identity);
|
|
3341
|
-
const regNew = {
|
|
3342
|
-
instanceId,
|
|
3343
|
-
serviceName: inserted.service_name,
|
|
3344
|
-
serviceGroup,
|
|
3345
|
-
queue: options.queue,
|
|
3346
|
-
target: options.target,
|
|
3347
|
-
rowId: inserted.id
|
|
3348
|
-
};
|
|
3349
|
-
context.servicesRegistry = regNew;
|
|
3350
|
-
context.runnerHeartbeat = regNew;
|
|
3351
|
-
context.logger.info?.(
|
|
3352
|
-
`[services-registry] registered instance_id=${instanceId} name=${inserted.service_name} group=${serviceGroup} queue=${options.queue} target=${options.target}`
|
|
3460
|
+
throw new Error(
|
|
3461
|
+
`[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
|
|
3353
3462
|
);
|
|
3354
|
-
return {
|
|
3355
|
-
instanceId,
|
|
3356
|
-
serviceName: inserted.service_name,
|
|
3357
|
-
serviceGroup,
|
|
3358
|
-
queue: options.queue,
|
|
3359
|
-
target: options.target,
|
|
3360
|
-
rowId: inserted.id,
|
|
3361
|
-
registryTable
|
|
3362
|
-
};
|
|
3363
3463
|
}
|
|
3364
3464
|
async function touchServicesRegistry(context, registration) {
|
|
3365
3465
|
const db = getDb2(context);
|
|
3366
|
-
const
|
|
3466
|
+
const serverName = import_node_os.default.hostname();
|
|
3367
3467
|
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
3368
|
-
await db(registration.registryTable).where({
|
|
3468
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
3369
3469
|
last_seen_at: db.fn.now(),
|
|
3370
|
-
|
|
3470
|
+
server_name: serverName,
|
|
3371
3471
|
pid
|
|
3372
3472
|
});
|
|
3373
3473
|
}
|
|
3374
3474
|
async function unregisterServicesRegistry(context, registration) {
|
|
3375
3475
|
const db = getDb2(context);
|
|
3376
|
-
await db(registration.registryTable).where({
|
|
3377
|
-
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName}
|
|
3476
|
+
await db(registration.registryTable).where({ id: registration.rowId }).delete();
|
|
3477
|
+
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
|
|
3378
3478
|
}
|
|
3379
3479
|
|
|
3380
|
-
// src/
|
|
3480
|
+
// src/tasks/taskLogs.js
|
|
3481
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
3482
|
+
|
|
3483
|
+
// src/filedatabase/index.js
|
|
3381
3484
|
var import_fs4 = __toESM(require("fs"), 1);
|
|
3382
3485
|
var import_path4 = __toESM(require("path"), 1);
|
|
3383
3486
|
|
|
3384
|
-
// src/filedatabase/serializers.
|
|
3487
|
+
// src/filedatabase/serializers.js
|
|
3385
3488
|
function detectDataType(data) {
|
|
3386
3489
|
if (Array.isArray(data)) {
|
|
3387
3490
|
return "json-array";
|
|
@@ -3413,7 +3516,7 @@ function deserializeData(rawData, dataType) {
|
|
|
3413
3516
|
}
|
|
3414
3517
|
}
|
|
3415
3518
|
|
|
3416
|
-
// src/filedatabase/index.
|
|
3519
|
+
// src/filedatabase/index.js
|
|
3417
3520
|
var FileDatabase = class _FileDatabase {
|
|
3418
3521
|
basePath;
|
|
3419
3522
|
namespace;
|
|
@@ -3499,7 +3602,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3499
3602
|
if (errors.length) {
|
|
3500
3603
|
throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
|
|
3501
3604
|
}
|
|
3502
|
-
|
|
3605
|
+
const parts = [this.basePath, this.namespace];
|
|
3503
3606
|
if (this.tableName) {
|
|
3504
3607
|
parts.push(...this.tableName.split("/"));
|
|
3505
3608
|
}
|
|
@@ -3992,14 +4095,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
3992
4095
|
* Prepare the instance for read or write operations
|
|
3993
4096
|
* This discovers state and sets up internal members based on mode and current data
|
|
3994
4097
|
*/
|
|
3995
|
-
async prepare(
|
|
4098
|
+
async prepare(options) {
|
|
4099
|
+
const { write, read, version, deferInitialVersion } = options;
|
|
3996
4100
|
if (write) {
|
|
3997
4101
|
if (this.versioned) {
|
|
3998
4102
|
if (this.currentVersion === null) {
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4103
|
+
if (!deferInitialVersion) {
|
|
4104
|
+
await this.makeNewVersion();
|
|
4105
|
+
this.metadata = this.getDefaultMetadata();
|
|
4106
|
+
this.metadata.version = this.currentVersion;
|
|
4107
|
+
this.makeNewFile();
|
|
4108
|
+
}
|
|
4003
4109
|
} else {
|
|
4004
4110
|
if (!this.metadata.files.length) {
|
|
4005
4111
|
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
@@ -4109,7 +4215,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4109
4215
|
if (options.forceNewVersion && !this.versioned) {
|
|
4110
4216
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
4111
4217
|
}
|
|
4112
|
-
await this.prepare({ write: true });
|
|
4218
|
+
await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
|
|
4113
4219
|
const incomingDataType = detectDataType(data);
|
|
4114
4220
|
this.metadata.dataType = incomingDataType;
|
|
4115
4221
|
if (options.forceNewVersion) {
|
|
@@ -4403,7 +4509,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4403
4509
|
}
|
|
4404
4510
|
};
|
|
4405
4511
|
|
|
4406
|
-
// src/tasks/taskLogs.
|
|
4512
|
+
// src/tasks/taskLogs.js
|
|
4407
4513
|
function getLogsState(context) {
|
|
4408
4514
|
const holder = context;
|
|
4409
4515
|
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
@@ -4456,33 +4562,128 @@ function getLogsState(context) {
|
|
|
4456
4562
|
holder.__tasksLogsState = state;
|
|
4457
4563
|
return state;
|
|
4458
4564
|
}
|
|
4459
|
-
function
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
if (level === "error" || level === "fatal") return true;
|
|
4464
|
-
if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
|
|
4465
|
-
return false;
|
|
4466
|
-
}
|
|
4467
|
-
if (typeof payload === "string") {
|
|
4468
|
-
return /\berror\b/i.test(payload);
|
|
4469
|
-
}
|
|
4470
|
-
return false;
|
|
4565
|
+
function ipcLogTargetKey(target) {
|
|
4566
|
+
const bp = target.basePath ?? "";
|
|
4567
|
+
const ns = target.namespace ?? "";
|
|
4568
|
+
return `${bp}::${ns}::${target.tableName}`;
|
|
4471
4569
|
}
|
|
4472
|
-
function
|
|
4473
|
-
const
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4570
|
+
function ipcFileLogsTableNameForSourceResource(source, resource) {
|
|
4571
|
+
const seg = (s) => {
|
|
4572
|
+
const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
4573
|
+
return t.length ? t : "x";
|
|
4574
|
+
};
|
|
4575
|
+
return `${seg(source)}/${seg(resource)}`;
|
|
4576
|
+
}
|
|
4577
|
+
async function readTaskIpcLogsSnapshot(context, options) {
|
|
4578
|
+
const holder = context;
|
|
4579
|
+
const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
|
|
4580
|
+
const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
|
|
4581
|
+
const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
|
|
4582
|
+
const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
|
|
4583
|
+
const fd = new FileDatabase({
|
|
4584
|
+
basePath,
|
|
4585
|
+
namespace,
|
|
4586
|
+
tableName,
|
|
4587
|
+
versioned: true,
|
|
4588
|
+
useMetadata: true,
|
|
4589
|
+
maxVersions: 30,
|
|
4590
|
+
pageSize: 2e3,
|
|
4591
|
+
logger: holder.logger
|
|
4592
|
+
});
|
|
4593
|
+
const versions = await fd.getVersions();
|
|
4594
|
+
if (versions.length === 0) {
|
|
4595
|
+
return { records: [], latestTs: null };
|
|
4596
|
+
}
|
|
4597
|
+
const latest = versions[versions.length - 1];
|
|
4598
|
+
const raw = await fd.read({ version: latest });
|
|
4599
|
+
const arr = Array.isArray(raw) ? raw : [];
|
|
4600
|
+
let filtered = arr;
|
|
4601
|
+
if (options.afterTs && String(options.afterTs).trim()) {
|
|
4602
|
+
const cut = String(options.afterTs).trim();
|
|
4603
|
+
filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
|
|
4604
|
+
}
|
|
4605
|
+
let latestTs = null;
|
|
4606
|
+
for (const r of filtered) {
|
|
4607
|
+
const ts = typeof r?.ts === "string" ? String(r.ts) : null;
|
|
4608
|
+
if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
|
|
4609
|
+
}
|
|
4610
|
+
const incremental = !!(options.afterTs && String(options.afterTs).trim());
|
|
4611
|
+
const maxReturn = incremental ? 1e4 : tail;
|
|
4612
|
+
const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
|
|
4613
|
+
return { records: sliced, latestTs };
|
|
4614
|
+
}
|
|
4615
|
+
function getLogsStateForTarget(context, target) {
|
|
4616
|
+
const holder = context;
|
|
4617
|
+
const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
|
|
4618
|
+
const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
4619
|
+
if (!enabled) return null;
|
|
4620
|
+
if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
|
|
4621
|
+
const map = holder.__tasksLogsTargetStates;
|
|
4622
|
+
const key = ipcLogTargetKey(target);
|
|
4623
|
+
if (map.has(key)) return map.get(key);
|
|
4624
|
+
const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
|
|
4625
|
+
const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
|
|
4626
|
+
const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
|
|
4627
|
+
const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
|
|
4628
|
+
const db = new FileDatabase({
|
|
4629
|
+
basePath,
|
|
4630
|
+
namespace,
|
|
4631
|
+
tableName: target.tableName,
|
|
4632
|
+
versioned: true,
|
|
4633
|
+
useMetadata: true,
|
|
4634
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4635
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4636
|
+
logger: holder.logger
|
|
4637
|
+
});
|
|
4638
|
+
const state = {
|
|
4639
|
+
db,
|
|
4640
|
+
errorDb: null,
|
|
4641
|
+
queue: Promise.resolve(),
|
|
4642
|
+
initialized: false,
|
|
4643
|
+
errorInitialized: false
|
|
4644
|
+
};
|
|
4645
|
+
map.set(key, state);
|
|
4646
|
+
return state;
|
|
4647
|
+
}
|
|
4648
|
+
function isErrorPayload(payload) {
|
|
4649
|
+
if (!payload) return false;
|
|
4650
|
+
if (typeof payload === "object") {
|
|
4651
|
+
const level = typeof payload.level === "string" ? payload.level.toLowerCase() : "";
|
|
4652
|
+
if (level === "error" || level === "fatal") return true;
|
|
4653
|
+
if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
|
|
4654
|
+
return false;
|
|
4655
|
+
}
|
|
4656
|
+
if (typeof payload === "string") {
|
|
4657
|
+
return /\berror\b/i.test(payload);
|
|
4658
|
+
}
|
|
4659
|
+
return false;
|
|
4660
|
+
}
|
|
4661
|
+
function buildLogRecord(task, payload) {
|
|
4662
|
+
const params = task.params && typeof task.params === "object" ? task.params : {};
|
|
4663
|
+
return {
|
|
4664
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4665
|
+
opid: task.opid ?? null,
|
|
4666
|
+
taskId: task.id,
|
|
4667
|
+
taskName: task.name,
|
|
4668
|
+
target: task.service_group,
|
|
4480
4669
|
source: typeof params.source === "string" ? params.source : null,
|
|
4481
4670
|
resource: typeof params.resource === "string" ? params.resource : null,
|
|
4482
4671
|
payload
|
|
4483
4672
|
};
|
|
4484
4673
|
}
|
|
4485
|
-
function appendTaskIpcLog(context, task, payload) {
|
|
4674
|
+
function appendTaskIpcLog(context, task, payload, target) {
|
|
4675
|
+
if (target) {
|
|
4676
|
+
const state2 = getLogsStateForTarget(context, target);
|
|
4677
|
+
if (!state2?.db) return;
|
|
4678
|
+
const record2 = buildLogRecord(task, payload);
|
|
4679
|
+
state2.queue = state2.queue.then(async () => {
|
|
4680
|
+
await state2.db.write([record2], { forceNewVersion: !state2.initialized });
|
|
4681
|
+
state2.initialized = true;
|
|
4682
|
+
}).catch((error) => {
|
|
4683
|
+
context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
|
|
4684
|
+
});
|
|
4685
|
+
return;
|
|
4686
|
+
}
|
|
4486
4687
|
const state = getLogsState(context);
|
|
4487
4688
|
if (!state.db && !state.errorDb) return;
|
|
4488
4689
|
const record = buildLogRecord(task, payload);
|
|
@@ -4500,83 +4701,280 @@ function appendTaskIpcLog(context, task, payload) {
|
|
|
4500
4701
|
});
|
|
4501
4702
|
}
|
|
4502
4703
|
|
|
4503
|
-
// src/tasks/
|
|
4504
|
-
var
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
if (last < first) {
|
|
4518
|
-
[first, last] = [last, first];
|
|
4519
|
-
}
|
|
4520
|
-
const values = [];
|
|
4521
|
-
for (let i = first; i <= last; i += 1) {
|
|
4522
|
-
values.push(i);
|
|
4523
|
-
}
|
|
4524
|
-
current = current.replace(raw, values.join(","));
|
|
4525
|
-
}
|
|
4526
|
-
return current;
|
|
4527
|
-
}
|
|
4528
|
-
function resolveSteps(field) {
|
|
4529
|
-
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
4530
|
-
if (!match) return field;
|
|
4531
|
-
const base = match[1];
|
|
4532
|
-
const step = Number(match[2]);
|
|
4533
|
-
if (!Number.isFinite(step) || step <= 0) return field;
|
|
4534
|
-
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
4535
|
-
}
|
|
4536
|
-
function convertPattern(pattern) {
|
|
4537
|
-
const parts = pattern.trim().split(/\s+/);
|
|
4538
|
-
if (parts.length !== 6) {
|
|
4539
|
-
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
4540
|
-
}
|
|
4541
|
-
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
4542
|
-
}
|
|
4543
|
-
function fieldMatches(field, value) {
|
|
4544
|
-
const allowed = field.split(",").map((v) => Number(v));
|
|
4545
|
-
return allowed.includes(value);
|
|
4546
|
-
}
|
|
4547
|
-
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
4548
|
-
const parsed = convertPattern(pattern);
|
|
4549
|
-
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());
|
|
4550
|
-
}
|
|
4551
|
-
|
|
4552
|
-
// src/tasks/TaskMaster.ts
|
|
4553
|
-
var TaskMaster = class {
|
|
4554
|
-
context;
|
|
4555
|
-
task;
|
|
4704
|
+
// src/tasks/AbstractTask.js
|
|
4705
|
+
var AbstractTask = class _AbstractTask {
|
|
4706
|
+
/**
|
|
4707
|
+
* Whether `send-task` should wait for completion (and print a result
|
|
4708
|
+
* report) when no explicit `--wait` / `--noWait` flag is given. Defaults
|
|
4709
|
+
* to false; short-lived probe tasks (e.g. `ping`) override to true.
|
|
4710
|
+
*
|
|
4711
|
+
* @type {boolean}
|
|
4712
|
+
*/
|
|
4713
|
+
static defaultWaitForResult = false;
|
|
4714
|
+
/**
|
|
4715
|
+
* @param {object} context Runner context (db, logger, params, emitter...).
|
|
4716
|
+
* @param {object} task Task row as claimed from the queue.
|
|
4717
|
+
*/
|
|
4556
4718
|
constructor(context, task) {
|
|
4557
4719
|
this.context = context;
|
|
4558
4720
|
this.task = task;
|
|
4559
4721
|
}
|
|
4722
|
+
/**
|
|
4723
|
+
* Return a short reason string when the task should be deferred (e.g. "locked
|
|
4724
|
+
* by source"), or `false`/falsy when it is free to run. Default: always `false`.
|
|
4725
|
+
*
|
|
4726
|
+
* @returns {string | false | Promise<string | false>}
|
|
4727
|
+
*/
|
|
4560
4728
|
cantRunReason() {
|
|
4561
4729
|
return false;
|
|
4562
4730
|
}
|
|
4731
|
+
/**
|
|
4732
|
+
* Called by the runner when a stop has been requested. Subclasses running
|
|
4733
|
+
* long loops should flip a flag here and check it between iterations.
|
|
4734
|
+
*
|
|
4735
|
+
* @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
|
|
4736
|
+
*/
|
|
4563
4737
|
requestStop(_allowanceMs) {
|
|
4564
4738
|
}
|
|
4739
|
+
/**
|
|
4740
|
+
* Perform the task. Must be implemented by subclasses.
|
|
4741
|
+
*
|
|
4742
|
+
* @param {(progress: unknown) => Promise<void>} _reportProgress
|
|
4743
|
+
* Updates the DB `progress` column. Accepts any serializable value;
|
|
4744
|
+
* strings are stored verbatim, objects are JSON-stringified.
|
|
4745
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
4746
|
+
*/
|
|
4747
|
+
async run(_reportProgress) {
|
|
4748
|
+
throw new Error("AbstractTask.run must be implemented by subclass");
|
|
4749
|
+
}
|
|
4750
|
+
/**
|
|
4751
|
+
* Resolve a complete row payload for this task — envelope fields (queue,
|
|
4752
|
+
* priority, targeting, schedule…) plus the inner `params` blob produced by
|
|
4753
|
+
* {@link AbstractTask.resolveCustomParams}. Output shape matches
|
|
4754
|
+
* {@link enqueueTask}'s `options` argument, so the typical call is:
|
|
4755
|
+
*
|
|
4756
|
+
* const payload = await TaskClass.resolveParams(context, { name });
|
|
4757
|
+
* await enqueueTask(context, payload);
|
|
4758
|
+
*
|
|
4759
|
+
* Validation failures throw {@link ParamError} so the script aborts before
|
|
4760
|
+
* a malformed row hits the DB.
|
|
4761
|
+
*
|
|
4762
|
+
* @param {object} context
|
|
4763
|
+
* @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
|
|
4764
|
+
* Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
|
|
4765
|
+
* `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
|
|
4766
|
+
* `nextRunAt`, plus `params` (object — overlay onto inner blob).
|
|
4767
|
+
* @returns {Promise<object>}
|
|
4768
|
+
*/
|
|
4769
|
+
static async resolveParams(context, overrides = {}) {
|
|
4770
|
+
const main = _AbstractTask._resolveMainFields(context, overrides);
|
|
4771
|
+
const params = await this.resolveCustomParams(context, overrides);
|
|
4772
|
+
return { ...main, params };
|
|
4773
|
+
}
|
|
4774
|
+
/**
|
|
4775
|
+
* Resolve the inner JSON blob stored in the `params` column. Default
|
|
4776
|
+
* implementation passes through `--paramsJson` (parsed as a JSON object)
|
|
4777
|
+
* overlaid with `overrides.params` when supplied; returns `null` when
|
|
4778
|
+
* neither is provided.
|
|
4779
|
+
*
|
|
4780
|
+
* Subclasses with typed fields should override and call
|
|
4781
|
+
* {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
|
|
4782
|
+
* resolution, then validate and throw {@link ParamError} on bad input.
|
|
4783
|
+
*
|
|
4784
|
+
* @param {object} context
|
|
4785
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4786
|
+
* @returns {Promise<object|null>}
|
|
4787
|
+
*/
|
|
4788
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
4789
|
+
return _AbstractTask._defaultParamsBlob(context, overrides);
|
|
4790
|
+
}
|
|
4791
|
+
/**
|
|
4792
|
+
* Read main task envelope fields from `context.params` (CLI/env), with
|
|
4793
|
+
* any matching key on `overrides` taking precedence. Internal; called by
|
|
4794
|
+
* {@link AbstractTask.resolveParams}.
|
|
4795
|
+
*
|
|
4796
|
+
* @param {object} context
|
|
4797
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4798
|
+
* @returns {object}
|
|
4799
|
+
*/
|
|
4800
|
+
static _resolveMainFields(context, overrides = {}) {
|
|
4801
|
+
const defs2 = {
|
|
4802
|
+
queueName: "string default tasks",
|
|
4803
|
+
priority: "number default 50",
|
|
4804
|
+
serviceGroup: "string",
|
|
4805
|
+
serviceName: "string",
|
|
4806
|
+
instanceNumber: "number",
|
|
4807
|
+
serverName: "string",
|
|
4808
|
+
opid: "string",
|
|
4809
|
+
schedule: "string"
|
|
4810
|
+
};
|
|
4811
|
+
const cli = context.params.getAllForModule("task-envelope", defs2);
|
|
4812
|
+
const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
|
|
4813
|
+
if (!name) {
|
|
4814
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
4815
|
+
}
|
|
4816
|
+
let instanceNumber;
|
|
4817
|
+
const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
|
|
4818
|
+
if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
|
|
4819
|
+
const n = Number(rawInstance);
|
|
4820
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
4821
|
+
throw new ParamError("--instanceNumber must be a positive integer when set");
|
|
4822
|
+
}
|
|
4823
|
+
instanceNumber = n;
|
|
4824
|
+
} else {
|
|
4825
|
+
instanceNumber = null;
|
|
4826
|
+
}
|
|
4827
|
+
const priorityRaw = overrides.priority ?? cli.priority ?? 50;
|
|
4828
|
+
const priority = Number(priorityRaw);
|
|
4829
|
+
if (!Number.isFinite(priority)) {
|
|
4830
|
+
throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
|
|
4831
|
+
}
|
|
4832
|
+
return {
|
|
4833
|
+
name,
|
|
4834
|
+
queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
|
|
4835
|
+
priority,
|
|
4836
|
+
serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
|
|
4837
|
+
serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
|
|
4838
|
+
instanceNumber,
|
|
4839
|
+
serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
|
|
4840
|
+
opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
|
|
4841
|
+
schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
|
|
4842
|
+
nextRunAt: overrides.nextRunAt ?? null
|
|
4843
|
+
};
|
|
4844
|
+
}
|
|
4845
|
+
/**
|
|
4846
|
+
* Default inner-params resolver: parses `--paramsJson` (must be a JSON
|
|
4847
|
+
* object), then overlays `overrides.params` on top. Returns `null` when
|
|
4848
|
+
* neither is provided.
|
|
4849
|
+
*
|
|
4850
|
+
* @param {object} context
|
|
4851
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4852
|
+
* @returns {object|null}
|
|
4853
|
+
*/
|
|
4854
|
+
static _defaultParamsBlob(context, overrides = {}) {
|
|
4855
|
+
const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
|
|
4856
|
+
const fromJson = parseParamsJson(cli.paramsJson);
|
|
4857
|
+
const fromOverride = pickParamsObject(overrides);
|
|
4858
|
+
if (!fromJson && !fromOverride) return null;
|
|
4859
|
+
return { ...fromJson ?? {}, ...fromOverride ?? {} };
|
|
4860
|
+
}
|
|
4861
|
+
/**
|
|
4862
|
+
* Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
|
|
4863
|
+
* params (per `defs`) plus `--paramsJson` under a module namespace, then
|
|
4864
|
+
* merges them with explicit `overrides.params` in increasing priority:
|
|
4865
|
+
*
|
|
4866
|
+
* typed CLI flags → --paramsJson → overrides.params
|
|
4867
|
+
*
|
|
4868
|
+
* Undefined values are dropped so defaults declared in `defs` aren't
|
|
4869
|
+
* overwritten by missing-flag noise. Returns the merged object; the
|
|
4870
|
+
* caller is responsible for validation and throwing `ParamError`.
|
|
4871
|
+
*
|
|
4872
|
+
* @param {object} context
|
|
4873
|
+
* @param {string} moduleName Namespace for `--showUsedParams` grouping.
|
|
4874
|
+
* @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
|
|
4875
|
+
* @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
|
|
4876
|
+
* @returns {Record<string, unknown>}
|
|
4877
|
+
*/
|
|
4878
|
+
static _mergeTypedParams(context, moduleName, defs2, overrides = {}) {
|
|
4879
|
+
const fullDefs = { ...defs2, paramsJson: "string" };
|
|
4880
|
+
const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
|
|
4881
|
+
const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
|
|
4882
|
+
const fromCli = {};
|
|
4883
|
+
for (const [k, v] of Object.entries(cliRaw)) {
|
|
4884
|
+
if (k === "paramsJson") continue;
|
|
4885
|
+
if (v !== void 0 && v !== null) fromCli[k] = v;
|
|
4886
|
+
}
|
|
4887
|
+
const fromOverride = pickParamsObject(overrides) ?? {};
|
|
4888
|
+
return { ...fromCli, ...fromJson, ...fromOverride };
|
|
4889
|
+
}
|
|
4565
4890
|
};
|
|
4891
|
+
function emptyToUndef(s) {
|
|
4892
|
+
if (s === void 0 || s === null) return void 0;
|
|
4893
|
+
if (typeof s !== "string") return s;
|
|
4894
|
+
const t = s.trim();
|
|
4895
|
+
return t.length ? t : void 0;
|
|
4896
|
+
}
|
|
4897
|
+
function parseParamsJson(raw) {
|
|
4898
|
+
if (raw == null) return null;
|
|
4899
|
+
const t = String(raw).trim();
|
|
4900
|
+
if (!t) return null;
|
|
4901
|
+
let parsed;
|
|
4902
|
+
try {
|
|
4903
|
+
parsed = JSON.parse(t);
|
|
4904
|
+
} catch (e) {
|
|
4905
|
+
throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
|
|
4906
|
+
}
|
|
4907
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4908
|
+
throw new ParamError("--paramsJson must be a JSON object");
|
|
4909
|
+
}
|
|
4910
|
+
return parsed;
|
|
4911
|
+
}
|
|
4912
|
+
function pickParamsObject(overrides) {
|
|
4913
|
+
const p = overrides?.params;
|
|
4914
|
+
if (p && typeof p === "object" && !Array.isArray(p)) return p;
|
|
4915
|
+
return void 0;
|
|
4916
|
+
}
|
|
4566
4917
|
|
|
4567
|
-
// src/tasks/coreTasks/TaskPing.
|
|
4568
|
-
var TaskPing = class extends
|
|
4918
|
+
// src/tasks/coreTasks/TaskPing.js
|
|
4919
|
+
var TaskPing = class extends AbstractTask {
|
|
4920
|
+
/** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
|
|
4921
|
+
static defaultWaitForResult = true;
|
|
4922
|
+
/** Ping takes no params. */
|
|
4923
|
+
static async resolveCustomParams() {
|
|
4924
|
+
return null;
|
|
4925
|
+
}
|
|
4926
|
+
/**
|
|
4927
|
+
* @returns {Promise<{ success: true, results: "pong" }>}
|
|
4928
|
+
*/
|
|
4569
4929
|
async run() {
|
|
4570
4930
|
this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
|
|
4571
4931
|
return { success: true, results: "pong" };
|
|
4572
4932
|
}
|
|
4573
4933
|
};
|
|
4574
4934
|
|
|
4575
|
-
// src/tasks/coreTasks/TaskSampleProcess.
|
|
4576
|
-
var TaskSampleProcess = class extends
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4935
|
+
// src/tasks/coreTasks/TaskSampleProcess.js
|
|
4936
|
+
var TaskSampleProcess = class extends AbstractTask {
|
|
4937
|
+
/**
|
|
4938
|
+
* @param {object} context
|
|
4939
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4940
|
+
* @returns {Promise<{ total: number, delay: number, name?: string }>}
|
|
4941
|
+
*/
|
|
4942
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
4943
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
|
|
4944
|
+
total: "number default 10",
|
|
4945
|
+
delay: "number default 1000",
|
|
4946
|
+
name: "string"
|
|
4947
|
+
}, overrides);
|
|
4948
|
+
const total = Number(merged.total);
|
|
4949
|
+
const delay = Number(merged.delay);
|
|
4950
|
+
if (!Number.isInteger(total) || total <= 0) {
|
|
4951
|
+
throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
|
|
4952
|
+
}
|
|
4953
|
+
if (!Number.isInteger(delay) || delay < 0) {
|
|
4954
|
+
throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
|
|
4955
|
+
}
|
|
4956
|
+
const out = { total, delay };
|
|
4957
|
+
if (typeof merged.name === "string" && merged.name.trim()) {
|
|
4958
|
+
out.name = merged.name.trim();
|
|
4959
|
+
}
|
|
4960
|
+
return out;
|
|
4961
|
+
}
|
|
4962
|
+
/**
|
|
4963
|
+
* @param {object} context
|
|
4964
|
+
* @param {object} task
|
|
4965
|
+
*/
|
|
4966
|
+
constructor(context, task) {
|
|
4967
|
+
super(context, task);
|
|
4968
|
+
this.stopRequested = false;
|
|
4969
|
+
this.stopAllowanceMs = 0;
|
|
4970
|
+
this.stopDecisionLogged = false;
|
|
4971
|
+
}
|
|
4972
|
+
/**
|
|
4973
|
+
* Runner-facing stop signal. Records the allowance window so the main loop
|
|
4974
|
+
* can decide per-iteration whether to finish or abort early.
|
|
4975
|
+
*
|
|
4976
|
+
* @param {number} allowanceMs
|
|
4977
|
+
*/
|
|
4580
4978
|
requestStop(allowanceMs) {
|
|
4581
4979
|
this.stopRequested = true;
|
|
4582
4980
|
this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
|
|
@@ -4584,6 +4982,14 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4584
4982
|
`[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
|
|
4585
4983
|
);
|
|
4586
4984
|
}
|
|
4985
|
+
/**
|
|
4986
|
+
* Iterate `total` times, sleeping `delay` ms between ticks and reporting
|
|
4987
|
+
* progress every iteration. Validates params up front; invalid values short-
|
|
4988
|
+
* circuit to a structured failure without starting the loop.
|
|
4989
|
+
*
|
|
4990
|
+
* @param {(progress: object) => Promise<void>} reportProgress
|
|
4991
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
4992
|
+
*/
|
|
4587
4993
|
async run(reportProgress) {
|
|
4588
4994
|
const totalRaw = this.task?.params?.total ?? 10;
|
|
4589
4995
|
const delayRaw = this.task?.params?.delay ?? 1e3;
|
|
@@ -4665,7 +5071,7 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4665
5071
|
}
|
|
4666
5072
|
};
|
|
4667
5073
|
|
|
4668
|
-
// src/tasks/coreTasks/TaskShellCommand.
|
|
5074
|
+
// src/tasks/coreTasks/TaskShellCommand.js
|
|
4669
5075
|
var import_node_child_process = require("child_process");
|
|
4670
5076
|
function runShellCommand(command, cwd) {
|
|
4671
5077
|
return new Promise((resolve2, reject) => {
|
|
@@ -4695,7 +5101,27 @@ function runShellCommand(command, cwd) {
|
|
|
4695
5101
|
});
|
|
4696
5102
|
});
|
|
4697
5103
|
}
|
|
4698
|
-
var TaskShellCommand = class extends
|
|
5104
|
+
var TaskShellCommand = class extends AbstractTask {
|
|
5105
|
+
/**
|
|
5106
|
+
* @param {object} context
|
|
5107
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5108
|
+
* @returns {Promise<{ command: string, cwd?: string }>}
|
|
5109
|
+
*/
|
|
5110
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5111
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
|
|
5112
|
+
command: "string",
|
|
5113
|
+
cwd: "string"
|
|
5114
|
+
}, overrides);
|
|
5115
|
+
const command = typeof merged.command === "string" ? merged.command.trim() : "";
|
|
5116
|
+
if (!command) {
|
|
5117
|
+
throw new ParamError('shellCommand: param "command" must be a non-empty string');
|
|
5118
|
+
}
|
|
5119
|
+
const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
|
|
5120
|
+
return cwd ? { command, cwd } : { command };
|
|
5121
|
+
}
|
|
5122
|
+
/**
|
|
5123
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5124
|
+
*/
|
|
4699
5125
|
async run() {
|
|
4700
5126
|
const params = this.task?.params;
|
|
4701
5127
|
const commandRaw = typeof params === "string" ? params : params?.command;
|
|
@@ -4744,9 +5170,9 @@ var TaskShellCommand = class extends TaskMaster {
|
|
|
4744
5170
|
}
|
|
4745
5171
|
};
|
|
4746
5172
|
|
|
4747
|
-
// src/tasks/coreTasks/TaskSystemInfo.
|
|
5173
|
+
// src/tasks/coreTasks/TaskSystemInfo.js
|
|
4748
5174
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
4749
|
-
var
|
|
5175
|
+
var import_promises = __toESM(require("fs/promises"), 1);
|
|
4750
5176
|
function toGb(valueBytes) {
|
|
4751
5177
|
return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
|
|
4752
5178
|
}
|
|
@@ -4754,7 +5180,7 @@ function toMb(valueBytes) {
|
|
|
4754
5180
|
return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
|
|
4755
5181
|
}
|
|
4756
5182
|
async function getDiskStats() {
|
|
4757
|
-
const stats = await
|
|
5183
|
+
const stats = await import_promises.default.statfs("/");
|
|
4758
5184
|
const total = Number(stats.bsize) * Number(stats.blocks);
|
|
4759
5185
|
const free = Number(stats.bsize) * Number(stats.bavail);
|
|
4760
5186
|
const used = total - free;
|
|
@@ -4764,7 +5190,16 @@ async function getDiskStats() {
|
|
|
4764
5190
|
free: toGb(free)
|
|
4765
5191
|
};
|
|
4766
5192
|
}
|
|
4767
|
-
var TaskSystemInfo = class extends
|
|
5193
|
+
var TaskSystemInfo = class extends AbstractTask {
|
|
5194
|
+
/** Same UX expectation as `ping` — short probe, print the result. */
|
|
5195
|
+
static defaultWaitForResult = true;
|
|
5196
|
+
/** systemInfo takes no params. */
|
|
5197
|
+
static async resolveCustomParams() {
|
|
5198
|
+
return null;
|
|
5199
|
+
}
|
|
5200
|
+
/**
|
|
5201
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5202
|
+
*/
|
|
4768
5203
|
async run() {
|
|
4769
5204
|
try {
|
|
4770
5205
|
const totalMemory = import_node_os2.default.totalmem();
|
|
@@ -4816,8 +5251,31 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
4816
5251
|
}
|
|
4817
5252
|
};
|
|
4818
5253
|
|
|
4819
|
-
// src/tasks/coreTasks/TaskSumAB.
|
|
4820
|
-
var TaskSumAB = class extends
|
|
5254
|
+
// src/tasks/coreTasks/TaskSumAB.js
|
|
5255
|
+
var TaskSumAB = class extends AbstractTask {
|
|
5256
|
+
/** Short, deterministic — wait by default so callers see the sum. */
|
|
5257
|
+
static defaultWaitForResult = true;
|
|
5258
|
+
/**
|
|
5259
|
+
* @param {object} context
|
|
5260
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5261
|
+
* @returns {Promise<{ a: number, b: number }>}
|
|
5262
|
+
*/
|
|
5263
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5264
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
|
|
5265
|
+
a: "number",
|
|
5266
|
+
b: "number"
|
|
5267
|
+
}, overrides);
|
|
5268
|
+
if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
|
|
5269
|
+
throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
|
|
5270
|
+
}
|
|
5271
|
+
if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
|
|
5272
|
+
throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
|
|
5273
|
+
}
|
|
5274
|
+
return { a: merged.a, b: merged.b };
|
|
5275
|
+
}
|
|
5276
|
+
/**
|
|
5277
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5278
|
+
*/
|
|
4821
5279
|
async run() {
|
|
4822
5280
|
const a = this.task?.params?.a;
|
|
4823
5281
|
const b = this.task?.params?.b;
|
|
@@ -4848,8 +5306,46 @@ var TaskSumAB = class extends TaskMaster {
|
|
|
4848
5306
|
}
|
|
4849
5307
|
};
|
|
4850
5308
|
|
|
4851
|
-
// src/tasks/coreTasks/TaskStopRunner.
|
|
4852
|
-
var TaskStopRunner = class extends
|
|
5309
|
+
// src/tasks/coreTasks/TaskStopRunner.js
|
|
5310
|
+
var TaskStopRunner = class extends AbstractTask {
|
|
5311
|
+
/**
|
|
5312
|
+
* Stop tasks must target a concrete instance — without `serviceName` the
|
|
5313
|
+
* row would race against any worker on the queue. Layered on top of the
|
|
5314
|
+
* envelope built by {@link AbstractTask.resolveParams}.
|
|
5315
|
+
*
|
|
5316
|
+
* @param {object} context
|
|
5317
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5318
|
+
* @returns {Promise<object>}
|
|
5319
|
+
*/
|
|
5320
|
+
static async resolveParams(context, overrides = {}) {
|
|
5321
|
+
const main = await super.resolveParams(context, overrides);
|
|
5322
|
+
if (!main.serviceName) {
|
|
5323
|
+
throw new ParamError(
|
|
5324
|
+
"stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
|
|
5325
|
+
);
|
|
5326
|
+
}
|
|
5327
|
+
return main;
|
|
5328
|
+
}
|
|
5329
|
+
/**
|
|
5330
|
+
* @param {object} context
|
|
5331
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5332
|
+
* @returns {Promise<{ allowanceMs: number }>}
|
|
5333
|
+
*/
|
|
5334
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5335
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
|
|
5336
|
+
allowanceMs: "number default 5000"
|
|
5337
|
+
}, overrides);
|
|
5338
|
+
const allowanceMs = Number(merged.allowanceMs);
|
|
5339
|
+
if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
|
|
5340
|
+
throw new ParamError(
|
|
5341
|
+
`stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
|
|
5342
|
+
);
|
|
5343
|
+
}
|
|
5344
|
+
return { allowanceMs };
|
|
5345
|
+
}
|
|
5346
|
+
/**
|
|
5347
|
+
* @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
|
|
5348
|
+
*/
|
|
4853
5349
|
async run() {
|
|
4854
5350
|
const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
|
|
4855
5351
|
this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
|
|
@@ -4864,42 +5360,200 @@ var TaskStopRunner = class extends TaskMaster {
|
|
|
4864
5360
|
}
|
|
4865
5361
|
};
|
|
4866
5362
|
|
|
4867
|
-
// src/tasks/
|
|
5363
|
+
// src/tasks/coreTasks/TaskGetLogs.js
|
|
5364
|
+
var TaskGetLogs = class extends AbstractTask {
|
|
5365
|
+
/**
|
|
5366
|
+
* @param {object} context
|
|
5367
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5368
|
+
* @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
|
|
5369
|
+
*/
|
|
5370
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5371
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
|
|
5372
|
+
source: "string",
|
|
5373
|
+
resource: "string",
|
|
5374
|
+
tail: "number default 100",
|
|
5375
|
+
afterTs: "string"
|
|
5376
|
+
}, overrides);
|
|
5377
|
+
const source = typeof merged.source === "string" ? merged.source.trim() : "";
|
|
5378
|
+
const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
|
|
5379
|
+
if (!source) throw new ParamError('getLogs: param "source" is required');
|
|
5380
|
+
if (!resource) throw new ParamError('getLogs: param "resource" is required');
|
|
5381
|
+
let tail = Number(merged.tail);
|
|
5382
|
+
if (!Number.isFinite(tail) || tail < 1) tail = 100;
|
|
5383
|
+
tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
|
|
5384
|
+
const out = { source, resource, tail };
|
|
5385
|
+
if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
|
|
5386
|
+
out.afterTs = merged.afterTs.trim();
|
|
5387
|
+
}
|
|
5388
|
+
return out;
|
|
5389
|
+
}
|
|
5390
|
+
/**
|
|
5391
|
+
* @param {unknown} _reportProgress Unused (single-shot read, no progress events).
|
|
5392
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5393
|
+
*/
|
|
5394
|
+
async run(_reportProgress) {
|
|
5395
|
+
const p = this.task.params ?? {};
|
|
5396
|
+
const source = String(p.source ?? "").trim();
|
|
5397
|
+
const resource = String(p.resource ?? "").trim();
|
|
5398
|
+
const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
|
|
5399
|
+
const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
|
|
5400
|
+
if (!source || !resource) {
|
|
5401
|
+
return {
|
|
5402
|
+
success: false,
|
|
5403
|
+
results: { error: 'getLogs requires params "source" and "resource"' }
|
|
5404
|
+
};
|
|
5405
|
+
}
|
|
5406
|
+
try {
|
|
5407
|
+
const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
|
|
5408
|
+
source,
|
|
5409
|
+
resource,
|
|
5410
|
+
tail,
|
|
5411
|
+
afterTs
|
|
5412
|
+
});
|
|
5413
|
+
return {
|
|
5414
|
+
success: true,
|
|
5415
|
+
results: { records, latestTs, source, resource }
|
|
5416
|
+
};
|
|
5417
|
+
} catch (e) {
|
|
5418
|
+
return {
|
|
5419
|
+
success: false,
|
|
5420
|
+
results: { error: e?.message ?? String(e) }
|
|
5421
|
+
};
|
|
5422
|
+
}
|
|
5423
|
+
}
|
|
5424
|
+
};
|
|
5425
|
+
|
|
5426
|
+
// src/tasks/TasksRegistry.js
|
|
4868
5427
|
var TasksRegistry = class _TasksRegistry {
|
|
4869
|
-
|
|
5428
|
+
/**
|
|
5429
|
+
* @param {Record<string, Function>} [initial] Optional seed entries to copy in.
|
|
5430
|
+
*/
|
|
4870
5431
|
constructor(initial) {
|
|
5432
|
+
this.map = {};
|
|
4871
5433
|
if (initial) {
|
|
4872
5434
|
this.addMany(initial);
|
|
4873
5435
|
}
|
|
4874
5436
|
}
|
|
5437
|
+
/**
|
|
5438
|
+
* Build a registry pre-populated with every core task plus legacy aliases.
|
|
5439
|
+
* Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
|
|
5440
|
+
*
|
|
5441
|
+
* @returns {TasksRegistry}
|
|
5442
|
+
*/
|
|
4875
5443
|
static withCoreTasks() {
|
|
4876
|
-
return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
|
|
5444
|
+
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);
|
|
4877
5445
|
}
|
|
5446
|
+
/**
|
|
5447
|
+
* Register a single task class under a name. Overwrites any previous entry.
|
|
5448
|
+
*
|
|
5449
|
+
* @param {string} taskName
|
|
5450
|
+
* @param {Function} taskClass Subclass of `AbstractTask`.
|
|
5451
|
+
* @returns {this}
|
|
5452
|
+
*/
|
|
4878
5453
|
add(taskName, taskClass) {
|
|
4879
5454
|
this.map[taskName] = taskClass;
|
|
4880
5455
|
return this;
|
|
4881
5456
|
}
|
|
5457
|
+
/**
|
|
5458
|
+
* Bulk-register a name → class map. Later calls override earlier ones.
|
|
5459
|
+
*
|
|
5460
|
+
* @param {Record<string, Function>} entries
|
|
5461
|
+
* @returns {this}
|
|
5462
|
+
*/
|
|
4882
5463
|
addMany(entries) {
|
|
4883
5464
|
for (const [name, klass] of Object.entries(entries)) {
|
|
4884
5465
|
this.add(name, klass);
|
|
4885
5466
|
}
|
|
4886
5467
|
return this;
|
|
4887
5468
|
}
|
|
5469
|
+
/**
|
|
5470
|
+
* Look up a task class by name. Returns `undefined` when the name is unknown;
|
|
5471
|
+
* the runner treats that as "some other worker may handle this" and skips.
|
|
5472
|
+
*
|
|
5473
|
+
* @param {string} taskName
|
|
5474
|
+
* @returns {Function | undefined}
|
|
5475
|
+
*/
|
|
4888
5476
|
get(taskName) {
|
|
4889
5477
|
return this.map[taskName];
|
|
4890
5478
|
}
|
|
5479
|
+
/**
|
|
5480
|
+
* Strict variant of {@link get}: throws {@link ParamError} (with the list
|
|
5481
|
+
* of supported names) when `taskName` is unknown. Use from enqueuer code
|
|
5482
|
+
* paths where an unknown name is a hard CLI/programmer error.
|
|
5483
|
+
*
|
|
5484
|
+
* @param {string} taskName
|
|
5485
|
+
* @returns {Function}
|
|
5486
|
+
*/
|
|
5487
|
+
requireClass(taskName) {
|
|
5488
|
+
const TaskClass = taskName ? this.map[taskName] : void 0;
|
|
5489
|
+
if (!TaskClass) {
|
|
5490
|
+
const supported = this.listSupportedTasks().join(", ") || "(none)";
|
|
5491
|
+
throw new ParamError(
|
|
5492
|
+
`Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
|
|
5493
|
+
);
|
|
5494
|
+
}
|
|
5495
|
+
return TaskClass;
|
|
5496
|
+
}
|
|
5497
|
+
/**
|
|
5498
|
+
* Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
|
|
5499
|
+
* from `overrides` or `context.params`, look up the class, and delegate
|
|
5500
|
+
* to its static {@link AbstractTask.resolveParams} with `name` seeded into
|
|
5501
|
+
* the overrides. The returned object is shaped for {@link enqueueTask}.
|
|
5502
|
+
*
|
|
5503
|
+
* Validation failures (unknown task, missing required custom params, etc.)
|
|
5504
|
+
* surface as {@link ParamError} so the caller aborts cleanly before any
|
|
5505
|
+
* row is inserted.
|
|
5506
|
+
*
|
|
5507
|
+
* @param {object} context
|
|
5508
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5509
|
+
* @returns {Promise<object>}
|
|
5510
|
+
*/
|
|
5511
|
+
async resolveTaskParams(context, overrides = {}) {
|
|
5512
|
+
const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
|
|
5513
|
+
const fromCli = context.params.get("name", "string");
|
|
5514
|
+
const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
|
|
5515
|
+
const name = overrideName || cliName;
|
|
5516
|
+
if (!name) {
|
|
5517
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
5518
|
+
}
|
|
5519
|
+
const TaskClass = this.requireClass(name);
|
|
5520
|
+
return TaskClass.resolveParams(context, { ...overrides, name });
|
|
5521
|
+
}
|
|
5522
|
+
/**
|
|
5523
|
+
* Names of every registered task, sorted alphabetically (useful for CLI output
|
|
5524
|
+
* and allowlist sanity checks).
|
|
5525
|
+
*
|
|
5526
|
+
* @returns {string[]}
|
|
5527
|
+
*/
|
|
4891
5528
|
listSupportedTasks() {
|
|
4892
5529
|
return Object.keys(this.map).sort();
|
|
4893
5530
|
}
|
|
5531
|
+
/**
|
|
5532
|
+
* Shallow copy of the internal map, for handing to `addMany` on another registry
|
|
5533
|
+
* or for serialization.
|
|
5534
|
+
*
|
|
5535
|
+
* @returns {Record<string, Function>}
|
|
5536
|
+
*/
|
|
4894
5537
|
toObject() {
|
|
4895
5538
|
return { ...this.map };
|
|
4896
5539
|
}
|
|
4897
5540
|
};
|
|
4898
5541
|
|
|
4899
|
-
// src/tasks/
|
|
5542
|
+
// src/tasks/serviceTaskAllowlist.js
|
|
5543
|
+
function normalizeAllowedTasks(value) {
|
|
5544
|
+
if (!value) return void 0;
|
|
5545
|
+
if (Array.isArray(value)) {
|
|
5546
|
+
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
5547
|
+
return out2.length ? out2 : void 0;
|
|
5548
|
+
}
|
|
5549
|
+
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
5550
|
+
return out.length ? out : void 0;
|
|
5551
|
+
}
|
|
5552
|
+
|
|
5553
|
+
// src/tasks/taskScriptRunner.js
|
|
4900
5554
|
var import_node_child_process2 = require("child_process");
|
|
4901
5555
|
|
|
4902
|
-
// src/tasks/index.
|
|
5556
|
+
// src/tasks/index.js
|
|
4903
5557
|
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
4904
5558
|
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
4905
5559
|
function getDb3(context) {
|
|
@@ -4914,15 +5568,6 @@ function normalizeRegistry(registry) {
|
|
|
4914
5568
|
if (registry instanceof TasksRegistry) return registry;
|
|
4915
5569
|
return new TasksRegistry().addMany(registry);
|
|
4916
5570
|
}
|
|
4917
|
-
function normalizeAllowedTasks(value) {
|
|
4918
|
-
if (!value) return void 0;
|
|
4919
|
-
if (Array.isArray(value)) {
|
|
4920
|
-
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
4921
|
-
return out2.length ? out2 : void 0;
|
|
4922
|
-
}
|
|
4923
|
-
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
4924
|
-
return out.length ? out : void 0;
|
|
4925
|
-
}
|
|
4926
5571
|
async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
|
|
4927
5572
|
context.logger.warn?.(`[tasks] signaling ${runningTaskInstances.size} running task(s) to stop`);
|
|
4928
5573
|
for (const [, taskInstance] of runningTaskInstances) {
|
|
@@ -4938,18 +5583,20 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
|
|
|
4938
5583
|
}
|
|
4939
5584
|
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
4940
5585
|
const db = getDb3(context);
|
|
4941
|
-
const taskName = row.
|
|
5586
|
+
const taskName = row.name;
|
|
4942
5587
|
const TaskClass = registry.get(taskName);
|
|
4943
|
-
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
4944
5588
|
if (!TaskClass) {
|
|
4945
5589
|
const err = { message: `Unknown task "${taskName}"` };
|
|
4946
|
-
await db(historyTable).insert(
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
5590
|
+
await db(historyTable).insert(
|
|
5591
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
5592
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5593
|
+
success: false,
|
|
5594
|
+
status: "failed",
|
|
5595
|
+
status_changed_at: db.fn.now(),
|
|
5596
|
+
params: toJsonColumn(row.params),
|
|
5597
|
+
results: toJsonColumn(err)
|
|
5598
|
+
})
|
|
5599
|
+
);
|
|
4953
5600
|
if (row.schedule) {
|
|
4954
5601
|
await db(tasksTable).where({ id: row.id }).update({
|
|
4955
5602
|
started_at: null,
|
|
@@ -4957,7 +5604,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4957
5604
|
success: false,
|
|
4958
5605
|
results: toJsonColumn(err),
|
|
4959
5606
|
past_due: null,
|
|
4960
|
-
|
|
5607
|
+
status: "paused",
|
|
5608
|
+
status_changed_at: db.fn.now(),
|
|
4961
5609
|
progress: LOCKED_BY_ERROR_MESSAGE
|
|
4962
5610
|
});
|
|
4963
5611
|
} else {
|
|
@@ -4984,13 +5632,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4984
5632
|
} finally {
|
|
4985
5633
|
runningTaskInstances.delete(row.id);
|
|
4986
5634
|
}
|
|
4987
|
-
await db(historyTable).insert(
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
5635
|
+
await db(historyTable).insert(
|
|
5636
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
5637
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5638
|
+
success,
|
|
5639
|
+
status: success ? "completed" : "failed",
|
|
5640
|
+
status_changed_at: db.fn.now(),
|
|
5641
|
+
params: toJsonColumn(row.params),
|
|
5642
|
+
results: toJsonColumn(results)
|
|
5643
|
+
})
|
|
5644
|
+
);
|
|
4994
5645
|
if (!success) {
|
|
4995
5646
|
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
4996
5647
|
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
@@ -5005,19 +5656,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5005
5656
|
const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
|
|
5006
5657
|
appendTaskIpcLog(context, row, {
|
|
5007
5658
|
level: "error",
|
|
5008
|
-
message: `[tasks] task failed: ${row.
|
|
5659
|
+
message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
|
|
5009
5660
|
details: results
|
|
5010
5661
|
});
|
|
5011
5662
|
}
|
|
5012
5663
|
if (row.schedule) {
|
|
5013
5664
|
if (success) {
|
|
5665
|
+
let nextRunAt = null;
|
|
5666
|
+
try {
|
|
5667
|
+
nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
|
|
5668
|
+
} catch (e) {
|
|
5669
|
+
context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
|
|
5670
|
+
}
|
|
5014
5671
|
await db(tasksTable).where({ id: row.id }).update({
|
|
5015
5672
|
started_at: null,
|
|
5016
5673
|
completed_at: /* @__PURE__ */ new Date(),
|
|
5017
5674
|
success,
|
|
5018
5675
|
results: toJsonColumn(results),
|
|
5019
5676
|
progress: null,
|
|
5020
|
-
past_due: null
|
|
5677
|
+
past_due: null,
|
|
5678
|
+
status: "idle",
|
|
5679
|
+
status_changed_at: db.fn.now(),
|
|
5680
|
+
next_run_at: nextRunAt,
|
|
5681
|
+
// Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
|
|
5682
|
+
service_name: null,
|
|
5683
|
+
server_name: null,
|
|
5684
|
+
instance_number: null
|
|
5021
5685
|
});
|
|
5022
5686
|
} else {
|
|
5023
5687
|
await db(tasksTable).where({ id: row.id }).update({
|
|
@@ -5025,7 +5689,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5025
5689
|
completed_at: /* @__PURE__ */ new Date(),
|
|
5026
5690
|
success,
|
|
5027
5691
|
results: toJsonColumn(results),
|
|
5028
|
-
|
|
5692
|
+
status: "paused",
|
|
5693
|
+
status_changed_at: db.fn.now(),
|
|
5029
5694
|
progress: LOCKED_BY_ERROR_MESSAGE,
|
|
5030
5695
|
past_due: null
|
|
5031
5696
|
});
|
|
@@ -5037,58 +5702,93 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5037
5702
|
const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
|
|
5038
5703
|
return { stopRunnerRequested, stopAllowanceMs };
|
|
5039
5704
|
}
|
|
5040
|
-
|
|
5705
|
+
function shuffleTaskRowsInPlace(rows) {
|
|
5706
|
+
for (let i = rows.length - 1; i > 0; i--) {
|
|
5707
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
5708
|
+
const t = rows[i];
|
|
5709
|
+
rows[i] = rows[j];
|
|
5710
|
+
rows[j] = t;
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5713
|
+
async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
|
|
5041
5714
|
const db = getDb3(context);
|
|
5042
|
-
let query = db(tasksTable).
|
|
5715
|
+
let query = db(tasksTable).where({ status: "idle" }).where(function() {
|
|
5716
|
+
this.whereNull("service_group").orWhere({ service_group: serviceGroup });
|
|
5717
|
+
}).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);
|
|
5043
5718
|
if (taskNames && taskNames.length > 0) {
|
|
5044
|
-
query = query.whereIn("
|
|
5719
|
+
query = query.whereIn("name", taskNames);
|
|
5720
|
+
}
|
|
5721
|
+
if (runnerIdentity) {
|
|
5722
|
+
query = query.where(function() {
|
|
5723
|
+
this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
|
|
5724
|
+
}).where(function() {
|
|
5725
|
+
this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
|
|
5726
|
+
}).where(function() {
|
|
5727
|
+
this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
|
|
5728
|
+
});
|
|
5729
|
+
} else {
|
|
5730
|
+
query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
|
|
5045
5731
|
}
|
|
5046
5732
|
const candidates = await query;
|
|
5733
|
+
shuffleTaskRowsInPlace(candidates);
|
|
5047
5734
|
for (const row of candidates) {
|
|
5048
5735
|
if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
|
|
5049
5736
|
continue;
|
|
5050
5737
|
}
|
|
5051
|
-
const TaskClass = registry.get(row.
|
|
5052
|
-
if (TaskClass) {
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5738
|
+
const TaskClass = registry.get(row.name);
|
|
5739
|
+
if (!TaskClass) {
|
|
5740
|
+
continue;
|
|
5741
|
+
}
|
|
5742
|
+
const taskInstance = new TaskClass(context, row);
|
|
5743
|
+
const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
|
|
5744
|
+
if (reason) {
|
|
5745
|
+
if (!row.past_due) {
|
|
5746
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
5747
|
+
past_due: db.fn.now(),
|
|
5748
|
+
progress: String(reason)
|
|
5749
|
+
});
|
|
5063
5750
|
}
|
|
5751
|
+
continue;
|
|
5752
|
+
}
|
|
5753
|
+
const claimPatch = {
|
|
5754
|
+
started_at: db.fn.now(),
|
|
5755
|
+
status: "running",
|
|
5756
|
+
status_changed_at: db.fn.now()
|
|
5757
|
+
};
|
|
5758
|
+
if (runnerIdentity) {
|
|
5759
|
+
claimPatch.service_name = runnerIdentity.service_name;
|
|
5760
|
+
claimPatch.server_name = runnerIdentity.server_name;
|
|
5761
|
+
claimPatch.instance_number = runnerIdentity.instance_number;
|
|
5064
5762
|
}
|
|
5065
|
-
const updated = await db(tasksTable).where({ id: row.id
|
|
5763
|
+
const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
|
|
5066
5764
|
const claimed = Array.isArray(updated) ? updated[0] : null;
|
|
5067
5765
|
if (claimed) return claimed;
|
|
5068
5766
|
}
|
|
5069
5767
|
return null;
|
|
5070
5768
|
}
|
|
5071
5769
|
async function runTasksLoop(context, options) {
|
|
5072
|
-
const
|
|
5770
|
+
const queueName = options.queueName ?? "tasks";
|
|
5073
5771
|
const target = options.target;
|
|
5074
5772
|
const pollMs = options.pollMs ?? 1e3;
|
|
5075
|
-
const
|
|
5773
|
+
const claimJitterMs = options.claimJitterMs ?? 0;
|
|
5774
|
+
const maxParallel = options.maxParallel ?? 32;
|
|
5076
5775
|
const scanLimit = options.scanLimit ?? 100;
|
|
5077
5776
|
const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5078
5777
|
const registry = normalizeRegistry(options.registry);
|
|
5079
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
5778
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
5080
5779
|
if (!target) throw new Error("runTasksLoop: target is required");
|
|
5780
|
+
context.tasksQueueName = queueName;
|
|
5081
5781
|
const runningPromises = /* @__PURE__ */ new Set();
|
|
5082
5782
|
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
5083
5783
|
let runningStopControlPromise = null;
|
|
5084
5784
|
let stopRequested = false;
|
|
5085
5785
|
let stopAllowanceMs = 5e3;
|
|
5086
|
-
context.
|
|
5786
|
+
context.tasksRunnerStop = false;
|
|
5087
5787
|
let registryReg = null;
|
|
5088
5788
|
let registryInterval = null;
|
|
5789
|
+
let runnerIdentity = null;
|
|
5089
5790
|
const hbGroup = options.runnerServiceGroup?.trim();
|
|
5090
5791
|
if (hbGroup) {
|
|
5091
|
-
const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
|
|
5092
5792
|
const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
|
|
5093
5793
|
const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
|
|
5094
5794
|
const defaultMeta = {
|
|
@@ -5096,16 +5796,21 @@ async function runTasksLoop(context, options) {
|
|
|
5096
5796
|
allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
|
|
5097
5797
|
};
|
|
5098
5798
|
registryReg = await registerInServicesRegistry(context, {
|
|
5099
|
-
|
|
5799
|
+
queueName,
|
|
5100
5800
|
target,
|
|
5101
5801
|
serviceGroup: hbGroup,
|
|
5102
5802
|
serviceName: options.runnerServiceName,
|
|
5103
|
-
|
|
5803
|
+
instanceNumber: options.runnerInstanceNumber,
|
|
5104
5804
|
staleMs,
|
|
5105
5805
|
groupMaxInstances: options.runnerGroupMaxInstances,
|
|
5106
5806
|
enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
|
|
5107
5807
|
metadata: options.runnerMetadata ?? defaultMeta
|
|
5108
5808
|
});
|
|
5809
|
+
runnerIdentity = {
|
|
5810
|
+
service_name: registryReg.serviceName,
|
|
5811
|
+
server_name: import_node_os3.default.hostname(),
|
|
5812
|
+
instance_number: registryReg.instanceNumber
|
|
5813
|
+
};
|
|
5109
5814
|
registryInterval = setInterval(() => {
|
|
5110
5815
|
void touchServicesRegistry(context, registryReg).catch((err) => {
|
|
5111
5816
|
context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
|
|
@@ -5113,7 +5818,7 @@ async function runTasksLoop(context, options) {
|
|
|
5113
5818
|
}, hbIntervalMs);
|
|
5114
5819
|
}
|
|
5115
5820
|
try {
|
|
5116
|
-
while (!context.isStop() && !stopRequested &&
|
|
5821
|
+
while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
|
|
5117
5822
|
if (!runningStopControlPromise) {
|
|
5118
5823
|
const claimedStopTask = await claimNextRunnableTask(
|
|
5119
5824
|
context,
|
|
@@ -5121,7 +5826,8 @@ async function runTasksLoop(context, options) {
|
|
|
5121
5826
|
target,
|
|
5122
5827
|
registry,
|
|
5123
5828
|
10,
|
|
5124
|
-
["stopRunner", "stop"]
|
|
5829
|
+
["stopRunner", "stop"],
|
|
5830
|
+
runnerIdentity
|
|
5125
5831
|
);
|
|
5126
5832
|
if (claimedStopTask) {
|
|
5127
5833
|
runningStopControlPromise = executeClaimedTask(
|
|
@@ -5135,7 +5841,7 @@ async function runTasksLoop(context, options) {
|
|
|
5135
5841
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5136
5842
|
stopRequested = true;
|
|
5137
5843
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5138
|
-
context.
|
|
5844
|
+
context.tasksRunnerStop = true;
|
|
5139
5845
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5140
5846
|
}
|
|
5141
5847
|
}).finally(() => {
|
|
@@ -5143,6 +5849,9 @@ async function runTasksLoop(context, options) {
|
|
|
5143
5849
|
});
|
|
5144
5850
|
}
|
|
5145
5851
|
}
|
|
5852
|
+
if (claimJitterMs > 0) {
|
|
5853
|
+
await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
|
|
5854
|
+
}
|
|
5146
5855
|
while (runningPromises.size < maxParallel) {
|
|
5147
5856
|
const claimed = await claimNextRunnableTask(
|
|
5148
5857
|
context,
|
|
@@ -5150,14 +5859,15 @@ async function runTasksLoop(context, options) {
|
|
|
5150
5859
|
target,
|
|
5151
5860
|
registry,
|
|
5152
5861
|
scanLimit,
|
|
5153
|
-
allowedTasks
|
|
5862
|
+
allowedTasks,
|
|
5863
|
+
runnerIdentity
|
|
5154
5864
|
);
|
|
5155
5865
|
if (!claimed) break;
|
|
5156
5866
|
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
5157
5867
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5158
5868
|
stopRequested = true;
|
|
5159
5869
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5160
|
-
context.
|
|
5870
|
+
context.tasksRunnerStop = true;
|
|
5161
5871
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5162
5872
|
}
|
|
5163
5873
|
}).finally(() => {
|
|
@@ -5165,7 +5875,16 @@ async function runTasksLoop(context, options) {
|
|
|
5165
5875
|
});
|
|
5166
5876
|
runningPromises.add(p);
|
|
5167
5877
|
}
|
|
5168
|
-
|
|
5878
|
+
const wakePromises = [...runningPromises];
|
|
5879
|
+
if (runningStopControlPromise) {
|
|
5880
|
+
wakePromises.push(runningStopControlPromise);
|
|
5881
|
+
}
|
|
5882
|
+
if (wakePromises.length === 0) {
|
|
5883
|
+
await sleepMs(pollMs);
|
|
5884
|
+
} else {
|
|
5885
|
+
const safe = wakePromises.map((p) => p.catch(() => void 0));
|
|
5886
|
+
await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
|
|
5887
|
+
}
|
|
5169
5888
|
}
|
|
5170
5889
|
if (context.isStop() && !stopRequested) {
|
|
5171
5890
|
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
@@ -5200,71 +5919,88 @@ async function runTasksLoop(context, options) {
|
|
|
5200
5919
|
}
|
|
5201
5920
|
}
|
|
5202
5921
|
var TasksManager = class _TasksManager {
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5922
|
+
/**
|
|
5923
|
+
* @param {object} context
|
|
5924
|
+
* @param {{
|
|
5925
|
+
* queueName?: string,
|
|
5926
|
+
* target?: string,
|
|
5927
|
+
* recreateTaskTables?: boolean,
|
|
5928
|
+
* pollMs?: number,
|
|
5929
|
+
* claimJitterMs?: number,
|
|
5930
|
+
* maxParallel?: number,
|
|
5931
|
+
* scanLimit?: number,
|
|
5932
|
+
* allowedTasks?: string | string[],
|
|
5933
|
+
* registry?: TasksRegistry | Record<string, Function>,
|
|
5934
|
+
* runnerServiceGroup?: string,
|
|
5935
|
+
* runnerServiceName?: string,
|
|
5936
|
+
* runnerInstanceNumber?: number,
|
|
5937
|
+
* runnerHeartbeatIntervalMs?: number,
|
|
5938
|
+
* runnerHeartbeatStaleMs?: number,
|
|
5939
|
+
* runnerGroupMaxInstances?: number,
|
|
5940
|
+
* runnerEnforceMaxInstances?: boolean,
|
|
5941
|
+
* runnerMetadata?: Record<string, unknown>,
|
|
5942
|
+
* }} [options]
|
|
5943
|
+
*/
|
|
5220
5944
|
constructor(context, options = {}) {
|
|
5221
5945
|
this.context = context;
|
|
5222
|
-
this.
|
|
5946
|
+
this.queueName = options.queueName ?? "tasks";
|
|
5223
5947
|
this.target = options.target ?? "localRunner";
|
|
5224
5948
|
this.recreateTaskTables = options.recreateTaskTables ?? false;
|
|
5225
5949
|
this.pollMs = options.pollMs ?? 1e3;
|
|
5950
|
+
this.claimJitterMs = options.claimJitterMs ?? 0;
|
|
5226
5951
|
this.maxParallel = options.maxParallel ?? 1;
|
|
5227
5952
|
this.scanLimit = options.scanLimit ?? 100;
|
|
5228
5953
|
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5229
5954
|
this.registry = normalizeRegistry(options.registry);
|
|
5230
5955
|
this.runnerServiceGroup = options.runnerServiceGroup;
|
|
5231
5956
|
this.runnerServiceName = options.runnerServiceName;
|
|
5232
|
-
this.
|
|
5957
|
+
this.runnerInstanceNumber = options.runnerInstanceNumber;
|
|
5233
5958
|
this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
|
|
5234
5959
|
this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
|
|
5235
5960
|
this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
|
|
5236
5961
|
this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
|
|
5237
5962
|
this.runnerMetadata = options.runnerMetadata;
|
|
5238
5963
|
}
|
|
5964
|
+
/**
|
|
5965
|
+
* Preferred factory: reads defaults from `context.params` (module namespace
|
|
5966
|
+
* `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
|
|
5967
|
+
* and inline options in one consistent resolver.
|
|
5968
|
+
*
|
|
5969
|
+
* @param {object} context
|
|
5970
|
+
* @param {ConstructorParameters<typeof TasksManager>[1]} [options]
|
|
5971
|
+
* @returns {TasksManager}
|
|
5972
|
+
*/
|
|
5239
5973
|
static init(context, options = {}) {
|
|
5240
5974
|
const defs2 = {
|
|
5241
5975
|
table: "string default tasks",
|
|
5242
5976
|
target: "string default localRunner",
|
|
5243
5977
|
recreateTaskTables: "boolean default false",
|
|
5244
5978
|
pollMs: "number default 1000",
|
|
5979
|
+
claimJitterMs: "number default 0",
|
|
5245
5980
|
maxParallel: "number default 1",
|
|
5246
5981
|
scanLimit: "number default 100",
|
|
5247
5982
|
allowedTasks: "string",
|
|
5248
5983
|
runnerServiceGroup: "string",
|
|
5249
5984
|
runnerServiceName: "string",
|
|
5250
|
-
|
|
5985
|
+
runnerInstanceNumber: "number",
|
|
5251
5986
|
runnerHeartbeatIntervalMs: "number default 10000",
|
|
5252
5987
|
runnerHeartbeatStaleMs: "number default 45000",
|
|
5253
5988
|
runnerGroupMaxInstances: "number",
|
|
5254
5989
|
runnerEnforceMaxInstances: "boolean default true"
|
|
5255
5990
|
};
|
|
5256
|
-
const discovered = context.params.getAllForModule(defs2);
|
|
5991
|
+
const discovered = context.params.getAllForModule("tasks", defs2);
|
|
5257
5992
|
const resolved = {
|
|
5258
|
-
|
|
5993
|
+
queueName: discovered.table,
|
|
5259
5994
|
target: discovered.target,
|
|
5260
5995
|
recreateTaskTables: discovered.recreateTaskTables,
|
|
5261
5996
|
pollMs: discovered.pollMs,
|
|
5997
|
+
claimJitterMs: discovered.claimJitterMs,
|
|
5262
5998
|
maxParallel: discovered.maxParallel,
|
|
5263
5999
|
scanLimit: discovered.scanLimit,
|
|
5264
6000
|
allowedTasks: discovered.allowedTasks,
|
|
5265
6001
|
runnerServiceGroup: discovered.runnerServiceGroup,
|
|
5266
6002
|
runnerServiceName: discovered.runnerServiceName,
|
|
5267
|
-
|
|
6003
|
+
runnerInstanceNumber: discovered.runnerInstanceNumber,
|
|
5268
6004
|
runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
|
|
5269
6005
|
runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
|
|
5270
6006
|
runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
|
|
@@ -5273,24 +6009,39 @@ var TasksManager = class _TasksManager {
|
|
|
5273
6009
|
};
|
|
5274
6010
|
return new _TasksManager(context, resolved);
|
|
5275
6011
|
}
|
|
6012
|
+
/**
|
|
6013
|
+
* Idempotently ensure the three backing tables exist for this queue.
|
|
6014
|
+
*
|
|
6015
|
+
* @param {{ recreate?: boolean }} [options]
|
|
6016
|
+
* @returns {Promise<void>}
|
|
6017
|
+
*/
|
|
5276
6018
|
async ensureTaskTables(options = {}) {
|
|
5277
6019
|
await ensureTaskTables(this.context, {
|
|
5278
|
-
|
|
6020
|
+
queueName: this.queueName,
|
|
5279
6021
|
recreate: options.recreate ?? this.recreateTaskTables
|
|
5280
6022
|
});
|
|
5281
6023
|
}
|
|
6024
|
+
/**
|
|
6025
|
+
* Start the runner loop using this manager's resolved config. Per-call
|
|
6026
|
+
* options override the stored defaults, but `runnerMetadata` still falls
|
|
6027
|
+
* through when omitted.
|
|
6028
|
+
*
|
|
6029
|
+
* @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
|
|
6030
|
+
* @returns {Promise<void>}
|
|
6031
|
+
*/
|
|
5282
6032
|
async runTasksLoop(options = {}) {
|
|
5283
6033
|
await runTasksLoop(this.context, {
|
|
5284
|
-
|
|
6034
|
+
queueName: options.queueName ?? this.queueName,
|
|
5285
6035
|
target: options.target ?? this.target,
|
|
5286
6036
|
pollMs: options.pollMs ?? this.pollMs,
|
|
6037
|
+
claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
|
|
5287
6038
|
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
5288
6039
|
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
5289
6040
|
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
5290
6041
|
registry: options.registry ?? this.registry,
|
|
5291
6042
|
runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
|
|
5292
6043
|
runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
|
|
5293
|
-
|
|
6044
|
+
runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
|
|
5294
6045
|
runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
|
|
5295
6046
|
runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
|
|
5296
6047
|
runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
|
|
@@ -5300,9 +6051,8 @@ var TasksManager = class _TasksManager {
|
|
|
5300
6051
|
}
|
|
5301
6052
|
};
|
|
5302
6053
|
|
|
5303
|
-
// src/scripts/cli-runner.
|
|
6054
|
+
// src/scripts/cli-runner.js
|
|
5304
6055
|
var defs = {
|
|
5305
|
-
dbName: "string default local",
|
|
5306
6056
|
tasksModule: "string"
|
|
5307
6057
|
};
|
|
5308
6058
|
async function loadTasksModule(modulePath) {
|
|
@@ -5314,11 +6064,8 @@ async function loadTasksModule(modulePath) {
|
|
|
5314
6064
|
return imported.tasksRegistry;
|
|
5315
6065
|
}
|
|
5316
6066
|
var flow = async (context) => {
|
|
5317
|
-
const {
|
|
5318
|
-
|
|
5319
|
-
tasksModule
|
|
5320
|
-
} = context.params.getAll(defs);
|
|
5321
|
-
const db = await dbInit(context, dbName);
|
|
6067
|
+
const { tasksModule } = context.params.getAll(defs);
|
|
6068
|
+
const db = await Db.init(context);
|
|
5322
6069
|
context.db = db;
|
|
5323
6070
|
const registry = new TasksRegistry().addMany(defaultTasksRegistry.toObject());
|
|
5324
6071
|
if (tasksModule) {
|