@nmakarov/cli-toolkit 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +1252 -604
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +1299 -650
- package/dist/cli-runner.js.map +1 -1
- package/dist/db.cjs +85 -157
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +84 -150
- package/dist/db.js.map +1 -1
- package/dist/errors.cjs +2 -2
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/filedatabase.cjs +19 -19
- package/dist/filedatabase.cjs.map +1 -1
- package/dist/filedatabase.js +19 -16
- package/dist/filedatabase.js.map +1 -1
- package/dist/http-client.cjs +9 -11
- package/dist/http-client.cjs.map +1 -1
- package/dist/http-client.js +10 -9
- package/dist/http-client.js.map +1 -1
- package/dist/http-client2.cjs +34 -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 +1739 -720
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1746 -721
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +81 -68
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +96 -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 +6 -9
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +6 -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
|
}
|
|
@@ -2131,11 +2125,11 @@ var Params = class _Params {
|
|
|
2131
2125
|
}
|
|
2132
2126
|
};
|
|
2133
2127
|
|
|
2134
|
-
// src/logger/index.
|
|
2128
|
+
// src/logger/index.js
|
|
2135
2129
|
var import_chalk = __toESM(require("chalk"), 1);
|
|
2136
2130
|
var import_util = __toESM(require("util"), 1);
|
|
2137
2131
|
|
|
2138
|
-
// src/logger/transports.
|
|
2132
|
+
// src/logger/transports.js
|
|
2139
2133
|
var ConsoleTransport = class {
|
|
2140
2134
|
write(payload) {
|
|
2141
2135
|
console.info(payload);
|
|
@@ -2155,7 +2149,7 @@ var ParentProcessTransport = class {
|
|
|
2155
2149
|
}
|
|
2156
2150
|
};
|
|
2157
2151
|
|
|
2158
|
-
// src/logger/index.
|
|
2152
|
+
// src/logger/index.js
|
|
2159
2153
|
var ALL_LEVELS = [
|
|
2160
2154
|
"silly",
|
|
2161
2155
|
"debug",
|
|
@@ -2228,6 +2222,7 @@ var Logger = class _Logger {
|
|
|
2228
2222
|
}
|
|
2229
2223
|
/**
|
|
2230
2224
|
* Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
|
|
2225
|
+
* Params are tracked under the `logger` module for --showUsedParams.
|
|
2231
2226
|
*/
|
|
2232
2227
|
static init(context, options) {
|
|
2233
2228
|
const paramDefs = {
|
|
@@ -2241,7 +2236,7 @@ var Logger = class _Logger {
|
|
|
2241
2236
|
progressWithTimes: "boolean default false",
|
|
2242
2237
|
progressThrottleMs: "number"
|
|
2243
2238
|
};
|
|
2244
|
-
const discovered = context.params.getAllForModule(paramDefs);
|
|
2239
|
+
const discovered = context.params.getAllForModule("logger", paramDefs);
|
|
2245
2240
|
const config2 = { ...discovered, ...options };
|
|
2246
2241
|
const logger = new _Logger(context, config2);
|
|
2247
2242
|
context.logger = logger;
|
|
@@ -2437,9 +2432,9 @@ var Logger = class _Logger {
|
|
|
2437
2432
|
}
|
|
2438
2433
|
};
|
|
2439
2434
|
|
|
2440
|
-
// src/init/index.
|
|
2435
|
+
// src/init/index.js
|
|
2441
2436
|
var import_events = require("events");
|
|
2442
|
-
function extractComponentOptions(opts,
|
|
2437
|
+
function extractComponentOptions(opts, _componentName) {
|
|
2443
2438
|
const reservedKeys = ["overrides", "defaults", "modules"];
|
|
2444
2439
|
const componentOptions = {};
|
|
2445
2440
|
for (const [key, value] of Object.entries(opts)) {
|
|
@@ -2506,6 +2501,19 @@ function printAllParameters(context) {
|
|
|
2506
2501
|
async function init(flow2, opts = {}) {
|
|
2507
2502
|
let stop = false;
|
|
2508
2503
|
let context = null;
|
|
2504
|
+
let cleanupRan = false;
|
|
2505
|
+
const runRegisteredCleanups = async (ctx) => {
|
|
2506
|
+
if (cleanupRan) return;
|
|
2507
|
+
cleanupRan = true;
|
|
2508
|
+
const fns = [...ctx.cleanupFunctions].reverse();
|
|
2509
|
+
for (const fn of fns) {
|
|
2510
|
+
try {
|
|
2511
|
+
await fn(ctx);
|
|
2512
|
+
} catch (error) {
|
|
2513
|
+
ctx.logger.warn("[cleanup] error in cleanup function:", error);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
};
|
|
2509
2517
|
try {
|
|
2510
2518
|
try {
|
|
2511
2519
|
const screenModule = await Promise.resolve().then(() => (init_screen(), screen_exports));
|
|
@@ -2528,13 +2536,24 @@ async function init(flow2, opts = {}) {
|
|
|
2528
2536
|
printAllParameters(context);
|
|
2529
2537
|
process.exit(0);
|
|
2530
2538
|
}
|
|
2539
|
+
let sigintCount = 0;
|
|
2531
2540
|
process.on("SIGINT", async () => {
|
|
2532
|
-
if (
|
|
2533
|
-
|
|
2534
|
-
|
|
2541
|
+
if (!context) return;
|
|
2542
|
+
sigintCount += 1;
|
|
2543
|
+
if (sigintCount === 1) {
|
|
2544
|
+
stop = true;
|
|
2545
|
+
context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
|
|
2546
|
+
context.emitter.emit("stop", stopAllowance);
|
|
2547
|
+
return;
|
|
2535
2548
|
}
|
|
2549
|
+
context.logger.warn("[process] second SIGINT: running cleanup then exit");
|
|
2550
|
+
await runRegisteredCleanups(context);
|
|
2551
|
+
process.exit(2);
|
|
2552
|
+
});
|
|
2553
|
+
process.on("SIGTERM", () => {
|
|
2554
|
+
if (!context || stop) return;
|
|
2536
2555
|
stop = true;
|
|
2537
|
-
context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
|
|
2556
|
+
context.logger.info(`>> SIGTERM: emitting stop with allowance ${stopAllowance}`);
|
|
2538
2557
|
context.emitter.emit("stop", stopAllowance);
|
|
2539
2558
|
});
|
|
2540
2559
|
await flow2(context);
|
|
@@ -2559,33 +2578,62 @@ async function init(flow2, opts = {}) {
|
|
|
2559
2578
|
}
|
|
2560
2579
|
} finally {
|
|
2561
2580
|
if (context) {
|
|
2562
|
-
|
|
2563
|
-
try {
|
|
2564
|
-
await fn(context);
|
|
2565
|
-
} catch (error) {
|
|
2566
|
-
context.logger.warn("[cleanup] error in cleanup function:", error);
|
|
2567
|
-
}
|
|
2568
|
-
}
|
|
2581
|
+
await runRegisteredCleanups(context);
|
|
2569
2582
|
}
|
|
2570
2583
|
}
|
|
2571
2584
|
}
|
|
2572
2585
|
|
|
2573
|
-
// src/db/index.
|
|
2586
|
+
// src/db/index.js
|
|
2574
2587
|
var import_knex = __toESM(require("knex"), 1);
|
|
2588
|
+
var KNEX_DEFAULTS = {
|
|
2589
|
+
testConnection: true,
|
|
2590
|
+
pool: { min: 2, max: 10 },
|
|
2591
|
+
acquireConnectionTimeout: 1e4,
|
|
2592
|
+
ssl: { rejectUnauthorized: false }
|
|
2593
|
+
};
|
|
2575
2594
|
var Db = class {
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2595
|
+
static async init(context, options = {}) {
|
|
2596
|
+
const defs2 = {
|
|
2597
|
+
dbName: "string",
|
|
2598
|
+
dbConnectionString: "string",
|
|
2599
|
+
dbProfile: "boolean default false"
|
|
2600
|
+
};
|
|
2601
|
+
const discovered = context?.params?.getAllForModule?.("db", defs2) ?? {};
|
|
2602
|
+
const merged = { ...discovered, ...options };
|
|
2603
|
+
let { dbName, dbConnectionString } = merged;
|
|
2604
|
+
const { dbProfile } = merged;
|
|
2605
|
+
if (!dbName && !dbConnectionString) {
|
|
2606
|
+
dbName = "local";
|
|
2607
|
+
}
|
|
2608
|
+
if (dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
|
|
2609
|
+
dbConnectionString = dbName;
|
|
2610
|
+
dbName = void 0;
|
|
2611
|
+
}
|
|
2612
|
+
if (dbName && !dbConnectionString) {
|
|
2613
|
+
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
2614
|
+
dbConnectionString = await context.params.get(paramName, "string");
|
|
2615
|
+
if (!dbConnectionString) {
|
|
2616
|
+
throw new ParamError(
|
|
2617
|
+
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
2618
|
+
);
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
const config2 = {
|
|
2622
|
+
...KNEX_DEFAULTS,
|
|
2623
|
+
connectionString: dbConnectionString,
|
|
2624
|
+
name: dbName || merged.name || "default",
|
|
2625
|
+
profile: !!dbProfile,
|
|
2626
|
+
logger: context.logger
|
|
2627
|
+
};
|
|
2628
|
+
return dbConnect(context, config2);
|
|
2629
|
+
}
|
|
2585
2630
|
constructor(config2) {
|
|
2586
|
-
if (!config2.connectionString) {
|
|
2631
|
+
if (!config2 || !config2.connectionString) {
|
|
2587
2632
|
throw new ParamError("Db: connectionString is required");
|
|
2588
2633
|
}
|
|
2634
|
+
this.knexInstance = null;
|
|
2635
|
+
this.isConnected = false;
|
|
2636
|
+
this.queriesLog = [];
|
|
2589
2637
|
this.config = {
|
|
2590
2638
|
testConnection: true,
|
|
2591
2639
|
profile: false,
|
|
@@ -2598,25 +2646,23 @@ var Db = class {
|
|
|
2598
2646
|
};
|
|
2599
2647
|
this.logger = this.config.logger;
|
|
2600
2648
|
const instance = this;
|
|
2601
|
-
const callableWrapper = function(
|
|
2649
|
+
const callableWrapper = function() {
|
|
2602
2650
|
throw new Error("This should never be called directly");
|
|
2603
2651
|
};
|
|
2604
2652
|
callableWrapper._instance = instance;
|
|
2605
2653
|
return new Proxy(callableWrapper, {
|
|
2606
|
-
|
|
2607
|
-
apply: (target, thisArg, argumentsList) => {
|
|
2654
|
+
apply: (target, _thisArg, argumentsList) => {
|
|
2608
2655
|
const inst = target._instance;
|
|
2609
2656
|
if (!inst.knexInstance) {
|
|
2610
2657
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
2611
2658
|
}
|
|
2612
2659
|
return inst.knexInstance(...argumentsList);
|
|
2613
2660
|
},
|
|
2614
|
-
// Intercept property access: db.schema, db.raw, etc.
|
|
2615
2661
|
get: (target, prop) => {
|
|
2616
2662
|
if (prop === "_instance") {
|
|
2617
2663
|
return target._instance;
|
|
2618
2664
|
}
|
|
2619
|
-
const
|
|
2665
|
+
const inst = target._instance;
|
|
2620
2666
|
const ownMethods = [
|
|
2621
2667
|
"connect",
|
|
2622
2668
|
"disconnect",
|
|
@@ -2629,26 +2675,26 @@ var Db = class {
|
|
|
2629
2675
|
"detectClient",
|
|
2630
2676
|
"attachProfiler"
|
|
2631
2677
|
];
|
|
2632
|
-
if (prop in
|
|
2633
|
-
const value =
|
|
2678
|
+
if (prop in inst) {
|
|
2679
|
+
const value = inst[prop];
|
|
2634
2680
|
if (typeof value === "function" && ownMethods.includes(prop)) {
|
|
2635
|
-
return value.bind(
|
|
2681
|
+
return value.bind(inst);
|
|
2636
2682
|
}
|
|
2637
2683
|
if (typeof value !== "function") {
|
|
2638
2684
|
return value;
|
|
2639
2685
|
}
|
|
2640
2686
|
}
|
|
2641
|
-
if (
|
|
2642
|
-
const knexProp =
|
|
2687
|
+
if (inst.knexInstance) {
|
|
2688
|
+
const knexProp = inst.knexInstance[prop];
|
|
2643
2689
|
if (typeof knexProp === "function") {
|
|
2644
|
-
return knexProp.bind(
|
|
2690
|
+
return knexProp.bind(inst.knexInstance);
|
|
2645
2691
|
}
|
|
2646
2692
|
return knexProp;
|
|
2647
2693
|
}
|
|
2648
|
-
if (prop in
|
|
2649
|
-
const method =
|
|
2694
|
+
if (prop in inst) {
|
|
2695
|
+
const method = inst[prop];
|
|
2650
2696
|
if (typeof method === "function") {
|
|
2651
|
-
return method.bind(
|
|
2697
|
+
return method.bind(inst);
|
|
2652
2698
|
}
|
|
2653
2699
|
return method;
|
|
2654
2700
|
}
|
|
@@ -2656,9 +2702,6 @@ var Db = class {
|
|
|
2656
2702
|
}
|
|
2657
2703
|
});
|
|
2658
2704
|
}
|
|
2659
|
-
/**
|
|
2660
|
-
* Detect database client type from connection string
|
|
2661
|
-
*/
|
|
2662
2705
|
detectClient(connectionString) {
|
|
2663
2706
|
if (connectionString.match(/^postgresql/)) {
|
|
2664
2707
|
return "pg";
|
|
@@ -2668,9 +2711,6 @@ var Db = class {
|
|
|
2668
2711
|
}
|
|
2669
2712
|
return null;
|
|
2670
2713
|
}
|
|
2671
|
-
/**
|
|
2672
|
-
* Connect to the database
|
|
2673
|
-
*/
|
|
2674
2714
|
async connect() {
|
|
2675
2715
|
if (this.isConnected && this.knexInstance) {
|
|
2676
2716
|
this.logger.warn?.("[Db] Already connected");
|
|
@@ -2679,14 +2719,13 @@ var Db = class {
|
|
|
2679
2719
|
const client = this.detectClient(this.config.connectionString);
|
|
2680
2720
|
if (!client) {
|
|
2681
2721
|
throw new ParamError(
|
|
2682
|
-
|
|
2722
|
+
"Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://"
|
|
2683
2723
|
);
|
|
2684
2724
|
}
|
|
2685
2725
|
try {
|
|
2686
2726
|
const connectionConfig = {
|
|
2687
2727
|
connectionString: this.config.connectionString,
|
|
2688
2728
|
family: 4
|
|
2689
|
-
// Force IPv4 only (disable IPv6)
|
|
2690
2729
|
};
|
|
2691
2730
|
this.knexInstance = (0, import_knex.default)({
|
|
2692
2731
|
client,
|
|
@@ -2702,7 +2741,9 @@ var Db = class {
|
|
|
2702
2741
|
await this.testConnection();
|
|
2703
2742
|
}
|
|
2704
2743
|
this.isConnected = true;
|
|
2705
|
-
this.logger.debug?.(
|
|
2744
|
+
this.logger.debug?.(
|
|
2745
|
+
`[Db] Connected to database "${this.config.name || this.config.connectionString}"`
|
|
2746
|
+
);
|
|
2706
2747
|
} catch (error) {
|
|
2707
2748
|
if (error instanceof ParamError) {
|
|
2708
2749
|
throw error;
|
|
@@ -2711,9 +2752,6 @@ var Db = class {
|
|
|
2711
2752
|
throw new ParamError(`Db: Connection failed - ${errorMsg}`);
|
|
2712
2753
|
}
|
|
2713
2754
|
}
|
|
2714
|
-
/**
|
|
2715
|
-
* Disconnect from the database
|
|
2716
|
-
*/
|
|
2717
2755
|
async disconnect() {
|
|
2718
2756
|
if (!this.knexInstance) {
|
|
2719
2757
|
return;
|
|
@@ -2723,16 +2761,15 @@ var Db = class {
|
|
|
2723
2761
|
this.knexInstance = null;
|
|
2724
2762
|
this.isConnected = false;
|
|
2725
2763
|
this.queriesLog = [];
|
|
2726
|
-
this.logger.debug?.(
|
|
2764
|
+
this.logger.debug?.(
|
|
2765
|
+
`[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`
|
|
2766
|
+
);
|
|
2727
2767
|
} catch (error) {
|
|
2728
2768
|
const errorMsg = this.getErrorMessage(error);
|
|
2729
2769
|
this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
|
|
2730
2770
|
throw error;
|
|
2731
2771
|
}
|
|
2732
2772
|
}
|
|
2733
|
-
/**
|
|
2734
|
-
* Extract error message from various error types
|
|
2735
|
-
*/
|
|
2736
2773
|
getErrorMessage(error) {
|
|
2737
2774
|
if (error instanceof AggregateError) {
|
|
2738
2775
|
const errors = error.errors || [];
|
|
@@ -2757,9 +2794,11 @@ var Db = class {
|
|
|
2757
2794
|
return `${code} (tried: ${addresses.join(", ")})`;
|
|
2758
2795
|
}
|
|
2759
2796
|
}
|
|
2760
|
-
const uniqueMessages = [
|
|
2761
|
-
|
|
2762
|
-
|
|
2797
|
+
const uniqueMessages = [
|
|
2798
|
+
...new Set(
|
|
2799
|
+
errors.map((e) => e instanceof Error ? e.message : String(e))
|
|
2800
|
+
)
|
|
2801
|
+
];
|
|
2763
2802
|
if (uniqueMessages.length === 1) {
|
|
2764
2803
|
return uniqueMessages[0];
|
|
2765
2804
|
}
|
|
@@ -2768,28 +2807,25 @@ var Db = class {
|
|
|
2768
2807
|
return error.message || "Multiple errors occurred";
|
|
2769
2808
|
}
|
|
2770
2809
|
if (error instanceof Error) {
|
|
2771
|
-
const
|
|
2772
|
-
if (
|
|
2773
|
-
return `${
|
|
2810
|
+
const code = error.code;
|
|
2811
|
+
if (code) {
|
|
2812
|
+
return `${code}: ${error.message || String(error)}`;
|
|
2774
2813
|
}
|
|
2775
2814
|
return error.message || String(error);
|
|
2776
2815
|
}
|
|
2777
2816
|
if (typeof error === "string") {
|
|
2778
2817
|
return error;
|
|
2779
2818
|
}
|
|
2780
|
-
if (error
|
|
2819
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
2781
2820
|
const msg = String(error.message);
|
|
2782
|
-
const
|
|
2783
|
-
if (
|
|
2784
|
-
return `${
|
|
2821
|
+
const code = error.code;
|
|
2822
|
+
if (code) {
|
|
2823
|
+
return `${code}: ${msg}`;
|
|
2785
2824
|
}
|
|
2786
2825
|
return msg;
|
|
2787
2826
|
}
|
|
2788
2827
|
return String(error) || "Unknown error";
|
|
2789
2828
|
}
|
|
2790
|
-
/**
|
|
2791
|
-
* Test database connection
|
|
2792
|
-
*/
|
|
2793
2829
|
async testConnection() {
|
|
2794
2830
|
if (!this.knexInstance) {
|
|
2795
2831
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -2805,9 +2841,6 @@ var Db = class {
|
|
|
2805
2841
|
throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
|
|
2806
2842
|
}
|
|
2807
2843
|
}
|
|
2808
|
-
/**
|
|
2809
|
-
* Attach query profiler to log all queries
|
|
2810
|
-
*/
|
|
2811
2844
|
attachProfiler() {
|
|
2812
2845
|
if (!this.knexInstance) {
|
|
2813
2846
|
return;
|
|
@@ -2817,7 +2850,7 @@ var Db = class {
|
|
|
2817
2850
|
this.knexInstance.on("query", (query) => {
|
|
2818
2851
|
query.__startTime = process.hrtime();
|
|
2819
2852
|
});
|
|
2820
|
-
this.knexInstance.on("query-response", (
|
|
2853
|
+
this.knexInstance.on("query-response", (_response, query) => {
|
|
2821
2854
|
const [seconds, nanoseconds] = process.hrtime(query.__startTime);
|
|
2822
2855
|
const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
|
|
2823
2856
|
const logEntry = {
|
|
@@ -2832,15 +2865,9 @@ var Db = class {
|
|
|
2832
2865
|
this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
|
|
2833
2866
|
});
|
|
2834
2867
|
}
|
|
2835
|
-
/**
|
|
2836
|
-
* Get query log (only available if profiling is enabled)
|
|
2837
|
-
*/
|
|
2838
2868
|
getQueryLog() {
|
|
2839
2869
|
return [...this.queriesLog];
|
|
2840
2870
|
}
|
|
2841
|
-
/**
|
|
2842
|
-
* Check if a table exists
|
|
2843
|
-
*/
|
|
2844
2871
|
async tableExists(tableName) {
|
|
2845
2872
|
if (!this.knexInstance) {
|
|
2846
2873
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -2852,65 +2879,28 @@ var Db = class {
|
|
|
2852
2879
|
throw error;
|
|
2853
2880
|
}
|
|
2854
2881
|
}
|
|
2855
|
-
/**
|
|
2856
|
-
* Get the underlying Knex instance (for advanced usage)
|
|
2857
|
-
*/
|
|
2858
2882
|
getKnex() {
|
|
2859
2883
|
if (!this.knexInstance) {
|
|
2860
2884
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
2861
2885
|
}
|
|
2862
2886
|
return this.knexInstance;
|
|
2863
2887
|
}
|
|
2864
|
-
/**
|
|
2865
|
-
* Get connection status
|
|
2866
|
-
*/
|
|
2867
2888
|
isConnectedToDb() {
|
|
2868
2889
|
return this.isConnected && this.knexInstance !== null;
|
|
2869
2890
|
}
|
|
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
2891
|
};
|
|
2878
2892
|
function capitalizeFirstLetter(str) {
|
|
2879
2893
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
2880
2894
|
}
|
|
2881
|
-
async function dbConnect(context,
|
|
2882
|
-
const defs2 = {
|
|
2883
|
-
testDbConnection: "boolean default true",
|
|
2884
|
-
name: "string",
|
|
2885
|
-
poolMin: "number default 2",
|
|
2886
|
-
poolMax: "number default 10",
|
|
2887
|
-
acquireConnectionTimeout: "number default 10000",
|
|
2888
|
-
sslRejectUnauthorized: "boolean default false"
|
|
2889
|
-
};
|
|
2890
|
-
const paramsConfig = context.params.getAllForModule(defs2);
|
|
2891
|
-
const config2 = {
|
|
2892
|
-
connectionString,
|
|
2893
|
-
name: paramsConfig.name || name || "default",
|
|
2894
|
-
testConnection: paramsConfig.testDbConnection,
|
|
2895
|
-
profile: dbProfile ?? false,
|
|
2896
|
-
pool: {
|
|
2897
|
-
min: paramsConfig.poolMin,
|
|
2898
|
-
max: paramsConfig.poolMax
|
|
2899
|
-
},
|
|
2900
|
-
acquireConnectionTimeout: paramsConfig.acquireConnectionTimeout,
|
|
2901
|
-
ssl: {
|
|
2902
|
-
rejectUnauthorized: paramsConfig.sslRejectUnauthorized
|
|
2903
|
-
},
|
|
2904
|
-
logger: context.logger
|
|
2905
|
-
};
|
|
2895
|
+
async function dbConnect(context, config2) {
|
|
2906
2896
|
try {
|
|
2907
2897
|
const db = new Db(config2);
|
|
2908
2898
|
context.registerCleanup(async () => {
|
|
2909
2899
|
await db.disconnect();
|
|
2910
|
-
context.logger.debug(`[Db] instance "${name
|
|
2900
|
+
context.logger.debug?.(`[Db] instance "${config2.name}" disconnected`);
|
|
2911
2901
|
});
|
|
2912
2902
|
await db.connect();
|
|
2913
|
-
context.logger.debug(`[Db] instance "${name
|
|
2903
|
+
context.logger.debug?.(`[Db] instance "${config2.name}" initialized`);
|
|
2914
2904
|
return db;
|
|
2915
2905
|
} catch (error) {
|
|
2916
2906
|
if (error instanceof ParamError) {
|
|
@@ -2920,48 +2910,11 @@ async function dbConnect(context, connectionString, name, dbProfile) {
|
|
|
2920
2910
|
throw new ParamError(`[Db] connect error: ${errorMsg}`);
|
|
2921
2911
|
}
|
|
2922
2912
|
}
|
|
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
2913
|
|
|
2964
|
-
// src/
|
|
2914
|
+
// src/tasks/index.js
|
|
2915
|
+
var import_node_os3 = __toESM(require("os"), 1);
|
|
2916
|
+
|
|
2917
|
+
// src/utils/date-utils.js
|
|
2965
2918
|
function isTimestampFolder(folderName) {
|
|
2966
2919
|
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
|
|
2967
2920
|
if (!isoRegex.test(folderName)) {
|
|
@@ -2971,7 +2924,7 @@ function isTimestampFolder(folderName) {
|
|
|
2971
2924
|
return !isNaN(date.getTime()) && date.getTime() > 0;
|
|
2972
2925
|
}
|
|
2973
2926
|
|
|
2974
|
-
// src/utils/fs-utils.
|
|
2927
|
+
// src/utils/fs-utils.js
|
|
2975
2928
|
var import_fs2 = __toESM(require("fs"), 1);
|
|
2976
2929
|
var import_path2 = __toESM(require("path"), 1);
|
|
2977
2930
|
async function ensurePath(...pathParts) {
|
|
@@ -2995,7 +2948,7 @@ function getFileExtension(dataType) {
|
|
|
2995
2948
|
}
|
|
2996
2949
|
}
|
|
2997
2950
|
|
|
2998
|
-
// src/utils/os-utils.
|
|
2951
|
+
// src/utils/os-utils.js
|
|
2999
2952
|
var import_fs3 = __toESM(require("fs"), 1);
|
|
3000
2953
|
var import_path3 = __toESM(require("path"), 1);
|
|
3001
2954
|
var import_child_process = require("child_process");
|
|
@@ -3024,7 +2977,7 @@ function getFreeDiskSpace(targetPath) {
|
|
|
3024
2977
|
}
|
|
3025
2978
|
}
|
|
3026
2979
|
|
|
3027
|
-
// src/utils/format-utils.
|
|
2980
|
+
// src/utils/format-utils.js
|
|
3028
2981
|
function bytesToHumanReadable(bytes) {
|
|
3029
2982
|
if (bytes === 0) return "0 B";
|
|
3030
2983
|
const k = 1024;
|
|
@@ -3033,7 +2986,7 @@ function bytesToHumanReadable(bytes) {
|
|
|
3033
2986
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
3034
2987
|
}
|
|
3035
2988
|
|
|
3036
|
-
// src/utils/core-utils.
|
|
2989
|
+
// src/utils/core-utils.js
|
|
3037
2990
|
function sleepMs(ms) {
|
|
3038
2991
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3039
2992
|
}
|
|
@@ -3042,14 +2995,82 @@ function toJsonColumn(value) {
|
|
|
3042
2995
|
return JSON.stringify(value);
|
|
3043
2996
|
}
|
|
3044
2997
|
|
|
3045
|
-
// src/tasks/servicesRegistry.
|
|
3046
|
-
var import_node_crypto2 = require("crypto");
|
|
3047
|
-
var import_promises = require("fs/promises");
|
|
2998
|
+
// src/tasks/servicesRegistry.js
|
|
3048
2999
|
var import_node_os = __toESM(require("os"), 1);
|
|
3049
|
-
var import_node_path = __toESM(require("path"), 1);
|
|
3050
3000
|
|
|
3051
|
-
// src/tasks/taskUtils.
|
|
3001
|
+
// src/tasks/taskUtils.js
|
|
3052
3002
|
var import_node_crypto = require("crypto");
|
|
3003
|
+
|
|
3004
|
+
// src/tasks/time-matcher.js
|
|
3005
|
+
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
3006
|
+
function resolveAsterisks(field, range) {
|
|
3007
|
+
return field.includes("*") ? field.replace("*", range) : field;
|
|
3008
|
+
}
|
|
3009
|
+
function resolveRanges(field) {
|
|
3010
|
+
const regex = /(\d+)-(\d+)/;
|
|
3011
|
+
let current = field;
|
|
3012
|
+
while (true) {
|
|
3013
|
+
const match = regex.exec(current);
|
|
3014
|
+
if (!match) break;
|
|
3015
|
+
const raw = match[0];
|
|
3016
|
+
let first = Number(match[1]);
|
|
3017
|
+
let last = Number(match[2]);
|
|
3018
|
+
if (last < first) {
|
|
3019
|
+
[first, last] = [last, first];
|
|
3020
|
+
}
|
|
3021
|
+
const values = [];
|
|
3022
|
+
for (let i = first; i <= last; i += 1) {
|
|
3023
|
+
values.push(i);
|
|
3024
|
+
}
|
|
3025
|
+
current = current.replace(raw, values.join(","));
|
|
3026
|
+
}
|
|
3027
|
+
return current;
|
|
3028
|
+
}
|
|
3029
|
+
function resolveSteps(field) {
|
|
3030
|
+
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
3031
|
+
if (!match) return field;
|
|
3032
|
+
const base = match[1];
|
|
3033
|
+
const step = Number(match[2]);
|
|
3034
|
+
if (!Number.isFinite(step) || step <= 0) return field;
|
|
3035
|
+
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
3036
|
+
}
|
|
3037
|
+
function convertPattern(pattern) {
|
|
3038
|
+
const parts = pattern.trim().split(/\s+/);
|
|
3039
|
+
if (parts.length !== 6) {
|
|
3040
|
+
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
3041
|
+
}
|
|
3042
|
+
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
3043
|
+
}
|
|
3044
|
+
function fieldMatches(field, value) {
|
|
3045
|
+
const allowed = field.split(",").map((v) => Number(v));
|
|
3046
|
+
return allowed.includes(value);
|
|
3047
|
+
}
|
|
3048
|
+
function matchesParsedPattern(parsed, date) {
|
|
3049
|
+
return fieldMatches(parsed[0], date.getSeconds()) && fieldMatches(parsed[1], date.getMinutes()) && fieldMatches(parsed[2], date.getHours()) && fieldMatches(parsed[3], date.getDate()) && fieldMatches(parsed[4], date.getMonth() + 1) && fieldMatches(parsed[5], date.getDay());
|
|
3050
|
+
}
|
|
3051
|
+
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
3052
|
+
const parsed = convertPattern(pattern);
|
|
3053
|
+
return matchesParsedPattern(parsed, date);
|
|
3054
|
+
}
|
|
3055
|
+
var MS_PER_SECOND = 1e3;
|
|
3056
|
+
var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
|
|
3057
|
+
function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
|
|
3058
|
+
const parsed = convertPattern(pattern);
|
|
3059
|
+
let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
|
|
3060
|
+
const end = t + maxSearchMs;
|
|
3061
|
+
while (t <= end) {
|
|
3062
|
+
const date = new Date(t);
|
|
3063
|
+
if (matchesParsedPattern(parsed, date)) {
|
|
3064
|
+
return date;
|
|
3065
|
+
}
|
|
3066
|
+
t += MS_PER_SECOND;
|
|
3067
|
+
}
|
|
3068
|
+
throw new Error(
|
|
3069
|
+
`nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
|
|
3070
|
+
);
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// src/tasks/taskUtils.js
|
|
3053
3074
|
function getDb(context) {
|
|
3054
3075
|
const db = context.db;
|
|
3055
3076
|
if (!db) {
|
|
@@ -3057,97 +3078,84 @@ function getDb(context) {
|
|
|
3057
3078
|
}
|
|
3058
3079
|
return db;
|
|
3059
3080
|
}
|
|
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
|
-
}
|
|
3081
|
+
function queueToTableNames(queueName) {
|
|
3064
3082
|
return {
|
|
3065
|
-
tasksTable:
|
|
3066
|
-
historyTable: `${
|
|
3083
|
+
tasksTable: queueName,
|
|
3084
|
+
historyTable: `${queueName}_history`,
|
|
3085
|
+
registryTable: `${queueName}_services_registry`
|
|
3067
3086
|
};
|
|
3068
3087
|
}
|
|
3069
|
-
function
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3088
|
+
function defineTasksTable(t, db, tableNameForIndex) {
|
|
3089
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
3090
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
3091
|
+
t.timestamp("started_at");
|
|
3092
|
+
t.timestamp("completed_at");
|
|
3093
|
+
t.integer("priority").notNullable().defaultTo(50);
|
|
3094
|
+
t.text("schedule");
|
|
3095
|
+
t.timestamp("next_run_at").defaultTo(null);
|
|
3096
|
+
t.timestamp("past_due").defaultTo(null);
|
|
3097
|
+
t.text("name").notNullable();
|
|
3098
|
+
t.text("opid");
|
|
3099
|
+
t.json("params");
|
|
3100
|
+
t.text("service_group");
|
|
3101
|
+
t.integer("instance_number");
|
|
3102
|
+
t.text("service_name");
|
|
3103
|
+
t.text("server_name");
|
|
3104
|
+
t.text("status").notNullable().defaultTo("idle");
|
|
3105
|
+
t.timestamp("status_changed_at").defaultTo(null);
|
|
3106
|
+
t.text("progress");
|
|
3107
|
+
t.boolean("success");
|
|
3108
|
+
t.json("results");
|
|
3109
|
+
t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
|
|
3110
|
+
t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
|
|
3111
|
+
}
|
|
3112
|
+
function taskHistoryInsertFromQueueRow(row, overrides) {
|
|
3113
|
+
const { id, ...snapshot } = row;
|
|
3114
|
+
void id;
|
|
3115
|
+
return {
|
|
3116
|
+
...snapshot,
|
|
3117
|
+
...overrides
|
|
3118
|
+
};
|
|
3074
3119
|
}
|
|
3075
3120
|
async function ensureTaskTables(context, options = {}) {
|
|
3076
|
-
const
|
|
3121
|
+
const queueName = options.queueName ?? "tasks";
|
|
3077
3122
|
const recreate = options.recreate ?? false;
|
|
3078
3123
|
const db = getDb(context);
|
|
3079
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
3124
|
+
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
3080
3125
|
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
3081
3126
|
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
3127
|
+
const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
|
|
3082
3128
|
if (recreate) {
|
|
3083
3129
|
await db.schema.dropTableIfExists(historyTable);
|
|
3084
3130
|
await db.schema.dropTableIfExists(tasksTable);
|
|
3131
|
+
await db.schema.dropTableIfExists(registryTable);
|
|
3085
3132
|
}
|
|
3086
3133
|
if (needsTasks) {
|
|
3087
3134
|
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
3088
3135
|
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`);
|
|
3136
|
+
defineTasksTable(t, db, tasksTable);
|
|
3109
3137
|
});
|
|
3110
3138
|
}
|
|
3111
3139
|
if (needsHistory) {
|
|
3112
3140
|
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`);
|
|
3141
|
+
defineTasksTable(t, db, historyTable);
|
|
3131
3142
|
});
|
|
3132
3143
|
}
|
|
3133
|
-
const registryTable = servicesRegistryTable(queue);
|
|
3134
|
-
const needsRegistry = !await db.tableExists(registryTable);
|
|
3135
3144
|
if (needsRegistry) {
|
|
3136
3145
|
await db.schema.createTable(registryTable, (t) => {
|
|
3137
3146
|
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
3138
|
-
t.
|
|
3139
|
-
t.text("queue").notNullable();
|
|
3147
|
+
t.text("queue_name").notNullable();
|
|
3140
3148
|
t.text("service_group").notNullable();
|
|
3149
|
+
t.integer("instance_number").notNullable().defaultTo(1);
|
|
3141
3150
|
t.text("service_name").notNullable();
|
|
3142
|
-
t.text("
|
|
3143
|
-
t.text("hostname");
|
|
3151
|
+
t.text("server_name").notNullable();
|
|
3144
3152
|
t.integer("pid");
|
|
3145
3153
|
t.json("metadata");
|
|
3146
3154
|
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
3147
3155
|
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
3148
|
-
t.unique(["
|
|
3149
|
-
t.index(["
|
|
3150
|
-
t.index(["
|
|
3156
|
+
t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
|
|
3157
|
+
t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
|
|
3158
|
+
t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
|
|
3151
3159
|
});
|
|
3152
3160
|
}
|
|
3153
3161
|
}
|
|
@@ -3158,7 +3166,7 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
|
3158
3166
|
});
|
|
3159
3167
|
}
|
|
3160
3168
|
|
|
3161
|
-
// src/tasks/servicesRegistry.
|
|
3169
|
+
// src/tasks/servicesRegistry.js
|
|
3162
3170
|
function getDb2(context) {
|
|
3163
3171
|
const db = context.db;
|
|
3164
3172
|
if (!db) {
|
|
@@ -3169,6 +3177,7 @@ function getDb2(context) {
|
|
|
3169
3177
|
var DEFAULT_GROUP_MAX_INSTANCES = {
|
|
3170
3178
|
intake: 1,
|
|
3171
3179
|
harvest: 1,
|
|
3180
|
+
harvester: 0,
|
|
3172
3181
|
loader: 0,
|
|
3173
3182
|
photos: 0,
|
|
3174
3183
|
photosprocessor: 0,
|
|
@@ -3178,25 +3187,6 @@ function sanitizeNamePart(raw) {
|
|
|
3178
3187
|
const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3179
3188
|
return s.slice(0, 80) || "runner";
|
|
3180
3189
|
}
|
|
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
3190
|
function resolveMaxInstances(serviceGroup, override) {
|
|
3201
3191
|
if (override !== void 0 && Number.isFinite(override)) {
|
|
3202
3192
|
return Math.max(0, Math.floor(Number(override)));
|
|
@@ -3204,184 +3194,198 @@ function resolveMaxInstances(serviceGroup, override) {
|
|
|
3204
3194
|
const g = serviceGroup.trim().toLowerCase();
|
|
3205
3195
|
return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
|
|
3206
3196
|
}
|
|
3207
|
-
async function countAliveInGroup(db, registryTable,
|
|
3197
|
+
async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
|
|
3208
3198
|
const cutoff = new Date(Date.now() - staleMs);
|
|
3209
|
-
let q = db(registryTable).where({
|
|
3210
|
-
if (
|
|
3211
|
-
q = q.whereNot("
|
|
3199
|
+
let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
|
|
3200
|
+
if (excludeRowId) {
|
|
3201
|
+
q = q.whereNot("id", excludeRowId);
|
|
3212
3202
|
}
|
|
3213
3203
|
const row = await q.count("id as count").first();
|
|
3214
3204
|
return Number(row?.count ?? 0);
|
|
3215
3205
|
}
|
|
3206
|
+
async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
|
|
3207
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
3208
|
+
const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
|
|
3209
|
+
const set = /* @__PURE__ */ new Set();
|
|
3210
|
+
for (const r of rows) {
|
|
3211
|
+
const n = Number(r.instance_number);
|
|
3212
|
+
if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
|
|
3213
|
+
}
|
|
3214
|
+
return set;
|
|
3215
|
+
}
|
|
3216
3216
|
function isUniqueViolation(error) {
|
|
3217
3217
|
const code = error?.code ?? error?.errno;
|
|
3218
3218
|
return code === "23505" || String(error?.message || "").includes("duplicate key");
|
|
3219
3219
|
}
|
|
3220
|
+
function buildMetadata(options) {
|
|
3221
|
+
const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
|
|
3222
|
+
if (options.target) {
|
|
3223
|
+
base.runnerTarget = options.target;
|
|
3224
|
+
}
|
|
3225
|
+
return toJsonColumn(Object.keys(base).length ? base : null);
|
|
3226
|
+
}
|
|
3227
|
+
function allocateInstanceNumber(occupied, explicit, maxSlots) {
|
|
3228
|
+
if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
|
|
3229
|
+
const e = Math.max(1, Math.floor(Number(explicit)));
|
|
3230
|
+
if (occupied.has(e)) {
|
|
3231
|
+
throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
|
|
3232
|
+
}
|
|
3233
|
+
if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
|
|
3234
|
+
throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
|
|
3235
|
+
}
|
|
3236
|
+
return e;
|
|
3237
|
+
}
|
|
3238
|
+
const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
|
|
3239
|
+
for (let n = 1; n <= cap; n++) {
|
|
3240
|
+
if (!occupied.has(n)) return n;
|
|
3241
|
+
}
|
|
3242
|
+
throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
|
|
3243
|
+
}
|
|
3244
|
+
function defaultServiceName(groupBase, hostBase, instanceNumber) {
|
|
3245
|
+
return `${groupBase}-${hostBase}-${instanceNumber}`;
|
|
3246
|
+
}
|
|
3220
3247
|
async function registerInServicesRegistry(context, options) {
|
|
3221
3248
|
const db = getDb2(context);
|
|
3222
|
-
const registryTable =
|
|
3249
|
+
const registryTable = queueToTableNames(options.queueName).registryTable;
|
|
3223
3250
|
const serviceGroup = options.serviceGroup.trim();
|
|
3224
3251
|
if (!serviceGroup) {
|
|
3225
3252
|
throw new Error("registerInServicesRegistry: serviceGroup is required");
|
|
3226
3253
|
}
|
|
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();
|
|
3254
|
+
const serverName = import_node_os.default.hostname();
|
|
3233
3255
|
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
|
-
}
|
|
3256
|
+
const meta = buildMetadata(options);
|
|
3257
|
+
const groupBase = sanitizeNamePart(serviceGroup);
|
|
3258
|
+
const hostBase = sanitizeNamePart(serverName);
|
|
3270
3259
|
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}).`;
|
|
3260
|
+
const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
|
|
3261
|
+
if (maxAllowed > 0 && aliveCount >= maxAllowed) {
|
|
3262
|
+
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
|
|
3281
3263
|
if (options.enforceMaxInstances) {
|
|
3282
3264
|
throw new Error(msg);
|
|
3283
3265
|
}
|
|
3284
3266
|
context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
|
|
3285
3267
|
}
|
|
3286
|
-
const
|
|
3287
|
-
const
|
|
3288
|
-
const
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3268
|
+
const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
|
|
3269
|
+
const cutoff = new Date(Date.now() - options.staleMs);
|
|
3270
|
+
const MAX_ATTEMPTS = 8;
|
|
3271
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
3272
|
+
const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
|
|
3273
|
+
const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
|
|
3274
|
+
const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
|
|
3275
|
+
const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
3276
|
+
if (existing) {
|
|
3277
|
+
const lastSeen = new Date(existing.last_seen_at);
|
|
3278
|
+
const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
|
|
3279
|
+
if (isAlive) {
|
|
3280
|
+
if (options.serviceName?.trim()) {
|
|
3281
|
+
throw new Error(
|
|
3282
|
+
`[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
|
|
3283
|
+
);
|
|
3284
|
+
}
|
|
3285
|
+
context.logger.warn?.(
|
|
3286
|
+
`[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
|
|
3287
|
+
);
|
|
3288
|
+
if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
|
|
3289
|
+
throw new Error(
|
|
3290
|
+
`[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
|
|
3291
|
+
);
|
|
3292
|
+
}
|
|
3293
|
+
await new Promise((r) => setTimeout(r, 50 + attempt * 30));
|
|
3294
|
+
continue;
|
|
3306
3295
|
}
|
|
3296
|
+
await db(registryTable).where({ id: existing.id }).update({
|
|
3297
|
+
server_name: serverName,
|
|
3298
|
+
pid,
|
|
3299
|
+
metadata: meta,
|
|
3300
|
+
service_group: serviceGroup,
|
|
3301
|
+
instance_number: instanceNumber,
|
|
3302
|
+
last_seen_at: db.fn.now()
|
|
3303
|
+
});
|
|
3304
|
+
const reg = {
|
|
3305
|
+
serviceName: serviceNameRaw,
|
|
3306
|
+
serviceGroup,
|
|
3307
|
+
queueName: options.queueName,
|
|
3308
|
+
target: options.target,
|
|
3309
|
+
rowId: String(existing.id),
|
|
3310
|
+
registryTable,
|
|
3311
|
+
instanceNumber
|
|
3312
|
+
};
|
|
3313
|
+
context.servicesRegistry = reg;
|
|
3314
|
+
context.runnerHeartbeat = reg;
|
|
3315
|
+
context.logger.info?.(
|
|
3316
|
+
`[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
3317
|
+
);
|
|
3318
|
+
return reg;
|
|
3307
3319
|
}
|
|
3308
|
-
}
|
|
3309
|
-
let inserted;
|
|
3310
|
-
for (const candidate of eachServiceNameCandidate(baseCandidates)) {
|
|
3311
3320
|
try {
|
|
3312
3321
|
const rows = await db(registryTable).insert({
|
|
3313
|
-
|
|
3314
|
-
queue: options.queue,
|
|
3322
|
+
queue_name: options.queueName,
|
|
3315
3323
|
service_group: serviceGroup,
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3324
|
+
instance_number: instanceNumber,
|
|
3325
|
+
service_name: serviceNameRaw,
|
|
3326
|
+
server_name: serverName,
|
|
3319
3327
|
pid,
|
|
3320
3328
|
metadata: meta,
|
|
3321
|
-
last_seen_at: db.fn.now()
|
|
3329
|
+
last_seen_at: db.fn.now(),
|
|
3330
|
+
created_at: db.fn.now()
|
|
3322
3331
|
}).returning(["id", "service_name"]);
|
|
3323
3332
|
const row = Array.isArray(rows) ? rows[0] : rows;
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3333
|
+
let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
|
|
3334
|
+
if (!rowId) {
|
|
3335
|
+
const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
3336
|
+
rowId = again?.id != null ? String(again.id) : "";
|
|
3327
3337
|
}
|
|
3338
|
+
if (!rowId) continue;
|
|
3339
|
+
const regNew = {
|
|
3340
|
+
serviceName: String(row?.service_name ?? serviceNameRaw),
|
|
3341
|
+
serviceGroup,
|
|
3342
|
+
queueName: options.queueName,
|
|
3343
|
+
target: options.target,
|
|
3344
|
+
rowId,
|
|
3345
|
+
registryTable,
|
|
3346
|
+
instanceNumber
|
|
3347
|
+
};
|
|
3348
|
+
context.servicesRegistry = regNew;
|
|
3349
|
+
context.runnerHeartbeat = regNew;
|
|
3350
|
+
context.logger.info?.(
|
|
3351
|
+
`[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
3352
|
+
);
|
|
3353
|
+
return regNew;
|
|
3328
3354
|
} catch (error) {
|
|
3329
3355
|
if (!isUniqueViolation(error)) {
|
|
3330
3356
|
throw error;
|
|
3331
3357
|
}
|
|
3358
|
+
context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
|
|
3332
3359
|
}
|
|
3333
3360
|
}
|
|
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}`
|
|
3361
|
+
throw new Error(
|
|
3362
|
+
`[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
|
|
3353
3363
|
);
|
|
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
3364
|
}
|
|
3364
3365
|
async function touchServicesRegistry(context, registration) {
|
|
3365
3366
|
const db = getDb2(context);
|
|
3366
|
-
const
|
|
3367
|
+
const serverName = import_node_os.default.hostname();
|
|
3367
3368
|
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
3368
|
-
await db(registration.registryTable).where({
|
|
3369
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
3369
3370
|
last_seen_at: db.fn.now(),
|
|
3370
|
-
|
|
3371
|
+
server_name: serverName,
|
|
3371
3372
|
pid
|
|
3372
3373
|
});
|
|
3373
3374
|
}
|
|
3374
3375
|
async function unregisterServicesRegistry(context, registration) {
|
|
3375
3376
|
const db = getDb2(context);
|
|
3376
|
-
await db(registration.registryTable).where({
|
|
3377
|
-
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName}
|
|
3377
|
+
await db(registration.registryTable).where({ id: registration.rowId }).delete();
|
|
3378
|
+
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
|
|
3378
3379
|
}
|
|
3379
3380
|
|
|
3380
|
-
// src/
|
|
3381
|
+
// src/tasks/taskLogs.js
|
|
3382
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
3383
|
+
|
|
3384
|
+
// src/filedatabase/index.js
|
|
3381
3385
|
var import_fs4 = __toESM(require("fs"), 1);
|
|
3382
3386
|
var import_path4 = __toESM(require("path"), 1);
|
|
3383
3387
|
|
|
3384
|
-
// src/filedatabase/serializers.
|
|
3388
|
+
// src/filedatabase/serializers.js
|
|
3385
3389
|
function detectDataType(data) {
|
|
3386
3390
|
if (Array.isArray(data)) {
|
|
3387
3391
|
return "json-array";
|
|
@@ -3413,7 +3417,7 @@ function deserializeData(rawData, dataType) {
|
|
|
3413
3417
|
}
|
|
3414
3418
|
}
|
|
3415
3419
|
|
|
3416
|
-
// src/filedatabase/index.
|
|
3420
|
+
// src/filedatabase/index.js
|
|
3417
3421
|
var FileDatabase = class _FileDatabase {
|
|
3418
3422
|
basePath;
|
|
3419
3423
|
namespace;
|
|
@@ -3499,7 +3503,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3499
3503
|
if (errors.length) {
|
|
3500
3504
|
throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
|
|
3501
3505
|
}
|
|
3502
|
-
|
|
3506
|
+
const parts = [this.basePath, this.namespace];
|
|
3503
3507
|
if (this.tableName) {
|
|
3504
3508
|
parts.push(...this.tableName.split("/"));
|
|
3505
3509
|
}
|
|
@@ -3992,14 +3996,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
3992
3996
|
* Prepare the instance for read or write operations
|
|
3993
3997
|
* This discovers state and sets up internal members based on mode and current data
|
|
3994
3998
|
*/
|
|
3995
|
-
async prepare(
|
|
3999
|
+
async prepare(options) {
|
|
4000
|
+
const { write, read, version, deferInitialVersion } = options;
|
|
3996
4001
|
if (write) {
|
|
3997
4002
|
if (this.versioned) {
|
|
3998
4003
|
if (this.currentVersion === null) {
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4004
|
+
if (!deferInitialVersion) {
|
|
4005
|
+
await this.makeNewVersion();
|
|
4006
|
+
this.metadata = this.getDefaultMetadata();
|
|
4007
|
+
this.metadata.version = this.currentVersion;
|
|
4008
|
+
this.makeNewFile();
|
|
4009
|
+
}
|
|
4003
4010
|
} else {
|
|
4004
4011
|
if (!this.metadata.files.length) {
|
|
4005
4012
|
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
@@ -4109,7 +4116,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4109
4116
|
if (options.forceNewVersion && !this.versioned) {
|
|
4110
4117
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
4111
4118
|
}
|
|
4112
|
-
await this.prepare({ write: true });
|
|
4119
|
+
await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
|
|
4113
4120
|
const incomingDataType = detectDataType(data);
|
|
4114
4121
|
this.metadata.dataType = incomingDataType;
|
|
4115
4122
|
if (options.forceNewVersion) {
|
|
@@ -4403,7 +4410,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4403
4410
|
}
|
|
4404
4411
|
};
|
|
4405
4412
|
|
|
4406
|
-
// src/tasks/taskLogs.
|
|
4413
|
+
// src/tasks/taskLogs.js
|
|
4407
4414
|
function getLogsState(context) {
|
|
4408
4415
|
const holder = context;
|
|
4409
4416
|
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
@@ -4456,6 +4463,89 @@ function getLogsState(context) {
|
|
|
4456
4463
|
holder.__tasksLogsState = state;
|
|
4457
4464
|
return state;
|
|
4458
4465
|
}
|
|
4466
|
+
function ipcLogTargetKey(target) {
|
|
4467
|
+
const bp = target.basePath ?? "";
|
|
4468
|
+
const ns = target.namespace ?? "";
|
|
4469
|
+
return `${bp}::${ns}::${target.tableName}`;
|
|
4470
|
+
}
|
|
4471
|
+
function ipcFileLogsTableNameForSourceResource(source, resource) {
|
|
4472
|
+
const seg = (s) => {
|
|
4473
|
+
const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
4474
|
+
return t.length ? t : "x";
|
|
4475
|
+
};
|
|
4476
|
+
return `${seg(source)}/${seg(resource)}`;
|
|
4477
|
+
}
|
|
4478
|
+
async function readTaskIpcLogsSnapshot(context, options) {
|
|
4479
|
+
const holder = context;
|
|
4480
|
+
const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
|
|
4481
|
+
const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
|
|
4482
|
+
const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
|
|
4483
|
+
const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
|
|
4484
|
+
const fd = new FileDatabase({
|
|
4485
|
+
basePath,
|
|
4486
|
+
namespace,
|
|
4487
|
+
tableName,
|
|
4488
|
+
versioned: true,
|
|
4489
|
+
useMetadata: true,
|
|
4490
|
+
maxVersions: 30,
|
|
4491
|
+
pageSize: 2e3,
|
|
4492
|
+
logger: holder.logger
|
|
4493
|
+
});
|
|
4494
|
+
const versions = await fd.getVersions();
|
|
4495
|
+
if (versions.length === 0) {
|
|
4496
|
+
return { records: [], latestTs: null };
|
|
4497
|
+
}
|
|
4498
|
+
const latest = versions[versions.length - 1];
|
|
4499
|
+
const raw = await fd.read({ version: latest });
|
|
4500
|
+
const arr = Array.isArray(raw) ? raw : [];
|
|
4501
|
+
let filtered = arr;
|
|
4502
|
+
if (options.afterTs && String(options.afterTs).trim()) {
|
|
4503
|
+
const cut = String(options.afterTs).trim();
|
|
4504
|
+
filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
|
|
4505
|
+
}
|
|
4506
|
+
let latestTs = null;
|
|
4507
|
+
for (const r of filtered) {
|
|
4508
|
+
const ts = typeof r?.ts === "string" ? String(r.ts) : null;
|
|
4509
|
+
if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
|
|
4510
|
+
}
|
|
4511
|
+
const incremental = !!(options.afterTs && String(options.afterTs).trim());
|
|
4512
|
+
const maxReturn = incremental ? 1e4 : tail;
|
|
4513
|
+
const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
|
|
4514
|
+
return { records: sliced, latestTs };
|
|
4515
|
+
}
|
|
4516
|
+
function getLogsStateForTarget(context, target) {
|
|
4517
|
+
const holder = context;
|
|
4518
|
+
const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
|
|
4519
|
+
const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
4520
|
+
if (!enabled) return null;
|
|
4521
|
+
if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
|
|
4522
|
+
const map = holder.__tasksLogsTargetStates;
|
|
4523
|
+
const key = ipcLogTargetKey(target);
|
|
4524
|
+
if (map.has(key)) return map.get(key);
|
|
4525
|
+
const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
|
|
4526
|
+
const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
|
|
4527
|
+
const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
|
|
4528
|
+
const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
|
|
4529
|
+
const db = new FileDatabase({
|
|
4530
|
+
basePath,
|
|
4531
|
+
namespace,
|
|
4532
|
+
tableName: target.tableName,
|
|
4533
|
+
versioned: true,
|
|
4534
|
+
useMetadata: true,
|
|
4535
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4536
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4537
|
+
logger: holder.logger
|
|
4538
|
+
});
|
|
4539
|
+
const state = {
|
|
4540
|
+
db,
|
|
4541
|
+
errorDb: null,
|
|
4542
|
+
queue: Promise.resolve(),
|
|
4543
|
+
initialized: false,
|
|
4544
|
+
errorInitialized: false
|
|
4545
|
+
};
|
|
4546
|
+
map.set(key, state);
|
|
4547
|
+
return state;
|
|
4548
|
+
}
|
|
4459
4549
|
function isErrorPayload(payload) {
|
|
4460
4550
|
if (!payload) return false;
|
|
4461
4551
|
if (typeof payload === "object") {
|
|
@@ -4475,14 +4565,26 @@ function buildLogRecord(task, payload) {
|
|
|
4475
4565
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4476
4566
|
opid: task.opid ?? null,
|
|
4477
4567
|
taskId: task.id,
|
|
4478
|
-
taskName: task.
|
|
4479
|
-
target: task.
|
|
4568
|
+
taskName: task.name,
|
|
4569
|
+
target: task.service_group,
|
|
4480
4570
|
source: typeof params.source === "string" ? params.source : null,
|
|
4481
4571
|
resource: typeof params.resource === "string" ? params.resource : null,
|
|
4482
4572
|
payload
|
|
4483
4573
|
};
|
|
4484
4574
|
}
|
|
4485
|
-
function appendTaskIpcLog(context, task, payload) {
|
|
4575
|
+
function appendTaskIpcLog(context, task, payload, target) {
|
|
4576
|
+
if (target) {
|
|
4577
|
+
const state2 = getLogsStateForTarget(context, target);
|
|
4578
|
+
if (!state2?.db) return;
|
|
4579
|
+
const record2 = buildLogRecord(task, payload);
|
|
4580
|
+
state2.queue = state2.queue.then(async () => {
|
|
4581
|
+
await state2.db.write([record2], { forceNewVersion: !state2.initialized });
|
|
4582
|
+
state2.initialized = true;
|
|
4583
|
+
}).catch((error) => {
|
|
4584
|
+
context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
|
|
4585
|
+
});
|
|
4586
|
+
return;
|
|
4587
|
+
}
|
|
4486
4588
|
const state = getLogsState(context);
|
|
4487
4589
|
if (!state.db && !state.errorDb) return;
|
|
4488
4590
|
const record = buildLogRecord(task, payload);
|
|
@@ -4500,83 +4602,280 @@ function appendTaskIpcLog(context, task, payload) {
|
|
|
4500
4602
|
});
|
|
4501
4603
|
}
|
|
4502
4604
|
|
|
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;
|
|
4605
|
+
// src/tasks/AbstractTask.js
|
|
4606
|
+
var AbstractTask = class _AbstractTask {
|
|
4607
|
+
/**
|
|
4608
|
+
* Whether `send-task` should wait for completion (and print a result
|
|
4609
|
+
* report) when no explicit `--wait` / `--noWait` flag is given. Defaults
|
|
4610
|
+
* to false; short-lived probe tasks (e.g. `ping`) override to true.
|
|
4611
|
+
*
|
|
4612
|
+
* @type {boolean}
|
|
4613
|
+
*/
|
|
4614
|
+
static defaultWaitForResult = false;
|
|
4615
|
+
/**
|
|
4616
|
+
* @param {object} context Runner context (db, logger, params, emitter...).
|
|
4617
|
+
* @param {object} task Task row as claimed from the queue.
|
|
4618
|
+
*/
|
|
4556
4619
|
constructor(context, task) {
|
|
4557
4620
|
this.context = context;
|
|
4558
4621
|
this.task = task;
|
|
4559
4622
|
}
|
|
4623
|
+
/**
|
|
4624
|
+
* Return a short reason string when the task should be deferred (e.g. "locked
|
|
4625
|
+
* by source"), or `false`/falsy when it is free to run. Default: always `false`.
|
|
4626
|
+
*
|
|
4627
|
+
* @returns {string | false | Promise<string | false>}
|
|
4628
|
+
*/
|
|
4560
4629
|
cantRunReason() {
|
|
4561
4630
|
return false;
|
|
4562
4631
|
}
|
|
4632
|
+
/**
|
|
4633
|
+
* Called by the runner when a stop has been requested. Subclasses running
|
|
4634
|
+
* long loops should flip a flag here and check it between iterations.
|
|
4635
|
+
*
|
|
4636
|
+
* @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
|
|
4637
|
+
*/
|
|
4563
4638
|
requestStop(_allowanceMs) {
|
|
4564
4639
|
}
|
|
4640
|
+
/**
|
|
4641
|
+
* Perform the task. Must be implemented by subclasses.
|
|
4642
|
+
*
|
|
4643
|
+
* @param {(progress: unknown) => Promise<void>} _reportProgress
|
|
4644
|
+
* Updates the DB `progress` column. Accepts any serializable value;
|
|
4645
|
+
* strings are stored verbatim, objects are JSON-stringified.
|
|
4646
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
4647
|
+
*/
|
|
4648
|
+
async run(_reportProgress) {
|
|
4649
|
+
throw new Error("AbstractTask.run must be implemented by subclass");
|
|
4650
|
+
}
|
|
4651
|
+
/**
|
|
4652
|
+
* Resolve a complete row payload for this task — envelope fields (queue,
|
|
4653
|
+
* priority, targeting, schedule…) plus the inner `params` blob produced by
|
|
4654
|
+
* {@link AbstractTask.resolveCustomParams}. Output shape matches
|
|
4655
|
+
* {@link enqueueTask}'s `options` argument, so the typical call is:
|
|
4656
|
+
*
|
|
4657
|
+
* const payload = await TaskClass.resolveParams(context, { name });
|
|
4658
|
+
* await enqueueTask(context, payload);
|
|
4659
|
+
*
|
|
4660
|
+
* Validation failures throw {@link ParamError} so the script aborts before
|
|
4661
|
+
* a malformed row hits the DB.
|
|
4662
|
+
*
|
|
4663
|
+
* @param {object} context
|
|
4664
|
+
* @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
|
|
4665
|
+
* Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
|
|
4666
|
+
* `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
|
|
4667
|
+
* `nextRunAt`, plus `params` (object — overlay onto inner blob).
|
|
4668
|
+
* @returns {Promise<object>}
|
|
4669
|
+
*/
|
|
4670
|
+
static async resolveParams(context, overrides = {}) {
|
|
4671
|
+
const main = _AbstractTask._resolveMainFields(context, overrides);
|
|
4672
|
+
const params = await this.resolveCustomParams(context, overrides);
|
|
4673
|
+
return { ...main, params };
|
|
4674
|
+
}
|
|
4675
|
+
/**
|
|
4676
|
+
* Resolve the inner JSON blob stored in the `params` column. Default
|
|
4677
|
+
* implementation passes through `--paramsJson` (parsed as a JSON object)
|
|
4678
|
+
* overlaid with `overrides.params` when supplied; returns `null` when
|
|
4679
|
+
* neither is provided.
|
|
4680
|
+
*
|
|
4681
|
+
* Subclasses with typed fields should override and call
|
|
4682
|
+
* {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
|
|
4683
|
+
* resolution, then validate and throw {@link ParamError} on bad input.
|
|
4684
|
+
*
|
|
4685
|
+
* @param {object} context
|
|
4686
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4687
|
+
* @returns {Promise<object|null>}
|
|
4688
|
+
*/
|
|
4689
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
4690
|
+
return _AbstractTask._defaultParamsBlob(context, overrides);
|
|
4691
|
+
}
|
|
4692
|
+
/**
|
|
4693
|
+
* Read main task envelope fields from `context.params` (CLI/env), with
|
|
4694
|
+
* any matching key on `overrides` taking precedence. Internal; called by
|
|
4695
|
+
* {@link AbstractTask.resolveParams}.
|
|
4696
|
+
*
|
|
4697
|
+
* @param {object} context
|
|
4698
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4699
|
+
* @returns {object}
|
|
4700
|
+
*/
|
|
4701
|
+
static _resolveMainFields(context, overrides = {}) {
|
|
4702
|
+
const defs2 = {
|
|
4703
|
+
queueName: "string default tasks",
|
|
4704
|
+
priority: "number default 50",
|
|
4705
|
+
serviceGroup: "string",
|
|
4706
|
+
serviceName: "string",
|
|
4707
|
+
instanceNumber: "number",
|
|
4708
|
+
serverName: "string",
|
|
4709
|
+
opid: "string",
|
|
4710
|
+
schedule: "string"
|
|
4711
|
+
};
|
|
4712
|
+
const cli = context.params.getAllForModule("task-envelope", defs2);
|
|
4713
|
+
const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
|
|
4714
|
+
if (!name) {
|
|
4715
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
4716
|
+
}
|
|
4717
|
+
let instanceNumber;
|
|
4718
|
+
const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
|
|
4719
|
+
if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
|
|
4720
|
+
const n = Number(rawInstance);
|
|
4721
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
4722
|
+
throw new ParamError("--instanceNumber must be a positive integer when set");
|
|
4723
|
+
}
|
|
4724
|
+
instanceNumber = n;
|
|
4725
|
+
} else {
|
|
4726
|
+
instanceNumber = null;
|
|
4727
|
+
}
|
|
4728
|
+
const priorityRaw = overrides.priority ?? cli.priority ?? 50;
|
|
4729
|
+
const priority = Number(priorityRaw);
|
|
4730
|
+
if (!Number.isFinite(priority)) {
|
|
4731
|
+
throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
|
|
4732
|
+
}
|
|
4733
|
+
return {
|
|
4734
|
+
name,
|
|
4735
|
+
queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
|
|
4736
|
+
priority,
|
|
4737
|
+
serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
|
|
4738
|
+
serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
|
|
4739
|
+
instanceNumber,
|
|
4740
|
+
serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
|
|
4741
|
+
opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
|
|
4742
|
+
schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
|
|
4743
|
+
nextRunAt: overrides.nextRunAt ?? null
|
|
4744
|
+
};
|
|
4745
|
+
}
|
|
4746
|
+
/**
|
|
4747
|
+
* Default inner-params resolver: parses `--paramsJson` (must be a JSON
|
|
4748
|
+
* object), then overlays `overrides.params` on top. Returns `null` when
|
|
4749
|
+
* neither is provided.
|
|
4750
|
+
*
|
|
4751
|
+
* @param {object} context
|
|
4752
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4753
|
+
* @returns {object|null}
|
|
4754
|
+
*/
|
|
4755
|
+
static _defaultParamsBlob(context, overrides = {}) {
|
|
4756
|
+
const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
|
|
4757
|
+
const fromJson = parseParamsJson(cli.paramsJson);
|
|
4758
|
+
const fromOverride = pickParamsObject(overrides);
|
|
4759
|
+
if (!fromJson && !fromOverride) return null;
|
|
4760
|
+
return { ...fromJson ?? {}, ...fromOverride ?? {} };
|
|
4761
|
+
}
|
|
4762
|
+
/**
|
|
4763
|
+
* Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
|
|
4764
|
+
* params (per `defs`) plus `--paramsJson` under a module namespace, then
|
|
4765
|
+
* merges them with explicit `overrides.params` in increasing priority:
|
|
4766
|
+
*
|
|
4767
|
+
* typed CLI flags → --paramsJson → overrides.params
|
|
4768
|
+
*
|
|
4769
|
+
* Undefined values are dropped so defaults declared in `defs` aren't
|
|
4770
|
+
* overwritten by missing-flag noise. Returns the merged object; the
|
|
4771
|
+
* caller is responsible for validation and throwing `ParamError`.
|
|
4772
|
+
*
|
|
4773
|
+
* @param {object} context
|
|
4774
|
+
* @param {string} moduleName Namespace for `--showUsedParams` grouping.
|
|
4775
|
+
* @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
|
|
4776
|
+
* @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
|
|
4777
|
+
* @returns {Record<string, unknown>}
|
|
4778
|
+
*/
|
|
4779
|
+
static _mergeTypedParams(context, moduleName, defs2, overrides = {}) {
|
|
4780
|
+
const fullDefs = { ...defs2, paramsJson: "string" };
|
|
4781
|
+
const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
|
|
4782
|
+
const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
|
|
4783
|
+
const fromCli = {};
|
|
4784
|
+
for (const [k, v] of Object.entries(cliRaw)) {
|
|
4785
|
+
if (k === "paramsJson") continue;
|
|
4786
|
+
if (v !== void 0 && v !== null) fromCli[k] = v;
|
|
4787
|
+
}
|
|
4788
|
+
const fromOverride = pickParamsObject(overrides) ?? {};
|
|
4789
|
+
return { ...fromCli, ...fromJson, ...fromOverride };
|
|
4790
|
+
}
|
|
4565
4791
|
};
|
|
4792
|
+
function emptyToUndef(s) {
|
|
4793
|
+
if (s === void 0 || s === null) return void 0;
|
|
4794
|
+
if (typeof s !== "string") return s;
|
|
4795
|
+
const t = s.trim();
|
|
4796
|
+
return t.length ? t : void 0;
|
|
4797
|
+
}
|
|
4798
|
+
function parseParamsJson(raw) {
|
|
4799
|
+
if (raw == null) return null;
|
|
4800
|
+
const t = String(raw).trim();
|
|
4801
|
+
if (!t) return null;
|
|
4802
|
+
let parsed;
|
|
4803
|
+
try {
|
|
4804
|
+
parsed = JSON.parse(t);
|
|
4805
|
+
} catch (e) {
|
|
4806
|
+
throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
|
|
4807
|
+
}
|
|
4808
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4809
|
+
throw new ParamError("--paramsJson must be a JSON object");
|
|
4810
|
+
}
|
|
4811
|
+
return parsed;
|
|
4812
|
+
}
|
|
4813
|
+
function pickParamsObject(overrides) {
|
|
4814
|
+
const p = overrides?.params;
|
|
4815
|
+
if (p && typeof p === "object" && !Array.isArray(p)) return p;
|
|
4816
|
+
return void 0;
|
|
4817
|
+
}
|
|
4566
4818
|
|
|
4567
|
-
// src/tasks/coreTasks/TaskPing.
|
|
4568
|
-
var TaskPing = class extends
|
|
4819
|
+
// src/tasks/coreTasks/TaskPing.js
|
|
4820
|
+
var TaskPing = class extends AbstractTask {
|
|
4821
|
+
/** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
|
|
4822
|
+
static defaultWaitForResult = true;
|
|
4823
|
+
/** Ping takes no params. */
|
|
4824
|
+
static async resolveCustomParams() {
|
|
4825
|
+
return null;
|
|
4826
|
+
}
|
|
4827
|
+
/**
|
|
4828
|
+
* @returns {Promise<{ success: true, results: "pong" }>}
|
|
4829
|
+
*/
|
|
4569
4830
|
async run() {
|
|
4570
4831
|
this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
|
|
4571
4832
|
return { success: true, results: "pong" };
|
|
4572
4833
|
}
|
|
4573
4834
|
};
|
|
4574
4835
|
|
|
4575
|
-
// src/tasks/coreTasks/TaskSampleProcess.
|
|
4576
|
-
var TaskSampleProcess = class extends
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4836
|
+
// src/tasks/coreTasks/TaskSampleProcess.js
|
|
4837
|
+
var TaskSampleProcess = class extends AbstractTask {
|
|
4838
|
+
/**
|
|
4839
|
+
* @param {object} context
|
|
4840
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4841
|
+
* @returns {Promise<{ total: number, delay: number, name?: string }>}
|
|
4842
|
+
*/
|
|
4843
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
4844
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
|
|
4845
|
+
total: "number default 10",
|
|
4846
|
+
delay: "number default 1000",
|
|
4847
|
+
name: "string"
|
|
4848
|
+
}, overrides);
|
|
4849
|
+
const total = Number(merged.total);
|
|
4850
|
+
const delay = Number(merged.delay);
|
|
4851
|
+
if (!Number.isInteger(total) || total <= 0) {
|
|
4852
|
+
throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
|
|
4853
|
+
}
|
|
4854
|
+
if (!Number.isInteger(delay) || delay < 0) {
|
|
4855
|
+
throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
|
|
4856
|
+
}
|
|
4857
|
+
const out = { total, delay };
|
|
4858
|
+
if (typeof merged.name === "string" && merged.name.trim()) {
|
|
4859
|
+
out.name = merged.name.trim();
|
|
4860
|
+
}
|
|
4861
|
+
return out;
|
|
4862
|
+
}
|
|
4863
|
+
/**
|
|
4864
|
+
* @param {object} context
|
|
4865
|
+
* @param {object} task
|
|
4866
|
+
*/
|
|
4867
|
+
constructor(context, task) {
|
|
4868
|
+
super(context, task);
|
|
4869
|
+
this.stopRequested = false;
|
|
4870
|
+
this.stopAllowanceMs = 0;
|
|
4871
|
+
this.stopDecisionLogged = false;
|
|
4872
|
+
}
|
|
4873
|
+
/**
|
|
4874
|
+
* Runner-facing stop signal. Records the allowance window so the main loop
|
|
4875
|
+
* can decide per-iteration whether to finish or abort early.
|
|
4876
|
+
*
|
|
4877
|
+
* @param {number} allowanceMs
|
|
4878
|
+
*/
|
|
4580
4879
|
requestStop(allowanceMs) {
|
|
4581
4880
|
this.stopRequested = true;
|
|
4582
4881
|
this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
|
|
@@ -4584,6 +4883,14 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4584
4883
|
`[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
|
|
4585
4884
|
);
|
|
4586
4885
|
}
|
|
4886
|
+
/**
|
|
4887
|
+
* Iterate `total` times, sleeping `delay` ms between ticks and reporting
|
|
4888
|
+
* progress every iteration. Validates params up front; invalid values short-
|
|
4889
|
+
* circuit to a structured failure without starting the loop.
|
|
4890
|
+
*
|
|
4891
|
+
* @param {(progress: object) => Promise<void>} reportProgress
|
|
4892
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
4893
|
+
*/
|
|
4587
4894
|
async run(reportProgress) {
|
|
4588
4895
|
const totalRaw = this.task?.params?.total ?? 10;
|
|
4589
4896
|
const delayRaw = this.task?.params?.delay ?? 1e3;
|
|
@@ -4665,7 +4972,7 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4665
4972
|
}
|
|
4666
4973
|
};
|
|
4667
4974
|
|
|
4668
|
-
// src/tasks/coreTasks/TaskShellCommand.
|
|
4975
|
+
// src/tasks/coreTasks/TaskShellCommand.js
|
|
4669
4976
|
var import_node_child_process = require("child_process");
|
|
4670
4977
|
function runShellCommand(command, cwd) {
|
|
4671
4978
|
return new Promise((resolve2, reject) => {
|
|
@@ -4695,7 +5002,27 @@ function runShellCommand(command, cwd) {
|
|
|
4695
5002
|
});
|
|
4696
5003
|
});
|
|
4697
5004
|
}
|
|
4698
|
-
var TaskShellCommand = class extends
|
|
5005
|
+
var TaskShellCommand = class extends AbstractTask {
|
|
5006
|
+
/**
|
|
5007
|
+
* @param {object} context
|
|
5008
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5009
|
+
* @returns {Promise<{ command: string, cwd?: string }>}
|
|
5010
|
+
*/
|
|
5011
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5012
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
|
|
5013
|
+
command: "string",
|
|
5014
|
+
cwd: "string"
|
|
5015
|
+
}, overrides);
|
|
5016
|
+
const command = typeof merged.command === "string" ? merged.command.trim() : "";
|
|
5017
|
+
if (!command) {
|
|
5018
|
+
throw new ParamError('shellCommand: param "command" must be a non-empty string');
|
|
5019
|
+
}
|
|
5020
|
+
const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
|
|
5021
|
+
return cwd ? { command, cwd } : { command };
|
|
5022
|
+
}
|
|
5023
|
+
/**
|
|
5024
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5025
|
+
*/
|
|
4699
5026
|
async run() {
|
|
4700
5027
|
const params = this.task?.params;
|
|
4701
5028
|
const commandRaw = typeof params === "string" ? params : params?.command;
|
|
@@ -4744,9 +5071,9 @@ var TaskShellCommand = class extends TaskMaster {
|
|
|
4744
5071
|
}
|
|
4745
5072
|
};
|
|
4746
5073
|
|
|
4747
|
-
// src/tasks/coreTasks/TaskSystemInfo.
|
|
5074
|
+
// src/tasks/coreTasks/TaskSystemInfo.js
|
|
4748
5075
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
4749
|
-
var
|
|
5076
|
+
var import_promises = __toESM(require("fs/promises"), 1);
|
|
4750
5077
|
function toGb(valueBytes) {
|
|
4751
5078
|
return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
|
|
4752
5079
|
}
|
|
@@ -4754,7 +5081,7 @@ function toMb(valueBytes) {
|
|
|
4754
5081
|
return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
|
|
4755
5082
|
}
|
|
4756
5083
|
async function getDiskStats() {
|
|
4757
|
-
const stats = await
|
|
5084
|
+
const stats = await import_promises.default.statfs("/");
|
|
4758
5085
|
const total = Number(stats.bsize) * Number(stats.blocks);
|
|
4759
5086
|
const free = Number(stats.bsize) * Number(stats.bavail);
|
|
4760
5087
|
const used = total - free;
|
|
@@ -4764,7 +5091,16 @@ async function getDiskStats() {
|
|
|
4764
5091
|
free: toGb(free)
|
|
4765
5092
|
};
|
|
4766
5093
|
}
|
|
4767
|
-
var TaskSystemInfo = class extends
|
|
5094
|
+
var TaskSystemInfo = class extends AbstractTask {
|
|
5095
|
+
/** Same UX expectation as `ping` — short probe, print the result. */
|
|
5096
|
+
static defaultWaitForResult = true;
|
|
5097
|
+
/** systemInfo takes no params. */
|
|
5098
|
+
static async resolveCustomParams() {
|
|
5099
|
+
return null;
|
|
5100
|
+
}
|
|
5101
|
+
/**
|
|
5102
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5103
|
+
*/
|
|
4768
5104
|
async run() {
|
|
4769
5105
|
try {
|
|
4770
5106
|
const totalMemory = import_node_os2.default.totalmem();
|
|
@@ -4816,8 +5152,31 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
4816
5152
|
}
|
|
4817
5153
|
};
|
|
4818
5154
|
|
|
4819
|
-
// src/tasks/coreTasks/TaskSumAB.
|
|
4820
|
-
var TaskSumAB = class extends
|
|
5155
|
+
// src/tasks/coreTasks/TaskSumAB.js
|
|
5156
|
+
var TaskSumAB = class extends AbstractTask {
|
|
5157
|
+
/** Short, deterministic — wait by default so callers see the sum. */
|
|
5158
|
+
static defaultWaitForResult = true;
|
|
5159
|
+
/**
|
|
5160
|
+
* @param {object} context
|
|
5161
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5162
|
+
* @returns {Promise<{ a: number, b: number }>}
|
|
5163
|
+
*/
|
|
5164
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5165
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
|
|
5166
|
+
a: "number",
|
|
5167
|
+
b: "number"
|
|
5168
|
+
}, overrides);
|
|
5169
|
+
if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
|
|
5170
|
+
throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
|
|
5171
|
+
}
|
|
5172
|
+
if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
|
|
5173
|
+
throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
|
|
5174
|
+
}
|
|
5175
|
+
return { a: merged.a, b: merged.b };
|
|
5176
|
+
}
|
|
5177
|
+
/**
|
|
5178
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5179
|
+
*/
|
|
4821
5180
|
async run() {
|
|
4822
5181
|
const a = this.task?.params?.a;
|
|
4823
5182
|
const b = this.task?.params?.b;
|
|
@@ -4848,8 +5207,46 @@ var TaskSumAB = class extends TaskMaster {
|
|
|
4848
5207
|
}
|
|
4849
5208
|
};
|
|
4850
5209
|
|
|
4851
|
-
// src/tasks/coreTasks/TaskStopRunner.
|
|
4852
|
-
var TaskStopRunner = class extends
|
|
5210
|
+
// src/tasks/coreTasks/TaskStopRunner.js
|
|
5211
|
+
var TaskStopRunner = class extends AbstractTask {
|
|
5212
|
+
/**
|
|
5213
|
+
* Stop tasks must target a concrete instance — without `serviceName` the
|
|
5214
|
+
* row would race against any worker on the queue. Layered on top of the
|
|
5215
|
+
* envelope built by {@link AbstractTask.resolveParams}.
|
|
5216
|
+
*
|
|
5217
|
+
* @param {object} context
|
|
5218
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5219
|
+
* @returns {Promise<object>}
|
|
5220
|
+
*/
|
|
5221
|
+
static async resolveParams(context, overrides = {}) {
|
|
5222
|
+
const main = await super.resolveParams(context, overrides);
|
|
5223
|
+
if (!main.serviceName) {
|
|
5224
|
+
throw new ParamError(
|
|
5225
|
+
"stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
|
|
5226
|
+
);
|
|
5227
|
+
}
|
|
5228
|
+
return main;
|
|
5229
|
+
}
|
|
5230
|
+
/**
|
|
5231
|
+
* @param {object} context
|
|
5232
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5233
|
+
* @returns {Promise<{ allowanceMs: number }>}
|
|
5234
|
+
*/
|
|
5235
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5236
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
|
|
5237
|
+
allowanceMs: "number default 5000"
|
|
5238
|
+
}, overrides);
|
|
5239
|
+
const allowanceMs = Number(merged.allowanceMs);
|
|
5240
|
+
if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
|
|
5241
|
+
throw new ParamError(
|
|
5242
|
+
`stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
|
|
5243
|
+
);
|
|
5244
|
+
}
|
|
5245
|
+
return { allowanceMs };
|
|
5246
|
+
}
|
|
5247
|
+
/**
|
|
5248
|
+
* @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
|
|
5249
|
+
*/
|
|
4853
5250
|
async run() {
|
|
4854
5251
|
const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
|
|
4855
5252
|
this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
|
|
@@ -4864,42 +5261,200 @@ var TaskStopRunner = class extends TaskMaster {
|
|
|
4864
5261
|
}
|
|
4865
5262
|
};
|
|
4866
5263
|
|
|
4867
|
-
// src/tasks/
|
|
5264
|
+
// src/tasks/coreTasks/TaskGetLogs.js
|
|
5265
|
+
var TaskGetLogs = class extends AbstractTask {
|
|
5266
|
+
/**
|
|
5267
|
+
* @param {object} context
|
|
5268
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5269
|
+
* @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
|
|
5270
|
+
*/
|
|
5271
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5272
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
|
|
5273
|
+
source: "string",
|
|
5274
|
+
resource: "string",
|
|
5275
|
+
tail: "number default 100",
|
|
5276
|
+
afterTs: "string"
|
|
5277
|
+
}, overrides);
|
|
5278
|
+
const source = typeof merged.source === "string" ? merged.source.trim() : "";
|
|
5279
|
+
const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
|
|
5280
|
+
if (!source) throw new ParamError('getLogs: param "source" is required');
|
|
5281
|
+
if (!resource) throw new ParamError('getLogs: param "resource" is required');
|
|
5282
|
+
let tail = Number(merged.tail);
|
|
5283
|
+
if (!Number.isFinite(tail) || tail < 1) tail = 100;
|
|
5284
|
+
tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
|
|
5285
|
+
const out = { source, resource, tail };
|
|
5286
|
+
if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
|
|
5287
|
+
out.afterTs = merged.afterTs.trim();
|
|
5288
|
+
}
|
|
5289
|
+
return out;
|
|
5290
|
+
}
|
|
5291
|
+
/**
|
|
5292
|
+
* @param {unknown} _reportProgress Unused (single-shot read, no progress events).
|
|
5293
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5294
|
+
*/
|
|
5295
|
+
async run(_reportProgress) {
|
|
5296
|
+
const p = this.task.params ?? {};
|
|
5297
|
+
const source = String(p.source ?? "").trim();
|
|
5298
|
+
const resource = String(p.resource ?? "").trim();
|
|
5299
|
+
const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
|
|
5300
|
+
const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
|
|
5301
|
+
if (!source || !resource) {
|
|
5302
|
+
return {
|
|
5303
|
+
success: false,
|
|
5304
|
+
results: { error: 'getLogs requires params "source" and "resource"' }
|
|
5305
|
+
};
|
|
5306
|
+
}
|
|
5307
|
+
try {
|
|
5308
|
+
const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
|
|
5309
|
+
source,
|
|
5310
|
+
resource,
|
|
5311
|
+
tail,
|
|
5312
|
+
afterTs
|
|
5313
|
+
});
|
|
5314
|
+
return {
|
|
5315
|
+
success: true,
|
|
5316
|
+
results: { records, latestTs, source, resource }
|
|
5317
|
+
};
|
|
5318
|
+
} catch (e) {
|
|
5319
|
+
return {
|
|
5320
|
+
success: false,
|
|
5321
|
+
results: { error: e?.message ?? String(e) }
|
|
5322
|
+
};
|
|
5323
|
+
}
|
|
5324
|
+
}
|
|
5325
|
+
};
|
|
5326
|
+
|
|
5327
|
+
// src/tasks/TasksRegistry.js
|
|
4868
5328
|
var TasksRegistry = class _TasksRegistry {
|
|
4869
|
-
|
|
5329
|
+
/**
|
|
5330
|
+
* @param {Record<string, Function>} [initial] Optional seed entries to copy in.
|
|
5331
|
+
*/
|
|
4870
5332
|
constructor(initial) {
|
|
5333
|
+
this.map = {};
|
|
4871
5334
|
if (initial) {
|
|
4872
5335
|
this.addMany(initial);
|
|
4873
5336
|
}
|
|
4874
5337
|
}
|
|
5338
|
+
/**
|
|
5339
|
+
* Build a registry pre-populated with every core task plus legacy aliases.
|
|
5340
|
+
* Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
|
|
5341
|
+
*
|
|
5342
|
+
* @returns {TasksRegistry}
|
|
5343
|
+
*/
|
|
4875
5344
|
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);
|
|
5345
|
+
return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("info", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner).add("getLogs", TaskGetLogs);
|
|
4877
5346
|
}
|
|
5347
|
+
/**
|
|
5348
|
+
* Register a single task class under a name. Overwrites any previous entry.
|
|
5349
|
+
*
|
|
5350
|
+
* @param {string} taskName
|
|
5351
|
+
* @param {Function} taskClass Subclass of `AbstractTask`.
|
|
5352
|
+
* @returns {this}
|
|
5353
|
+
*/
|
|
4878
5354
|
add(taskName, taskClass) {
|
|
4879
5355
|
this.map[taskName] = taskClass;
|
|
4880
5356
|
return this;
|
|
4881
5357
|
}
|
|
5358
|
+
/**
|
|
5359
|
+
* Bulk-register a name → class map. Later calls override earlier ones.
|
|
5360
|
+
*
|
|
5361
|
+
* @param {Record<string, Function>} entries
|
|
5362
|
+
* @returns {this}
|
|
5363
|
+
*/
|
|
4882
5364
|
addMany(entries) {
|
|
4883
5365
|
for (const [name, klass] of Object.entries(entries)) {
|
|
4884
5366
|
this.add(name, klass);
|
|
4885
5367
|
}
|
|
4886
5368
|
return this;
|
|
4887
5369
|
}
|
|
5370
|
+
/**
|
|
5371
|
+
* Look up a task class by name. Returns `undefined` when the name is unknown;
|
|
5372
|
+
* the runner treats that as "some other worker may handle this" and skips.
|
|
5373
|
+
*
|
|
5374
|
+
* @param {string} taskName
|
|
5375
|
+
* @returns {Function | undefined}
|
|
5376
|
+
*/
|
|
4888
5377
|
get(taskName) {
|
|
4889
5378
|
return this.map[taskName];
|
|
4890
5379
|
}
|
|
5380
|
+
/**
|
|
5381
|
+
* Strict variant of {@link get}: throws {@link ParamError} (with the list
|
|
5382
|
+
* of supported names) when `taskName` is unknown. Use from enqueuer code
|
|
5383
|
+
* paths where an unknown name is a hard CLI/programmer error.
|
|
5384
|
+
*
|
|
5385
|
+
* @param {string} taskName
|
|
5386
|
+
* @returns {Function}
|
|
5387
|
+
*/
|
|
5388
|
+
requireClass(taskName) {
|
|
5389
|
+
const TaskClass = taskName ? this.map[taskName] : void 0;
|
|
5390
|
+
if (!TaskClass) {
|
|
5391
|
+
const supported = this.listSupportedTasks().join(", ") || "(none)";
|
|
5392
|
+
throw new ParamError(
|
|
5393
|
+
`Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
|
|
5394
|
+
);
|
|
5395
|
+
}
|
|
5396
|
+
return TaskClass;
|
|
5397
|
+
}
|
|
5398
|
+
/**
|
|
5399
|
+
* Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
|
|
5400
|
+
* from `overrides` or `context.params`, look up the class, and delegate
|
|
5401
|
+
* to its static {@link AbstractTask.resolveParams} with `name` seeded into
|
|
5402
|
+
* the overrides. The returned object is shaped for {@link enqueueTask}.
|
|
5403
|
+
*
|
|
5404
|
+
* Validation failures (unknown task, missing required custom params, etc.)
|
|
5405
|
+
* surface as {@link ParamError} so the caller aborts cleanly before any
|
|
5406
|
+
* row is inserted.
|
|
5407
|
+
*
|
|
5408
|
+
* @param {object} context
|
|
5409
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5410
|
+
* @returns {Promise<object>}
|
|
5411
|
+
*/
|
|
5412
|
+
async resolveTaskParams(context, overrides = {}) {
|
|
5413
|
+
const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
|
|
5414
|
+
const fromCli = context.params.get("name", "string");
|
|
5415
|
+
const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
|
|
5416
|
+
const name = overrideName || cliName;
|
|
5417
|
+
if (!name) {
|
|
5418
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
5419
|
+
}
|
|
5420
|
+
const TaskClass = this.requireClass(name);
|
|
5421
|
+
return TaskClass.resolveParams(context, { ...overrides, name });
|
|
5422
|
+
}
|
|
5423
|
+
/**
|
|
5424
|
+
* Names of every registered task, sorted alphabetically (useful for CLI output
|
|
5425
|
+
* and allowlist sanity checks).
|
|
5426
|
+
*
|
|
5427
|
+
* @returns {string[]}
|
|
5428
|
+
*/
|
|
4891
5429
|
listSupportedTasks() {
|
|
4892
5430
|
return Object.keys(this.map).sort();
|
|
4893
5431
|
}
|
|
5432
|
+
/**
|
|
5433
|
+
* Shallow copy of the internal map, for handing to `addMany` on another registry
|
|
5434
|
+
* or for serialization.
|
|
5435
|
+
*
|
|
5436
|
+
* @returns {Record<string, Function>}
|
|
5437
|
+
*/
|
|
4894
5438
|
toObject() {
|
|
4895
5439
|
return { ...this.map };
|
|
4896
5440
|
}
|
|
4897
5441
|
};
|
|
4898
5442
|
|
|
4899
|
-
// src/tasks/
|
|
5443
|
+
// src/tasks/serviceTaskAllowlist.js
|
|
5444
|
+
function normalizeAllowedTasks(value) {
|
|
5445
|
+
if (!value) return void 0;
|
|
5446
|
+
if (Array.isArray(value)) {
|
|
5447
|
+
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
5448
|
+
return out2.length ? out2 : void 0;
|
|
5449
|
+
}
|
|
5450
|
+
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
5451
|
+
return out.length ? out : void 0;
|
|
5452
|
+
}
|
|
5453
|
+
|
|
5454
|
+
// src/tasks/taskScriptRunner.js
|
|
4900
5455
|
var import_node_child_process2 = require("child_process");
|
|
4901
5456
|
|
|
4902
|
-
// src/tasks/index.
|
|
5457
|
+
// src/tasks/index.js
|
|
4903
5458
|
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
4904
5459
|
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
4905
5460
|
function getDb3(context) {
|
|
@@ -4914,15 +5469,6 @@ function normalizeRegistry(registry) {
|
|
|
4914
5469
|
if (registry instanceof TasksRegistry) return registry;
|
|
4915
5470
|
return new TasksRegistry().addMany(registry);
|
|
4916
5471
|
}
|
|
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
5472
|
async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
|
|
4927
5473
|
context.logger.warn?.(`[tasks] signaling ${runningTaskInstances.size} running task(s) to stop`);
|
|
4928
5474
|
for (const [, taskInstance] of runningTaskInstances) {
|
|
@@ -4938,18 +5484,20 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
|
|
|
4938
5484
|
}
|
|
4939
5485
|
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
4940
5486
|
const db = getDb3(context);
|
|
4941
|
-
const taskName = row.
|
|
5487
|
+
const taskName = row.name;
|
|
4942
5488
|
const TaskClass = registry.get(taskName);
|
|
4943
|
-
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
4944
5489
|
if (!TaskClass) {
|
|
4945
5490
|
const err = { message: `Unknown task "${taskName}"` };
|
|
4946
|
-
await db(historyTable).insert(
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
5491
|
+
await db(historyTable).insert(
|
|
5492
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
5493
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5494
|
+
success: false,
|
|
5495
|
+
status: "failed",
|
|
5496
|
+
status_changed_at: db.fn.now(),
|
|
5497
|
+
params: toJsonColumn(row.params),
|
|
5498
|
+
results: toJsonColumn(err)
|
|
5499
|
+
})
|
|
5500
|
+
);
|
|
4953
5501
|
if (row.schedule) {
|
|
4954
5502
|
await db(tasksTable).where({ id: row.id }).update({
|
|
4955
5503
|
started_at: null,
|
|
@@ -4957,7 +5505,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4957
5505
|
success: false,
|
|
4958
5506
|
results: toJsonColumn(err),
|
|
4959
5507
|
past_due: null,
|
|
4960
|
-
|
|
5508
|
+
status: "paused",
|
|
5509
|
+
status_changed_at: db.fn.now(),
|
|
4961
5510
|
progress: LOCKED_BY_ERROR_MESSAGE
|
|
4962
5511
|
});
|
|
4963
5512
|
} else {
|
|
@@ -4984,13 +5533,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4984
5533
|
} finally {
|
|
4985
5534
|
runningTaskInstances.delete(row.id);
|
|
4986
5535
|
}
|
|
4987
|
-
await db(historyTable).insert(
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
5536
|
+
await db(historyTable).insert(
|
|
5537
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
5538
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5539
|
+
success,
|
|
5540
|
+
status: success ? "completed" : "failed",
|
|
5541
|
+
status_changed_at: db.fn.now(),
|
|
5542
|
+
params: toJsonColumn(row.params),
|
|
5543
|
+
results: toJsonColumn(results)
|
|
5544
|
+
})
|
|
5545
|
+
);
|
|
4994
5546
|
if (!success) {
|
|
4995
5547
|
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
4996
5548
|
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
@@ -5005,19 +5557,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5005
5557
|
const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
|
|
5006
5558
|
appendTaskIpcLog(context, row, {
|
|
5007
5559
|
level: "error",
|
|
5008
|
-
message: `[tasks] task failed: ${row.
|
|
5560
|
+
message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
|
|
5009
5561
|
details: results
|
|
5010
5562
|
});
|
|
5011
5563
|
}
|
|
5012
5564
|
if (row.schedule) {
|
|
5013
5565
|
if (success) {
|
|
5566
|
+
let nextRunAt = null;
|
|
5567
|
+
try {
|
|
5568
|
+
nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
|
|
5569
|
+
} catch (e) {
|
|
5570
|
+
context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
|
|
5571
|
+
}
|
|
5014
5572
|
await db(tasksTable).where({ id: row.id }).update({
|
|
5015
5573
|
started_at: null,
|
|
5016
5574
|
completed_at: /* @__PURE__ */ new Date(),
|
|
5017
5575
|
success,
|
|
5018
5576
|
results: toJsonColumn(results),
|
|
5019
5577
|
progress: null,
|
|
5020
|
-
past_due: null
|
|
5578
|
+
past_due: null,
|
|
5579
|
+
status: "idle",
|
|
5580
|
+
status_changed_at: db.fn.now(),
|
|
5581
|
+
next_run_at: nextRunAt,
|
|
5582
|
+
// Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
|
|
5583
|
+
service_name: null,
|
|
5584
|
+
server_name: null,
|
|
5585
|
+
instance_number: null
|
|
5021
5586
|
});
|
|
5022
5587
|
} else {
|
|
5023
5588
|
await db(tasksTable).where({ id: row.id }).update({
|
|
@@ -5025,7 +5590,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5025
5590
|
completed_at: /* @__PURE__ */ new Date(),
|
|
5026
5591
|
success,
|
|
5027
5592
|
results: toJsonColumn(results),
|
|
5028
|
-
|
|
5593
|
+
status: "paused",
|
|
5594
|
+
status_changed_at: db.fn.now(),
|
|
5029
5595
|
progress: LOCKED_BY_ERROR_MESSAGE,
|
|
5030
5596
|
past_due: null
|
|
5031
5597
|
});
|
|
@@ -5037,58 +5603,93 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5037
5603
|
const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
|
|
5038
5604
|
return { stopRunnerRequested, stopAllowanceMs };
|
|
5039
5605
|
}
|
|
5040
|
-
|
|
5606
|
+
function shuffleTaskRowsInPlace(rows) {
|
|
5607
|
+
for (let i = rows.length - 1; i > 0; i--) {
|
|
5608
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
5609
|
+
const t = rows[i];
|
|
5610
|
+
rows[i] = rows[j];
|
|
5611
|
+
rows[j] = t;
|
|
5612
|
+
}
|
|
5613
|
+
}
|
|
5614
|
+
async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
|
|
5041
5615
|
const db = getDb3(context);
|
|
5042
|
-
let query = db(tasksTable).
|
|
5616
|
+
let query = db(tasksTable).where({ status: "idle" }).where(function() {
|
|
5617
|
+
this.whereNull("service_group").orWhere({ service_group: serviceGroup });
|
|
5618
|
+
}).orderByRaw("CASE WHEN past_due IS NULL THEN 1 ELSE 0 END ASC").orderBy([{ column: "priority", order: "asc" }]).orderByRaw("CASE WHEN completed_at IS NULL THEN 0 ELSE 1 END ASC").orderBy([{ column: "completed_at", order: "asc" }, { column: "created_at", order: "asc" }]).limit(scanLimit);
|
|
5043
5619
|
if (taskNames && taskNames.length > 0) {
|
|
5044
|
-
query = query.whereIn("
|
|
5620
|
+
query = query.whereIn("name", taskNames);
|
|
5621
|
+
}
|
|
5622
|
+
if (runnerIdentity) {
|
|
5623
|
+
query = query.where(function() {
|
|
5624
|
+
this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
|
|
5625
|
+
}).where(function() {
|
|
5626
|
+
this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
|
|
5627
|
+
}).where(function() {
|
|
5628
|
+
this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
|
|
5629
|
+
});
|
|
5630
|
+
} else {
|
|
5631
|
+
query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
|
|
5045
5632
|
}
|
|
5046
5633
|
const candidates = await query;
|
|
5634
|
+
shuffleTaskRowsInPlace(candidates);
|
|
5047
5635
|
for (const row of candidates) {
|
|
5048
5636
|
if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
|
|
5049
5637
|
continue;
|
|
5050
5638
|
}
|
|
5051
|
-
const TaskClass = registry.get(row.
|
|
5052
|
-
if (TaskClass) {
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5639
|
+
const TaskClass = registry.get(row.name);
|
|
5640
|
+
if (!TaskClass) {
|
|
5641
|
+
continue;
|
|
5642
|
+
}
|
|
5643
|
+
const taskInstance = new TaskClass(context, row);
|
|
5644
|
+
const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
|
|
5645
|
+
if (reason) {
|
|
5646
|
+
if (!row.past_due) {
|
|
5647
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
5648
|
+
past_due: db.fn.now(),
|
|
5649
|
+
progress: String(reason)
|
|
5650
|
+
});
|
|
5063
5651
|
}
|
|
5652
|
+
continue;
|
|
5653
|
+
}
|
|
5654
|
+
const claimPatch = {
|
|
5655
|
+
started_at: db.fn.now(),
|
|
5656
|
+
status: "running",
|
|
5657
|
+
status_changed_at: db.fn.now()
|
|
5658
|
+
};
|
|
5659
|
+
if (runnerIdentity) {
|
|
5660
|
+
claimPatch.service_name = runnerIdentity.service_name;
|
|
5661
|
+
claimPatch.server_name = runnerIdentity.server_name;
|
|
5662
|
+
claimPatch.instance_number = runnerIdentity.instance_number;
|
|
5064
5663
|
}
|
|
5065
|
-
const updated = await db(tasksTable).where({ id: row.id
|
|
5664
|
+
const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
|
|
5066
5665
|
const claimed = Array.isArray(updated) ? updated[0] : null;
|
|
5067
5666
|
if (claimed) return claimed;
|
|
5068
5667
|
}
|
|
5069
5668
|
return null;
|
|
5070
5669
|
}
|
|
5071
5670
|
async function runTasksLoop(context, options) {
|
|
5072
|
-
const
|
|
5671
|
+
const queueName = options.queueName ?? "tasks";
|
|
5073
5672
|
const target = options.target;
|
|
5074
5673
|
const pollMs = options.pollMs ?? 1e3;
|
|
5075
|
-
const
|
|
5674
|
+
const claimJitterMs = options.claimJitterMs ?? 0;
|
|
5675
|
+
const maxParallel = options.maxParallel ?? 32;
|
|
5076
5676
|
const scanLimit = options.scanLimit ?? 100;
|
|
5077
5677
|
const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5078
5678
|
const registry = normalizeRegistry(options.registry);
|
|
5079
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
5679
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
5080
5680
|
if (!target) throw new Error("runTasksLoop: target is required");
|
|
5681
|
+
context.tasksQueueName = queueName;
|
|
5081
5682
|
const runningPromises = /* @__PURE__ */ new Set();
|
|
5082
5683
|
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
5083
5684
|
let runningStopControlPromise = null;
|
|
5084
5685
|
let stopRequested = false;
|
|
5085
5686
|
let stopAllowanceMs = 5e3;
|
|
5086
|
-
context.
|
|
5687
|
+
context.tasksRunnerStop = false;
|
|
5087
5688
|
let registryReg = null;
|
|
5088
5689
|
let registryInterval = null;
|
|
5690
|
+
let runnerIdentity = null;
|
|
5089
5691
|
const hbGroup = options.runnerServiceGroup?.trim();
|
|
5090
5692
|
if (hbGroup) {
|
|
5091
|
-
const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
|
|
5092
5693
|
const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
|
|
5093
5694
|
const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
|
|
5094
5695
|
const defaultMeta = {
|
|
@@ -5096,16 +5697,21 @@ async function runTasksLoop(context, options) {
|
|
|
5096
5697
|
allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
|
|
5097
5698
|
};
|
|
5098
5699
|
registryReg = await registerInServicesRegistry(context, {
|
|
5099
|
-
|
|
5700
|
+
queueName,
|
|
5100
5701
|
target,
|
|
5101
5702
|
serviceGroup: hbGroup,
|
|
5102
5703
|
serviceName: options.runnerServiceName,
|
|
5103
|
-
|
|
5704
|
+
instanceNumber: options.runnerInstanceNumber,
|
|
5104
5705
|
staleMs,
|
|
5105
5706
|
groupMaxInstances: options.runnerGroupMaxInstances,
|
|
5106
5707
|
enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
|
|
5107
5708
|
metadata: options.runnerMetadata ?? defaultMeta
|
|
5108
5709
|
});
|
|
5710
|
+
runnerIdentity = {
|
|
5711
|
+
service_name: registryReg.serviceName,
|
|
5712
|
+
server_name: import_node_os3.default.hostname(),
|
|
5713
|
+
instance_number: registryReg.instanceNumber
|
|
5714
|
+
};
|
|
5109
5715
|
registryInterval = setInterval(() => {
|
|
5110
5716
|
void touchServicesRegistry(context, registryReg).catch((err) => {
|
|
5111
5717
|
context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
|
|
@@ -5113,7 +5719,7 @@ async function runTasksLoop(context, options) {
|
|
|
5113
5719
|
}, hbIntervalMs);
|
|
5114
5720
|
}
|
|
5115
5721
|
try {
|
|
5116
|
-
while (!context.isStop() && !stopRequested &&
|
|
5722
|
+
while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
|
|
5117
5723
|
if (!runningStopControlPromise) {
|
|
5118
5724
|
const claimedStopTask = await claimNextRunnableTask(
|
|
5119
5725
|
context,
|
|
@@ -5121,7 +5727,8 @@ async function runTasksLoop(context, options) {
|
|
|
5121
5727
|
target,
|
|
5122
5728
|
registry,
|
|
5123
5729
|
10,
|
|
5124
|
-
["stopRunner", "stop"]
|
|
5730
|
+
["stopRunner", "stop"],
|
|
5731
|
+
runnerIdentity
|
|
5125
5732
|
);
|
|
5126
5733
|
if (claimedStopTask) {
|
|
5127
5734
|
runningStopControlPromise = executeClaimedTask(
|
|
@@ -5135,7 +5742,7 @@ async function runTasksLoop(context, options) {
|
|
|
5135
5742
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5136
5743
|
stopRequested = true;
|
|
5137
5744
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5138
|
-
context.
|
|
5745
|
+
context.tasksRunnerStop = true;
|
|
5139
5746
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5140
5747
|
}
|
|
5141
5748
|
}).finally(() => {
|
|
@@ -5143,6 +5750,9 @@ async function runTasksLoop(context, options) {
|
|
|
5143
5750
|
});
|
|
5144
5751
|
}
|
|
5145
5752
|
}
|
|
5753
|
+
if (claimJitterMs > 0) {
|
|
5754
|
+
await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
|
|
5755
|
+
}
|
|
5146
5756
|
while (runningPromises.size < maxParallel) {
|
|
5147
5757
|
const claimed = await claimNextRunnableTask(
|
|
5148
5758
|
context,
|
|
@@ -5150,14 +5760,15 @@ async function runTasksLoop(context, options) {
|
|
|
5150
5760
|
target,
|
|
5151
5761
|
registry,
|
|
5152
5762
|
scanLimit,
|
|
5153
|
-
allowedTasks
|
|
5763
|
+
allowedTasks,
|
|
5764
|
+
runnerIdentity
|
|
5154
5765
|
);
|
|
5155
5766
|
if (!claimed) break;
|
|
5156
5767
|
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
5157
5768
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5158
5769
|
stopRequested = true;
|
|
5159
5770
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5160
|
-
context.
|
|
5771
|
+
context.tasksRunnerStop = true;
|
|
5161
5772
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5162
5773
|
}
|
|
5163
5774
|
}).finally(() => {
|
|
@@ -5165,7 +5776,16 @@ async function runTasksLoop(context, options) {
|
|
|
5165
5776
|
});
|
|
5166
5777
|
runningPromises.add(p);
|
|
5167
5778
|
}
|
|
5168
|
-
|
|
5779
|
+
const wakePromises = [...runningPromises];
|
|
5780
|
+
if (runningStopControlPromise) {
|
|
5781
|
+
wakePromises.push(runningStopControlPromise);
|
|
5782
|
+
}
|
|
5783
|
+
if (wakePromises.length === 0) {
|
|
5784
|
+
await sleepMs(pollMs);
|
|
5785
|
+
} else {
|
|
5786
|
+
const safe = wakePromises.map((p) => p.catch(() => void 0));
|
|
5787
|
+
await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
|
|
5788
|
+
}
|
|
5169
5789
|
}
|
|
5170
5790
|
if (context.isStop() && !stopRequested) {
|
|
5171
5791
|
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
@@ -5200,71 +5820,88 @@ async function runTasksLoop(context, options) {
|
|
|
5200
5820
|
}
|
|
5201
5821
|
}
|
|
5202
5822
|
var TasksManager = class _TasksManager {
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5823
|
+
/**
|
|
5824
|
+
* @param {object} context
|
|
5825
|
+
* @param {{
|
|
5826
|
+
* queueName?: string,
|
|
5827
|
+
* target?: string,
|
|
5828
|
+
* recreateTaskTables?: boolean,
|
|
5829
|
+
* pollMs?: number,
|
|
5830
|
+
* claimJitterMs?: number,
|
|
5831
|
+
* maxParallel?: number,
|
|
5832
|
+
* scanLimit?: number,
|
|
5833
|
+
* allowedTasks?: string | string[],
|
|
5834
|
+
* registry?: TasksRegistry | Record<string, Function>,
|
|
5835
|
+
* runnerServiceGroup?: string,
|
|
5836
|
+
* runnerServiceName?: string,
|
|
5837
|
+
* runnerInstanceNumber?: number,
|
|
5838
|
+
* runnerHeartbeatIntervalMs?: number,
|
|
5839
|
+
* runnerHeartbeatStaleMs?: number,
|
|
5840
|
+
* runnerGroupMaxInstances?: number,
|
|
5841
|
+
* runnerEnforceMaxInstances?: boolean,
|
|
5842
|
+
* runnerMetadata?: Record<string, unknown>,
|
|
5843
|
+
* }} [options]
|
|
5844
|
+
*/
|
|
5220
5845
|
constructor(context, options = {}) {
|
|
5221
5846
|
this.context = context;
|
|
5222
|
-
this.
|
|
5847
|
+
this.queueName = options.queueName ?? "tasks";
|
|
5223
5848
|
this.target = options.target ?? "localRunner";
|
|
5224
5849
|
this.recreateTaskTables = options.recreateTaskTables ?? false;
|
|
5225
5850
|
this.pollMs = options.pollMs ?? 1e3;
|
|
5851
|
+
this.claimJitterMs = options.claimJitterMs ?? 0;
|
|
5226
5852
|
this.maxParallel = options.maxParallel ?? 1;
|
|
5227
5853
|
this.scanLimit = options.scanLimit ?? 100;
|
|
5228
5854
|
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5229
5855
|
this.registry = normalizeRegistry(options.registry);
|
|
5230
5856
|
this.runnerServiceGroup = options.runnerServiceGroup;
|
|
5231
5857
|
this.runnerServiceName = options.runnerServiceName;
|
|
5232
|
-
this.
|
|
5858
|
+
this.runnerInstanceNumber = options.runnerInstanceNumber;
|
|
5233
5859
|
this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
|
|
5234
5860
|
this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
|
|
5235
5861
|
this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
|
|
5236
5862
|
this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
|
|
5237
5863
|
this.runnerMetadata = options.runnerMetadata;
|
|
5238
5864
|
}
|
|
5865
|
+
/**
|
|
5866
|
+
* Preferred factory: reads defaults from `context.params` (module namespace
|
|
5867
|
+
* `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
|
|
5868
|
+
* and inline options in one consistent resolver.
|
|
5869
|
+
*
|
|
5870
|
+
* @param {object} context
|
|
5871
|
+
* @param {ConstructorParameters<typeof TasksManager>[1]} [options]
|
|
5872
|
+
* @returns {TasksManager}
|
|
5873
|
+
*/
|
|
5239
5874
|
static init(context, options = {}) {
|
|
5240
5875
|
const defs2 = {
|
|
5241
5876
|
table: "string default tasks",
|
|
5242
5877
|
target: "string default localRunner",
|
|
5243
5878
|
recreateTaskTables: "boolean default false",
|
|
5244
5879
|
pollMs: "number default 1000",
|
|
5880
|
+
claimJitterMs: "number default 0",
|
|
5245
5881
|
maxParallel: "number default 1",
|
|
5246
5882
|
scanLimit: "number default 100",
|
|
5247
5883
|
allowedTasks: "string",
|
|
5248
5884
|
runnerServiceGroup: "string",
|
|
5249
5885
|
runnerServiceName: "string",
|
|
5250
|
-
|
|
5886
|
+
runnerInstanceNumber: "number",
|
|
5251
5887
|
runnerHeartbeatIntervalMs: "number default 10000",
|
|
5252
5888
|
runnerHeartbeatStaleMs: "number default 45000",
|
|
5253
5889
|
runnerGroupMaxInstances: "number",
|
|
5254
5890
|
runnerEnforceMaxInstances: "boolean default true"
|
|
5255
5891
|
};
|
|
5256
|
-
const discovered = context.params.getAllForModule(defs2);
|
|
5892
|
+
const discovered = context.params.getAllForModule("tasks", defs2);
|
|
5257
5893
|
const resolved = {
|
|
5258
|
-
|
|
5894
|
+
queueName: discovered.table,
|
|
5259
5895
|
target: discovered.target,
|
|
5260
5896
|
recreateTaskTables: discovered.recreateTaskTables,
|
|
5261
5897
|
pollMs: discovered.pollMs,
|
|
5898
|
+
claimJitterMs: discovered.claimJitterMs,
|
|
5262
5899
|
maxParallel: discovered.maxParallel,
|
|
5263
5900
|
scanLimit: discovered.scanLimit,
|
|
5264
5901
|
allowedTasks: discovered.allowedTasks,
|
|
5265
5902
|
runnerServiceGroup: discovered.runnerServiceGroup,
|
|
5266
5903
|
runnerServiceName: discovered.runnerServiceName,
|
|
5267
|
-
|
|
5904
|
+
runnerInstanceNumber: discovered.runnerInstanceNumber,
|
|
5268
5905
|
runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
|
|
5269
5906
|
runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
|
|
5270
5907
|
runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
|
|
@@ -5273,24 +5910,39 @@ var TasksManager = class _TasksManager {
|
|
|
5273
5910
|
};
|
|
5274
5911
|
return new _TasksManager(context, resolved);
|
|
5275
5912
|
}
|
|
5913
|
+
/**
|
|
5914
|
+
* Idempotently ensure the three backing tables exist for this queue.
|
|
5915
|
+
*
|
|
5916
|
+
* @param {{ recreate?: boolean }} [options]
|
|
5917
|
+
* @returns {Promise<void>}
|
|
5918
|
+
*/
|
|
5276
5919
|
async ensureTaskTables(options = {}) {
|
|
5277
5920
|
await ensureTaskTables(this.context, {
|
|
5278
|
-
|
|
5921
|
+
queueName: this.queueName,
|
|
5279
5922
|
recreate: options.recreate ?? this.recreateTaskTables
|
|
5280
5923
|
});
|
|
5281
5924
|
}
|
|
5925
|
+
/**
|
|
5926
|
+
* Start the runner loop using this manager's resolved config. Per-call
|
|
5927
|
+
* options override the stored defaults, but `runnerMetadata` still falls
|
|
5928
|
+
* through when omitted.
|
|
5929
|
+
*
|
|
5930
|
+
* @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
|
|
5931
|
+
* @returns {Promise<void>}
|
|
5932
|
+
*/
|
|
5282
5933
|
async runTasksLoop(options = {}) {
|
|
5283
5934
|
await runTasksLoop(this.context, {
|
|
5284
|
-
|
|
5935
|
+
queueName: options.queueName ?? this.queueName,
|
|
5285
5936
|
target: options.target ?? this.target,
|
|
5286
5937
|
pollMs: options.pollMs ?? this.pollMs,
|
|
5938
|
+
claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
|
|
5287
5939
|
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
5288
5940
|
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
5289
5941
|
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
5290
5942
|
registry: options.registry ?? this.registry,
|
|
5291
5943
|
runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
|
|
5292
5944
|
runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
|
|
5293
|
-
|
|
5945
|
+
runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
|
|
5294
5946
|
runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
|
|
5295
5947
|
runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
|
|
5296
5948
|
runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
|
|
@@ -5300,9 +5952,8 @@ var TasksManager = class _TasksManager {
|
|
|
5300
5952
|
}
|
|
5301
5953
|
};
|
|
5302
5954
|
|
|
5303
|
-
// src/scripts/cli-runner.
|
|
5955
|
+
// src/scripts/cli-runner.js
|
|
5304
5956
|
var defs = {
|
|
5305
|
-
dbName: "string default local",
|
|
5306
5957
|
tasksModule: "string"
|
|
5307
5958
|
};
|
|
5308
5959
|
async function loadTasksModule(modulePath) {
|
|
@@ -5314,11 +5965,8 @@ async function loadTasksModule(modulePath) {
|
|
|
5314
5965
|
return imported.tasksRegistry;
|
|
5315
5966
|
}
|
|
5316
5967
|
var flow = async (context) => {
|
|
5317
|
-
const {
|
|
5318
|
-
|
|
5319
|
-
tasksModule
|
|
5320
|
-
} = context.params.getAll(defs);
|
|
5321
|
-
const db = await dbInit(context, dbName);
|
|
5968
|
+
const { tasksModule } = context.params.getAll(defs);
|
|
5969
|
+
const db = await Db.init(context);
|
|
5322
5970
|
context.db = db;
|
|
5323
5971
|
const registry = new TasksRegistry().addMany(defaultTasksRegistry.toObject());
|
|
5324
5972
|
if (tasksModule) {
|