@nmakarov/cli-toolkit 0.18.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/args.cjs +1 -4
- package/dist/args.cjs.map +1 -1
- package/dist/args.js +1 -1
- package/dist/args.js.map +1 -1
- package/dist/cli-runner.cjs +1493 -516
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +1509 -531
- package/dist/cli-runner.js.map +1 -1
- package/dist/db.cjs +85 -157
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +84 -150
- package/dist/db.js.map +1 -1
- package/dist/errors.cjs +2 -2
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/filedatabase.cjs +19 -19
- package/dist/filedatabase.cjs.map +1 -1
- package/dist/filedatabase.js +19 -16
- package/dist/filedatabase.js.map +1 -1
- package/dist/http-client.cjs +9 -11
- package/dist/http-client.cjs.map +1 -1
- package/dist/http-client.js +10 -9
- package/dist/http-client.js.map +1 -1
- package/dist/http-client2.cjs +34 -33
- package/dist/http-client2.cjs.map +1 -1
- package/dist/http-client2.js +34 -30
- package/dist/http-client2.js.map +1 -1
- package/dist/index.cjs +2063 -658
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2063 -663
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +97 -69
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +112 -83
- package/dist/init.js.map +1 -1
- package/dist/logger.cjs +5 -5
- package/dist/logger.cjs.map +1 -1
- package/dist/logger.js +5 -4
- package/dist/logger.js.map +1 -1
- package/dist/mock-server.cjs +21 -33
- package/dist/mock-server.cjs.map +1 -1
- package/dist/mock-server.js +21 -28
- package/dist/mock-server.js.map +1 -1
- package/dist/params.cjs +22 -10
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +22 -7
- package/dist/params.js.map +1 -1
- package/dist/s3.cjs +286 -0
- package/dist/s3.cjs.map +1 -0
- package/dist/s3.js +273 -0
- package/dist/s3.js.map +1 -0
- package/dist/screen.cjs +34 -39
- package/dist/screen.cjs.map +1 -1
- package/dist/screen.js +48 -46
- package/dist/screen.js.map +1 -1
- package/dist/tasks.cjs +1640 -416
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +1614 -412
- package/dist/tasks.js.map +1 -1
- package/dist/utils.cjs +7 -8
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.js +6 -6
- package/dist/utils.js.map +1 -1
- package/package.json +36 -44
- package/scripts/ssm/parse-cli.js +35 -0
- package/scripts/ssm/ssm-admin.js +151 -0
- package/scripts/ssm/ssm-pull.js +147 -0
package/dist/cli-runner.js
CHANGED
|
@@ -15,7 +15,7 @@ var __export = (target, all) => {
|
|
|
15
15
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
// src/screen/components.
|
|
18
|
+
// src/screen/components.js
|
|
19
19
|
import { createElement as h } from "react";
|
|
20
20
|
import { Box, Text } from "ink";
|
|
21
21
|
function getScreenWidth(maxWidth = null) {
|
|
@@ -90,13 +90,12 @@ function ScreenFooter({ lines, textStyle }) {
|
|
|
90
90
|
);
|
|
91
91
|
}
|
|
92
92
|
var init_components = __esm({
|
|
93
|
-
"src/screen/components.
|
|
94
|
-
"use strict";
|
|
93
|
+
"src/screen/components.js"() {
|
|
95
94
|
}
|
|
96
95
|
});
|
|
97
96
|
|
|
98
|
-
// src/screen/list-components.
|
|
99
|
-
import
|
|
97
|
+
// src/screen/list-components.js
|
|
98
|
+
import React, { useState, useEffect, useRef, createElement } from "react";
|
|
100
99
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
101
100
|
function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
|
|
102
101
|
const [, forceUpdate] = useState({});
|
|
@@ -232,11 +231,11 @@ function MultiColumnListWithPreviewComponent({
|
|
|
232
231
|
const previewRows = [];
|
|
233
232
|
if (typeof previewContent === "string") {
|
|
234
233
|
previewRows.push(h2(ScreenRow, { key: "preview-string", children: h2(Text2, { bold: true }, previewContent) }));
|
|
235
|
-
} else if (typeof previewContent === "object" && !
|
|
234
|
+
} else if (typeof previewContent === "object" && !React.isValidElement(previewContent) && previewContent !== null) {
|
|
236
235
|
Object.entries(previewContent).forEach(([key, value], idx) => {
|
|
237
236
|
previewRows.push(h2(ScreenRow, { key: `preview-${key}-${idx}`, children: h2(Text2, {}, `${key}: ${value}`) }));
|
|
238
237
|
});
|
|
239
|
-
} else if (
|
|
238
|
+
} else if (React.isValidElement(previewContent)) {
|
|
240
239
|
previewRows.push(h2(ScreenRow, { key: "preview-element", children: previewContent }));
|
|
241
240
|
}
|
|
242
241
|
return h2(
|
|
@@ -249,11 +248,13 @@ function MultiColumnListWithPreviewComponent({
|
|
|
249
248
|
...previewRows
|
|
250
249
|
);
|
|
251
250
|
}
|
|
252
|
-
function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " " }) {
|
|
251
|
+
function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " ", onSelectionChange }) {
|
|
253
252
|
const [, forceUpdate] = useState({});
|
|
254
253
|
const [sortOrder, setSortOrder] = useState("none");
|
|
255
254
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
256
255
|
const scrollStateRef = useRef({ scrollOffset: 0, maxHeight: 0, totalItems: 0 });
|
|
256
|
+
const itemsRef = useRef(items);
|
|
257
|
+
itemsRef.current = items;
|
|
257
258
|
const defaultGetTitle = (item) => {
|
|
258
259
|
return getTitle ? getTitle(item) : typeof item.value === "string" ? item.value : item.value?.title || item.name;
|
|
259
260
|
};
|
|
@@ -267,18 +268,24 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
267
268
|
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
268
269
|
}
|
|
269
270
|
}) : items;
|
|
271
|
+
const displayItemsRef = useRef(displayItems);
|
|
272
|
+
displayItemsRef.current = displayItems;
|
|
270
273
|
const effectiveMaxHeight = maxHeight || displayItems.length;
|
|
271
|
-
const
|
|
274
|
+
const _canScroll = displayItems.length > effectiveMaxHeight;
|
|
272
275
|
const maxScrollOffset = Math.max(0, displayItems.length - effectiveMaxHeight);
|
|
273
276
|
const clampedScrollOffset = Math.min(Math.max(0, scrollOffset), maxScrollOffset);
|
|
274
277
|
const visibleItems = displayItems.slice(clampedScrollOffset, clampedScrollOffset + effectiveMaxHeight);
|
|
275
278
|
const canScrollUp = clampedScrollOffset > 0;
|
|
276
279
|
const canScrollDown = clampedScrollOffset < maxScrollOffset;
|
|
277
280
|
scrollStateRef.current = { scrollOffset, maxHeight: effectiveMaxHeight, totalItems: displayItems.length };
|
|
281
|
+
const onSelectionChangeRef = useRef(onSelectionChange);
|
|
282
|
+
onSelectionChangeRef.current = onSelectionChange;
|
|
278
283
|
useEffect(() => {
|
|
279
284
|
ctx.setAction("moveUp", () => {
|
|
280
285
|
const newIndex = Math.max(0, selectedIndexRef.current - 1);
|
|
281
286
|
selectedIndexRef.current = newIndex;
|
|
287
|
+
const list = displayItemsRef.current;
|
|
288
|
+
onSelectionChangeRef.current?.(newIndex, list[newIndex]);
|
|
282
289
|
const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
|
|
283
290
|
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
284
291
|
const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
|
|
@@ -288,18 +295,11 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
288
295
|
forceUpdate({});
|
|
289
296
|
});
|
|
290
297
|
ctx.setAction("moveDown", () => {
|
|
291
|
-
const currentItems =
|
|
292
|
-
|
|
293
|
-
const titleB = titleGetter(b).toLowerCase();
|
|
294
|
-
if (sortOrder === "asc") {
|
|
295
|
-
return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
|
|
296
|
-
} else {
|
|
297
|
-
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
298
|
-
}
|
|
299
|
-
}) : items;
|
|
300
|
-
const maxIndex = currentItems.length - 1;
|
|
298
|
+
const currentItems = displayItemsRef.current;
|
|
299
|
+
const maxIndex = Math.max(0, currentItems.length - 1);
|
|
301
300
|
const newIndex = Math.min(maxIndex, selectedIndexRef.current + 1);
|
|
302
301
|
selectedIndexRef.current = newIndex;
|
|
302
|
+
onSelectionChangeRef.current?.(newIndex, currentItems[newIndex]);
|
|
303
303
|
const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
|
|
304
304
|
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
305
305
|
const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
|
|
@@ -309,8 +309,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
309
309
|
forceUpdate({});
|
|
310
310
|
});
|
|
311
311
|
ctx.setAction("scrollUp", () => {
|
|
312
|
-
const { scrollOffset: currentScrollOffset
|
|
313
|
-
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
312
|
+
const { scrollOffset: currentScrollOffset } = scrollStateRef.current;
|
|
314
313
|
const newScrollOffset = Math.max(0, currentScrollOffset - 1);
|
|
315
314
|
setScrollOffset(newScrollOffset);
|
|
316
315
|
forceUpdate({});
|
|
@@ -325,9 +324,9 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
325
324
|
if (sortable) {
|
|
326
325
|
ctx.setAction("toggleSort", () => {
|
|
327
326
|
const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
|
|
328
|
-
const currentSelectedItem =
|
|
327
|
+
const currentSelectedItem = displayItemsRef.current[selectedIndexRef.current];
|
|
329
328
|
setSortOrder(nextSort);
|
|
330
|
-
const newSortedItems = nextSort !== "none" ? [...
|
|
329
|
+
const newSortedItems = nextSort !== "none" ? [...itemsRef.current].sort((a, b) => {
|
|
331
330
|
const titleA = titleGetter(a).toLowerCase();
|
|
332
331
|
const titleB = titleGetter(b).toLowerCase();
|
|
333
332
|
if (nextSort === "asc") {
|
|
@@ -335,7 +334,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
335
334
|
} else {
|
|
336
335
|
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
337
336
|
}
|
|
338
|
-
}) :
|
|
337
|
+
}) : itemsRef.current;
|
|
339
338
|
const newIndex = newSortedItems.findIndex((item) => item === currentSelectedItem);
|
|
340
339
|
if (newIndex !== -1) {
|
|
341
340
|
selectedIndexRef.current = newIndex;
|
|
@@ -473,14 +472,13 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
473
472
|
}
|
|
474
473
|
var h2;
|
|
475
474
|
var init_list_components = __esm({
|
|
476
|
-
"src/screen/list-components.
|
|
477
|
-
"use strict";
|
|
475
|
+
"src/screen/list-components.js"() {
|
|
478
476
|
init_components();
|
|
479
477
|
h2 = createElement;
|
|
480
478
|
}
|
|
481
479
|
});
|
|
482
480
|
|
|
483
|
-
// src/screen/screens.
|
|
481
|
+
// src/screen/screens.js
|
|
484
482
|
import { useState as useState2, createElement as h3 } from "react";
|
|
485
483
|
import { render, useInput, Text as Text3 } from "ink";
|
|
486
484
|
function groupKeyBindings(bindings) {
|
|
@@ -558,7 +556,7 @@ async function showScreen(config2) {
|
|
|
558
556
|
let renderResult = null;
|
|
559
557
|
let initialized = false;
|
|
560
558
|
const Screen = () => {
|
|
561
|
-
const [
|
|
559
|
+
const [, setUpdateCounter] = useState2(0);
|
|
562
560
|
if (!initialized) {
|
|
563
561
|
const defaultBindings = [
|
|
564
562
|
{ key: "escape", caption: "go back", action: "back", protected: true, order: 1 },
|
|
@@ -677,7 +675,7 @@ async function showScreen(config2) {
|
|
|
677
675
|
}
|
|
678
676
|
}
|
|
679
677
|
if (matchedBinding && actions[matchedBinding.action]) {
|
|
680
|
-
|
|
678
|
+
actions[matchedBinding.action]({
|
|
681
679
|
input,
|
|
682
680
|
key,
|
|
683
681
|
binding: matchedBinding
|
|
@@ -807,8 +805,7 @@ async function showMultiColumnListWithPreviewScreen(config2) {
|
|
|
807
805
|
}
|
|
808
806
|
var showMenuScreen, showWordGridScreen;
|
|
809
807
|
var init_screens = __esm({
|
|
810
|
-
"src/screen/screens.
|
|
811
|
-
"use strict";
|
|
808
|
+
"src/screen/screens.js"() {
|
|
812
809
|
init_components();
|
|
813
810
|
init_list_components();
|
|
814
811
|
showMenuScreen = showListScreen;
|
|
@@ -816,9 +813,9 @@ var init_screens = __esm({
|
|
|
816
813
|
}
|
|
817
814
|
});
|
|
818
815
|
|
|
819
|
-
// src/screen/ui-elements.
|
|
816
|
+
// src/screen/ui-elements.js
|
|
820
817
|
import { createElement as h4 } from "react";
|
|
821
|
-
import { Box as
|
|
818
|
+
import { Box as Box3, Text as Text4 } from "ink";
|
|
822
819
|
function ListItem({
|
|
823
820
|
children,
|
|
824
821
|
isSelected = false,
|
|
@@ -828,7 +825,7 @@ function ListItem({
|
|
|
828
825
|
dimColor = false
|
|
829
826
|
}) {
|
|
830
827
|
return h4(
|
|
831
|
-
|
|
828
|
+
Box3,
|
|
832
829
|
{},
|
|
833
830
|
h4(Text4, {
|
|
834
831
|
color: isSelected ? backgroundColor || "green" : color,
|
|
@@ -843,10 +840,10 @@ function TextBlock({
|
|
|
843
840
|
color = "white",
|
|
844
841
|
dimmed = false,
|
|
845
842
|
bold = false,
|
|
846
|
-
maxWidth
|
|
843
|
+
maxWidth: _maxWidth
|
|
847
844
|
}) {
|
|
848
845
|
return h4(
|
|
849
|
-
|
|
846
|
+
Box3,
|
|
850
847
|
{},
|
|
851
848
|
h4(Text4, {
|
|
852
849
|
color,
|
|
@@ -857,7 +854,7 @@ function TextBlock({
|
|
|
857
854
|
}
|
|
858
855
|
function Divider({ character = "\u2500", width = 80 }) {
|
|
859
856
|
return h4(
|
|
860
|
-
|
|
857
|
+
Box3,
|
|
861
858
|
{ marginY: 1 },
|
|
862
859
|
h4(Text4, { dimColor: true }, character.repeat(width))
|
|
863
860
|
);
|
|
@@ -872,7 +869,7 @@ function GridCell({
|
|
|
872
869
|
align = "left"
|
|
873
870
|
}) {
|
|
874
871
|
return h4(
|
|
875
|
-
|
|
872
|
+
Box3,
|
|
876
873
|
{ width },
|
|
877
874
|
h4(Text4, {
|
|
878
875
|
color,
|
|
@@ -883,44 +880,42 @@ function GridCell({
|
|
|
883
880
|
}, children)
|
|
884
881
|
);
|
|
885
882
|
}
|
|
886
|
-
function InputField({ prompt, value, onChange, onSubmit }) {
|
|
883
|
+
function InputField({ prompt, value, onChange: _onChange, onSubmit: _onSubmit }) {
|
|
887
884
|
return h4(
|
|
888
|
-
|
|
885
|
+
Box3,
|
|
889
886
|
{ flexDirection: "column" },
|
|
890
887
|
h4(Text4, {}, prompt),
|
|
891
888
|
h4(
|
|
892
|
-
|
|
889
|
+
Box3,
|
|
893
890
|
{ marginTop: 1 },
|
|
894
891
|
h4(Text4, { color: "cyan" }, " > ", value, "_")
|
|
895
892
|
)
|
|
896
893
|
);
|
|
897
894
|
}
|
|
898
895
|
var init_ui_elements = __esm({
|
|
899
|
-
"src/screen/ui-elements.
|
|
900
|
-
"use strict";
|
|
896
|
+
"src/screen/ui-elements.js"() {
|
|
901
897
|
}
|
|
902
898
|
});
|
|
903
899
|
|
|
904
|
-
// src/screen/utils.
|
|
900
|
+
// src/screen/utils.js
|
|
905
901
|
function buildBreadcrumb(parts) {
|
|
906
902
|
if (parts.length === 0) return "";
|
|
907
903
|
if (parts.length === 1) return parts[0];
|
|
908
904
|
return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
|
|
909
905
|
}
|
|
910
|
-
function buildDetailBreadcrumb(
|
|
911
|
-
if (
|
|
912
|
-
return suffix ? `\u2190 ${suffix}` :
|
|
906
|
+
function buildDetailBreadcrumb(path6, suffix = "") {
|
|
907
|
+
if (path6.length <= 1) {
|
|
908
|
+
return suffix ? `\u2190 ${suffix}` : path6[0] || "";
|
|
913
909
|
}
|
|
914
|
-
const breadcrumb = buildBreadcrumb(
|
|
910
|
+
const breadcrumb = buildBreadcrumb(path6);
|
|
915
911
|
return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
|
|
916
912
|
}
|
|
917
913
|
var init_utils = __esm({
|
|
918
|
-
"src/screen/utils.
|
|
919
|
-
"use strict";
|
|
914
|
+
"src/screen/utils.js"() {
|
|
920
915
|
}
|
|
921
916
|
});
|
|
922
917
|
|
|
923
|
-
// src/screen/footer-builder.
|
|
918
|
+
// src/screen/footer-builder.js
|
|
924
919
|
function buildFooter(config2 = {}) {
|
|
925
920
|
const {
|
|
926
921
|
navigation = null,
|
|
@@ -971,8 +966,7 @@ function organizeFooterMessages(messages) {
|
|
|
971
966
|
}
|
|
972
967
|
var FooterPresets;
|
|
973
968
|
var init_footer_builder = __esm({
|
|
974
|
-
"src/screen/footer-builder.
|
|
975
|
-
"use strict";
|
|
969
|
+
"src/screen/footer-builder.js"() {
|
|
976
970
|
FooterPresets = {
|
|
977
971
|
/**
|
|
978
972
|
* Menu screen footer
|
|
@@ -1031,10 +1025,10 @@ var init_footer_builder = __esm({
|
|
|
1031
1025
|
}
|
|
1032
1026
|
});
|
|
1033
1027
|
|
|
1034
|
-
// src/screen/index.
|
|
1028
|
+
// src/screen/index.js
|
|
1035
1029
|
var screen_exports = {};
|
|
1036
1030
|
__export(screen_exports, {
|
|
1037
|
-
Box: () =>
|
|
1031
|
+
Box: () => Box4,
|
|
1038
1032
|
Divider: () => Divider,
|
|
1039
1033
|
FooterPresets: () => FooterPresets,
|
|
1040
1034
|
GridCell: () => GridCell,
|
|
@@ -1043,7 +1037,7 @@ __export(screen_exports, {
|
|
|
1043
1037
|
ListItem: () => ListItem,
|
|
1044
1038
|
MultiColumnListComponent: () => MultiColumnListComponent,
|
|
1045
1039
|
MultiColumnListWithPreviewComponent: () => MultiColumnListWithPreviewComponent,
|
|
1046
|
-
React: () =>
|
|
1040
|
+
React: () => React2,
|
|
1047
1041
|
ScreenBody: () => ScreenBody,
|
|
1048
1042
|
ScreenContainer: () => ScreenContainer,
|
|
1049
1043
|
ScreenDivider: () => ScreenDivider,
|
|
@@ -1057,6 +1051,7 @@ __export(screen_exports, {
|
|
|
1057
1051
|
buildFooter: () => buildFooter,
|
|
1058
1052
|
h: () => createElement2,
|
|
1059
1053
|
load: () => load,
|
|
1054
|
+
memo: () => memo,
|
|
1060
1055
|
organizeFooterMessages: () => organizeFooterMessages,
|
|
1061
1056
|
showListScreen: () => showListScreen,
|
|
1062
1057
|
showMenuScreen: () => showMenuScreen,
|
|
@@ -1065,14 +1060,15 @@ __export(screen_exports, {
|
|
|
1065
1060
|
showScreen: () => showScreen,
|
|
1066
1061
|
showWordGridScreen: () => showWordGridScreen,
|
|
1067
1062
|
useCallback: () => useCallback,
|
|
1068
|
-
useEffect: () =>
|
|
1063
|
+
useEffect: () => useEffect2,
|
|
1069
1064
|
useInput: () => useInput2,
|
|
1065
|
+
useLayoutEffect: () => useLayoutEffect,
|
|
1070
1066
|
useMemo: () => useMemo,
|
|
1071
|
-
useRef: () =>
|
|
1067
|
+
useRef: () => useRef2,
|
|
1072
1068
|
useState: () => useState3
|
|
1073
1069
|
});
|
|
1074
|
-
import
|
|
1075
|
-
import { Box as
|
|
1070
|
+
import React2, { useState as useState3, useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useMemo, useCallback, memo, createElement as createElement2 } from "react";
|
|
1071
|
+
import { Box as Box4, Text as Text5, useInput as useInput2 } from "ink";
|
|
1076
1072
|
async function load() {
|
|
1077
1073
|
if (loadPromise) return loadPromise;
|
|
1078
1074
|
loadPromise = Promise.all([
|
|
@@ -1084,8 +1080,7 @@ async function load() {
|
|
|
1084
1080
|
}
|
|
1085
1081
|
var loadPromise;
|
|
1086
1082
|
var init_screen = __esm({
|
|
1087
|
-
"src/screen/index.
|
|
1088
|
-
"use strict";
|
|
1083
|
+
"src/screen/index.js"() {
|
|
1089
1084
|
init_screens();
|
|
1090
1085
|
init_list_components();
|
|
1091
1086
|
init_components();
|
|
@@ -1100,11 +1095,11 @@ var init_screen = __esm({
|
|
|
1100
1095
|
}
|
|
1101
1096
|
});
|
|
1102
1097
|
|
|
1103
|
-
// src/scripts/cli-runner.
|
|
1104
|
-
import
|
|
1098
|
+
// src/scripts/cli-runner.js
|
|
1099
|
+
import path5 from "path";
|
|
1105
1100
|
import { pathToFileURL } from "url";
|
|
1106
1101
|
|
|
1107
|
-
// src/args/index.
|
|
1102
|
+
// src/args/index.js
|
|
1108
1103
|
import { readFileSync, existsSync } from "fs";
|
|
1109
1104
|
import { resolve, dirname, basename, extname, join, isAbsolute } from "path";
|
|
1110
1105
|
import { config } from "dotenv";
|
|
@@ -1587,10 +1582,10 @@ var Args = class _Args {
|
|
|
1587
1582
|
}
|
|
1588
1583
|
};
|
|
1589
1584
|
|
|
1590
|
-
// src/params/index.
|
|
1585
|
+
// src/params/index.js
|
|
1591
1586
|
import Joi from "joi";
|
|
1592
1587
|
|
|
1593
|
-
// src/errors.
|
|
1588
|
+
// src/errors.js
|
|
1594
1589
|
var FrameworkError = class extends Error {
|
|
1595
1590
|
constructor(message) {
|
|
1596
1591
|
super(message);
|
|
@@ -1616,7 +1611,7 @@ var FileDatabaseError = class extends FrameworkError {
|
|
|
1616
1611
|
}
|
|
1617
1612
|
};
|
|
1618
1613
|
|
|
1619
|
-
// src/params/custom-types.
|
|
1614
|
+
// src/params/custom-types.js
|
|
1620
1615
|
var joiEdateType = (value, helpers) => {
|
|
1621
1616
|
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(value)) {
|
|
1622
1617
|
const testDate = new Date(value);
|
|
@@ -1709,7 +1704,7 @@ function calculateTimeOffset(amount, unit, sign) {
|
|
|
1709
1704
|
}
|
|
1710
1705
|
return sign === "+" ? amount * multiplier : -amount * multiplier;
|
|
1711
1706
|
}
|
|
1712
|
-
var joiStringArrayType = (type) => (value,
|
|
1707
|
+
var joiStringArrayType = (type) => (value, _helpers) => {
|
|
1713
1708
|
if (value === void 0 || typeof value === "function") {
|
|
1714
1709
|
return [];
|
|
1715
1710
|
}
|
|
@@ -1735,7 +1730,7 @@ var joiStringArrayType = (type) => (value, helpers) => {
|
|
|
1735
1730
|
return arr;
|
|
1736
1731
|
};
|
|
1737
1732
|
|
|
1738
|
-
// src/params/index.
|
|
1733
|
+
// src/params/index.js
|
|
1739
1734
|
var Params = class _Params {
|
|
1740
1735
|
context;
|
|
1741
1736
|
// Partial context during initialization
|
|
@@ -1927,7 +1922,7 @@ var Params = class _Params {
|
|
|
1927
1922
|
throw new ParamError(`default value "${defValObj.value}" type mismatch`);
|
|
1928
1923
|
}
|
|
1929
1924
|
type = type.default(defValObj.value);
|
|
1930
|
-
} else if (str.match(
|
|
1925
|
+
} else if (str.match(/\s*required\s*/)) {
|
|
1931
1926
|
type = type.required();
|
|
1932
1927
|
} else {
|
|
1933
1928
|
type = type.optional();
|
|
@@ -1995,7 +1990,7 @@ var Params = class _Params {
|
|
|
1995
1990
|
definition = val;
|
|
1996
1991
|
val = val.value;
|
|
1997
1992
|
}
|
|
1998
|
-
|
|
1993
|
+
this.assignDefinition(key, definition);
|
|
1999
1994
|
if (!this.runAllRegisteredSetters(key, val)) {
|
|
2000
1995
|
this.params[key] = val;
|
|
2001
1996
|
}
|
|
@@ -2003,6 +1998,8 @@ var Params = class _Params {
|
|
|
2003
1998
|
/**
|
|
2004
1999
|
* Get all parameters from definitions (main script).
|
|
2005
2000
|
* Same as getAllForModule("script", defs). Processes left-to-right for cross-parameter references.
|
|
2001
|
+
* Libraries should use {@link getAllForModule} with an explicit module name (or {@link runWithModule}
|
|
2002
|
+
* around {@link get}) so --showUsedParams groups usage correctly.
|
|
2006
2003
|
*/
|
|
2007
2004
|
getAll(defs2) {
|
|
2008
2005
|
return this.getAllForModule("script", defs2);
|
|
@@ -2039,6 +2036,19 @@ var Params = class _Params {
|
|
|
2039
2036
|
this._currentModule = prev;
|
|
2040
2037
|
}
|
|
2041
2038
|
}
|
|
2039
|
+
/**
|
|
2040
|
+
* Run a callback with {@link _currentModule} set so single {@link get} calls are tracked
|
|
2041
|
+
* under the same module (for --showUsedParams / getFiguredByModule).
|
|
2042
|
+
*/
|
|
2043
|
+
runWithModule(moduleName, fn) {
|
|
2044
|
+
const prev = this._currentModule;
|
|
2045
|
+
this._currentModule = moduleName;
|
|
2046
|
+
try {
|
|
2047
|
+
return fn();
|
|
2048
|
+
} finally {
|
|
2049
|
+
this._currentModule = prev;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2042
2052
|
/**
|
|
2043
2053
|
* Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
|
|
2044
2054
|
*/
|
|
@@ -2052,9 +2062,9 @@ var Params = class _Params {
|
|
|
2052
2062
|
if (!parenMatch) continue;
|
|
2053
2063
|
const parts = parenMatch[1].split(":");
|
|
2054
2064
|
if (parts.length < 3) continue;
|
|
2055
|
-
const
|
|
2056
|
-
if (!
|
|
2057
|
-
const srcMatch =
|
|
2065
|
+
const path6 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
|
|
2066
|
+
if (!path6 || path6.includes(paramsIndexPath)) continue;
|
|
2067
|
+
const srcMatch = path6.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
|
|
2058
2068
|
if (srcMatch) return srcMatch[1];
|
|
2059
2069
|
}
|
|
2060
2070
|
return "script";
|
|
@@ -2099,11 +2109,11 @@ var Params = class _Params {
|
|
|
2099
2109
|
}
|
|
2100
2110
|
};
|
|
2101
2111
|
|
|
2102
|
-
// src/logger/index.
|
|
2112
|
+
// src/logger/index.js
|
|
2103
2113
|
import chalk from "chalk";
|
|
2104
2114
|
import util from "util";
|
|
2105
2115
|
|
|
2106
|
-
// src/logger/transports.
|
|
2116
|
+
// src/logger/transports.js
|
|
2107
2117
|
var ConsoleTransport = class {
|
|
2108
2118
|
write(payload) {
|
|
2109
2119
|
console.info(payload);
|
|
@@ -2123,7 +2133,7 @@ var ParentProcessTransport = class {
|
|
|
2123
2133
|
}
|
|
2124
2134
|
};
|
|
2125
2135
|
|
|
2126
|
-
// src/logger/index.
|
|
2136
|
+
// src/logger/index.js
|
|
2127
2137
|
var ALL_LEVELS = [
|
|
2128
2138
|
"silly",
|
|
2129
2139
|
"debug",
|
|
@@ -2196,6 +2206,7 @@ var Logger = class _Logger {
|
|
|
2196
2206
|
}
|
|
2197
2207
|
/**
|
|
2198
2208
|
* Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
|
|
2209
|
+
* Params are tracked under the `logger` module for --showUsedParams.
|
|
2199
2210
|
*/
|
|
2200
2211
|
static init(context, options) {
|
|
2201
2212
|
const paramDefs = {
|
|
@@ -2209,7 +2220,7 @@ var Logger = class _Logger {
|
|
|
2209
2220
|
progressWithTimes: "boolean default false",
|
|
2210
2221
|
progressThrottleMs: "number"
|
|
2211
2222
|
};
|
|
2212
|
-
const discovered = context.params.getAllForModule(paramDefs);
|
|
2223
|
+
const discovered = context.params.getAllForModule("logger", paramDefs);
|
|
2213
2224
|
const config2 = { ...discovered, ...options };
|
|
2214
2225
|
const logger = new _Logger(context, config2);
|
|
2215
2226
|
context.logger = logger;
|
|
@@ -2405,9 +2416,9 @@ var Logger = class _Logger {
|
|
|
2405
2416
|
}
|
|
2406
2417
|
};
|
|
2407
2418
|
|
|
2408
|
-
// src/init/index.
|
|
2419
|
+
// src/init/index.js
|
|
2409
2420
|
import { EventEmitter } from "events";
|
|
2410
|
-
function extractComponentOptions(opts,
|
|
2421
|
+
function extractComponentOptions(opts, _componentName) {
|
|
2411
2422
|
const reservedKeys = ["overrides", "defaults", "modules"];
|
|
2412
2423
|
const componentOptions = {};
|
|
2413
2424
|
for (const [key, value] of Object.entries(opts)) {
|
|
@@ -2474,6 +2485,19 @@ function printAllParameters(context) {
|
|
|
2474
2485
|
async function init(flow2, opts = {}) {
|
|
2475
2486
|
let stop = false;
|
|
2476
2487
|
let context = null;
|
|
2488
|
+
let cleanupRan = false;
|
|
2489
|
+
const runRegisteredCleanups = async (ctx) => {
|
|
2490
|
+
if (cleanupRan) return;
|
|
2491
|
+
cleanupRan = true;
|
|
2492
|
+
const fns = [...ctx.cleanupFunctions].reverse();
|
|
2493
|
+
for (const fn of fns) {
|
|
2494
|
+
try {
|
|
2495
|
+
await fn(ctx);
|
|
2496
|
+
} catch (error) {
|
|
2497
|
+
ctx.logger.warn("[cleanup] error in cleanup function:", error);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
};
|
|
2477
2501
|
try {
|
|
2478
2502
|
try {
|
|
2479
2503
|
const screenModule = await Promise.resolve().then(() => (init_screen(), screen_exports));
|
|
@@ -2496,13 +2520,24 @@ async function init(flow2, opts = {}) {
|
|
|
2496
2520
|
printAllParameters(context);
|
|
2497
2521
|
process.exit(0);
|
|
2498
2522
|
}
|
|
2523
|
+
let sigintCount = 0;
|
|
2499
2524
|
process.on("SIGINT", async () => {
|
|
2500
|
-
if (
|
|
2501
|
-
|
|
2502
|
-
|
|
2525
|
+
if (!context) return;
|
|
2526
|
+
sigintCount += 1;
|
|
2527
|
+
if (sigintCount === 1) {
|
|
2528
|
+
stop = true;
|
|
2529
|
+
context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
|
|
2530
|
+
context.emitter.emit("stop", stopAllowance);
|
|
2531
|
+
return;
|
|
2503
2532
|
}
|
|
2533
|
+
context.logger.warn("[process] second SIGINT: running cleanup then exit");
|
|
2534
|
+
await runRegisteredCleanups(context);
|
|
2535
|
+
process.exit(2);
|
|
2536
|
+
});
|
|
2537
|
+
process.on("SIGTERM", () => {
|
|
2538
|
+
if (!context || stop) return;
|
|
2504
2539
|
stop = true;
|
|
2505
|
-
context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
|
|
2540
|
+
context.logger.info(`>> SIGTERM: emitting stop with allowance ${stopAllowance}`);
|
|
2506
2541
|
context.emitter.emit("stop", stopAllowance);
|
|
2507
2542
|
});
|
|
2508
2543
|
await flow2(context);
|
|
@@ -2527,33 +2562,62 @@ async function init(flow2, opts = {}) {
|
|
|
2527
2562
|
}
|
|
2528
2563
|
} finally {
|
|
2529
2564
|
if (context) {
|
|
2530
|
-
|
|
2531
|
-
try {
|
|
2532
|
-
await fn(context);
|
|
2533
|
-
} catch (error) {
|
|
2534
|
-
context.logger.warn("[cleanup] error in cleanup function:", error);
|
|
2535
|
-
}
|
|
2536
|
-
}
|
|
2565
|
+
await runRegisteredCleanups(context);
|
|
2537
2566
|
}
|
|
2538
2567
|
}
|
|
2539
2568
|
}
|
|
2540
2569
|
|
|
2541
|
-
// src/db/index.
|
|
2570
|
+
// src/db/index.js
|
|
2542
2571
|
import knex from "knex";
|
|
2572
|
+
var KNEX_DEFAULTS = {
|
|
2573
|
+
testConnection: true,
|
|
2574
|
+
pool: { min: 2, max: 10 },
|
|
2575
|
+
acquireConnectionTimeout: 1e4,
|
|
2576
|
+
ssl: { rejectUnauthorized: false }
|
|
2577
|
+
};
|
|
2543
2578
|
var Db = class {
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2579
|
+
static async init(context, options = {}) {
|
|
2580
|
+
const defs2 = {
|
|
2581
|
+
dbName: "string",
|
|
2582
|
+
dbConnectionString: "string",
|
|
2583
|
+
dbProfile: "boolean default false"
|
|
2584
|
+
};
|
|
2585
|
+
const discovered = context?.params?.getAllForModule?.("db", defs2) ?? {};
|
|
2586
|
+
const merged = { ...discovered, ...options };
|
|
2587
|
+
let { dbName, dbConnectionString } = merged;
|
|
2588
|
+
const { dbProfile } = merged;
|
|
2589
|
+
if (!dbName && !dbConnectionString) {
|
|
2590
|
+
dbName = "local";
|
|
2591
|
+
}
|
|
2592
|
+
if (dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
|
|
2593
|
+
dbConnectionString = dbName;
|
|
2594
|
+
dbName = void 0;
|
|
2595
|
+
}
|
|
2596
|
+
if (dbName && !dbConnectionString) {
|
|
2597
|
+
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
2598
|
+
dbConnectionString = await context.params.get(paramName, "string");
|
|
2599
|
+
if (!dbConnectionString) {
|
|
2600
|
+
throw new ParamError(
|
|
2601
|
+
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
const config2 = {
|
|
2606
|
+
...KNEX_DEFAULTS,
|
|
2607
|
+
connectionString: dbConnectionString,
|
|
2608
|
+
name: dbName || merged.name || "default",
|
|
2609
|
+
profile: !!dbProfile,
|
|
2610
|
+
logger: context.logger
|
|
2611
|
+
};
|
|
2612
|
+
return dbConnect(context, config2);
|
|
2613
|
+
}
|
|
2553
2614
|
constructor(config2) {
|
|
2554
|
-
if (!config2.connectionString) {
|
|
2615
|
+
if (!config2 || !config2.connectionString) {
|
|
2555
2616
|
throw new ParamError("Db: connectionString is required");
|
|
2556
2617
|
}
|
|
2618
|
+
this.knexInstance = null;
|
|
2619
|
+
this.isConnected = false;
|
|
2620
|
+
this.queriesLog = [];
|
|
2557
2621
|
this.config = {
|
|
2558
2622
|
testConnection: true,
|
|
2559
2623
|
profile: false,
|
|
@@ -2566,25 +2630,23 @@ var Db = class {
|
|
|
2566
2630
|
};
|
|
2567
2631
|
this.logger = this.config.logger;
|
|
2568
2632
|
const instance = this;
|
|
2569
|
-
const callableWrapper = function(
|
|
2633
|
+
const callableWrapper = function() {
|
|
2570
2634
|
throw new Error("This should never be called directly");
|
|
2571
2635
|
};
|
|
2572
2636
|
callableWrapper._instance = instance;
|
|
2573
2637
|
return new Proxy(callableWrapper, {
|
|
2574
|
-
|
|
2575
|
-
apply: (target, thisArg, argumentsList) => {
|
|
2638
|
+
apply: (target, _thisArg, argumentsList) => {
|
|
2576
2639
|
const inst = target._instance;
|
|
2577
2640
|
if (!inst.knexInstance) {
|
|
2578
2641
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
2579
2642
|
}
|
|
2580
2643
|
return inst.knexInstance(...argumentsList);
|
|
2581
2644
|
},
|
|
2582
|
-
// Intercept property access: db.schema, db.raw, etc.
|
|
2583
2645
|
get: (target, prop) => {
|
|
2584
2646
|
if (prop === "_instance") {
|
|
2585
2647
|
return target._instance;
|
|
2586
2648
|
}
|
|
2587
|
-
const
|
|
2649
|
+
const inst = target._instance;
|
|
2588
2650
|
const ownMethods = [
|
|
2589
2651
|
"connect",
|
|
2590
2652
|
"disconnect",
|
|
@@ -2597,26 +2659,26 @@ var Db = class {
|
|
|
2597
2659
|
"detectClient",
|
|
2598
2660
|
"attachProfiler"
|
|
2599
2661
|
];
|
|
2600
|
-
if (prop in
|
|
2601
|
-
const value =
|
|
2662
|
+
if (prop in inst) {
|
|
2663
|
+
const value = inst[prop];
|
|
2602
2664
|
if (typeof value === "function" && ownMethods.includes(prop)) {
|
|
2603
|
-
return value.bind(
|
|
2665
|
+
return value.bind(inst);
|
|
2604
2666
|
}
|
|
2605
2667
|
if (typeof value !== "function") {
|
|
2606
2668
|
return value;
|
|
2607
2669
|
}
|
|
2608
2670
|
}
|
|
2609
|
-
if (
|
|
2610
|
-
const knexProp =
|
|
2671
|
+
if (inst.knexInstance) {
|
|
2672
|
+
const knexProp = inst.knexInstance[prop];
|
|
2611
2673
|
if (typeof knexProp === "function") {
|
|
2612
|
-
return knexProp.bind(
|
|
2674
|
+
return knexProp.bind(inst.knexInstance);
|
|
2613
2675
|
}
|
|
2614
2676
|
return knexProp;
|
|
2615
2677
|
}
|
|
2616
|
-
if (prop in
|
|
2617
|
-
const method =
|
|
2678
|
+
if (prop in inst) {
|
|
2679
|
+
const method = inst[prop];
|
|
2618
2680
|
if (typeof method === "function") {
|
|
2619
|
-
return method.bind(
|
|
2681
|
+
return method.bind(inst);
|
|
2620
2682
|
}
|
|
2621
2683
|
return method;
|
|
2622
2684
|
}
|
|
@@ -2624,9 +2686,6 @@ var Db = class {
|
|
|
2624
2686
|
}
|
|
2625
2687
|
});
|
|
2626
2688
|
}
|
|
2627
|
-
/**
|
|
2628
|
-
* Detect database client type from connection string
|
|
2629
|
-
*/
|
|
2630
2689
|
detectClient(connectionString) {
|
|
2631
2690
|
if (connectionString.match(/^postgresql/)) {
|
|
2632
2691
|
return "pg";
|
|
@@ -2636,9 +2695,6 @@ var Db = class {
|
|
|
2636
2695
|
}
|
|
2637
2696
|
return null;
|
|
2638
2697
|
}
|
|
2639
|
-
/**
|
|
2640
|
-
* Connect to the database
|
|
2641
|
-
*/
|
|
2642
2698
|
async connect() {
|
|
2643
2699
|
if (this.isConnected && this.knexInstance) {
|
|
2644
2700
|
this.logger.warn?.("[Db] Already connected");
|
|
@@ -2647,14 +2703,13 @@ var Db = class {
|
|
|
2647
2703
|
const client = this.detectClient(this.config.connectionString);
|
|
2648
2704
|
if (!client) {
|
|
2649
2705
|
throw new ParamError(
|
|
2650
|
-
|
|
2706
|
+
"Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://"
|
|
2651
2707
|
);
|
|
2652
2708
|
}
|
|
2653
2709
|
try {
|
|
2654
2710
|
const connectionConfig = {
|
|
2655
2711
|
connectionString: this.config.connectionString,
|
|
2656
2712
|
family: 4
|
|
2657
|
-
// Force IPv4 only (disable IPv6)
|
|
2658
2713
|
};
|
|
2659
2714
|
this.knexInstance = knex({
|
|
2660
2715
|
client,
|
|
@@ -2670,7 +2725,9 @@ var Db = class {
|
|
|
2670
2725
|
await this.testConnection();
|
|
2671
2726
|
}
|
|
2672
2727
|
this.isConnected = true;
|
|
2673
|
-
this.logger.debug?.(
|
|
2728
|
+
this.logger.debug?.(
|
|
2729
|
+
`[Db] Connected to database "${this.config.name || this.config.connectionString}"`
|
|
2730
|
+
);
|
|
2674
2731
|
} catch (error) {
|
|
2675
2732
|
if (error instanceof ParamError) {
|
|
2676
2733
|
throw error;
|
|
@@ -2679,9 +2736,6 @@ var Db = class {
|
|
|
2679
2736
|
throw new ParamError(`Db: Connection failed - ${errorMsg}`);
|
|
2680
2737
|
}
|
|
2681
2738
|
}
|
|
2682
|
-
/**
|
|
2683
|
-
* Disconnect from the database
|
|
2684
|
-
*/
|
|
2685
2739
|
async disconnect() {
|
|
2686
2740
|
if (!this.knexInstance) {
|
|
2687
2741
|
return;
|
|
@@ -2691,16 +2745,15 @@ var Db = class {
|
|
|
2691
2745
|
this.knexInstance = null;
|
|
2692
2746
|
this.isConnected = false;
|
|
2693
2747
|
this.queriesLog = [];
|
|
2694
|
-
this.logger.debug?.(
|
|
2748
|
+
this.logger.debug?.(
|
|
2749
|
+
`[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`
|
|
2750
|
+
);
|
|
2695
2751
|
} catch (error) {
|
|
2696
2752
|
const errorMsg = this.getErrorMessage(error);
|
|
2697
2753
|
this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
|
|
2698
2754
|
throw error;
|
|
2699
2755
|
}
|
|
2700
2756
|
}
|
|
2701
|
-
/**
|
|
2702
|
-
* Extract error message from various error types
|
|
2703
|
-
*/
|
|
2704
2757
|
getErrorMessage(error) {
|
|
2705
2758
|
if (error instanceof AggregateError) {
|
|
2706
2759
|
const errors = error.errors || [];
|
|
@@ -2725,9 +2778,11 @@ var Db = class {
|
|
|
2725
2778
|
return `${code} (tried: ${addresses.join(", ")})`;
|
|
2726
2779
|
}
|
|
2727
2780
|
}
|
|
2728
|
-
const uniqueMessages = [
|
|
2729
|
-
|
|
2730
|
-
|
|
2781
|
+
const uniqueMessages = [
|
|
2782
|
+
...new Set(
|
|
2783
|
+
errors.map((e) => e instanceof Error ? e.message : String(e))
|
|
2784
|
+
)
|
|
2785
|
+
];
|
|
2731
2786
|
if (uniqueMessages.length === 1) {
|
|
2732
2787
|
return uniqueMessages[0];
|
|
2733
2788
|
}
|
|
@@ -2736,28 +2791,25 @@ var Db = class {
|
|
|
2736
2791
|
return error.message || "Multiple errors occurred";
|
|
2737
2792
|
}
|
|
2738
2793
|
if (error instanceof Error) {
|
|
2739
|
-
const
|
|
2740
|
-
if (
|
|
2741
|
-
return `${
|
|
2794
|
+
const code = error.code;
|
|
2795
|
+
if (code) {
|
|
2796
|
+
return `${code}: ${error.message || String(error)}`;
|
|
2742
2797
|
}
|
|
2743
2798
|
return error.message || String(error);
|
|
2744
2799
|
}
|
|
2745
2800
|
if (typeof error === "string") {
|
|
2746
2801
|
return error;
|
|
2747
2802
|
}
|
|
2748
|
-
if (error
|
|
2803
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
2749
2804
|
const msg = String(error.message);
|
|
2750
|
-
const
|
|
2751
|
-
if (
|
|
2752
|
-
return `${
|
|
2805
|
+
const code = error.code;
|
|
2806
|
+
if (code) {
|
|
2807
|
+
return `${code}: ${msg}`;
|
|
2753
2808
|
}
|
|
2754
2809
|
return msg;
|
|
2755
2810
|
}
|
|
2756
2811
|
return String(error) || "Unknown error";
|
|
2757
2812
|
}
|
|
2758
|
-
/**
|
|
2759
|
-
* Test database connection
|
|
2760
|
-
*/
|
|
2761
2813
|
async testConnection() {
|
|
2762
2814
|
if (!this.knexInstance) {
|
|
2763
2815
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -2773,9 +2825,6 @@ var Db = class {
|
|
|
2773
2825
|
throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
|
|
2774
2826
|
}
|
|
2775
2827
|
}
|
|
2776
|
-
/**
|
|
2777
|
-
* Attach query profiler to log all queries
|
|
2778
|
-
*/
|
|
2779
2828
|
attachProfiler() {
|
|
2780
2829
|
if (!this.knexInstance) {
|
|
2781
2830
|
return;
|
|
@@ -2785,7 +2834,7 @@ var Db = class {
|
|
|
2785
2834
|
this.knexInstance.on("query", (query) => {
|
|
2786
2835
|
query.__startTime = process.hrtime();
|
|
2787
2836
|
});
|
|
2788
|
-
this.knexInstance.on("query-response", (
|
|
2837
|
+
this.knexInstance.on("query-response", (_response, query) => {
|
|
2789
2838
|
const [seconds, nanoseconds] = process.hrtime(query.__startTime);
|
|
2790
2839
|
const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
|
|
2791
2840
|
const logEntry = {
|
|
@@ -2800,15 +2849,9 @@ var Db = class {
|
|
|
2800
2849
|
this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
|
|
2801
2850
|
});
|
|
2802
2851
|
}
|
|
2803
|
-
/**
|
|
2804
|
-
* Get query log (only available if profiling is enabled)
|
|
2805
|
-
*/
|
|
2806
2852
|
getQueryLog() {
|
|
2807
2853
|
return [...this.queriesLog];
|
|
2808
2854
|
}
|
|
2809
|
-
/**
|
|
2810
|
-
* Check if a table exists
|
|
2811
|
-
*/
|
|
2812
2855
|
async tableExists(tableName) {
|
|
2813
2856
|
if (!this.knexInstance) {
|
|
2814
2857
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -2820,65 +2863,28 @@ var Db = class {
|
|
|
2820
2863
|
throw error;
|
|
2821
2864
|
}
|
|
2822
2865
|
}
|
|
2823
|
-
/**
|
|
2824
|
-
* Get the underlying Knex instance (for advanced usage)
|
|
2825
|
-
*/
|
|
2826
2866
|
getKnex() {
|
|
2827
2867
|
if (!this.knexInstance) {
|
|
2828
2868
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
2829
2869
|
}
|
|
2830
2870
|
return this.knexInstance;
|
|
2831
2871
|
}
|
|
2832
|
-
/**
|
|
2833
|
-
* Get connection status
|
|
2834
|
-
*/
|
|
2835
2872
|
isConnectedToDb() {
|
|
2836
2873
|
return this.isConnected && this.knexInstance !== null;
|
|
2837
2874
|
}
|
|
2838
|
-
/**
|
|
2839
|
-
* Initialize Db with context (connects and registers disconnect cleanup).
|
|
2840
|
-
* Params are read via getAllForModule("db", defs). Same as dbInit(context, dbNameOrConnectionString).
|
|
2841
|
-
*/
|
|
2842
|
-
static async init(context, dbNameOrConnectionString) {
|
|
2843
|
-
return dbFindAndConnect(context, dbNameOrConnectionString);
|
|
2844
|
-
}
|
|
2845
2875
|
};
|
|
2846
2876
|
function capitalizeFirstLetter(str) {
|
|
2847
2877
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
2848
2878
|
}
|
|
2849
|
-
async function dbConnect(context,
|
|
2850
|
-
const defs2 = {
|
|
2851
|
-
testDbConnection: "boolean default true",
|
|
2852
|
-
name: "string",
|
|
2853
|
-
poolMin: "number default 2",
|
|
2854
|
-
poolMax: "number default 10",
|
|
2855
|
-
acquireConnectionTimeout: "number default 10000",
|
|
2856
|
-
sslRejectUnauthorized: "boolean default false"
|
|
2857
|
-
};
|
|
2858
|
-
const paramsConfig = context.params.getAllForModule(defs2);
|
|
2859
|
-
const config2 = {
|
|
2860
|
-
connectionString,
|
|
2861
|
-
name: paramsConfig.name || name || "default",
|
|
2862
|
-
testConnection: paramsConfig.testDbConnection,
|
|
2863
|
-
profile: dbProfile ?? false,
|
|
2864
|
-
pool: {
|
|
2865
|
-
min: paramsConfig.poolMin,
|
|
2866
|
-
max: paramsConfig.poolMax
|
|
2867
|
-
},
|
|
2868
|
-
acquireConnectionTimeout: paramsConfig.acquireConnectionTimeout,
|
|
2869
|
-
ssl: {
|
|
2870
|
-
rejectUnauthorized: paramsConfig.sslRejectUnauthorized
|
|
2871
|
-
},
|
|
2872
|
-
logger: context.logger
|
|
2873
|
-
};
|
|
2879
|
+
async function dbConnect(context, config2) {
|
|
2874
2880
|
try {
|
|
2875
2881
|
const db = new Db(config2);
|
|
2876
2882
|
context.registerCleanup(async () => {
|
|
2877
2883
|
await db.disconnect();
|
|
2878
|
-
context.logger.debug(`[Db] instance "${name
|
|
2884
|
+
context.logger.debug?.(`[Db] instance "${config2.name}" disconnected`);
|
|
2879
2885
|
});
|
|
2880
2886
|
await db.connect();
|
|
2881
|
-
context.logger.debug(`[Db] instance "${name
|
|
2887
|
+
context.logger.debug?.(`[Db] instance "${config2.name}" initialized`);
|
|
2882
2888
|
return db;
|
|
2883
2889
|
} catch (error) {
|
|
2884
2890
|
if (error instanceof ParamError) {
|
|
@@ -2888,48 +2894,11 @@ async function dbConnect(context, connectionString, name, dbProfile) {
|
|
|
2888
2894
|
throw new ParamError(`[Db] connect error: ${errorMsg}`);
|
|
2889
2895
|
}
|
|
2890
2896
|
}
|
|
2891
|
-
async function dbFindAndConnect(context, dbNameOrConnectionString) {
|
|
2892
|
-
let dbName;
|
|
2893
|
-
let dbConnectionString;
|
|
2894
|
-
let dbProfile;
|
|
2895
|
-
if (dbNameOrConnectionString) {
|
|
2896
|
-
if (dbNameOrConnectionString.match(/^(postgresql|mysql):\/\/[^\s]+:[^\s]+@[^\s]+:\d+\/[^\s]+$/)) {
|
|
2897
|
-
dbName = void 0;
|
|
2898
|
-
dbConnectionString = dbNameOrConnectionString;
|
|
2899
|
-
} else {
|
|
2900
|
-
dbName = dbNameOrConnectionString;
|
|
2901
|
-
}
|
|
2902
|
-
} else {
|
|
2903
|
-
const defs2 = {
|
|
2904
|
-
dbName: "string",
|
|
2905
|
-
dbConnectionString: "string",
|
|
2906
|
-
dbProfile: "boolean default false"
|
|
2907
|
-
};
|
|
2908
|
-
const paramsConfig = context.params.getAllForModule(defs2);
|
|
2909
|
-
dbName = paramsConfig.dbName;
|
|
2910
|
-
dbConnectionString = paramsConfig.dbConnectionString;
|
|
2911
|
-
dbProfile = paramsConfig.dbProfile;
|
|
2912
|
-
}
|
|
2913
|
-
if (!dbName && !dbConnectionString) {
|
|
2914
|
-
throw new ParamError("Db: either dbName or dbConnectionString must be specified");
|
|
2915
|
-
}
|
|
2916
|
-
if (dbName) {
|
|
2917
|
-
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
2918
|
-
dbConnectionString = await context.params.get(paramName, "string");
|
|
2919
|
-
if (!dbConnectionString) {
|
|
2920
|
-
throw new ParamError(
|
|
2921
|
-
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
2922
|
-
);
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
|
-
const db = await dbConnect(context, dbConnectionString, dbName, dbProfile);
|
|
2926
|
-
return db;
|
|
2927
|
-
}
|
|
2928
|
-
async function dbInit(context, dbNameOrConnectionString) {
|
|
2929
|
-
return await dbFindAndConnect(context, dbNameOrConnectionString);
|
|
2930
|
-
}
|
|
2931
2897
|
|
|
2932
|
-
// src/
|
|
2898
|
+
// src/tasks/index.js
|
|
2899
|
+
import os3 from "os";
|
|
2900
|
+
|
|
2901
|
+
// src/utils/date-utils.js
|
|
2933
2902
|
function isTimestampFolder(folderName) {
|
|
2934
2903
|
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
|
|
2935
2904
|
if (!isoRegex.test(folderName)) {
|
|
@@ -2939,7 +2908,7 @@ function isTimestampFolder(folderName) {
|
|
|
2939
2908
|
return !isNaN(date.getTime()) && date.getTime() > 0;
|
|
2940
2909
|
}
|
|
2941
2910
|
|
|
2942
|
-
// src/utils/fs-utils.
|
|
2911
|
+
// src/utils/fs-utils.js
|
|
2943
2912
|
import fs from "fs";
|
|
2944
2913
|
import path from "path";
|
|
2945
2914
|
async function ensurePath(...pathParts) {
|
|
@@ -2963,7 +2932,7 @@ function getFileExtension(dataType) {
|
|
|
2963
2932
|
}
|
|
2964
2933
|
}
|
|
2965
2934
|
|
|
2966
|
-
// src/utils/os-utils.
|
|
2935
|
+
// src/utils/os-utils.js
|
|
2967
2936
|
import fs2 from "fs";
|
|
2968
2937
|
import path2 from "path";
|
|
2969
2938
|
import { execSync } from "child_process";
|
|
@@ -2992,7 +2961,7 @@ function getFreeDiskSpace(targetPath) {
|
|
|
2992
2961
|
}
|
|
2993
2962
|
}
|
|
2994
2963
|
|
|
2995
|
-
// src/utils/format-utils.
|
|
2964
|
+
// src/utils/format-utils.js
|
|
2996
2965
|
function bytesToHumanReadable(bytes) {
|
|
2997
2966
|
if (bytes === 0) return "0 B";
|
|
2998
2967
|
const k = 1024;
|
|
@@ -3001,7 +2970,7 @@ function bytesToHumanReadable(bytes) {
|
|
|
3001
2970
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
3002
2971
|
}
|
|
3003
2972
|
|
|
3004
|
-
// src/utils/core-utils.
|
|
2973
|
+
// src/utils/core-utils.js
|
|
3005
2974
|
function sleepMs(ms) {
|
|
3006
2975
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3007
2976
|
}
|
|
@@ -3010,8 +2979,82 @@ function toJsonColumn(value) {
|
|
|
3010
2979
|
return JSON.stringify(value);
|
|
3011
2980
|
}
|
|
3012
2981
|
|
|
3013
|
-
// src/tasks/
|
|
2982
|
+
// src/tasks/servicesRegistry.js
|
|
2983
|
+
import os from "os";
|
|
2984
|
+
|
|
2985
|
+
// src/tasks/taskUtils.js
|
|
3014
2986
|
import { randomUUID } from "crypto";
|
|
2987
|
+
|
|
2988
|
+
// src/tasks/time-matcher.js
|
|
2989
|
+
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
2990
|
+
function resolveAsterisks(field, range) {
|
|
2991
|
+
return field.includes("*") ? field.replace("*", range) : field;
|
|
2992
|
+
}
|
|
2993
|
+
function resolveRanges(field) {
|
|
2994
|
+
const regex = /(\d+)-(\d+)/;
|
|
2995
|
+
let current = field;
|
|
2996
|
+
while (true) {
|
|
2997
|
+
const match = regex.exec(current);
|
|
2998
|
+
if (!match) break;
|
|
2999
|
+
const raw = match[0];
|
|
3000
|
+
let first = Number(match[1]);
|
|
3001
|
+
let last = Number(match[2]);
|
|
3002
|
+
if (last < first) {
|
|
3003
|
+
[first, last] = [last, first];
|
|
3004
|
+
}
|
|
3005
|
+
const values = [];
|
|
3006
|
+
for (let i = first; i <= last; i += 1) {
|
|
3007
|
+
values.push(i);
|
|
3008
|
+
}
|
|
3009
|
+
current = current.replace(raw, values.join(","));
|
|
3010
|
+
}
|
|
3011
|
+
return current;
|
|
3012
|
+
}
|
|
3013
|
+
function resolveSteps(field) {
|
|
3014
|
+
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
3015
|
+
if (!match) return field;
|
|
3016
|
+
const base = match[1];
|
|
3017
|
+
const step = Number(match[2]);
|
|
3018
|
+
if (!Number.isFinite(step) || step <= 0) return field;
|
|
3019
|
+
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
3020
|
+
}
|
|
3021
|
+
function convertPattern(pattern) {
|
|
3022
|
+
const parts = pattern.trim().split(/\s+/);
|
|
3023
|
+
if (parts.length !== 6) {
|
|
3024
|
+
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
3025
|
+
}
|
|
3026
|
+
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
3027
|
+
}
|
|
3028
|
+
function fieldMatches(field, value) {
|
|
3029
|
+
const allowed = field.split(",").map((v) => Number(v));
|
|
3030
|
+
return allowed.includes(value);
|
|
3031
|
+
}
|
|
3032
|
+
function matchesParsedPattern(parsed, date) {
|
|
3033
|
+
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());
|
|
3034
|
+
}
|
|
3035
|
+
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
3036
|
+
const parsed = convertPattern(pattern);
|
|
3037
|
+
return matchesParsedPattern(parsed, date);
|
|
3038
|
+
}
|
|
3039
|
+
var MS_PER_SECOND = 1e3;
|
|
3040
|
+
var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
|
|
3041
|
+
function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
|
|
3042
|
+
const parsed = convertPattern(pattern);
|
|
3043
|
+
let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
|
|
3044
|
+
const end = t + maxSearchMs;
|
|
3045
|
+
while (t <= end) {
|
|
3046
|
+
const date = new Date(t);
|
|
3047
|
+
if (matchesParsedPattern(parsed, date)) {
|
|
3048
|
+
return date;
|
|
3049
|
+
}
|
|
3050
|
+
t += MS_PER_SECOND;
|
|
3051
|
+
}
|
|
3052
|
+
throw new Error(
|
|
3053
|
+
`nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
|
|
3054
|
+
);
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
// src/tasks/taskUtils.js
|
|
3015
3058
|
function getDb(context) {
|
|
3016
3059
|
const db = context.db;
|
|
3017
3060
|
if (!db) {
|
|
@@ -3019,89 +3062,84 @@ function getDb(context) {
|
|
|
3019
3062
|
}
|
|
3020
3063
|
return db;
|
|
3021
3064
|
}
|
|
3022
|
-
function queueToTableNames(
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3065
|
+
function queueToTableNames(queueName) {
|
|
3066
|
+
return {
|
|
3067
|
+
tasksTable: queueName,
|
|
3068
|
+
historyTable: `${queueName}_history`,
|
|
3069
|
+
registryTable: `${queueName}_services_registry`
|
|
3070
|
+
};
|
|
3071
|
+
}
|
|
3072
|
+
function defineTasksTable(t, db, tableNameForIndex) {
|
|
3073
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
3074
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
3075
|
+
t.timestamp("started_at");
|
|
3076
|
+
t.timestamp("completed_at");
|
|
3077
|
+
t.integer("priority").notNullable().defaultTo(50);
|
|
3078
|
+
t.text("schedule");
|
|
3079
|
+
t.timestamp("next_run_at").defaultTo(null);
|
|
3080
|
+
t.timestamp("past_due").defaultTo(null);
|
|
3081
|
+
t.text("name").notNullable();
|
|
3082
|
+
t.text("opid");
|
|
3083
|
+
t.json("params");
|
|
3084
|
+
t.text("service_group");
|
|
3085
|
+
t.integer("instance_number");
|
|
3086
|
+
t.text("service_name");
|
|
3087
|
+
t.text("server_name");
|
|
3088
|
+
t.text("status").notNullable().defaultTo("idle");
|
|
3089
|
+
t.timestamp("status_changed_at").defaultTo(null);
|
|
3090
|
+
t.text("progress");
|
|
3091
|
+
t.boolean("success");
|
|
3092
|
+
t.json("results");
|
|
3093
|
+
t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
|
|
3094
|
+
t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
|
|
3095
|
+
}
|
|
3096
|
+
function taskHistoryInsertFromQueueRow(row, overrides) {
|
|
3097
|
+
const { id, ...snapshot } = row;
|
|
3098
|
+
void id;
|
|
3026
3099
|
return {
|
|
3027
|
-
|
|
3028
|
-
|
|
3100
|
+
...snapshot,
|
|
3101
|
+
...overrides
|
|
3029
3102
|
};
|
|
3030
3103
|
}
|
|
3031
3104
|
async function ensureTaskTables(context, options = {}) {
|
|
3032
|
-
const
|
|
3105
|
+
const queueName = options.queueName ?? "tasks";
|
|
3033
3106
|
const recreate = options.recreate ?? false;
|
|
3034
3107
|
const db = getDb(context);
|
|
3035
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
3108
|
+
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
3036
3109
|
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
3037
3110
|
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
3111
|
+
const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
|
|
3038
3112
|
if (recreate) {
|
|
3039
3113
|
await db.schema.dropTableIfExists(historyTable);
|
|
3040
3114
|
await db.schema.dropTableIfExists(tasksTable);
|
|
3115
|
+
await db.schema.dropTableIfExists(registryTable);
|
|
3041
3116
|
}
|
|
3042
3117
|
if (needsTasks) {
|
|
3043
3118
|
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
3044
3119
|
await db.schema.createTable(tasksTable, (t) => {
|
|
3045
|
-
t
|
|
3046
|
-
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
3047
|
-
t.timestamp("started_at");
|
|
3048
|
-
t.timestamp("completed_at");
|
|
3049
|
-
t.integer("priority").notNullable().defaultTo(0);
|
|
3050
|
-
t.text("schedule");
|
|
3051
|
-
t.timestamp("past_due").defaultTo(null);
|
|
3052
|
-
t.text("target").notNullable();
|
|
3053
|
-
t.text("task").notNullable();
|
|
3054
|
-
t.json("params");
|
|
3055
|
-
t.text("opid");
|
|
3056
|
-
t.timestamp("paused_at").defaultTo(null);
|
|
3057
|
-
t.text("progress");
|
|
3058
|
-
t.boolean("success");
|
|
3059
|
-
t.json("results");
|
|
3060
|
-
});
|
|
3061
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
3062
|
-
t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
|
|
3063
|
-
t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
|
|
3064
|
-
t.index(["target", "task"], `${tasksTable}_target_task_idx`);
|
|
3065
|
-
});
|
|
3066
|
-
}
|
|
3067
|
-
const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
|
|
3068
|
-
if (!tasksHasOpid) {
|
|
3069
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
3070
|
-
t.text("opid");
|
|
3071
|
-
});
|
|
3072
|
-
}
|
|
3073
|
-
const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
|
|
3074
|
-
if (!tasksHasPausedAt) {
|
|
3075
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
3076
|
-
t.timestamp("paused_at").defaultTo(null);
|
|
3120
|
+
defineTasksTable(t, db, tasksTable);
|
|
3077
3121
|
});
|
|
3078
3122
|
}
|
|
3079
3123
|
if (needsHistory) {
|
|
3080
3124
|
await db.schema.createTable(historyTable, (t) => {
|
|
3081
|
-
t
|
|
3082
|
-
t.timestamp("created_at").notNullable();
|
|
3083
|
-
t.timestamp("started_at");
|
|
3084
|
-
t.timestamp("completed_at");
|
|
3085
|
-
t.integer("priority").notNullable().defaultTo(0);
|
|
3086
|
-
t.text("schedule");
|
|
3087
|
-
t.timestamp("past_due").defaultTo(null);
|
|
3088
|
-
t.text("target").notNullable();
|
|
3089
|
-
t.text("task").notNullable();
|
|
3090
|
-
t.json("params");
|
|
3091
|
-
t.text("opid");
|
|
3092
|
-
t.text("progress");
|
|
3093
|
-
t.boolean("success");
|
|
3094
|
-
t.json("results");
|
|
3095
|
-
});
|
|
3096
|
-
await db.schema.alterTable(historyTable, (t) => {
|
|
3097
|
-
t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
|
|
3098
|
-
t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
|
|
3125
|
+
defineTasksTable(t, db, historyTable);
|
|
3099
3126
|
});
|
|
3100
3127
|
}
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
t.text("
|
|
3128
|
+
if (needsRegistry) {
|
|
3129
|
+
await db.schema.createTable(registryTable, (t) => {
|
|
3130
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
3131
|
+
t.text("queue_name").notNullable();
|
|
3132
|
+
t.text("service_group").notNullable();
|
|
3133
|
+
t.integer("instance_number").notNullable().defaultTo(1);
|
|
3134
|
+
t.text("service_name").notNullable();
|
|
3135
|
+
t.text("server_name").notNullable();
|
|
3136
|
+
t.integer("pid");
|
|
3137
|
+
t.json("metadata");
|
|
3138
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
3139
|
+
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
3140
|
+
t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
|
|
3141
|
+
t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
|
|
3142
|
+
t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
|
|
3105
3143
|
});
|
|
3106
3144
|
}
|
|
3107
3145
|
}
|
|
@@ -3112,11 +3150,226 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
|
3112
3150
|
});
|
|
3113
3151
|
}
|
|
3114
3152
|
|
|
3115
|
-
// src/
|
|
3153
|
+
// src/tasks/servicesRegistry.js
|
|
3154
|
+
function getDb2(context) {
|
|
3155
|
+
const db = context.db;
|
|
3156
|
+
if (!db) {
|
|
3157
|
+
throw new Error("Services registry requires context.db");
|
|
3158
|
+
}
|
|
3159
|
+
return db;
|
|
3160
|
+
}
|
|
3161
|
+
var DEFAULT_GROUP_MAX_INSTANCES = {
|
|
3162
|
+
intake: 1,
|
|
3163
|
+
harvest: 1,
|
|
3164
|
+
harvester: 0,
|
|
3165
|
+
loader: 0,
|
|
3166
|
+
photos: 0,
|
|
3167
|
+
photosprocessor: 0,
|
|
3168
|
+
ingest: 0
|
|
3169
|
+
};
|
|
3170
|
+
function sanitizeNamePart(raw) {
|
|
3171
|
+
const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3172
|
+
return s.slice(0, 80) || "runner";
|
|
3173
|
+
}
|
|
3174
|
+
function resolveMaxInstances(serviceGroup, override) {
|
|
3175
|
+
if (override !== void 0 && Number.isFinite(override)) {
|
|
3176
|
+
return Math.max(0, Math.floor(Number(override)));
|
|
3177
|
+
}
|
|
3178
|
+
const g = serviceGroup.trim().toLowerCase();
|
|
3179
|
+
return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
|
|
3180
|
+
}
|
|
3181
|
+
async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
|
|
3182
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
3183
|
+
let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
|
|
3184
|
+
if (excludeRowId) {
|
|
3185
|
+
q = q.whereNot("id", excludeRowId);
|
|
3186
|
+
}
|
|
3187
|
+
const row = await q.count("id as count").first();
|
|
3188
|
+
return Number(row?.count ?? 0);
|
|
3189
|
+
}
|
|
3190
|
+
async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
|
|
3191
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
3192
|
+
const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
|
|
3193
|
+
const set = /* @__PURE__ */ new Set();
|
|
3194
|
+
for (const r of rows) {
|
|
3195
|
+
const n = Number(r.instance_number);
|
|
3196
|
+
if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
|
|
3197
|
+
}
|
|
3198
|
+
return set;
|
|
3199
|
+
}
|
|
3200
|
+
function isUniqueViolation(error) {
|
|
3201
|
+
const code = error?.code ?? error?.errno;
|
|
3202
|
+
return code === "23505" || String(error?.message || "").includes("duplicate key");
|
|
3203
|
+
}
|
|
3204
|
+
function buildMetadata(options) {
|
|
3205
|
+
const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
|
|
3206
|
+
if (options.target) {
|
|
3207
|
+
base.runnerTarget = options.target;
|
|
3208
|
+
}
|
|
3209
|
+
return toJsonColumn(Object.keys(base).length ? base : null);
|
|
3210
|
+
}
|
|
3211
|
+
function allocateInstanceNumber(occupied, explicit, maxSlots) {
|
|
3212
|
+
if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
|
|
3213
|
+
const e = Math.max(1, Math.floor(Number(explicit)));
|
|
3214
|
+
if (occupied.has(e)) {
|
|
3215
|
+
throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
|
|
3216
|
+
}
|
|
3217
|
+
if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
|
|
3218
|
+
throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
|
|
3219
|
+
}
|
|
3220
|
+
return e;
|
|
3221
|
+
}
|
|
3222
|
+
const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
|
|
3223
|
+
for (let n = 1; n <= cap; n++) {
|
|
3224
|
+
if (!occupied.has(n)) return n;
|
|
3225
|
+
}
|
|
3226
|
+
throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
|
|
3227
|
+
}
|
|
3228
|
+
function defaultServiceName(groupBase, hostBase, instanceNumber) {
|
|
3229
|
+
return `${groupBase}-${hostBase}-${instanceNumber}`;
|
|
3230
|
+
}
|
|
3231
|
+
async function registerInServicesRegistry(context, options) {
|
|
3232
|
+
const db = getDb2(context);
|
|
3233
|
+
const registryTable = queueToTableNames(options.queueName).registryTable;
|
|
3234
|
+
const serviceGroup = options.serviceGroup.trim();
|
|
3235
|
+
if (!serviceGroup) {
|
|
3236
|
+
throw new Error("registerInServicesRegistry: serviceGroup is required");
|
|
3237
|
+
}
|
|
3238
|
+
const serverName = os.hostname();
|
|
3239
|
+
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
3240
|
+
const meta = buildMetadata(options);
|
|
3241
|
+
const groupBase = sanitizeNamePart(serviceGroup);
|
|
3242
|
+
const hostBase = sanitizeNamePart(serverName);
|
|
3243
|
+
const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
|
|
3244
|
+
const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
|
|
3245
|
+
if (maxAllowed > 0 && aliveCount >= maxAllowed) {
|
|
3246
|
+
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
|
|
3247
|
+
if (options.enforceMaxInstances) {
|
|
3248
|
+
throw new Error(msg);
|
|
3249
|
+
}
|
|
3250
|
+
context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
|
|
3251
|
+
}
|
|
3252
|
+
const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
|
|
3253
|
+
const cutoff = new Date(Date.now() - options.staleMs);
|
|
3254
|
+
const MAX_ATTEMPTS = 8;
|
|
3255
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
3256
|
+
const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
|
|
3257
|
+
const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
|
|
3258
|
+
const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
|
|
3259
|
+
const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
3260
|
+
if (existing) {
|
|
3261
|
+
const lastSeen = new Date(existing.last_seen_at);
|
|
3262
|
+
const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
|
|
3263
|
+
if (isAlive) {
|
|
3264
|
+
if (options.serviceName?.trim()) {
|
|
3265
|
+
throw new Error(
|
|
3266
|
+
`[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
|
|
3267
|
+
);
|
|
3268
|
+
}
|
|
3269
|
+
context.logger.warn?.(
|
|
3270
|
+
`[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
|
|
3271
|
+
);
|
|
3272
|
+
if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
|
|
3273
|
+
throw new Error(
|
|
3274
|
+
`[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
|
|
3275
|
+
);
|
|
3276
|
+
}
|
|
3277
|
+
await new Promise((r) => setTimeout(r, 50 + attempt * 30));
|
|
3278
|
+
continue;
|
|
3279
|
+
}
|
|
3280
|
+
await db(registryTable).where({ id: existing.id }).update({
|
|
3281
|
+
server_name: serverName,
|
|
3282
|
+
pid,
|
|
3283
|
+
metadata: meta,
|
|
3284
|
+
service_group: serviceGroup,
|
|
3285
|
+
instance_number: instanceNumber,
|
|
3286
|
+
last_seen_at: db.fn.now()
|
|
3287
|
+
});
|
|
3288
|
+
const reg = {
|
|
3289
|
+
serviceName: serviceNameRaw,
|
|
3290
|
+
serviceGroup,
|
|
3291
|
+
queueName: options.queueName,
|
|
3292
|
+
target: options.target,
|
|
3293
|
+
rowId: String(existing.id),
|
|
3294
|
+
registryTable,
|
|
3295
|
+
instanceNumber
|
|
3296
|
+
};
|
|
3297
|
+
context.servicesRegistry = reg;
|
|
3298
|
+
context.runnerHeartbeat = reg;
|
|
3299
|
+
context.logger.info?.(
|
|
3300
|
+
`[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
3301
|
+
);
|
|
3302
|
+
return reg;
|
|
3303
|
+
}
|
|
3304
|
+
try {
|
|
3305
|
+
const rows = await db(registryTable).insert({
|
|
3306
|
+
queue_name: options.queueName,
|
|
3307
|
+
service_group: serviceGroup,
|
|
3308
|
+
instance_number: instanceNumber,
|
|
3309
|
+
service_name: serviceNameRaw,
|
|
3310
|
+
server_name: serverName,
|
|
3311
|
+
pid,
|
|
3312
|
+
metadata: meta,
|
|
3313
|
+
last_seen_at: db.fn.now(),
|
|
3314
|
+
created_at: db.fn.now()
|
|
3315
|
+
}).returning(["id", "service_name"]);
|
|
3316
|
+
const row = Array.isArray(rows) ? rows[0] : rows;
|
|
3317
|
+
let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
|
|
3318
|
+
if (!rowId) {
|
|
3319
|
+
const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
3320
|
+
rowId = again?.id != null ? String(again.id) : "";
|
|
3321
|
+
}
|
|
3322
|
+
if (!rowId) continue;
|
|
3323
|
+
const regNew = {
|
|
3324
|
+
serviceName: String(row?.service_name ?? serviceNameRaw),
|
|
3325
|
+
serviceGroup,
|
|
3326
|
+
queueName: options.queueName,
|
|
3327
|
+
target: options.target,
|
|
3328
|
+
rowId,
|
|
3329
|
+
registryTable,
|
|
3330
|
+
instanceNumber
|
|
3331
|
+
};
|
|
3332
|
+
context.servicesRegistry = regNew;
|
|
3333
|
+
context.runnerHeartbeat = regNew;
|
|
3334
|
+
context.logger.info?.(
|
|
3335
|
+
`[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
3336
|
+
);
|
|
3337
|
+
return regNew;
|
|
3338
|
+
} catch (error) {
|
|
3339
|
+
if (!isUniqueViolation(error)) {
|
|
3340
|
+
throw error;
|
|
3341
|
+
}
|
|
3342
|
+
context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
throw new Error(
|
|
3346
|
+
`[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
|
|
3347
|
+
);
|
|
3348
|
+
}
|
|
3349
|
+
async function touchServicesRegistry(context, registration) {
|
|
3350
|
+
const db = getDb2(context);
|
|
3351
|
+
const serverName = os.hostname();
|
|
3352
|
+
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
3353
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
3354
|
+
last_seen_at: db.fn.now(),
|
|
3355
|
+
server_name: serverName,
|
|
3356
|
+
pid
|
|
3357
|
+
});
|
|
3358
|
+
}
|
|
3359
|
+
async function unregisterServicesRegistry(context, registration) {
|
|
3360
|
+
const db = getDb2(context);
|
|
3361
|
+
await db(registration.registryTable).where({ id: registration.rowId }).delete();
|
|
3362
|
+
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
// src/tasks/taskLogs.js
|
|
3366
|
+
import path4 from "path";
|
|
3367
|
+
|
|
3368
|
+
// src/filedatabase/index.js
|
|
3116
3369
|
import fs3 from "fs";
|
|
3117
3370
|
import path3 from "path";
|
|
3118
3371
|
|
|
3119
|
-
// src/filedatabase/serializers.
|
|
3372
|
+
// src/filedatabase/serializers.js
|
|
3120
3373
|
function detectDataType(data) {
|
|
3121
3374
|
if (Array.isArray(data)) {
|
|
3122
3375
|
return "json-array";
|
|
@@ -3148,7 +3401,7 @@ function deserializeData(rawData, dataType) {
|
|
|
3148
3401
|
}
|
|
3149
3402
|
}
|
|
3150
3403
|
|
|
3151
|
-
// src/filedatabase/index.
|
|
3404
|
+
// src/filedatabase/index.js
|
|
3152
3405
|
var FileDatabase = class _FileDatabase {
|
|
3153
3406
|
basePath;
|
|
3154
3407
|
namespace;
|
|
@@ -3234,7 +3487,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3234
3487
|
if (errors.length) {
|
|
3235
3488
|
throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
|
|
3236
3489
|
}
|
|
3237
|
-
|
|
3490
|
+
const parts = [this.basePath, this.namespace];
|
|
3238
3491
|
if (this.tableName) {
|
|
3239
3492
|
parts.push(...this.tableName.split("/"));
|
|
3240
3493
|
}
|
|
@@ -3727,14 +3980,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
3727
3980
|
* Prepare the instance for read or write operations
|
|
3728
3981
|
* This discovers state and sets up internal members based on mode and current data
|
|
3729
3982
|
*/
|
|
3730
|
-
async prepare(
|
|
3983
|
+
async prepare(options) {
|
|
3984
|
+
const { write, read, version, deferInitialVersion } = options;
|
|
3731
3985
|
if (write) {
|
|
3732
3986
|
if (this.versioned) {
|
|
3733
3987
|
if (this.currentVersion === null) {
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3988
|
+
if (!deferInitialVersion) {
|
|
3989
|
+
await this.makeNewVersion();
|
|
3990
|
+
this.metadata = this.getDefaultMetadata();
|
|
3991
|
+
this.metadata.version = this.currentVersion;
|
|
3992
|
+
this.makeNewFile();
|
|
3993
|
+
}
|
|
3738
3994
|
} else {
|
|
3739
3995
|
if (!this.metadata.files.length) {
|
|
3740
3996
|
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
@@ -3844,7 +4100,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3844
4100
|
if (options.forceNewVersion && !this.versioned) {
|
|
3845
4101
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
3846
4102
|
}
|
|
3847
|
-
await this.prepare({ write: true });
|
|
4103
|
+
await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
|
|
3848
4104
|
const incomingDataType = detectDataType(data);
|
|
3849
4105
|
this.metadata.dataType = incomingDataType;
|
|
3850
4106
|
if (options.forceNewVersion) {
|
|
@@ -4138,7 +4394,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4138
4394
|
}
|
|
4139
4395
|
};
|
|
4140
4396
|
|
|
4141
|
-
// src/tasks/taskLogs.
|
|
4397
|
+
// src/tasks/taskLogs.js
|
|
4142
4398
|
function getLogsState(context) {
|
|
4143
4399
|
const holder = context;
|
|
4144
4400
|
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
@@ -4191,6 +4447,89 @@ function getLogsState(context) {
|
|
|
4191
4447
|
holder.__tasksLogsState = state;
|
|
4192
4448
|
return state;
|
|
4193
4449
|
}
|
|
4450
|
+
function ipcLogTargetKey(target) {
|
|
4451
|
+
const bp = target.basePath ?? "";
|
|
4452
|
+
const ns = target.namespace ?? "";
|
|
4453
|
+
return `${bp}::${ns}::${target.tableName}`;
|
|
4454
|
+
}
|
|
4455
|
+
function ipcFileLogsTableNameForSourceResource(source, resource) {
|
|
4456
|
+
const seg = (s) => {
|
|
4457
|
+
const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
4458
|
+
return t.length ? t : "x";
|
|
4459
|
+
};
|
|
4460
|
+
return `${seg(source)}/${seg(resource)}`;
|
|
4461
|
+
}
|
|
4462
|
+
async function readTaskIpcLogsSnapshot(context, options) {
|
|
4463
|
+
const holder = context;
|
|
4464
|
+
const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
|
|
4465
|
+
const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
|
|
4466
|
+
const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
|
|
4467
|
+
const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
|
|
4468
|
+
const fd = new FileDatabase({
|
|
4469
|
+
basePath,
|
|
4470
|
+
namespace,
|
|
4471
|
+
tableName,
|
|
4472
|
+
versioned: true,
|
|
4473
|
+
useMetadata: true,
|
|
4474
|
+
maxVersions: 30,
|
|
4475
|
+
pageSize: 2e3,
|
|
4476
|
+
logger: holder.logger
|
|
4477
|
+
});
|
|
4478
|
+
const versions = await fd.getVersions();
|
|
4479
|
+
if (versions.length === 0) {
|
|
4480
|
+
return { records: [], latestTs: null };
|
|
4481
|
+
}
|
|
4482
|
+
const latest = versions[versions.length - 1];
|
|
4483
|
+
const raw = await fd.read({ version: latest });
|
|
4484
|
+
const arr = Array.isArray(raw) ? raw : [];
|
|
4485
|
+
let filtered = arr;
|
|
4486
|
+
if (options.afterTs && String(options.afterTs).trim()) {
|
|
4487
|
+
const cut = String(options.afterTs).trim();
|
|
4488
|
+
filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
|
|
4489
|
+
}
|
|
4490
|
+
let latestTs = null;
|
|
4491
|
+
for (const r of filtered) {
|
|
4492
|
+
const ts = typeof r?.ts === "string" ? String(r.ts) : null;
|
|
4493
|
+
if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
|
|
4494
|
+
}
|
|
4495
|
+
const incremental = !!(options.afterTs && String(options.afterTs).trim());
|
|
4496
|
+
const maxReturn = incremental ? 1e4 : tail;
|
|
4497
|
+
const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
|
|
4498
|
+
return { records: sliced, latestTs };
|
|
4499
|
+
}
|
|
4500
|
+
function getLogsStateForTarget(context, target) {
|
|
4501
|
+
const holder = context;
|
|
4502
|
+
const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
|
|
4503
|
+
const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
4504
|
+
if (!enabled) return null;
|
|
4505
|
+
if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
|
|
4506
|
+
const map = holder.__tasksLogsTargetStates;
|
|
4507
|
+
const key = ipcLogTargetKey(target);
|
|
4508
|
+
if (map.has(key)) return map.get(key);
|
|
4509
|
+
const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
|
|
4510
|
+
const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
|
|
4511
|
+
const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
|
|
4512
|
+
const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
|
|
4513
|
+
const db = new FileDatabase({
|
|
4514
|
+
basePath,
|
|
4515
|
+
namespace,
|
|
4516
|
+
tableName: target.tableName,
|
|
4517
|
+
versioned: true,
|
|
4518
|
+
useMetadata: true,
|
|
4519
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4520
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4521
|
+
logger: holder.logger
|
|
4522
|
+
});
|
|
4523
|
+
const state = {
|
|
4524
|
+
db,
|
|
4525
|
+
errorDb: null,
|
|
4526
|
+
queue: Promise.resolve(),
|
|
4527
|
+
initialized: false,
|
|
4528
|
+
errorInitialized: false
|
|
4529
|
+
};
|
|
4530
|
+
map.set(key, state);
|
|
4531
|
+
return state;
|
|
4532
|
+
}
|
|
4194
4533
|
function isErrorPayload(payload) {
|
|
4195
4534
|
if (!payload) return false;
|
|
4196
4535
|
if (typeof payload === "object") {
|
|
@@ -4210,14 +4549,26 @@ function buildLogRecord(task, payload) {
|
|
|
4210
4549
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4211
4550
|
opid: task.opid ?? null,
|
|
4212
4551
|
taskId: task.id,
|
|
4213
|
-
taskName: task.
|
|
4214
|
-
target: task.
|
|
4552
|
+
taskName: task.name,
|
|
4553
|
+
target: task.service_group,
|
|
4215
4554
|
source: typeof params.source === "string" ? params.source : null,
|
|
4216
4555
|
resource: typeof params.resource === "string" ? params.resource : null,
|
|
4217
4556
|
payload
|
|
4218
4557
|
};
|
|
4219
4558
|
}
|
|
4220
|
-
function appendTaskIpcLog(context, task, payload) {
|
|
4559
|
+
function appendTaskIpcLog(context, task, payload, target) {
|
|
4560
|
+
if (target) {
|
|
4561
|
+
const state2 = getLogsStateForTarget(context, target);
|
|
4562
|
+
if (!state2?.db) return;
|
|
4563
|
+
const record2 = buildLogRecord(task, payload);
|
|
4564
|
+
state2.queue = state2.queue.then(async () => {
|
|
4565
|
+
await state2.db.write([record2], { forceNewVersion: !state2.initialized });
|
|
4566
|
+
state2.initialized = true;
|
|
4567
|
+
}).catch((error) => {
|
|
4568
|
+
context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
|
|
4569
|
+
});
|
|
4570
|
+
return;
|
|
4571
|
+
}
|
|
4221
4572
|
const state = getLogsState(context);
|
|
4222
4573
|
if (!state.db && !state.errorDb) return;
|
|
4223
4574
|
const record = buildLogRecord(task, payload);
|
|
@@ -4235,83 +4586,280 @@ function appendTaskIpcLog(context, task, payload) {
|
|
|
4235
4586
|
});
|
|
4236
4587
|
}
|
|
4237
4588
|
|
|
4238
|
-
// src/tasks/
|
|
4239
|
-
var
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
if (last < first) {
|
|
4253
|
-
[first, last] = [last, first];
|
|
4254
|
-
}
|
|
4255
|
-
const values = [];
|
|
4256
|
-
for (let i = first; i <= last; i += 1) {
|
|
4257
|
-
values.push(i);
|
|
4258
|
-
}
|
|
4259
|
-
current = current.replace(raw, values.join(","));
|
|
4260
|
-
}
|
|
4261
|
-
return current;
|
|
4262
|
-
}
|
|
4263
|
-
function resolveSteps(field) {
|
|
4264
|
-
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
4265
|
-
if (!match) return field;
|
|
4266
|
-
const base = match[1];
|
|
4267
|
-
const step = Number(match[2]);
|
|
4268
|
-
if (!Number.isFinite(step) || step <= 0) return field;
|
|
4269
|
-
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
4270
|
-
}
|
|
4271
|
-
function convertPattern(pattern) {
|
|
4272
|
-
const parts = pattern.trim().split(/\s+/);
|
|
4273
|
-
if (parts.length !== 6) {
|
|
4274
|
-
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
4275
|
-
}
|
|
4276
|
-
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
4277
|
-
}
|
|
4278
|
-
function fieldMatches(field, value) {
|
|
4279
|
-
const allowed = field.split(",").map((v) => Number(v));
|
|
4280
|
-
return allowed.includes(value);
|
|
4281
|
-
}
|
|
4282
|
-
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
4283
|
-
const parsed = convertPattern(pattern);
|
|
4284
|
-
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());
|
|
4285
|
-
}
|
|
4286
|
-
|
|
4287
|
-
// src/tasks/TaskMaster.ts
|
|
4288
|
-
var TaskMaster = class {
|
|
4289
|
-
context;
|
|
4290
|
-
task;
|
|
4589
|
+
// src/tasks/AbstractTask.js
|
|
4590
|
+
var AbstractTask = class _AbstractTask {
|
|
4591
|
+
/**
|
|
4592
|
+
* Whether `send-task` should wait for completion (and print a result
|
|
4593
|
+
* report) when no explicit `--wait` / `--noWait` flag is given. Defaults
|
|
4594
|
+
* to false; short-lived probe tasks (e.g. `ping`) override to true.
|
|
4595
|
+
*
|
|
4596
|
+
* @type {boolean}
|
|
4597
|
+
*/
|
|
4598
|
+
static defaultWaitForResult = false;
|
|
4599
|
+
/**
|
|
4600
|
+
* @param {object} context Runner context (db, logger, params, emitter...).
|
|
4601
|
+
* @param {object} task Task row as claimed from the queue.
|
|
4602
|
+
*/
|
|
4291
4603
|
constructor(context, task) {
|
|
4292
4604
|
this.context = context;
|
|
4293
4605
|
this.task = task;
|
|
4294
4606
|
}
|
|
4607
|
+
/**
|
|
4608
|
+
* Return a short reason string when the task should be deferred (e.g. "locked
|
|
4609
|
+
* by source"), or `false`/falsy when it is free to run. Default: always `false`.
|
|
4610
|
+
*
|
|
4611
|
+
* @returns {string | false | Promise<string | false>}
|
|
4612
|
+
*/
|
|
4295
4613
|
cantRunReason() {
|
|
4296
4614
|
return false;
|
|
4297
4615
|
}
|
|
4616
|
+
/**
|
|
4617
|
+
* Called by the runner when a stop has been requested. Subclasses running
|
|
4618
|
+
* long loops should flip a flag here and check it between iterations.
|
|
4619
|
+
*
|
|
4620
|
+
* @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
|
|
4621
|
+
*/
|
|
4298
4622
|
requestStop(_allowanceMs) {
|
|
4299
4623
|
}
|
|
4624
|
+
/**
|
|
4625
|
+
* Perform the task. Must be implemented by subclasses.
|
|
4626
|
+
*
|
|
4627
|
+
* @param {(progress: unknown) => Promise<void>} _reportProgress
|
|
4628
|
+
* Updates the DB `progress` column. Accepts any serializable value;
|
|
4629
|
+
* strings are stored verbatim, objects are JSON-stringified.
|
|
4630
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
4631
|
+
*/
|
|
4632
|
+
async run(_reportProgress) {
|
|
4633
|
+
throw new Error("AbstractTask.run must be implemented by subclass");
|
|
4634
|
+
}
|
|
4635
|
+
/**
|
|
4636
|
+
* Resolve a complete row payload for this task — envelope fields (queue,
|
|
4637
|
+
* priority, targeting, schedule…) plus the inner `params` blob produced by
|
|
4638
|
+
* {@link AbstractTask.resolveCustomParams}. Output shape matches
|
|
4639
|
+
* {@link enqueueTask}'s `options` argument, so the typical call is:
|
|
4640
|
+
*
|
|
4641
|
+
* const payload = await TaskClass.resolveParams(context, { name });
|
|
4642
|
+
* await enqueueTask(context, payload);
|
|
4643
|
+
*
|
|
4644
|
+
* Validation failures throw {@link ParamError} so the script aborts before
|
|
4645
|
+
* a malformed row hits the DB.
|
|
4646
|
+
*
|
|
4647
|
+
* @param {object} context
|
|
4648
|
+
* @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
|
|
4649
|
+
* Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
|
|
4650
|
+
* `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
|
|
4651
|
+
* `nextRunAt`, plus `params` (object — overlay onto inner blob).
|
|
4652
|
+
* @returns {Promise<object>}
|
|
4653
|
+
*/
|
|
4654
|
+
static async resolveParams(context, overrides = {}) {
|
|
4655
|
+
const main = _AbstractTask._resolveMainFields(context, overrides);
|
|
4656
|
+
const params = await this.resolveCustomParams(context, overrides);
|
|
4657
|
+
return { ...main, params };
|
|
4658
|
+
}
|
|
4659
|
+
/**
|
|
4660
|
+
* Resolve the inner JSON blob stored in the `params` column. Default
|
|
4661
|
+
* implementation passes through `--paramsJson` (parsed as a JSON object)
|
|
4662
|
+
* overlaid with `overrides.params` when supplied; returns `null` when
|
|
4663
|
+
* neither is provided.
|
|
4664
|
+
*
|
|
4665
|
+
* Subclasses with typed fields should override and call
|
|
4666
|
+
* {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
|
|
4667
|
+
* resolution, then validate and throw {@link ParamError} on bad input.
|
|
4668
|
+
*
|
|
4669
|
+
* @param {object} context
|
|
4670
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4671
|
+
* @returns {Promise<object|null>}
|
|
4672
|
+
*/
|
|
4673
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
4674
|
+
return _AbstractTask._defaultParamsBlob(context, overrides);
|
|
4675
|
+
}
|
|
4676
|
+
/**
|
|
4677
|
+
* Read main task envelope fields from `context.params` (CLI/env), with
|
|
4678
|
+
* any matching key on `overrides` taking precedence. Internal; called by
|
|
4679
|
+
* {@link AbstractTask.resolveParams}.
|
|
4680
|
+
*
|
|
4681
|
+
* @param {object} context
|
|
4682
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4683
|
+
* @returns {object}
|
|
4684
|
+
*/
|
|
4685
|
+
static _resolveMainFields(context, overrides = {}) {
|
|
4686
|
+
const defs2 = {
|
|
4687
|
+
queueName: "string default tasks",
|
|
4688
|
+
priority: "number default 50",
|
|
4689
|
+
serviceGroup: "string",
|
|
4690
|
+
serviceName: "string",
|
|
4691
|
+
instanceNumber: "number",
|
|
4692
|
+
serverName: "string",
|
|
4693
|
+
opid: "string",
|
|
4694
|
+
schedule: "string"
|
|
4695
|
+
};
|
|
4696
|
+
const cli = context.params.getAllForModule("task-envelope", defs2);
|
|
4697
|
+
const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
|
|
4698
|
+
if (!name) {
|
|
4699
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
4700
|
+
}
|
|
4701
|
+
let instanceNumber;
|
|
4702
|
+
const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
|
|
4703
|
+
if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
|
|
4704
|
+
const n = Number(rawInstance);
|
|
4705
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
4706
|
+
throw new ParamError("--instanceNumber must be a positive integer when set");
|
|
4707
|
+
}
|
|
4708
|
+
instanceNumber = n;
|
|
4709
|
+
} else {
|
|
4710
|
+
instanceNumber = null;
|
|
4711
|
+
}
|
|
4712
|
+
const priorityRaw = overrides.priority ?? cli.priority ?? 50;
|
|
4713
|
+
const priority = Number(priorityRaw);
|
|
4714
|
+
if (!Number.isFinite(priority)) {
|
|
4715
|
+
throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
|
|
4716
|
+
}
|
|
4717
|
+
return {
|
|
4718
|
+
name,
|
|
4719
|
+
queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
|
|
4720
|
+
priority,
|
|
4721
|
+
serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
|
|
4722
|
+
serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
|
|
4723
|
+
instanceNumber,
|
|
4724
|
+
serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
|
|
4725
|
+
opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
|
|
4726
|
+
schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
|
|
4727
|
+
nextRunAt: overrides.nextRunAt ?? null
|
|
4728
|
+
};
|
|
4729
|
+
}
|
|
4730
|
+
/**
|
|
4731
|
+
* Default inner-params resolver: parses `--paramsJson` (must be a JSON
|
|
4732
|
+
* object), then overlays `overrides.params` on top. Returns `null` when
|
|
4733
|
+
* neither is provided.
|
|
4734
|
+
*
|
|
4735
|
+
* @param {object} context
|
|
4736
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4737
|
+
* @returns {object|null}
|
|
4738
|
+
*/
|
|
4739
|
+
static _defaultParamsBlob(context, overrides = {}) {
|
|
4740
|
+
const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
|
|
4741
|
+
const fromJson = parseParamsJson(cli.paramsJson);
|
|
4742
|
+
const fromOverride = pickParamsObject(overrides);
|
|
4743
|
+
if (!fromJson && !fromOverride) return null;
|
|
4744
|
+
return { ...fromJson ?? {}, ...fromOverride ?? {} };
|
|
4745
|
+
}
|
|
4746
|
+
/**
|
|
4747
|
+
* Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
|
|
4748
|
+
* params (per `defs`) plus `--paramsJson` under a module namespace, then
|
|
4749
|
+
* merges them with explicit `overrides.params` in increasing priority:
|
|
4750
|
+
*
|
|
4751
|
+
* typed CLI flags → --paramsJson → overrides.params
|
|
4752
|
+
*
|
|
4753
|
+
* Undefined values are dropped so defaults declared in `defs` aren't
|
|
4754
|
+
* overwritten by missing-flag noise. Returns the merged object; the
|
|
4755
|
+
* caller is responsible for validation and throwing `ParamError`.
|
|
4756
|
+
*
|
|
4757
|
+
* @param {object} context
|
|
4758
|
+
* @param {string} moduleName Namespace for `--showUsedParams` grouping.
|
|
4759
|
+
* @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
|
|
4760
|
+
* @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
|
|
4761
|
+
* @returns {Record<string, unknown>}
|
|
4762
|
+
*/
|
|
4763
|
+
static _mergeTypedParams(context, moduleName, defs2, overrides = {}) {
|
|
4764
|
+
const fullDefs = { ...defs2, paramsJson: "string" };
|
|
4765
|
+
const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
|
|
4766
|
+
const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
|
|
4767
|
+
const fromCli = {};
|
|
4768
|
+
for (const [k, v] of Object.entries(cliRaw)) {
|
|
4769
|
+
if (k === "paramsJson") continue;
|
|
4770
|
+
if (v !== void 0 && v !== null) fromCli[k] = v;
|
|
4771
|
+
}
|
|
4772
|
+
const fromOverride = pickParamsObject(overrides) ?? {};
|
|
4773
|
+
return { ...fromCli, ...fromJson, ...fromOverride };
|
|
4774
|
+
}
|
|
4300
4775
|
};
|
|
4776
|
+
function emptyToUndef(s) {
|
|
4777
|
+
if (s === void 0 || s === null) return void 0;
|
|
4778
|
+
if (typeof s !== "string") return s;
|
|
4779
|
+
const t = s.trim();
|
|
4780
|
+
return t.length ? t : void 0;
|
|
4781
|
+
}
|
|
4782
|
+
function parseParamsJson(raw) {
|
|
4783
|
+
if (raw == null) return null;
|
|
4784
|
+
const t = String(raw).trim();
|
|
4785
|
+
if (!t) return null;
|
|
4786
|
+
let parsed;
|
|
4787
|
+
try {
|
|
4788
|
+
parsed = JSON.parse(t);
|
|
4789
|
+
} catch (e) {
|
|
4790
|
+
throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
|
|
4791
|
+
}
|
|
4792
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4793
|
+
throw new ParamError("--paramsJson must be a JSON object");
|
|
4794
|
+
}
|
|
4795
|
+
return parsed;
|
|
4796
|
+
}
|
|
4797
|
+
function pickParamsObject(overrides) {
|
|
4798
|
+
const p = overrides?.params;
|
|
4799
|
+
if (p && typeof p === "object" && !Array.isArray(p)) return p;
|
|
4800
|
+
return void 0;
|
|
4801
|
+
}
|
|
4301
4802
|
|
|
4302
|
-
// src/tasks/coreTasks/TaskPing.
|
|
4303
|
-
var TaskPing = class extends
|
|
4803
|
+
// src/tasks/coreTasks/TaskPing.js
|
|
4804
|
+
var TaskPing = class extends AbstractTask {
|
|
4805
|
+
/** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
|
|
4806
|
+
static defaultWaitForResult = true;
|
|
4807
|
+
/** Ping takes no params. */
|
|
4808
|
+
static async resolveCustomParams() {
|
|
4809
|
+
return null;
|
|
4810
|
+
}
|
|
4811
|
+
/**
|
|
4812
|
+
* @returns {Promise<{ success: true, results: "pong" }>}
|
|
4813
|
+
*/
|
|
4304
4814
|
async run() {
|
|
4305
4815
|
this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
|
|
4306
4816
|
return { success: true, results: "pong" };
|
|
4307
4817
|
}
|
|
4308
4818
|
};
|
|
4309
4819
|
|
|
4310
|
-
// src/tasks/coreTasks/TaskSampleProcess.
|
|
4311
|
-
var TaskSampleProcess = class extends
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4820
|
+
// src/tasks/coreTasks/TaskSampleProcess.js
|
|
4821
|
+
var TaskSampleProcess = class extends AbstractTask {
|
|
4822
|
+
/**
|
|
4823
|
+
* @param {object} context
|
|
4824
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4825
|
+
* @returns {Promise<{ total: number, delay: number, name?: string }>}
|
|
4826
|
+
*/
|
|
4827
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
4828
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
|
|
4829
|
+
total: "number default 10",
|
|
4830
|
+
delay: "number default 1000",
|
|
4831
|
+
name: "string"
|
|
4832
|
+
}, overrides);
|
|
4833
|
+
const total = Number(merged.total);
|
|
4834
|
+
const delay = Number(merged.delay);
|
|
4835
|
+
if (!Number.isInteger(total) || total <= 0) {
|
|
4836
|
+
throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
|
|
4837
|
+
}
|
|
4838
|
+
if (!Number.isInteger(delay) || delay < 0) {
|
|
4839
|
+
throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
|
|
4840
|
+
}
|
|
4841
|
+
const out = { total, delay };
|
|
4842
|
+
if (typeof merged.name === "string" && merged.name.trim()) {
|
|
4843
|
+
out.name = merged.name.trim();
|
|
4844
|
+
}
|
|
4845
|
+
return out;
|
|
4846
|
+
}
|
|
4847
|
+
/**
|
|
4848
|
+
* @param {object} context
|
|
4849
|
+
* @param {object} task
|
|
4850
|
+
*/
|
|
4851
|
+
constructor(context, task) {
|
|
4852
|
+
super(context, task);
|
|
4853
|
+
this.stopRequested = false;
|
|
4854
|
+
this.stopAllowanceMs = 0;
|
|
4855
|
+
this.stopDecisionLogged = false;
|
|
4856
|
+
}
|
|
4857
|
+
/**
|
|
4858
|
+
* Runner-facing stop signal. Records the allowance window so the main loop
|
|
4859
|
+
* can decide per-iteration whether to finish or abort early.
|
|
4860
|
+
*
|
|
4861
|
+
* @param {number} allowanceMs
|
|
4862
|
+
*/
|
|
4315
4863
|
requestStop(allowanceMs) {
|
|
4316
4864
|
this.stopRequested = true;
|
|
4317
4865
|
this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
|
|
@@ -4319,6 +4867,14 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4319
4867
|
`[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
|
|
4320
4868
|
);
|
|
4321
4869
|
}
|
|
4870
|
+
/**
|
|
4871
|
+
* Iterate `total` times, sleeping `delay` ms between ticks and reporting
|
|
4872
|
+
* progress every iteration. Validates params up front; invalid values short-
|
|
4873
|
+
* circuit to a structured failure without starting the loop.
|
|
4874
|
+
*
|
|
4875
|
+
* @param {(progress: object) => Promise<void>} reportProgress
|
|
4876
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
4877
|
+
*/
|
|
4322
4878
|
async run(reportProgress) {
|
|
4323
4879
|
const totalRaw = this.task?.params?.total ?? 10;
|
|
4324
4880
|
const delayRaw = this.task?.params?.delay ?? 1e3;
|
|
@@ -4400,7 +4956,7 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4400
4956
|
}
|
|
4401
4957
|
};
|
|
4402
4958
|
|
|
4403
|
-
// src/tasks/coreTasks/TaskShellCommand.
|
|
4959
|
+
// src/tasks/coreTasks/TaskShellCommand.js
|
|
4404
4960
|
import { spawn } from "child_process";
|
|
4405
4961
|
function runShellCommand(command, cwd) {
|
|
4406
4962
|
return new Promise((resolve2, reject) => {
|
|
@@ -4430,7 +4986,27 @@ function runShellCommand(command, cwd) {
|
|
|
4430
4986
|
});
|
|
4431
4987
|
});
|
|
4432
4988
|
}
|
|
4433
|
-
var TaskShellCommand = class extends
|
|
4989
|
+
var TaskShellCommand = class extends AbstractTask {
|
|
4990
|
+
/**
|
|
4991
|
+
* @param {object} context
|
|
4992
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4993
|
+
* @returns {Promise<{ command: string, cwd?: string }>}
|
|
4994
|
+
*/
|
|
4995
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
4996
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
|
|
4997
|
+
command: "string",
|
|
4998
|
+
cwd: "string"
|
|
4999
|
+
}, overrides);
|
|
5000
|
+
const command = typeof merged.command === "string" ? merged.command.trim() : "";
|
|
5001
|
+
if (!command) {
|
|
5002
|
+
throw new ParamError('shellCommand: param "command" must be a non-empty string');
|
|
5003
|
+
}
|
|
5004
|
+
const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
|
|
5005
|
+
return cwd ? { command, cwd } : { command };
|
|
5006
|
+
}
|
|
5007
|
+
/**
|
|
5008
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5009
|
+
*/
|
|
4434
5010
|
async run() {
|
|
4435
5011
|
const params = this.task?.params;
|
|
4436
5012
|
const commandRaw = typeof params === "string" ? params : params?.command;
|
|
@@ -4479,8 +5055,8 @@ var TaskShellCommand = class extends TaskMaster {
|
|
|
4479
5055
|
}
|
|
4480
5056
|
};
|
|
4481
5057
|
|
|
4482
|
-
// src/tasks/coreTasks/TaskSystemInfo.
|
|
4483
|
-
import
|
|
5058
|
+
// src/tasks/coreTasks/TaskSystemInfo.js
|
|
5059
|
+
import os2 from "os";
|
|
4484
5060
|
import fs4 from "fs/promises";
|
|
4485
5061
|
function toGb(valueBytes) {
|
|
4486
5062
|
return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
|
|
@@ -4499,13 +5075,22 @@ async function getDiskStats() {
|
|
|
4499
5075
|
free: toGb(free)
|
|
4500
5076
|
};
|
|
4501
5077
|
}
|
|
4502
|
-
var TaskSystemInfo = class extends
|
|
5078
|
+
var TaskSystemInfo = class extends AbstractTask {
|
|
5079
|
+
/** Same UX expectation as `ping` — short probe, print the result. */
|
|
5080
|
+
static defaultWaitForResult = true;
|
|
5081
|
+
/** systemInfo takes no params. */
|
|
5082
|
+
static async resolveCustomParams() {
|
|
5083
|
+
return null;
|
|
5084
|
+
}
|
|
5085
|
+
/**
|
|
5086
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5087
|
+
*/
|
|
4503
5088
|
async run() {
|
|
4504
5089
|
try {
|
|
4505
|
-
const totalMemory =
|
|
4506
|
-
const freeMemory =
|
|
5090
|
+
const totalMemory = os2.totalmem();
|
|
5091
|
+
const freeMemory = os2.freemem();
|
|
4507
5092
|
const usedMemory = totalMemory - freeMemory;
|
|
4508
|
-
const cpus =
|
|
5093
|
+
const cpus = os2.cpus();
|
|
4509
5094
|
const cpuUtilization = cpus.map((cpu) => {
|
|
4510
5095
|
const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
|
|
4511
5096
|
const usage = (total - cpu.times.idle) / total * 100;
|
|
@@ -4531,10 +5116,10 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
4531
5116
|
utilization: cpuUtilization
|
|
4532
5117
|
},
|
|
4533
5118
|
runtime: {
|
|
4534
|
-
platform:
|
|
4535
|
-
arch:
|
|
4536
|
-
uptimeSec:
|
|
4537
|
-
hostname:
|
|
5119
|
+
platform: os2.platform(),
|
|
5120
|
+
arch: os2.arch(),
|
|
5121
|
+
uptimeSec: os2.uptime(),
|
|
5122
|
+
hostname: os2.hostname()
|
|
4538
5123
|
}
|
|
4539
5124
|
};
|
|
4540
5125
|
this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
|
|
@@ -4551,8 +5136,31 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
4551
5136
|
}
|
|
4552
5137
|
};
|
|
4553
5138
|
|
|
4554
|
-
// src/tasks/coreTasks/TaskSumAB.
|
|
4555
|
-
var TaskSumAB = class extends
|
|
5139
|
+
// src/tasks/coreTasks/TaskSumAB.js
|
|
5140
|
+
var TaskSumAB = class extends AbstractTask {
|
|
5141
|
+
/** Short, deterministic — wait by default so callers see the sum. */
|
|
5142
|
+
static defaultWaitForResult = true;
|
|
5143
|
+
/**
|
|
5144
|
+
* @param {object} context
|
|
5145
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5146
|
+
* @returns {Promise<{ a: number, b: number }>}
|
|
5147
|
+
*/
|
|
5148
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5149
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
|
|
5150
|
+
a: "number",
|
|
5151
|
+
b: "number"
|
|
5152
|
+
}, overrides);
|
|
5153
|
+
if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
|
|
5154
|
+
throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
|
|
5155
|
+
}
|
|
5156
|
+
if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
|
|
5157
|
+
throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
|
|
5158
|
+
}
|
|
5159
|
+
return { a: merged.a, b: merged.b };
|
|
5160
|
+
}
|
|
5161
|
+
/**
|
|
5162
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5163
|
+
*/
|
|
4556
5164
|
async run() {
|
|
4557
5165
|
const a = this.task?.params?.a;
|
|
4558
5166
|
const b = this.task?.params?.b;
|
|
@@ -4583,8 +5191,46 @@ var TaskSumAB = class extends TaskMaster {
|
|
|
4583
5191
|
}
|
|
4584
5192
|
};
|
|
4585
5193
|
|
|
4586
|
-
// src/tasks/coreTasks/TaskStopRunner.
|
|
4587
|
-
var TaskStopRunner = class extends
|
|
5194
|
+
// src/tasks/coreTasks/TaskStopRunner.js
|
|
5195
|
+
var TaskStopRunner = class extends AbstractTask {
|
|
5196
|
+
/**
|
|
5197
|
+
* Stop tasks must target a concrete instance — without `serviceName` the
|
|
5198
|
+
* row would race against any worker on the queue. Layered on top of the
|
|
5199
|
+
* envelope built by {@link AbstractTask.resolveParams}.
|
|
5200
|
+
*
|
|
5201
|
+
* @param {object} context
|
|
5202
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5203
|
+
* @returns {Promise<object>}
|
|
5204
|
+
*/
|
|
5205
|
+
static async resolveParams(context, overrides = {}) {
|
|
5206
|
+
const main = await super.resolveParams(context, overrides);
|
|
5207
|
+
if (!main.serviceName) {
|
|
5208
|
+
throw new ParamError(
|
|
5209
|
+
"stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
|
|
5210
|
+
);
|
|
5211
|
+
}
|
|
5212
|
+
return main;
|
|
5213
|
+
}
|
|
5214
|
+
/**
|
|
5215
|
+
* @param {object} context
|
|
5216
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5217
|
+
* @returns {Promise<{ allowanceMs: number }>}
|
|
5218
|
+
*/
|
|
5219
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5220
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
|
|
5221
|
+
allowanceMs: "number default 5000"
|
|
5222
|
+
}, overrides);
|
|
5223
|
+
const allowanceMs = Number(merged.allowanceMs);
|
|
5224
|
+
if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
|
|
5225
|
+
throw new ParamError(
|
|
5226
|
+
`stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
|
|
5227
|
+
);
|
|
5228
|
+
}
|
|
5229
|
+
return { allowanceMs };
|
|
5230
|
+
}
|
|
5231
|
+
/**
|
|
5232
|
+
* @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
|
|
5233
|
+
*/
|
|
4588
5234
|
async run() {
|
|
4589
5235
|
const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
|
|
4590
5236
|
this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
|
|
@@ -4599,45 +5245,203 @@ var TaskStopRunner = class extends TaskMaster {
|
|
|
4599
5245
|
}
|
|
4600
5246
|
};
|
|
4601
5247
|
|
|
4602
|
-
// src/tasks/
|
|
5248
|
+
// src/tasks/coreTasks/TaskGetLogs.js
|
|
5249
|
+
var TaskGetLogs = class extends AbstractTask {
|
|
5250
|
+
/**
|
|
5251
|
+
* @param {object} context
|
|
5252
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5253
|
+
* @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
|
|
5254
|
+
*/
|
|
5255
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5256
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
|
|
5257
|
+
source: "string",
|
|
5258
|
+
resource: "string",
|
|
5259
|
+
tail: "number default 100",
|
|
5260
|
+
afterTs: "string"
|
|
5261
|
+
}, overrides);
|
|
5262
|
+
const source = typeof merged.source === "string" ? merged.source.trim() : "";
|
|
5263
|
+
const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
|
|
5264
|
+
if (!source) throw new ParamError('getLogs: param "source" is required');
|
|
5265
|
+
if (!resource) throw new ParamError('getLogs: param "resource" is required');
|
|
5266
|
+
let tail = Number(merged.tail);
|
|
5267
|
+
if (!Number.isFinite(tail) || tail < 1) tail = 100;
|
|
5268
|
+
tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
|
|
5269
|
+
const out = { source, resource, tail };
|
|
5270
|
+
if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
|
|
5271
|
+
out.afterTs = merged.afterTs.trim();
|
|
5272
|
+
}
|
|
5273
|
+
return out;
|
|
5274
|
+
}
|
|
5275
|
+
/**
|
|
5276
|
+
* @param {unknown} _reportProgress Unused (single-shot read, no progress events).
|
|
5277
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5278
|
+
*/
|
|
5279
|
+
async run(_reportProgress) {
|
|
5280
|
+
const p = this.task.params ?? {};
|
|
5281
|
+
const source = String(p.source ?? "").trim();
|
|
5282
|
+
const resource = String(p.resource ?? "").trim();
|
|
5283
|
+
const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
|
|
5284
|
+
const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
|
|
5285
|
+
if (!source || !resource) {
|
|
5286
|
+
return {
|
|
5287
|
+
success: false,
|
|
5288
|
+
results: { error: 'getLogs requires params "source" and "resource"' }
|
|
5289
|
+
};
|
|
5290
|
+
}
|
|
5291
|
+
try {
|
|
5292
|
+
const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
|
|
5293
|
+
source,
|
|
5294
|
+
resource,
|
|
5295
|
+
tail,
|
|
5296
|
+
afterTs
|
|
5297
|
+
});
|
|
5298
|
+
return {
|
|
5299
|
+
success: true,
|
|
5300
|
+
results: { records, latestTs, source, resource }
|
|
5301
|
+
};
|
|
5302
|
+
} catch (e) {
|
|
5303
|
+
return {
|
|
5304
|
+
success: false,
|
|
5305
|
+
results: { error: e?.message ?? String(e) }
|
|
5306
|
+
};
|
|
5307
|
+
}
|
|
5308
|
+
}
|
|
5309
|
+
};
|
|
5310
|
+
|
|
5311
|
+
// src/tasks/TasksRegistry.js
|
|
4603
5312
|
var TasksRegistry = class _TasksRegistry {
|
|
4604
|
-
|
|
5313
|
+
/**
|
|
5314
|
+
* @param {Record<string, Function>} [initial] Optional seed entries to copy in.
|
|
5315
|
+
*/
|
|
4605
5316
|
constructor(initial) {
|
|
5317
|
+
this.map = {};
|
|
4606
5318
|
if (initial) {
|
|
4607
5319
|
this.addMany(initial);
|
|
4608
5320
|
}
|
|
4609
5321
|
}
|
|
5322
|
+
/**
|
|
5323
|
+
* Build a registry pre-populated with every core task plus legacy aliases.
|
|
5324
|
+
* Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
|
|
5325
|
+
*
|
|
5326
|
+
* @returns {TasksRegistry}
|
|
5327
|
+
*/
|
|
4610
5328
|
static withCoreTasks() {
|
|
4611
|
-
return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
|
|
5329
|
+
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);
|
|
4612
5330
|
}
|
|
5331
|
+
/**
|
|
5332
|
+
* Register a single task class under a name. Overwrites any previous entry.
|
|
5333
|
+
*
|
|
5334
|
+
* @param {string} taskName
|
|
5335
|
+
* @param {Function} taskClass Subclass of `AbstractTask`.
|
|
5336
|
+
* @returns {this}
|
|
5337
|
+
*/
|
|
4613
5338
|
add(taskName, taskClass) {
|
|
4614
5339
|
this.map[taskName] = taskClass;
|
|
4615
5340
|
return this;
|
|
4616
5341
|
}
|
|
5342
|
+
/**
|
|
5343
|
+
* Bulk-register a name → class map. Later calls override earlier ones.
|
|
5344
|
+
*
|
|
5345
|
+
* @param {Record<string, Function>} entries
|
|
5346
|
+
* @returns {this}
|
|
5347
|
+
*/
|
|
4617
5348
|
addMany(entries) {
|
|
4618
5349
|
for (const [name, klass] of Object.entries(entries)) {
|
|
4619
5350
|
this.add(name, klass);
|
|
4620
5351
|
}
|
|
4621
5352
|
return this;
|
|
4622
5353
|
}
|
|
5354
|
+
/**
|
|
5355
|
+
* Look up a task class by name. Returns `undefined` when the name is unknown;
|
|
5356
|
+
* the runner treats that as "some other worker may handle this" and skips.
|
|
5357
|
+
*
|
|
5358
|
+
* @param {string} taskName
|
|
5359
|
+
* @returns {Function | undefined}
|
|
5360
|
+
*/
|
|
4623
5361
|
get(taskName) {
|
|
4624
5362
|
return this.map[taskName];
|
|
4625
5363
|
}
|
|
5364
|
+
/**
|
|
5365
|
+
* Strict variant of {@link get}: throws {@link ParamError} (with the list
|
|
5366
|
+
* of supported names) when `taskName` is unknown. Use from enqueuer code
|
|
5367
|
+
* paths where an unknown name is a hard CLI/programmer error.
|
|
5368
|
+
*
|
|
5369
|
+
* @param {string} taskName
|
|
5370
|
+
* @returns {Function}
|
|
5371
|
+
*/
|
|
5372
|
+
requireClass(taskName) {
|
|
5373
|
+
const TaskClass = taskName ? this.map[taskName] : void 0;
|
|
5374
|
+
if (!TaskClass) {
|
|
5375
|
+
const supported = this.listSupportedTasks().join(", ") || "(none)";
|
|
5376
|
+
throw new ParamError(
|
|
5377
|
+
`Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
|
|
5378
|
+
);
|
|
5379
|
+
}
|
|
5380
|
+
return TaskClass;
|
|
5381
|
+
}
|
|
5382
|
+
/**
|
|
5383
|
+
* Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
|
|
5384
|
+
* from `overrides` or `context.params`, look up the class, and delegate
|
|
5385
|
+
* to its static {@link AbstractTask.resolveParams} with `name` seeded into
|
|
5386
|
+
* the overrides. The returned object is shaped for {@link enqueueTask}.
|
|
5387
|
+
*
|
|
5388
|
+
* Validation failures (unknown task, missing required custom params, etc.)
|
|
5389
|
+
* surface as {@link ParamError} so the caller aborts cleanly before any
|
|
5390
|
+
* row is inserted.
|
|
5391
|
+
*
|
|
5392
|
+
* @param {object} context
|
|
5393
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5394
|
+
* @returns {Promise<object>}
|
|
5395
|
+
*/
|
|
5396
|
+
async resolveTaskParams(context, overrides = {}) {
|
|
5397
|
+
const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
|
|
5398
|
+
const fromCli = context.params.get("name", "string");
|
|
5399
|
+
const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
|
|
5400
|
+
const name = overrideName || cliName;
|
|
5401
|
+
if (!name) {
|
|
5402
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
5403
|
+
}
|
|
5404
|
+
const TaskClass = this.requireClass(name);
|
|
5405
|
+
return TaskClass.resolveParams(context, { ...overrides, name });
|
|
5406
|
+
}
|
|
5407
|
+
/**
|
|
5408
|
+
* Names of every registered task, sorted alphabetically (useful for CLI output
|
|
5409
|
+
* and allowlist sanity checks).
|
|
5410
|
+
*
|
|
5411
|
+
* @returns {string[]}
|
|
5412
|
+
*/
|
|
4626
5413
|
listSupportedTasks() {
|
|
4627
5414
|
return Object.keys(this.map).sort();
|
|
4628
5415
|
}
|
|
5416
|
+
/**
|
|
5417
|
+
* Shallow copy of the internal map, for handing to `addMany` on another registry
|
|
5418
|
+
* or for serialization.
|
|
5419
|
+
*
|
|
5420
|
+
* @returns {Record<string, Function>}
|
|
5421
|
+
*/
|
|
4629
5422
|
toObject() {
|
|
4630
5423
|
return { ...this.map };
|
|
4631
5424
|
}
|
|
4632
5425
|
};
|
|
4633
5426
|
|
|
4634
|
-
// src/tasks/
|
|
5427
|
+
// src/tasks/serviceTaskAllowlist.js
|
|
5428
|
+
function normalizeAllowedTasks(value) {
|
|
5429
|
+
if (!value) return void 0;
|
|
5430
|
+
if (Array.isArray(value)) {
|
|
5431
|
+
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
5432
|
+
return out2.length ? out2 : void 0;
|
|
5433
|
+
}
|
|
5434
|
+
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
5435
|
+
return out.length ? out : void 0;
|
|
5436
|
+
}
|
|
5437
|
+
|
|
5438
|
+
// src/tasks/taskScriptRunner.js
|
|
4635
5439
|
import { spawn as spawn2 } from "child_process";
|
|
4636
5440
|
|
|
4637
|
-
// src/tasks/index.
|
|
5441
|
+
// src/tasks/index.js
|
|
4638
5442
|
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
4639
5443
|
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
4640
|
-
function
|
|
5444
|
+
function getDb3(context) {
|
|
4641
5445
|
const db = context.db;
|
|
4642
5446
|
if (!db) {
|
|
4643
5447
|
throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
|
|
@@ -4649,15 +5453,6 @@ function normalizeRegistry(registry) {
|
|
|
4649
5453
|
if (registry instanceof TasksRegistry) return registry;
|
|
4650
5454
|
return new TasksRegistry().addMany(registry);
|
|
4651
5455
|
}
|
|
4652
|
-
function normalizeAllowedTasks(value) {
|
|
4653
|
-
if (!value) return void 0;
|
|
4654
|
-
if (Array.isArray(value)) {
|
|
4655
|
-
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
4656
|
-
return out2.length ? out2 : void 0;
|
|
4657
|
-
}
|
|
4658
|
-
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
4659
|
-
return out.length ? out : void 0;
|
|
4660
|
-
}
|
|
4661
5456
|
async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
|
|
4662
5457
|
context.logger.warn?.(`[tasks] signaling ${runningTaskInstances.size} running task(s) to stop`);
|
|
4663
5458
|
for (const [, taskInstance] of runningTaskInstances) {
|
|
@@ -4672,19 +5467,21 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
|
|
|
4672
5467
|
context.emitter.emit("stop", allowanceMs);
|
|
4673
5468
|
}
|
|
4674
5469
|
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
4675
|
-
const db =
|
|
4676
|
-
const taskName = row.
|
|
5470
|
+
const db = getDb3(context);
|
|
5471
|
+
const taskName = row.name;
|
|
4677
5472
|
const TaskClass = registry.get(taskName);
|
|
4678
|
-
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
4679
5473
|
if (!TaskClass) {
|
|
4680
5474
|
const err = { message: `Unknown task "${taskName}"` };
|
|
4681
|
-
await db(historyTable).insert(
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
5475
|
+
await db(historyTable).insert(
|
|
5476
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
5477
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5478
|
+
success: false,
|
|
5479
|
+
status: "failed",
|
|
5480
|
+
status_changed_at: db.fn.now(),
|
|
5481
|
+
params: toJsonColumn(row.params),
|
|
5482
|
+
results: toJsonColumn(err)
|
|
5483
|
+
})
|
|
5484
|
+
);
|
|
4688
5485
|
if (row.schedule) {
|
|
4689
5486
|
await db(tasksTable).where({ id: row.id }).update({
|
|
4690
5487
|
started_at: null,
|
|
@@ -4692,7 +5489,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4692
5489
|
success: false,
|
|
4693
5490
|
results: toJsonColumn(err),
|
|
4694
5491
|
past_due: null,
|
|
4695
|
-
|
|
5492
|
+
status: "paused",
|
|
5493
|
+
status_changed_at: db.fn.now(),
|
|
4696
5494
|
progress: LOCKED_BY_ERROR_MESSAGE
|
|
4697
5495
|
});
|
|
4698
5496
|
} else {
|
|
@@ -4719,13 +5517,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4719
5517
|
} finally {
|
|
4720
5518
|
runningTaskInstances.delete(row.id);
|
|
4721
5519
|
}
|
|
4722
|
-
await db(historyTable).insert(
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
5520
|
+
await db(historyTable).insert(
|
|
5521
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
5522
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5523
|
+
success,
|
|
5524
|
+
status: success ? "completed" : "failed",
|
|
5525
|
+
status_changed_at: db.fn.now(),
|
|
5526
|
+
params: toJsonColumn(row.params),
|
|
5527
|
+
results: toJsonColumn(results)
|
|
5528
|
+
})
|
|
5529
|
+
);
|
|
4729
5530
|
if (!success) {
|
|
4730
5531
|
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
4731
5532
|
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
@@ -4740,19 +5541,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4740
5541
|
const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
|
|
4741
5542
|
appendTaskIpcLog(context, row, {
|
|
4742
5543
|
level: "error",
|
|
4743
|
-
message: `[tasks] task failed: ${row.
|
|
5544
|
+
message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
|
|
4744
5545
|
details: results
|
|
4745
5546
|
});
|
|
4746
5547
|
}
|
|
4747
5548
|
if (row.schedule) {
|
|
4748
5549
|
if (success) {
|
|
5550
|
+
let nextRunAt = null;
|
|
5551
|
+
try {
|
|
5552
|
+
nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
|
|
5553
|
+
} catch (e) {
|
|
5554
|
+
context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
|
|
5555
|
+
}
|
|
4749
5556
|
await db(tasksTable).where({ id: row.id }).update({
|
|
4750
5557
|
started_at: null,
|
|
4751
5558
|
completed_at: /* @__PURE__ */ new Date(),
|
|
4752
5559
|
success,
|
|
4753
5560
|
results: toJsonColumn(results),
|
|
4754
5561
|
progress: null,
|
|
4755
|
-
past_due: null
|
|
5562
|
+
past_due: null,
|
|
5563
|
+
status: "idle",
|
|
5564
|
+
status_changed_at: db.fn.now(),
|
|
5565
|
+
next_run_at: nextRunAt,
|
|
5566
|
+
// Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
|
|
5567
|
+
service_name: null,
|
|
5568
|
+
server_name: null,
|
|
5569
|
+
instance_number: null
|
|
4756
5570
|
});
|
|
4757
5571
|
} else {
|
|
4758
5572
|
await db(tasksTable).where({ id: row.id }).update({
|
|
@@ -4760,7 +5574,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4760
5574
|
completed_at: /* @__PURE__ */ new Date(),
|
|
4761
5575
|
success,
|
|
4762
5576
|
results: toJsonColumn(results),
|
|
4763
|
-
|
|
5577
|
+
status: "paused",
|
|
5578
|
+
status_changed_at: db.fn.now(),
|
|
4764
5579
|
progress: LOCKED_BY_ERROR_MESSAGE,
|
|
4765
5580
|
past_due: null
|
|
4766
5581
|
});
|
|
@@ -4772,195 +5587,361 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4772
5587
|
const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
|
|
4773
5588
|
return { stopRunnerRequested, stopAllowanceMs };
|
|
4774
5589
|
}
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
5590
|
+
function shuffleTaskRowsInPlace(rows) {
|
|
5591
|
+
for (let i = rows.length - 1; i > 0; i--) {
|
|
5592
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
5593
|
+
const t = rows[i];
|
|
5594
|
+
rows[i] = rows[j];
|
|
5595
|
+
rows[j] = t;
|
|
5596
|
+
}
|
|
5597
|
+
}
|
|
5598
|
+
async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
|
|
5599
|
+
const db = getDb3(context);
|
|
5600
|
+
let query = db(tasksTable).where({ status: "idle" }).where(function() {
|
|
5601
|
+
this.whereNull("service_group").orWhere({ service_group: serviceGroup });
|
|
5602
|
+
}).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);
|
|
4778
5603
|
if (taskNames && taskNames.length > 0) {
|
|
4779
|
-
query = query.whereIn("
|
|
5604
|
+
query = query.whereIn("name", taskNames);
|
|
5605
|
+
}
|
|
5606
|
+
if (runnerIdentity) {
|
|
5607
|
+
query = query.where(function() {
|
|
5608
|
+
this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
|
|
5609
|
+
}).where(function() {
|
|
5610
|
+
this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
|
|
5611
|
+
}).where(function() {
|
|
5612
|
+
this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
|
|
5613
|
+
});
|
|
5614
|
+
} else {
|
|
5615
|
+
query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
|
|
4780
5616
|
}
|
|
4781
5617
|
const candidates = await query;
|
|
5618
|
+
shuffleTaskRowsInPlace(candidates);
|
|
4782
5619
|
for (const row of candidates) {
|
|
4783
5620
|
if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
|
|
4784
5621
|
continue;
|
|
4785
5622
|
}
|
|
4786
|
-
const TaskClass = registry.get(row.
|
|
4787
|
-
if (TaskClass) {
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
5623
|
+
const TaskClass = registry.get(row.name);
|
|
5624
|
+
if (!TaskClass) {
|
|
5625
|
+
continue;
|
|
5626
|
+
}
|
|
5627
|
+
const taskInstance = new TaskClass(context, row);
|
|
5628
|
+
const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
|
|
5629
|
+
if (reason) {
|
|
5630
|
+
if (!row.past_due) {
|
|
5631
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
5632
|
+
past_due: db.fn.now(),
|
|
5633
|
+
progress: String(reason)
|
|
5634
|
+
});
|
|
4798
5635
|
}
|
|
5636
|
+
continue;
|
|
4799
5637
|
}
|
|
4800
|
-
const
|
|
5638
|
+
const claimPatch = {
|
|
5639
|
+
started_at: db.fn.now(),
|
|
5640
|
+
status: "running",
|
|
5641
|
+
status_changed_at: db.fn.now()
|
|
5642
|
+
};
|
|
5643
|
+
if (runnerIdentity) {
|
|
5644
|
+
claimPatch.service_name = runnerIdentity.service_name;
|
|
5645
|
+
claimPatch.server_name = runnerIdentity.server_name;
|
|
5646
|
+
claimPatch.instance_number = runnerIdentity.instance_number;
|
|
5647
|
+
}
|
|
5648
|
+
const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
|
|
4801
5649
|
const claimed = Array.isArray(updated) ? updated[0] : null;
|
|
4802
5650
|
if (claimed) return claimed;
|
|
4803
5651
|
}
|
|
4804
5652
|
return null;
|
|
4805
5653
|
}
|
|
4806
5654
|
async function runTasksLoop(context, options) {
|
|
4807
|
-
const
|
|
5655
|
+
const queueName = options.queueName ?? "tasks";
|
|
4808
5656
|
const target = options.target;
|
|
4809
5657
|
const pollMs = options.pollMs ?? 1e3;
|
|
4810
|
-
const
|
|
5658
|
+
const claimJitterMs = options.claimJitterMs ?? 0;
|
|
5659
|
+
const maxParallel = options.maxParallel ?? 32;
|
|
4811
5660
|
const scanLimit = options.scanLimit ?? 100;
|
|
4812
5661
|
const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
4813
5662
|
const registry = normalizeRegistry(options.registry);
|
|
4814
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
5663
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
4815
5664
|
if (!target) throw new Error("runTasksLoop: target is required");
|
|
5665
|
+
context.tasksQueueName = queueName;
|
|
4816
5666
|
const runningPromises = /* @__PURE__ */ new Set();
|
|
4817
5667
|
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
4818
5668
|
let runningStopControlPromise = null;
|
|
4819
5669
|
let stopRequested = false;
|
|
4820
5670
|
let stopAllowanceMs = 5e3;
|
|
4821
|
-
context.
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
)
|
|
4832
|
-
|
|
4833
|
-
|
|
5671
|
+
context.tasksRunnerStop = false;
|
|
5672
|
+
let registryReg = null;
|
|
5673
|
+
let registryInterval = null;
|
|
5674
|
+
let runnerIdentity = null;
|
|
5675
|
+
const hbGroup = options.runnerServiceGroup?.trim();
|
|
5676
|
+
if (hbGroup) {
|
|
5677
|
+
const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
|
|
5678
|
+
const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
|
|
5679
|
+
const defaultMeta = {
|
|
5680
|
+
component: "tasks-runner",
|
|
5681
|
+
allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
|
|
5682
|
+
};
|
|
5683
|
+
registryReg = await registerInServicesRegistry(context, {
|
|
5684
|
+
queueName,
|
|
5685
|
+
target,
|
|
5686
|
+
serviceGroup: hbGroup,
|
|
5687
|
+
serviceName: options.runnerServiceName,
|
|
5688
|
+
instanceNumber: options.runnerInstanceNumber,
|
|
5689
|
+
staleMs,
|
|
5690
|
+
groupMaxInstances: options.runnerGroupMaxInstances,
|
|
5691
|
+
enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
|
|
5692
|
+
metadata: options.runnerMetadata ?? defaultMeta
|
|
5693
|
+
});
|
|
5694
|
+
runnerIdentity = {
|
|
5695
|
+
service_name: registryReg.serviceName,
|
|
5696
|
+
server_name: os3.hostname(),
|
|
5697
|
+
instance_number: registryReg.instanceNumber
|
|
5698
|
+
};
|
|
5699
|
+
registryInterval = setInterval(() => {
|
|
5700
|
+
void touchServicesRegistry(context, registryReg).catch((err) => {
|
|
5701
|
+
context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
|
|
5702
|
+
});
|
|
5703
|
+
}, hbIntervalMs);
|
|
5704
|
+
}
|
|
5705
|
+
try {
|
|
5706
|
+
while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
|
|
5707
|
+
if (!runningStopControlPromise) {
|
|
5708
|
+
const claimedStopTask = await claimNextRunnableTask(
|
|
5709
|
+
context,
|
|
5710
|
+
tasksTable,
|
|
5711
|
+
target,
|
|
5712
|
+
registry,
|
|
5713
|
+
10,
|
|
5714
|
+
["stopRunner", "stop"],
|
|
5715
|
+
runnerIdentity
|
|
5716
|
+
);
|
|
5717
|
+
if (claimedStopTask) {
|
|
5718
|
+
runningStopControlPromise = executeClaimedTask(
|
|
5719
|
+
context,
|
|
5720
|
+
tasksTable,
|
|
5721
|
+
historyTable,
|
|
5722
|
+
claimedStopTask,
|
|
5723
|
+
registry,
|
|
5724
|
+
runningTaskInstances
|
|
5725
|
+
).then(async (outcome) => {
|
|
5726
|
+
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5727
|
+
stopRequested = true;
|
|
5728
|
+
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5729
|
+
context.tasksRunnerStop = true;
|
|
5730
|
+
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5731
|
+
}
|
|
5732
|
+
}).finally(() => {
|
|
5733
|
+
runningStopControlPromise = null;
|
|
5734
|
+
});
|
|
5735
|
+
}
|
|
5736
|
+
}
|
|
5737
|
+
if (claimJitterMs > 0) {
|
|
5738
|
+
await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
|
|
5739
|
+
}
|
|
5740
|
+
while (runningPromises.size < maxParallel) {
|
|
5741
|
+
const claimed = await claimNextRunnableTask(
|
|
4834
5742
|
context,
|
|
4835
5743
|
tasksTable,
|
|
4836
|
-
|
|
4837
|
-
claimedStopTask,
|
|
5744
|
+
target,
|
|
4838
5745
|
registry,
|
|
4839
|
-
|
|
4840
|
-
|
|
5746
|
+
scanLimit,
|
|
5747
|
+
allowedTasks,
|
|
5748
|
+
runnerIdentity
|
|
5749
|
+
);
|
|
5750
|
+
if (!claimed) break;
|
|
5751
|
+
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
4841
5752
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
4842
5753
|
stopRequested = true;
|
|
4843
5754
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
4844
|
-
context.
|
|
5755
|
+
context.tasksRunnerStop = true;
|
|
4845
5756
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
4846
5757
|
}
|
|
4847
5758
|
}).finally(() => {
|
|
4848
|
-
|
|
5759
|
+
runningPromises.delete(p);
|
|
4849
5760
|
});
|
|
5761
|
+
runningPromises.add(p);
|
|
5762
|
+
}
|
|
5763
|
+
const wakePromises = [...runningPromises];
|
|
5764
|
+
if (runningStopControlPromise) {
|
|
5765
|
+
wakePromises.push(runningStopControlPromise);
|
|
5766
|
+
}
|
|
5767
|
+
if (wakePromises.length === 0) {
|
|
5768
|
+
await sleepMs(pollMs);
|
|
5769
|
+
} else {
|
|
5770
|
+
const safe = wakePromises.map((p) => p.catch(() => void 0));
|
|
5771
|
+
await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
|
|
4850
5772
|
}
|
|
4851
5773
|
}
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
context,
|
|
4855
|
-
tasksTable,
|
|
4856
|
-
target,
|
|
4857
|
-
registry,
|
|
4858
|
-
scanLimit,
|
|
4859
|
-
allowedTasks
|
|
4860
|
-
);
|
|
4861
|
-
if (!claimed) break;
|
|
4862
|
-
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
4863
|
-
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
4864
|
-
stopRequested = true;
|
|
4865
|
-
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
4866
|
-
context.__tasksRunnerStop = true;
|
|
4867
|
-
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
4868
|
-
}
|
|
4869
|
-
}).finally(() => {
|
|
4870
|
-
runningPromises.delete(p);
|
|
4871
|
-
});
|
|
4872
|
-
runningPromises.add(p);
|
|
5774
|
+
if (context.isStop() && !stopRequested) {
|
|
5775
|
+
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
4873
5776
|
}
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
5777
|
+
if (runningPromises.size > 0) {
|
|
5778
|
+
if (stopRequested) {
|
|
5779
|
+
await Promise.race([
|
|
5780
|
+
Promise.allSettled(Array.from(runningPromises)),
|
|
5781
|
+
sleepMs(stopAllowanceMs).then(() => {
|
|
5782
|
+
context.logger.warn?.(
|
|
5783
|
+
`[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
|
|
5784
|
+
);
|
|
5785
|
+
})
|
|
5786
|
+
]);
|
|
5787
|
+
} else {
|
|
5788
|
+
await Promise.allSettled(Array.from(runningPromises));
|
|
5789
|
+
}
|
|
5790
|
+
}
|
|
5791
|
+
} finally {
|
|
5792
|
+
if (registryInterval) {
|
|
5793
|
+
clearInterval(registryInterval);
|
|
5794
|
+
registryInterval = null;
|
|
5795
|
+
}
|
|
5796
|
+
if (registryReg) {
|
|
5797
|
+
await unregisterServicesRegistry(context, registryReg).catch((err) => {
|
|
5798
|
+
context.logger.warn?.(`[services-registry] unregister failed: ${err?.message ?? String(err)}`);
|
|
5799
|
+
});
|
|
5800
|
+
registryReg = null;
|
|
5801
|
+
delete context.servicesRegistry;
|
|
5802
|
+
delete context.runnerHeartbeat;
|
|
4891
5803
|
}
|
|
4892
5804
|
}
|
|
4893
5805
|
}
|
|
4894
5806
|
var TasksManager = class _TasksManager {
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
5807
|
+
/**
|
|
5808
|
+
* @param {object} context
|
|
5809
|
+
* @param {{
|
|
5810
|
+
* queueName?: string,
|
|
5811
|
+
* target?: string,
|
|
5812
|
+
* recreateTaskTables?: boolean,
|
|
5813
|
+
* pollMs?: number,
|
|
5814
|
+
* claimJitterMs?: number,
|
|
5815
|
+
* maxParallel?: number,
|
|
5816
|
+
* scanLimit?: number,
|
|
5817
|
+
* allowedTasks?: string | string[],
|
|
5818
|
+
* registry?: TasksRegistry | Record<string, Function>,
|
|
5819
|
+
* runnerServiceGroup?: string,
|
|
5820
|
+
* runnerServiceName?: string,
|
|
5821
|
+
* runnerInstanceNumber?: number,
|
|
5822
|
+
* runnerHeartbeatIntervalMs?: number,
|
|
5823
|
+
* runnerHeartbeatStaleMs?: number,
|
|
5824
|
+
* runnerGroupMaxInstances?: number,
|
|
5825
|
+
* runnerEnforceMaxInstances?: boolean,
|
|
5826
|
+
* runnerMetadata?: Record<string, unknown>,
|
|
5827
|
+
* }} [options]
|
|
5828
|
+
*/
|
|
4904
5829
|
constructor(context, options = {}) {
|
|
4905
5830
|
this.context = context;
|
|
4906
|
-
this.
|
|
5831
|
+
this.queueName = options.queueName ?? "tasks";
|
|
4907
5832
|
this.target = options.target ?? "localRunner";
|
|
4908
5833
|
this.recreateTaskTables = options.recreateTaskTables ?? false;
|
|
4909
5834
|
this.pollMs = options.pollMs ?? 1e3;
|
|
5835
|
+
this.claimJitterMs = options.claimJitterMs ?? 0;
|
|
4910
5836
|
this.maxParallel = options.maxParallel ?? 1;
|
|
4911
5837
|
this.scanLimit = options.scanLimit ?? 100;
|
|
4912
5838
|
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
4913
5839
|
this.registry = normalizeRegistry(options.registry);
|
|
5840
|
+
this.runnerServiceGroup = options.runnerServiceGroup;
|
|
5841
|
+
this.runnerServiceName = options.runnerServiceName;
|
|
5842
|
+
this.runnerInstanceNumber = options.runnerInstanceNumber;
|
|
5843
|
+
this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
|
|
5844
|
+
this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
|
|
5845
|
+
this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
|
|
5846
|
+
this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
|
|
5847
|
+
this.runnerMetadata = options.runnerMetadata;
|
|
4914
5848
|
}
|
|
5849
|
+
/**
|
|
5850
|
+
* Preferred factory: reads defaults from `context.params` (module namespace
|
|
5851
|
+
* `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
|
|
5852
|
+
* and inline options in one consistent resolver.
|
|
5853
|
+
*
|
|
5854
|
+
* @param {object} context
|
|
5855
|
+
* @param {ConstructorParameters<typeof TasksManager>[1]} [options]
|
|
5856
|
+
* @returns {TasksManager}
|
|
5857
|
+
*/
|
|
4915
5858
|
static init(context, options = {}) {
|
|
4916
5859
|
const defs2 = {
|
|
4917
5860
|
table: "string default tasks",
|
|
4918
5861
|
target: "string default localRunner",
|
|
4919
5862
|
recreateTaskTables: "boolean default false",
|
|
4920
5863
|
pollMs: "number default 1000",
|
|
5864
|
+
claimJitterMs: "number default 0",
|
|
4921
5865
|
maxParallel: "number default 1",
|
|
4922
5866
|
scanLimit: "number default 100",
|
|
4923
|
-
allowedTasks: "string"
|
|
5867
|
+
allowedTasks: "string",
|
|
5868
|
+
runnerServiceGroup: "string",
|
|
5869
|
+
runnerServiceName: "string",
|
|
5870
|
+
runnerInstanceNumber: "number",
|
|
5871
|
+
runnerHeartbeatIntervalMs: "number default 10000",
|
|
5872
|
+
runnerHeartbeatStaleMs: "number default 45000",
|
|
5873
|
+
runnerGroupMaxInstances: "number",
|
|
5874
|
+
runnerEnforceMaxInstances: "boolean default true"
|
|
4924
5875
|
};
|
|
4925
|
-
const discovered = context.params.getAllForModule(defs2);
|
|
5876
|
+
const discovered = context.params.getAllForModule("tasks", defs2);
|
|
4926
5877
|
const resolved = {
|
|
4927
|
-
|
|
5878
|
+
queueName: discovered.table,
|
|
4928
5879
|
target: discovered.target,
|
|
4929
5880
|
recreateTaskTables: discovered.recreateTaskTables,
|
|
4930
5881
|
pollMs: discovered.pollMs,
|
|
5882
|
+
claimJitterMs: discovered.claimJitterMs,
|
|
4931
5883
|
maxParallel: discovered.maxParallel,
|
|
4932
5884
|
scanLimit: discovered.scanLimit,
|
|
4933
5885
|
allowedTasks: discovered.allowedTasks,
|
|
5886
|
+
runnerServiceGroup: discovered.runnerServiceGroup,
|
|
5887
|
+
runnerServiceName: discovered.runnerServiceName,
|
|
5888
|
+
runnerInstanceNumber: discovered.runnerInstanceNumber,
|
|
5889
|
+
runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
|
|
5890
|
+
runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
|
|
5891
|
+
runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
|
|
5892
|
+
runnerEnforceMaxInstances: discovered.runnerEnforceMaxInstances,
|
|
4934
5893
|
...options
|
|
4935
5894
|
};
|
|
4936
5895
|
return new _TasksManager(context, resolved);
|
|
4937
5896
|
}
|
|
5897
|
+
/**
|
|
5898
|
+
* Idempotently ensure the three backing tables exist for this queue.
|
|
5899
|
+
*
|
|
5900
|
+
* @param {{ recreate?: boolean }} [options]
|
|
5901
|
+
* @returns {Promise<void>}
|
|
5902
|
+
*/
|
|
4938
5903
|
async ensureTaskTables(options = {}) {
|
|
4939
5904
|
await ensureTaskTables(this.context, {
|
|
4940
|
-
|
|
5905
|
+
queueName: this.queueName,
|
|
4941
5906
|
recreate: options.recreate ?? this.recreateTaskTables
|
|
4942
5907
|
});
|
|
4943
5908
|
}
|
|
5909
|
+
/**
|
|
5910
|
+
* Start the runner loop using this manager's resolved config. Per-call
|
|
5911
|
+
* options override the stored defaults, but `runnerMetadata` still falls
|
|
5912
|
+
* through when omitted.
|
|
5913
|
+
*
|
|
5914
|
+
* @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
|
|
5915
|
+
* @returns {Promise<void>}
|
|
5916
|
+
*/
|
|
4944
5917
|
async runTasksLoop(options = {}) {
|
|
4945
5918
|
await runTasksLoop(this.context, {
|
|
4946
|
-
|
|
5919
|
+
queueName: options.queueName ?? this.queueName,
|
|
4947
5920
|
target: options.target ?? this.target,
|
|
4948
5921
|
pollMs: options.pollMs ?? this.pollMs,
|
|
5922
|
+
claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
|
|
4949
5923
|
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
4950
5924
|
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
4951
5925
|
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
4952
|
-
registry: options.registry ?? this.registry
|
|
5926
|
+
registry: options.registry ?? this.registry,
|
|
5927
|
+
runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
|
|
5928
|
+
runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
|
|
5929
|
+
runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
|
|
5930
|
+
runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
|
|
5931
|
+
runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
|
|
5932
|
+
runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
|
|
5933
|
+
runnerEnforceMaxInstances: options.runnerEnforceMaxInstances ?? this.runnerEnforceMaxInstances,
|
|
5934
|
+
runnerMetadata: options.runnerMetadata ?? this.runnerMetadata
|
|
4953
5935
|
});
|
|
4954
5936
|
}
|
|
4955
5937
|
};
|
|
4956
5938
|
|
|
4957
|
-
// src/scripts/cli-runner.
|
|
5939
|
+
// src/scripts/cli-runner.js
|
|
4958
5940
|
var defs = {
|
|
4959
|
-
dbName: "string default local",
|
|
4960
5941
|
tasksModule: "string"
|
|
4961
5942
|
};
|
|
4962
5943
|
async function loadTasksModule(modulePath) {
|
|
4963
|
-
const absolute =
|
|
5944
|
+
const absolute = path5.isAbsolute(modulePath) ? modulePath : path5.resolve(process.cwd(), modulePath);
|
|
4964
5945
|
const imported = await import(pathToFileURL(absolute).href);
|
|
4965
5946
|
if (!imported.tasksRegistry || typeof imported.tasksRegistry !== "object") {
|
|
4966
5947
|
throw new Error(`tasksModule "${modulePath}" must export "tasksRegistry" object`);
|
|
@@ -4968,11 +5949,8 @@ async function loadTasksModule(modulePath) {
|
|
|
4968
5949
|
return imported.tasksRegistry;
|
|
4969
5950
|
}
|
|
4970
5951
|
var flow = async (context) => {
|
|
4971
|
-
const {
|
|
4972
|
-
|
|
4973
|
-
tasksModule
|
|
4974
|
-
} = context.params.getAll(defs);
|
|
4975
|
-
const db = await dbInit(context, dbName);
|
|
5952
|
+
const { tasksModule } = context.params.getAll(defs);
|
|
5953
|
+
const db = await Db.init(context);
|
|
4976
5954
|
context.db = db;
|
|
4977
5955
|
const registry = new TasksRegistry().addMany(defaultTasksRegistry.toObject());
|
|
4978
5956
|
if (tasksModule) {
|