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