@nmakarov/cli-toolkit 0.21.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/args.cjs +1 -4
- package/dist/args.cjs.map +1 -1
- package/dist/args.js +1 -1
- package/dist/args.js.map +1 -1
- package/dist/cli-runner.cjs +1370 -623
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +1415 -667
- package/dist/cli-runner.js.map +1 -1
- package/dist/db.cjs +173 -158
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +172 -151
- package/dist/db.js.map +1 -1
- package/dist/errors.cjs +2 -2
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/filedatabase.cjs +19 -19
- package/dist/filedatabase.cjs.map +1 -1
- package/dist/filedatabase.js +19 -16
- package/dist/filedatabase.js.map +1 -1
- package/dist/http-client.cjs +9 -11
- package/dist/http-client.cjs.map +1 -1
- package/dist/http-client.js +10 -9
- package/dist/http-client.js.map +1 -1
- package/dist/http-client2.cjs +34 -37
- package/dist/http-client2.cjs.map +1 -1
- package/dist/http-client2.js +34 -34
- package/dist/http-client2.js.map +1 -1
- package/dist/index.cjs +1831 -713
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1837 -713
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +93 -68
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +108 -82
- package/dist/init.js.map +1 -1
- package/dist/logger.cjs +5 -5
- package/dist/logger.cjs.map +1 -1
- package/dist/logger.js +5 -4
- package/dist/logger.js.map +1 -1
- package/dist/mock-server.cjs +21 -33
- package/dist/mock-server.cjs.map +1 -1
- package/dist/mock-server.js +21 -28
- package/dist/mock-server.js.map +1 -1
- package/dist/params.cjs +18 -9
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +18 -6
- package/dist/params.js.map +1 -1
- package/dist/s3.cjs +286 -0
- package/dist/s3.cjs.map +1 -0
- package/dist/s3.js +273 -0
- package/dist/s3.js.map +1 -0
- package/dist/screen.cjs +34 -39
- package/dist/screen.cjs.map +1 -1
- package/dist/screen.js +48 -46
- package/dist/screen.js.map +1 -1
- package/dist/tasks.cjs +1354 -501
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +1369 -527
- package/dist/tasks.js.map +1 -1
- package/dist/utils.cjs +7 -8
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.js +6 -6
- package/dist/utils.js.map +1 -1
- package/package.json +32 -47
- package/scripts/ssm/{parse-cli.ts → parse-cli.js} +4 -4
- package/scripts/ssm/{ssm-admin.ts → ssm-admin.js} +12 -12
- package/scripts/ssm/{ssm-pull.ts → ssm-pull.js} +10 -13
package/dist/cli-runner.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,25 +880,24 @@ 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];
|
|
@@ -915,12 +911,11 @@ function buildDetailBreadcrumb(path6, suffix = "") {
|
|
|
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.
|
|
1098
|
+
// src/scripts/cli-runner.js
|
|
1104
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
|
|
@@ -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
|
}
|
|
@@ -2054,6 +2049,18 @@ var Params = class _Params {
|
|
|
2054
2049
|
this._currentModule = prev;
|
|
2055
2050
|
}
|
|
2056
2051
|
}
|
|
2052
|
+
/**
|
|
2053
|
+
* Async variant of {@link runWithModule} for modules that await params.get().
|
|
2054
|
+
*/
|
|
2055
|
+
async runWithModuleAsync(moduleName, fn) {
|
|
2056
|
+
const prev = this._currentModule;
|
|
2057
|
+
this._currentModule = moduleName;
|
|
2058
|
+
try {
|
|
2059
|
+
return await fn();
|
|
2060
|
+
} finally {
|
|
2061
|
+
this._currentModule = prev;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2057
2064
|
/**
|
|
2058
2065
|
* Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
|
|
2059
2066
|
*/
|
|
@@ -2114,11 +2121,11 @@ var Params = class _Params {
|
|
|
2114
2121
|
}
|
|
2115
2122
|
};
|
|
2116
2123
|
|
|
2117
|
-
// src/logger/index.
|
|
2124
|
+
// src/logger/index.js
|
|
2118
2125
|
import chalk from "chalk";
|
|
2119
2126
|
import util from "util";
|
|
2120
2127
|
|
|
2121
|
-
// src/logger/transports.
|
|
2128
|
+
// src/logger/transports.js
|
|
2122
2129
|
var ConsoleTransport = class {
|
|
2123
2130
|
write(payload) {
|
|
2124
2131
|
console.info(payload);
|
|
@@ -2138,7 +2145,7 @@ var ParentProcessTransport = class {
|
|
|
2138
2145
|
}
|
|
2139
2146
|
};
|
|
2140
2147
|
|
|
2141
|
-
// src/logger/index.
|
|
2148
|
+
// src/logger/index.js
|
|
2142
2149
|
var ALL_LEVELS = [
|
|
2143
2150
|
"silly",
|
|
2144
2151
|
"debug",
|
|
@@ -2211,6 +2218,7 @@ var Logger = class _Logger {
|
|
|
2211
2218
|
}
|
|
2212
2219
|
/**
|
|
2213
2220
|
* Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
|
|
2221
|
+
* Params are tracked under the `logger` module for --showUsedParams.
|
|
2214
2222
|
*/
|
|
2215
2223
|
static init(context, options) {
|
|
2216
2224
|
const paramDefs = {
|
|
@@ -2224,7 +2232,7 @@ var Logger = class _Logger {
|
|
|
2224
2232
|
progressWithTimes: "boolean default false",
|
|
2225
2233
|
progressThrottleMs: "number"
|
|
2226
2234
|
};
|
|
2227
|
-
const discovered = context.params.getAllForModule(paramDefs);
|
|
2235
|
+
const discovered = context.params.getAllForModule("logger", paramDefs);
|
|
2228
2236
|
const config2 = { ...discovered, ...options };
|
|
2229
2237
|
const logger = new _Logger(context, config2);
|
|
2230
2238
|
context.logger = logger;
|
|
@@ -2420,9 +2428,9 @@ var Logger = class _Logger {
|
|
|
2420
2428
|
}
|
|
2421
2429
|
};
|
|
2422
2430
|
|
|
2423
|
-
// src/init/index.
|
|
2431
|
+
// src/init/index.js
|
|
2424
2432
|
import { EventEmitter } from "events";
|
|
2425
|
-
function extractComponentOptions(opts,
|
|
2433
|
+
function extractComponentOptions(opts, _componentName) {
|
|
2426
2434
|
const reservedKeys = ["overrides", "defaults", "modules"];
|
|
2427
2435
|
const componentOptions = {};
|
|
2428
2436
|
for (const [key, value] of Object.entries(opts)) {
|
|
@@ -2489,6 +2497,19 @@ function printAllParameters(context) {
|
|
|
2489
2497
|
async function init(flow2, opts = {}) {
|
|
2490
2498
|
let stop = false;
|
|
2491
2499
|
let context = null;
|
|
2500
|
+
let cleanupRan = false;
|
|
2501
|
+
const runRegisteredCleanups = async (ctx) => {
|
|
2502
|
+
if (cleanupRan) return;
|
|
2503
|
+
cleanupRan = true;
|
|
2504
|
+
const fns = [...ctx.cleanupFunctions].reverse();
|
|
2505
|
+
for (const fn of fns) {
|
|
2506
|
+
try {
|
|
2507
|
+
await fn(ctx);
|
|
2508
|
+
} catch (error) {
|
|
2509
|
+
ctx.logger.warn("[cleanup] error in cleanup function:", error);
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
};
|
|
2492
2513
|
try {
|
|
2493
2514
|
try {
|
|
2494
2515
|
const screenModule = await Promise.resolve().then(() => (init_screen(), screen_exports));
|
|
@@ -2511,13 +2532,24 @@ async function init(flow2, opts = {}) {
|
|
|
2511
2532
|
printAllParameters(context);
|
|
2512
2533
|
process.exit(0);
|
|
2513
2534
|
}
|
|
2535
|
+
let sigintCount = 0;
|
|
2514
2536
|
process.on("SIGINT", async () => {
|
|
2515
|
-
if (
|
|
2516
|
-
|
|
2517
|
-
|
|
2537
|
+
if (!context) return;
|
|
2538
|
+
sigintCount += 1;
|
|
2539
|
+
if (sigintCount === 1) {
|
|
2540
|
+
stop = true;
|
|
2541
|
+
context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
|
|
2542
|
+
context.emitter.emit("stop", stopAllowance);
|
|
2543
|
+
return;
|
|
2518
2544
|
}
|
|
2545
|
+
context.logger.warn("[process] second SIGINT: running cleanup then exit");
|
|
2546
|
+
await runRegisteredCleanups(context);
|
|
2547
|
+
process.exit(2);
|
|
2548
|
+
});
|
|
2549
|
+
process.on("SIGTERM", () => {
|
|
2550
|
+
if (!context || stop) return;
|
|
2519
2551
|
stop = true;
|
|
2520
|
-
context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
|
|
2552
|
+
context.logger.info(`>> SIGTERM: emitting stop with allowance ${stopAllowance}`);
|
|
2521
2553
|
context.emitter.emit("stop", stopAllowance);
|
|
2522
2554
|
});
|
|
2523
2555
|
await flow2(context);
|
|
@@ -2542,33 +2574,95 @@ async function init(flow2, opts = {}) {
|
|
|
2542
2574
|
}
|
|
2543
2575
|
} finally {
|
|
2544
2576
|
if (context) {
|
|
2545
|
-
|
|
2546
|
-
try {
|
|
2547
|
-
await fn(context);
|
|
2548
|
-
} catch (error) {
|
|
2549
|
-
context.logger.warn("[cleanup] error in cleanup function:", error);
|
|
2550
|
-
}
|
|
2551
|
-
}
|
|
2577
|
+
await runRegisteredCleanups(context);
|
|
2552
2578
|
}
|
|
2553
2579
|
}
|
|
2554
2580
|
}
|
|
2555
2581
|
|
|
2556
|
-
// src/db/index.
|
|
2582
|
+
// src/db/index.js
|
|
2557
2583
|
import knex from "knex";
|
|
2584
|
+
var KNEX_DEFAULTS = {
|
|
2585
|
+
testConnection: true,
|
|
2586
|
+
pool: { min: 2, max: 10 },
|
|
2587
|
+
acquireConnectionTimeout: 1e4,
|
|
2588
|
+
ssl: { rejectUnauthorized: false }
|
|
2589
|
+
};
|
|
2558
2590
|
var Db = class {
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2591
|
+
static async init(context, options = {}) {
|
|
2592
|
+
const buildConfig = async () => {
|
|
2593
|
+
const defs2 = {
|
|
2594
|
+
dbName: "string",
|
|
2595
|
+
dbProfile: "boolean default false"
|
|
2596
|
+
};
|
|
2597
|
+
const discovered = context?.params?.getAllForModule?.("db", defs2) ?? {};
|
|
2598
|
+
const merged = { ...discovered, ...options };
|
|
2599
|
+
let { dbName, dbProfile } = merged;
|
|
2600
|
+
let dbConnectionString = options.dbConnectionString ?? options.connectionString;
|
|
2601
|
+
let connectionParam = dbConnectionString ? "options" : null;
|
|
2602
|
+
if (!dbConnectionString) {
|
|
2603
|
+
const src = context?.args?.getSource?.("dbConnectionString");
|
|
2604
|
+
if (src === "cli" || src === "overrides" || src === "config") {
|
|
2605
|
+
dbConnectionString = await context.params.get("dbConnectionString", "string");
|
|
2606
|
+
connectionParam = "dbConnectionString";
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
if (!dbConnectionString && dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
|
|
2610
|
+
dbConnectionString = dbName;
|
|
2611
|
+
dbName = void 0;
|
|
2612
|
+
}
|
|
2613
|
+
if (!dbConnectionString && dbName) {
|
|
2614
|
+
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
2615
|
+
dbConnectionString = await context.params.get(paramName, "string");
|
|
2616
|
+
connectionParam = paramName;
|
|
2617
|
+
if (!dbConnectionString) {
|
|
2618
|
+
throw new ParamError(
|
|
2619
|
+
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
2620
|
+
);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
if (!dbConnectionString) {
|
|
2624
|
+
dbConnectionString = await context.params.get("dbConnectionString", "string");
|
|
2625
|
+
if (dbConnectionString) {
|
|
2626
|
+
connectionParam = "dbConnectionString";
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
if (!dbConnectionString) {
|
|
2630
|
+
if (!dbName) {
|
|
2631
|
+
dbName = "local";
|
|
2632
|
+
}
|
|
2633
|
+
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
2634
|
+
dbConnectionString = await context.params.get(paramName, "string");
|
|
2635
|
+
connectionParam = paramName;
|
|
2636
|
+
if (!dbConnectionString) {
|
|
2637
|
+
throw new ParamError(
|
|
2638
|
+
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
2639
|
+
);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
const displayName = resolveDbDisplayName(
|
|
2643
|
+
dbName,
|
|
2644
|
+
connectionParam,
|
|
2645
|
+
context?.args?.env,
|
|
2646
|
+
merged.name
|
|
2647
|
+
);
|
|
2648
|
+
return {
|
|
2649
|
+
...KNEX_DEFAULTS,
|
|
2650
|
+
connectionString: dbConnectionString,
|
|
2651
|
+
name: displayName,
|
|
2652
|
+
profile: !!dbProfile,
|
|
2653
|
+
logger: context.logger
|
|
2654
|
+
};
|
|
2655
|
+
};
|
|
2656
|
+
const config2 = context?.params?.runWithModuleAsync ? await context.params.runWithModuleAsync("db", buildConfig) : await buildConfig();
|
|
2657
|
+
return dbConnect(context, config2);
|
|
2658
|
+
}
|
|
2568
2659
|
constructor(config2) {
|
|
2569
|
-
if (!config2.connectionString) {
|
|
2660
|
+
if (!config2 || !config2.connectionString) {
|
|
2570
2661
|
throw new ParamError("Db: connectionString is required");
|
|
2571
2662
|
}
|
|
2663
|
+
this.knexInstance = null;
|
|
2664
|
+
this.isConnected = false;
|
|
2665
|
+
this.queriesLog = [];
|
|
2572
2666
|
this.config = {
|
|
2573
2667
|
testConnection: true,
|
|
2574
2668
|
profile: false,
|
|
@@ -2576,30 +2670,27 @@ var Db = class {
|
|
|
2576
2670
|
acquireConnectionTimeout: 1e4,
|
|
2577
2671
|
ssl: { rejectUnauthorized: false },
|
|
2578
2672
|
logger: console,
|
|
2579
|
-
name: "default",
|
|
2580
2673
|
...config2
|
|
2581
2674
|
};
|
|
2582
2675
|
this.logger = this.config.logger;
|
|
2583
2676
|
const instance = this;
|
|
2584
|
-
const callableWrapper = function(
|
|
2677
|
+
const callableWrapper = function() {
|
|
2585
2678
|
throw new Error("This should never be called directly");
|
|
2586
2679
|
};
|
|
2587
2680
|
callableWrapper._instance = instance;
|
|
2588
2681
|
return new Proxy(callableWrapper, {
|
|
2589
|
-
|
|
2590
|
-
apply: (target, thisArg, argumentsList) => {
|
|
2682
|
+
apply: (target, _thisArg, argumentsList) => {
|
|
2591
2683
|
const inst = target._instance;
|
|
2592
2684
|
if (!inst.knexInstance) {
|
|
2593
2685
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
2594
2686
|
}
|
|
2595
2687
|
return inst.knexInstance(...argumentsList);
|
|
2596
2688
|
},
|
|
2597
|
-
// Intercept property access: db.schema, db.raw, etc.
|
|
2598
2689
|
get: (target, prop) => {
|
|
2599
2690
|
if (prop === "_instance") {
|
|
2600
2691
|
return target._instance;
|
|
2601
2692
|
}
|
|
2602
|
-
const
|
|
2693
|
+
const inst = target._instance;
|
|
2603
2694
|
const ownMethods = [
|
|
2604
2695
|
"connect",
|
|
2605
2696
|
"disconnect",
|
|
@@ -2612,26 +2703,26 @@ var Db = class {
|
|
|
2612
2703
|
"detectClient",
|
|
2613
2704
|
"attachProfiler"
|
|
2614
2705
|
];
|
|
2615
|
-
if (prop in
|
|
2616
|
-
const value =
|
|
2706
|
+
if (prop in inst) {
|
|
2707
|
+
const value = inst[prop];
|
|
2617
2708
|
if (typeof value === "function" && ownMethods.includes(prop)) {
|
|
2618
|
-
return value.bind(
|
|
2709
|
+
return value.bind(inst);
|
|
2619
2710
|
}
|
|
2620
2711
|
if (typeof value !== "function") {
|
|
2621
2712
|
return value;
|
|
2622
2713
|
}
|
|
2623
2714
|
}
|
|
2624
|
-
if (
|
|
2625
|
-
const knexProp =
|
|
2715
|
+
if (inst.knexInstance) {
|
|
2716
|
+
const knexProp = inst.knexInstance[prop];
|
|
2626
2717
|
if (typeof knexProp === "function") {
|
|
2627
|
-
return knexProp.bind(
|
|
2718
|
+
return knexProp.bind(inst.knexInstance);
|
|
2628
2719
|
}
|
|
2629
2720
|
return knexProp;
|
|
2630
2721
|
}
|
|
2631
|
-
if (prop in
|
|
2632
|
-
const method =
|
|
2722
|
+
if (prop in inst) {
|
|
2723
|
+
const method = inst[prop];
|
|
2633
2724
|
if (typeof method === "function") {
|
|
2634
|
-
return method.bind(
|
|
2725
|
+
return method.bind(inst);
|
|
2635
2726
|
}
|
|
2636
2727
|
return method;
|
|
2637
2728
|
}
|
|
@@ -2639,9 +2730,6 @@ var Db = class {
|
|
|
2639
2730
|
}
|
|
2640
2731
|
});
|
|
2641
2732
|
}
|
|
2642
|
-
/**
|
|
2643
|
-
* Detect database client type from connection string
|
|
2644
|
-
*/
|
|
2645
2733
|
detectClient(connectionString) {
|
|
2646
2734
|
if (connectionString.match(/^postgresql/)) {
|
|
2647
2735
|
return "pg";
|
|
@@ -2651,9 +2739,6 @@ var Db = class {
|
|
|
2651
2739
|
}
|
|
2652
2740
|
return null;
|
|
2653
2741
|
}
|
|
2654
|
-
/**
|
|
2655
|
-
* Connect to the database
|
|
2656
|
-
*/
|
|
2657
2742
|
async connect() {
|
|
2658
2743
|
if (this.isConnected && this.knexInstance) {
|
|
2659
2744
|
this.logger.warn?.("[Db] Already connected");
|
|
@@ -2662,14 +2747,13 @@ var Db = class {
|
|
|
2662
2747
|
const client = this.detectClient(this.config.connectionString);
|
|
2663
2748
|
if (!client) {
|
|
2664
2749
|
throw new ParamError(
|
|
2665
|
-
|
|
2750
|
+
"Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://"
|
|
2666
2751
|
);
|
|
2667
2752
|
}
|
|
2668
2753
|
try {
|
|
2669
2754
|
const connectionConfig = {
|
|
2670
2755
|
connectionString: this.config.connectionString,
|
|
2671
2756
|
family: 4
|
|
2672
|
-
// Force IPv4 only (disable IPv6)
|
|
2673
2757
|
};
|
|
2674
2758
|
this.knexInstance = knex({
|
|
2675
2759
|
client,
|
|
@@ -2685,7 +2769,7 @@ var Db = class {
|
|
|
2685
2769
|
await this.testConnection();
|
|
2686
2770
|
}
|
|
2687
2771
|
this.isConnected = true;
|
|
2688
|
-
this.logger.debug?.(
|
|
2772
|
+
this.logger.debug?.(formatDbConnectMessage(this.config.name, this.config.connectionString));
|
|
2689
2773
|
} catch (error) {
|
|
2690
2774
|
if (error instanceof ParamError) {
|
|
2691
2775
|
throw error;
|
|
@@ -2694,9 +2778,6 @@ var Db = class {
|
|
|
2694
2778
|
throw new ParamError(`Db: Connection failed - ${errorMsg}`);
|
|
2695
2779
|
}
|
|
2696
2780
|
}
|
|
2697
|
-
/**
|
|
2698
|
-
* Disconnect from the database
|
|
2699
|
-
*/
|
|
2700
2781
|
async disconnect() {
|
|
2701
2782
|
if (!this.knexInstance) {
|
|
2702
2783
|
return;
|
|
@@ -2706,16 +2787,13 @@ var Db = class {
|
|
|
2706
2787
|
this.knexInstance = null;
|
|
2707
2788
|
this.isConnected = false;
|
|
2708
2789
|
this.queriesLog = [];
|
|
2709
|
-
this.logger.debug?.(
|
|
2790
|
+
this.logger.debug?.(formatDbDisconnectMessage(this.config.name, this.config.connectionString));
|
|
2710
2791
|
} catch (error) {
|
|
2711
2792
|
const errorMsg = this.getErrorMessage(error);
|
|
2712
2793
|
this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
|
|
2713
2794
|
throw error;
|
|
2714
2795
|
}
|
|
2715
2796
|
}
|
|
2716
|
-
/**
|
|
2717
|
-
* Extract error message from various error types
|
|
2718
|
-
*/
|
|
2719
2797
|
getErrorMessage(error) {
|
|
2720
2798
|
if (error instanceof AggregateError) {
|
|
2721
2799
|
const errors = error.errors || [];
|
|
@@ -2740,9 +2818,11 @@ var Db = class {
|
|
|
2740
2818
|
return `${code} (tried: ${addresses.join(", ")})`;
|
|
2741
2819
|
}
|
|
2742
2820
|
}
|
|
2743
|
-
const uniqueMessages = [
|
|
2744
|
-
|
|
2745
|
-
|
|
2821
|
+
const uniqueMessages = [
|
|
2822
|
+
...new Set(
|
|
2823
|
+
errors.map((e) => e instanceof Error ? e.message : String(e))
|
|
2824
|
+
)
|
|
2825
|
+
];
|
|
2746
2826
|
if (uniqueMessages.length === 1) {
|
|
2747
2827
|
return uniqueMessages[0];
|
|
2748
2828
|
}
|
|
@@ -2751,28 +2831,25 @@ var Db = class {
|
|
|
2751
2831
|
return error.message || "Multiple errors occurred";
|
|
2752
2832
|
}
|
|
2753
2833
|
if (error instanceof Error) {
|
|
2754
|
-
const
|
|
2755
|
-
if (
|
|
2756
|
-
return `${
|
|
2834
|
+
const code = error.code;
|
|
2835
|
+
if (code) {
|
|
2836
|
+
return `${code}: ${error.message || String(error)}`;
|
|
2757
2837
|
}
|
|
2758
2838
|
return error.message || String(error);
|
|
2759
2839
|
}
|
|
2760
2840
|
if (typeof error === "string") {
|
|
2761
2841
|
return error;
|
|
2762
2842
|
}
|
|
2763
|
-
if (error
|
|
2843
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
2764
2844
|
const msg = String(error.message);
|
|
2765
|
-
const
|
|
2766
|
-
if (
|
|
2767
|
-
return `${
|
|
2845
|
+
const code = error.code;
|
|
2846
|
+
if (code) {
|
|
2847
|
+
return `${code}: ${msg}`;
|
|
2768
2848
|
}
|
|
2769
2849
|
return msg;
|
|
2770
2850
|
}
|
|
2771
2851
|
return String(error) || "Unknown error";
|
|
2772
2852
|
}
|
|
2773
|
-
/**
|
|
2774
|
-
* Test database connection
|
|
2775
|
-
*/
|
|
2776
2853
|
async testConnection() {
|
|
2777
2854
|
if (!this.knexInstance) {
|
|
2778
2855
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -2788,9 +2865,6 @@ var Db = class {
|
|
|
2788
2865
|
throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
|
|
2789
2866
|
}
|
|
2790
2867
|
}
|
|
2791
|
-
/**
|
|
2792
|
-
* Attach query profiler to log all queries
|
|
2793
|
-
*/
|
|
2794
2868
|
attachProfiler() {
|
|
2795
2869
|
if (!this.knexInstance) {
|
|
2796
2870
|
return;
|
|
@@ -2800,7 +2874,7 @@ var Db = class {
|
|
|
2800
2874
|
this.knexInstance.on("query", (query) => {
|
|
2801
2875
|
query.__startTime = process.hrtime();
|
|
2802
2876
|
});
|
|
2803
|
-
this.knexInstance.on("query-response", (
|
|
2877
|
+
this.knexInstance.on("query-response", (_response, query) => {
|
|
2804
2878
|
const [seconds, nanoseconds] = process.hrtime(query.__startTime);
|
|
2805
2879
|
const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
|
|
2806
2880
|
const logEntry = {
|
|
@@ -2815,15 +2889,9 @@ var Db = class {
|
|
|
2815
2889
|
this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
|
|
2816
2890
|
});
|
|
2817
2891
|
}
|
|
2818
|
-
/**
|
|
2819
|
-
* Get query log (only available if profiling is enabled)
|
|
2820
|
-
*/
|
|
2821
2892
|
getQueryLog() {
|
|
2822
2893
|
return [...this.queriesLog];
|
|
2823
2894
|
}
|
|
2824
|
-
/**
|
|
2825
|
-
* Check if a table exists
|
|
2826
|
-
*/
|
|
2827
2895
|
async tableExists(tableName) {
|
|
2828
2896
|
if (!this.knexInstance) {
|
|
2829
2897
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -2835,65 +2903,87 @@ var Db = class {
|
|
|
2835
2903
|
throw error;
|
|
2836
2904
|
}
|
|
2837
2905
|
}
|
|
2838
|
-
/**
|
|
2839
|
-
* Get the underlying Knex instance (for advanced usage)
|
|
2840
|
-
*/
|
|
2841
2906
|
getKnex() {
|
|
2842
2907
|
if (!this.knexInstance) {
|
|
2843
2908
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
2844
2909
|
}
|
|
2845
2910
|
return this.knexInstance;
|
|
2846
2911
|
}
|
|
2847
|
-
/**
|
|
2848
|
-
* Get connection status
|
|
2849
|
-
*/
|
|
2850
2912
|
isConnectedToDb() {
|
|
2851
2913
|
return this.isConnected && this.knexInstance !== null;
|
|
2852
2914
|
}
|
|
2853
|
-
/**
|
|
2854
|
-
* Initialize Db with context (connects and registers disconnect cleanup).
|
|
2855
|
-
* Params are read via getAllForModule("db", defs). Same as dbInit(context, dbNameOrConnectionString).
|
|
2856
|
-
*/
|
|
2857
|
-
static async init(context, dbNameOrConnectionString) {
|
|
2858
|
-
return dbFindAndConnect(context, dbNameOrConnectionString);
|
|
2859
|
-
}
|
|
2860
2915
|
};
|
|
2861
2916
|
function capitalizeFirstLetter(str) {
|
|
2862
2917
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
2863
2918
|
}
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
}
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2919
|
+
function resolveDbDisplayName(dbName, connectionParam, argsEnv, mergedName) {
|
|
2920
|
+
if (dbName) {
|
|
2921
|
+
return dbName;
|
|
2922
|
+
}
|
|
2923
|
+
if (mergedName) {
|
|
2924
|
+
return mergedName;
|
|
2925
|
+
}
|
|
2926
|
+
if (connectionParam?.startsWith("dbConnectionString") && connectionParam.length > "dbConnectionString".length) {
|
|
2927
|
+
return connectionParam.slice("dbConnectionString".length).toLowerCase();
|
|
2928
|
+
}
|
|
2929
|
+
if (connectionParam === "dbConnectionString" && argsEnv) {
|
|
2930
|
+
return argsEnv;
|
|
2931
|
+
}
|
|
2932
|
+
return void 0;
|
|
2933
|
+
}
|
|
2934
|
+
function formatDbConnectMessage(name, connectionString) {
|
|
2935
|
+
const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
|
|
2936
|
+
if (name) {
|
|
2937
|
+
return `[Db] Connected to database "${name}"${endpointSuffix}`;
|
|
2938
|
+
}
|
|
2939
|
+
return `[Db] Connected${endpointSuffix}`;
|
|
2940
|
+
}
|
|
2941
|
+
function formatDbDisconnectMessage(name, connectionString) {
|
|
2942
|
+
const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
|
|
2943
|
+
if (name) {
|
|
2944
|
+
return `[Db] Disconnected from database "${name}"${endpointSuffix}`;
|
|
2945
|
+
}
|
|
2946
|
+
return `[Db] Disconnected${endpointSuffix}`;
|
|
2947
|
+
}
|
|
2948
|
+
function formatDbInstanceMessage(action, name) {
|
|
2949
|
+
if (name) {
|
|
2950
|
+
return `[Db] instance "${name}" ${action}`;
|
|
2951
|
+
}
|
|
2952
|
+
return `[Db] instance ${action}`;
|
|
2953
|
+
}
|
|
2954
|
+
function formatConnectionEndpointSuffix(connectionString) {
|
|
2955
|
+
const endpoint = formatConnectionEndpoint(connectionString);
|
|
2956
|
+
return endpoint ? ` (${endpoint})` : "";
|
|
2957
|
+
}
|
|
2958
|
+
function formatConnectionEndpoint(connectionString) {
|
|
2959
|
+
try {
|
|
2960
|
+
const url = new URL(connectionString);
|
|
2961
|
+
const host = url.hostname;
|
|
2962
|
+
if (!host) {
|
|
2963
|
+
return null;
|
|
2964
|
+
}
|
|
2965
|
+
let port = url.port;
|
|
2966
|
+
if (!port) {
|
|
2967
|
+
if (url.protocol === "postgresql:") {
|
|
2968
|
+
port = "5432";
|
|
2969
|
+
} else if (url.protocol === "mysql:") {
|
|
2970
|
+
port = "3306";
|
|
2971
|
+
}
|
|
2972
|
+
}
|
|
2973
|
+
return port ? `${host}:${port}` : host;
|
|
2974
|
+
} catch {
|
|
2975
|
+
return null;
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
async function dbConnect(context, config2) {
|
|
2889
2979
|
try {
|
|
2890
2980
|
const db = new Db(config2);
|
|
2891
2981
|
context.registerCleanup(async () => {
|
|
2892
2982
|
await db.disconnect();
|
|
2893
|
-
context.logger.debug(
|
|
2983
|
+
context.logger.debug?.(formatDbInstanceMessage("disconnected", config2.name));
|
|
2894
2984
|
});
|
|
2895
2985
|
await db.connect();
|
|
2896
|
-
context.logger.debug(
|
|
2986
|
+
context.logger.debug?.(formatDbInstanceMessage("initialized", config2.name));
|
|
2897
2987
|
return db;
|
|
2898
2988
|
} catch (error) {
|
|
2899
2989
|
if (error instanceof ParamError) {
|
|
@@ -2903,48 +2993,11 @@ async function dbConnect(context, connectionString, name, dbProfile) {
|
|
|
2903
2993
|
throw new ParamError(`[Db] connect error: ${errorMsg}`);
|
|
2904
2994
|
}
|
|
2905
2995
|
}
|
|
2906
|
-
async function dbFindAndConnect(context, dbNameOrConnectionString) {
|
|
2907
|
-
let dbName;
|
|
2908
|
-
let dbConnectionString;
|
|
2909
|
-
let dbProfile;
|
|
2910
|
-
if (dbNameOrConnectionString) {
|
|
2911
|
-
if (dbNameOrConnectionString.match(/^(postgresql|mysql):\/\/[^\s]+:[^\s]+@[^\s]+:\d+\/[^\s]+$/)) {
|
|
2912
|
-
dbName = void 0;
|
|
2913
|
-
dbConnectionString = dbNameOrConnectionString;
|
|
2914
|
-
} else {
|
|
2915
|
-
dbName = dbNameOrConnectionString;
|
|
2916
|
-
}
|
|
2917
|
-
} else {
|
|
2918
|
-
const defs2 = {
|
|
2919
|
-
dbName: "string",
|
|
2920
|
-
dbConnectionString: "string",
|
|
2921
|
-
dbProfile: "boolean default false"
|
|
2922
|
-
};
|
|
2923
|
-
const paramsConfig = context.params.getAll(defs2);
|
|
2924
|
-
dbName = paramsConfig.dbName;
|
|
2925
|
-
dbConnectionString = paramsConfig.dbConnectionString;
|
|
2926
|
-
dbProfile = paramsConfig.dbProfile;
|
|
2927
|
-
}
|
|
2928
|
-
if (!dbName && !dbConnectionString) {
|
|
2929
|
-
throw new ParamError("Db: either dbName or dbConnectionString must be specified");
|
|
2930
|
-
}
|
|
2931
|
-
if (dbName) {
|
|
2932
|
-
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
2933
|
-
dbConnectionString = await context.params.get(paramName, "string");
|
|
2934
|
-
if (!dbConnectionString) {
|
|
2935
|
-
throw new ParamError(
|
|
2936
|
-
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
2937
|
-
);
|
|
2938
|
-
}
|
|
2939
|
-
}
|
|
2940
|
-
const db = await dbConnect(context, dbConnectionString, dbName, dbProfile);
|
|
2941
|
-
return db;
|
|
2942
|
-
}
|
|
2943
|
-
async function dbInit(context, dbNameOrConnectionString) {
|
|
2944
|
-
return await dbFindAndConnect(context, dbNameOrConnectionString);
|
|
2945
|
-
}
|
|
2946
2996
|
|
|
2947
|
-
// src/
|
|
2997
|
+
// src/tasks/index.js
|
|
2998
|
+
import os3 from "os";
|
|
2999
|
+
|
|
3000
|
+
// src/utils/date-utils.js
|
|
2948
3001
|
function isTimestampFolder(folderName) {
|
|
2949
3002
|
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
|
|
2950
3003
|
if (!isoRegex.test(folderName)) {
|
|
@@ -2954,7 +3007,7 @@ function isTimestampFolder(folderName) {
|
|
|
2954
3007
|
return !isNaN(date.getTime()) && date.getTime() > 0;
|
|
2955
3008
|
}
|
|
2956
3009
|
|
|
2957
|
-
// src/utils/fs-utils.
|
|
3010
|
+
// src/utils/fs-utils.js
|
|
2958
3011
|
import fs from "fs";
|
|
2959
3012
|
import path from "path";
|
|
2960
3013
|
async function ensurePath(...pathParts) {
|
|
@@ -2978,7 +3031,7 @@ function getFileExtension(dataType) {
|
|
|
2978
3031
|
}
|
|
2979
3032
|
}
|
|
2980
3033
|
|
|
2981
|
-
// src/utils/os-utils.
|
|
3034
|
+
// src/utils/os-utils.js
|
|
2982
3035
|
import fs2 from "fs";
|
|
2983
3036
|
import path2 from "path";
|
|
2984
3037
|
import { execSync } from "child_process";
|
|
@@ -3007,7 +3060,7 @@ function getFreeDiskSpace(targetPath) {
|
|
|
3007
3060
|
}
|
|
3008
3061
|
}
|
|
3009
3062
|
|
|
3010
|
-
// src/utils/format-utils.
|
|
3063
|
+
// src/utils/format-utils.js
|
|
3011
3064
|
function bytesToHumanReadable(bytes) {
|
|
3012
3065
|
if (bytes === 0) return "0 B";
|
|
3013
3066
|
const k = 1024;
|
|
@@ -3016,7 +3069,7 @@ function bytesToHumanReadable(bytes) {
|
|
|
3016
3069
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
3017
3070
|
}
|
|
3018
3071
|
|
|
3019
|
-
// src/utils/core-utils.
|
|
3072
|
+
// src/utils/core-utils.js
|
|
3020
3073
|
function sleepMs(ms) {
|
|
3021
3074
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3022
3075
|
}
|
|
@@ -3025,14 +3078,82 @@ function toJsonColumn(value) {
|
|
|
3025
3078
|
return JSON.stringify(value);
|
|
3026
3079
|
}
|
|
3027
3080
|
|
|
3028
|
-
// src/tasks/servicesRegistry.
|
|
3029
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
3030
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
3081
|
+
// src/tasks/servicesRegistry.js
|
|
3031
3082
|
import os from "os";
|
|
3032
|
-
import path3 from "path";
|
|
3033
3083
|
|
|
3034
|
-
// src/tasks/taskUtils.
|
|
3084
|
+
// src/tasks/taskUtils.js
|
|
3035
3085
|
import { randomUUID } from "crypto";
|
|
3086
|
+
|
|
3087
|
+
// src/tasks/time-matcher.js
|
|
3088
|
+
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
3089
|
+
function resolveAsterisks(field, range) {
|
|
3090
|
+
return field.includes("*") ? field.replace("*", range) : field;
|
|
3091
|
+
}
|
|
3092
|
+
function resolveRanges(field) {
|
|
3093
|
+
const regex = /(\d+)-(\d+)/;
|
|
3094
|
+
let current = field;
|
|
3095
|
+
while (true) {
|
|
3096
|
+
const match = regex.exec(current);
|
|
3097
|
+
if (!match) break;
|
|
3098
|
+
const raw = match[0];
|
|
3099
|
+
let first = Number(match[1]);
|
|
3100
|
+
let last = Number(match[2]);
|
|
3101
|
+
if (last < first) {
|
|
3102
|
+
[first, last] = [last, first];
|
|
3103
|
+
}
|
|
3104
|
+
const values = [];
|
|
3105
|
+
for (let i = first; i <= last; i += 1) {
|
|
3106
|
+
values.push(i);
|
|
3107
|
+
}
|
|
3108
|
+
current = current.replace(raw, values.join(","));
|
|
3109
|
+
}
|
|
3110
|
+
return current;
|
|
3111
|
+
}
|
|
3112
|
+
function resolveSteps(field) {
|
|
3113
|
+
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
3114
|
+
if (!match) return field;
|
|
3115
|
+
const base = match[1];
|
|
3116
|
+
const step = Number(match[2]);
|
|
3117
|
+
if (!Number.isFinite(step) || step <= 0) return field;
|
|
3118
|
+
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
3119
|
+
}
|
|
3120
|
+
function convertPattern(pattern) {
|
|
3121
|
+
const parts = pattern.trim().split(/\s+/);
|
|
3122
|
+
if (parts.length !== 6) {
|
|
3123
|
+
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
3124
|
+
}
|
|
3125
|
+
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
3126
|
+
}
|
|
3127
|
+
function fieldMatches(field, value) {
|
|
3128
|
+
const allowed = field.split(",").map((v) => Number(v));
|
|
3129
|
+
return allowed.includes(value);
|
|
3130
|
+
}
|
|
3131
|
+
function matchesParsedPattern(parsed, date) {
|
|
3132
|
+
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());
|
|
3133
|
+
}
|
|
3134
|
+
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
3135
|
+
const parsed = convertPattern(pattern);
|
|
3136
|
+
return matchesParsedPattern(parsed, date);
|
|
3137
|
+
}
|
|
3138
|
+
var MS_PER_SECOND = 1e3;
|
|
3139
|
+
var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
|
|
3140
|
+
function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
|
|
3141
|
+
const parsed = convertPattern(pattern);
|
|
3142
|
+
let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
|
|
3143
|
+
const end = t + maxSearchMs;
|
|
3144
|
+
while (t <= end) {
|
|
3145
|
+
const date = new Date(t);
|
|
3146
|
+
if (matchesParsedPattern(parsed, date)) {
|
|
3147
|
+
return date;
|
|
3148
|
+
}
|
|
3149
|
+
t += MS_PER_SECOND;
|
|
3150
|
+
}
|
|
3151
|
+
throw new Error(
|
|
3152
|
+
`nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
|
|
3153
|
+
);
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
// src/tasks/taskUtils.js
|
|
3036
3157
|
function getDb(context) {
|
|
3037
3158
|
const db = context.db;
|
|
3038
3159
|
if (!db) {
|
|
@@ -3040,97 +3161,84 @@ function getDb(context) {
|
|
|
3040
3161
|
}
|
|
3041
3162
|
return db;
|
|
3042
3163
|
}
|
|
3043
|
-
function queueToTableNames(
|
|
3044
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
|
|
3045
|
-
throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
|
|
3046
|
-
}
|
|
3164
|
+
function queueToTableNames(queueName) {
|
|
3047
3165
|
return {
|
|
3048
|
-
tasksTable:
|
|
3049
|
-
historyTable: `${
|
|
3166
|
+
tasksTable: queueName,
|
|
3167
|
+
historyTable: `${queueName}_history`,
|
|
3168
|
+
registryTable: `${queueName}_services_registry`
|
|
3050
3169
|
};
|
|
3051
3170
|
}
|
|
3052
|
-
function
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3171
|
+
function defineTasksTable(t, db, tableNameForIndex) {
|
|
3172
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
3173
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
3174
|
+
t.timestamp("started_at");
|
|
3175
|
+
t.timestamp("completed_at");
|
|
3176
|
+
t.integer("priority").notNullable().defaultTo(50);
|
|
3177
|
+
t.text("schedule");
|
|
3178
|
+
t.timestamp("next_run_at").defaultTo(null);
|
|
3179
|
+
t.timestamp("past_due").defaultTo(null);
|
|
3180
|
+
t.text("name").notNullable();
|
|
3181
|
+
t.text("opid");
|
|
3182
|
+
t.json("params");
|
|
3183
|
+
t.text("service_group");
|
|
3184
|
+
t.integer("instance_number");
|
|
3185
|
+
t.text("service_name");
|
|
3186
|
+
t.text("server_name");
|
|
3187
|
+
t.text("status").notNullable().defaultTo("idle");
|
|
3188
|
+
t.timestamp("status_changed_at").defaultTo(null);
|
|
3189
|
+
t.text("progress");
|
|
3190
|
+
t.boolean("success");
|
|
3191
|
+
t.json("results");
|
|
3192
|
+
t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
|
|
3193
|
+
t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
|
|
3194
|
+
}
|
|
3195
|
+
function taskHistoryInsertFromQueueRow(row, overrides) {
|
|
3196
|
+
const { id, ...snapshot } = row;
|
|
3197
|
+
void id;
|
|
3198
|
+
return {
|
|
3199
|
+
...snapshot,
|
|
3200
|
+
...overrides
|
|
3201
|
+
};
|
|
3057
3202
|
}
|
|
3058
3203
|
async function ensureTaskTables(context, options = {}) {
|
|
3059
|
-
const
|
|
3204
|
+
const queueName = options.queueName ?? "tasks";
|
|
3060
3205
|
const recreate = options.recreate ?? false;
|
|
3061
3206
|
const db = getDb(context);
|
|
3062
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
3207
|
+
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
3063
3208
|
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
3064
3209
|
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
3210
|
+
const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
|
|
3065
3211
|
if (recreate) {
|
|
3066
3212
|
await db.schema.dropTableIfExists(historyTable);
|
|
3067
3213
|
await db.schema.dropTableIfExists(tasksTable);
|
|
3214
|
+
await db.schema.dropTableIfExists(registryTable);
|
|
3068
3215
|
}
|
|
3069
3216
|
if (needsTasks) {
|
|
3070
3217
|
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
3071
3218
|
await db.schema.createTable(tasksTable, (t) => {
|
|
3072
|
-
t
|
|
3073
|
-
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
3074
|
-
t.timestamp("started_at");
|
|
3075
|
-
t.timestamp("completed_at");
|
|
3076
|
-
t.integer("priority").notNullable().defaultTo(0);
|
|
3077
|
-
t.text("schedule");
|
|
3078
|
-
t.timestamp("past_due").defaultTo(null);
|
|
3079
|
-
t.text("target").notNullable();
|
|
3080
|
-
t.text("task").notNullable();
|
|
3081
|
-
t.json("params");
|
|
3082
|
-
t.text("opid");
|
|
3083
|
-
t.timestamp("paused_at").defaultTo(null);
|
|
3084
|
-
t.text("progress");
|
|
3085
|
-
t.boolean("success");
|
|
3086
|
-
t.json("results");
|
|
3087
|
-
});
|
|
3088
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
3089
|
-
t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
|
|
3090
|
-
t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
|
|
3091
|
-
t.index(["target", "task"], `${tasksTable}_target_task_idx`);
|
|
3219
|
+
defineTasksTable(t, db, tasksTable);
|
|
3092
3220
|
});
|
|
3093
3221
|
}
|
|
3094
3222
|
if (needsHistory) {
|
|
3095
3223
|
await db.schema.createTable(historyTable, (t) => {
|
|
3096
|
-
t
|
|
3097
|
-
t.timestamp("created_at").notNullable();
|
|
3098
|
-
t.timestamp("started_at");
|
|
3099
|
-
t.timestamp("completed_at");
|
|
3100
|
-
t.integer("priority").notNullable().defaultTo(0);
|
|
3101
|
-
t.text("schedule");
|
|
3102
|
-
t.timestamp("past_due").defaultTo(null);
|
|
3103
|
-
t.text("target").notNullable();
|
|
3104
|
-
t.text("task").notNullable();
|
|
3105
|
-
t.json("params");
|
|
3106
|
-
t.text("opid");
|
|
3107
|
-
t.text("progress");
|
|
3108
|
-
t.boolean("success");
|
|
3109
|
-
t.json("results");
|
|
3110
|
-
});
|
|
3111
|
-
await db.schema.alterTable(historyTable, (t) => {
|
|
3112
|
-
t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
|
|
3113
|
-
t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
|
|
3224
|
+
defineTasksTable(t, db, historyTable);
|
|
3114
3225
|
});
|
|
3115
3226
|
}
|
|
3116
|
-
const registryTable = servicesRegistryTable(queue);
|
|
3117
|
-
const needsRegistry = !await db.tableExists(registryTable);
|
|
3118
3227
|
if (needsRegistry) {
|
|
3119
3228
|
await db.schema.createTable(registryTable, (t) => {
|
|
3120
3229
|
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
3121
|
-
t.
|
|
3122
|
-
t.text("queue").notNullable();
|
|
3230
|
+
t.text("queue_name").notNullable();
|
|
3123
3231
|
t.text("service_group").notNullable();
|
|
3232
|
+
t.integer("instance_number").notNullable().defaultTo(1);
|
|
3124
3233
|
t.text("service_name").notNullable();
|
|
3125
|
-
t.text("
|
|
3126
|
-
t.text("hostname");
|
|
3234
|
+
t.text("server_name").notNullable();
|
|
3127
3235
|
t.integer("pid");
|
|
3128
3236
|
t.json("metadata");
|
|
3129
3237
|
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
3130
3238
|
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
3131
|
-
t.unique(["
|
|
3132
|
-
t.index(["
|
|
3133
|
-
t.index(["
|
|
3239
|
+
t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
|
|
3240
|
+
t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
|
|
3241
|
+
t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
|
|
3134
3242
|
});
|
|
3135
3243
|
}
|
|
3136
3244
|
}
|
|
@@ -3141,7 +3249,7 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
|
3141
3249
|
});
|
|
3142
3250
|
}
|
|
3143
3251
|
|
|
3144
|
-
// src/tasks/servicesRegistry.
|
|
3252
|
+
// src/tasks/servicesRegistry.js
|
|
3145
3253
|
function getDb2(context) {
|
|
3146
3254
|
const db = context.db;
|
|
3147
3255
|
if (!db) {
|
|
@@ -3152,6 +3260,7 @@ function getDb2(context) {
|
|
|
3152
3260
|
var DEFAULT_GROUP_MAX_INSTANCES = {
|
|
3153
3261
|
intake: 1,
|
|
3154
3262
|
harvest: 1,
|
|
3263
|
+
harvester: 0,
|
|
3155
3264
|
loader: 0,
|
|
3156
3265
|
photos: 0,
|
|
3157
3266
|
photosprocessor: 0,
|
|
@@ -3161,25 +3270,6 @@ function sanitizeNamePart(raw) {
|
|
|
3161
3270
|
const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3162
3271
|
return s.slice(0, 80) || "runner";
|
|
3163
3272
|
}
|
|
3164
|
-
function identityFilePath(identityDir, queue, serviceGroup) {
|
|
3165
|
-
const safeQ = sanitizeNamePart(queue);
|
|
3166
|
-
const safeG = sanitizeNamePart(serviceGroup);
|
|
3167
|
-
return path3.join(identityDir, `${safeQ}_${safeG}.json`);
|
|
3168
|
-
}
|
|
3169
|
-
async function readIdentityFile(filePath) {
|
|
3170
|
-
try {
|
|
3171
|
-
const text = await readFile(filePath, "utf8");
|
|
3172
|
-
const parsed = JSON.parse(text);
|
|
3173
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
3174
|
-
} catch {
|
|
3175
|
-
return {};
|
|
3176
|
-
}
|
|
3177
|
-
}
|
|
3178
|
-
async function writeIdentityFile(filePath, data) {
|
|
3179
|
-
await mkdir(path3.dirname(filePath), { recursive: true });
|
|
3180
|
-
await writeFile(filePath, `${JSON.stringify(data, null, 2)}
|
|
3181
|
-
`, "utf8");
|
|
3182
|
-
}
|
|
3183
3273
|
function resolveMaxInstances(serviceGroup, override) {
|
|
3184
3274
|
if (override !== void 0 && Number.isFinite(override)) {
|
|
3185
3275
|
return Math.max(0, Math.floor(Number(override)));
|
|
@@ -3187,184 +3277,198 @@ function resolveMaxInstances(serviceGroup, override) {
|
|
|
3187
3277
|
const g = serviceGroup.trim().toLowerCase();
|
|
3188
3278
|
return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
|
|
3189
3279
|
}
|
|
3190
|
-
async function countAliveInGroup(db, registryTable,
|
|
3280
|
+
async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
|
|
3191
3281
|
const cutoff = new Date(Date.now() - staleMs);
|
|
3192
|
-
let q = db(registryTable).where({
|
|
3193
|
-
if (
|
|
3194
|
-
q = q.whereNot("
|
|
3282
|
+
let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
|
|
3283
|
+
if (excludeRowId) {
|
|
3284
|
+
q = q.whereNot("id", excludeRowId);
|
|
3195
3285
|
}
|
|
3196
3286
|
const row = await q.count("id as count").first();
|
|
3197
3287
|
return Number(row?.count ?? 0);
|
|
3198
3288
|
}
|
|
3289
|
+
async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
|
|
3290
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
3291
|
+
const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
|
|
3292
|
+
const set = /* @__PURE__ */ new Set();
|
|
3293
|
+
for (const r of rows) {
|
|
3294
|
+
const n = Number(r.instance_number);
|
|
3295
|
+
if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
|
|
3296
|
+
}
|
|
3297
|
+
return set;
|
|
3298
|
+
}
|
|
3199
3299
|
function isUniqueViolation(error) {
|
|
3200
3300
|
const code = error?.code ?? error?.errno;
|
|
3201
3301
|
return code === "23505" || String(error?.message || "").includes("duplicate key");
|
|
3202
3302
|
}
|
|
3303
|
+
function buildMetadata(options) {
|
|
3304
|
+
const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
|
|
3305
|
+
if (options.target) {
|
|
3306
|
+
base.runnerTarget = options.target;
|
|
3307
|
+
}
|
|
3308
|
+
return toJsonColumn(Object.keys(base).length ? base : null);
|
|
3309
|
+
}
|
|
3310
|
+
function allocateInstanceNumber(occupied, explicit, maxSlots) {
|
|
3311
|
+
if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
|
|
3312
|
+
const e = Math.max(1, Math.floor(Number(explicit)));
|
|
3313
|
+
if (occupied.has(e)) {
|
|
3314
|
+
throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
|
|
3315
|
+
}
|
|
3316
|
+
if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
|
|
3317
|
+
throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
|
|
3318
|
+
}
|
|
3319
|
+
return e;
|
|
3320
|
+
}
|
|
3321
|
+
const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
|
|
3322
|
+
for (let n = 1; n <= cap; n++) {
|
|
3323
|
+
if (!occupied.has(n)) return n;
|
|
3324
|
+
}
|
|
3325
|
+
throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
|
|
3326
|
+
}
|
|
3327
|
+
function defaultServiceName(groupBase, hostBase, instanceNumber) {
|
|
3328
|
+
return `${groupBase}-${hostBase}-${instanceNumber}`;
|
|
3329
|
+
}
|
|
3203
3330
|
async function registerInServicesRegistry(context, options) {
|
|
3204
3331
|
const db = getDb2(context);
|
|
3205
|
-
const registryTable =
|
|
3332
|
+
const registryTable = queueToTableNames(options.queueName).registryTable;
|
|
3206
3333
|
const serviceGroup = options.serviceGroup.trim();
|
|
3207
3334
|
if (!serviceGroup) {
|
|
3208
3335
|
throw new Error("registerInServicesRegistry: serviceGroup is required");
|
|
3209
3336
|
}
|
|
3210
|
-
const
|
|
3211
|
-
let identity = await readIdentityFile(identityPath);
|
|
3212
|
-
let instanceId = typeof identity.instanceId === "string" && identity.instanceId.trim() ? identity.instanceId.trim() : randomUUID2();
|
|
3213
|
-
identity.instanceId = instanceId;
|
|
3214
|
-
await writeIdentityFile(identityPath, identity);
|
|
3215
|
-
const hostname = os.hostname();
|
|
3337
|
+
const serverName = os.hostname();
|
|
3216
3338
|
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
3217
|
-
const meta =
|
|
3218
|
-
const
|
|
3219
|
-
|
|
3220
|
-
await db(registryTable).where({ instance_id: instanceId }).update({
|
|
3221
|
-
target: options.target,
|
|
3222
|
-
hostname,
|
|
3223
|
-
pid,
|
|
3224
|
-
metadata: meta,
|
|
3225
|
-
last_seen_at: db.fn.now()
|
|
3226
|
-
});
|
|
3227
|
-
const serviceName = String(existing.service_name);
|
|
3228
|
-
identity.serviceName = serviceName;
|
|
3229
|
-
await writeIdentityFile(identityPath, identity);
|
|
3230
|
-
const reg = {
|
|
3231
|
-
instanceId,
|
|
3232
|
-
serviceName,
|
|
3233
|
-
serviceGroup,
|
|
3234
|
-
queue: options.queue,
|
|
3235
|
-
target: options.target,
|
|
3236
|
-
rowId: String(existing.id)
|
|
3237
|
-
};
|
|
3238
|
-
context.servicesRegistry = reg;
|
|
3239
|
-
context.runnerHeartbeat = reg;
|
|
3240
|
-
context.logger.info?.(
|
|
3241
|
-
`[services-registry] resumed instance_id=${instanceId} name=${serviceName} group=${serviceGroup} queue=${options.queue}`
|
|
3242
|
-
);
|
|
3243
|
-
return {
|
|
3244
|
-
instanceId,
|
|
3245
|
-
serviceName,
|
|
3246
|
-
serviceGroup,
|
|
3247
|
-
queue: options.queue,
|
|
3248
|
-
target: options.target,
|
|
3249
|
-
rowId: String(existing.id),
|
|
3250
|
-
registryTable
|
|
3251
|
-
};
|
|
3252
|
-
}
|
|
3339
|
+
const meta = buildMetadata(options);
|
|
3340
|
+
const groupBase = sanitizeNamePart(serviceGroup);
|
|
3341
|
+
const hostBase = sanitizeNamePart(serverName);
|
|
3253
3342
|
const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
|
|
3254
|
-
const
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
options.queue,
|
|
3258
|
-
serviceGroup,
|
|
3259
|
-
options.staleMs,
|
|
3260
|
-
instanceId
|
|
3261
|
-
);
|
|
3262
|
-
if (maxAllowed > 0 && aliveOthers >= maxAllowed) {
|
|
3263
|
-
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveOthers} alive (max ${maxAllowed}, queue=${options.queue}).`;
|
|
3343
|
+
const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
|
|
3344
|
+
if (maxAllowed > 0 && aliveCount >= maxAllowed) {
|
|
3345
|
+
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
|
|
3264
3346
|
if (options.enforceMaxInstances) {
|
|
3265
3347
|
throw new Error(msg);
|
|
3266
3348
|
}
|
|
3267
3349
|
context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
|
|
3268
3350
|
}
|
|
3269
|
-
const
|
|
3270
|
-
const
|
|
3271
|
-
const
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3351
|
+
const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
|
|
3352
|
+
const cutoff = new Date(Date.now() - options.staleMs);
|
|
3353
|
+
const MAX_ATTEMPTS = 8;
|
|
3354
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
3355
|
+
const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
|
|
3356
|
+
const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
|
|
3357
|
+
const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
|
|
3358
|
+
const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
3359
|
+
if (existing) {
|
|
3360
|
+
const lastSeen = new Date(existing.last_seen_at);
|
|
3361
|
+
const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
|
|
3362
|
+
if (isAlive) {
|
|
3363
|
+
if (options.serviceName?.trim()) {
|
|
3364
|
+
throw new Error(
|
|
3365
|
+
`[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
|
|
3366
|
+
);
|
|
3367
|
+
}
|
|
3368
|
+
context.logger.warn?.(
|
|
3369
|
+
`[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
|
|
3370
|
+
);
|
|
3371
|
+
if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
|
|
3372
|
+
throw new Error(
|
|
3373
|
+
`[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
|
|
3374
|
+
);
|
|
3375
|
+
}
|
|
3376
|
+
await new Promise((r) => setTimeout(r, 50 + attempt * 30));
|
|
3377
|
+
continue;
|
|
3289
3378
|
}
|
|
3379
|
+
await db(registryTable).where({ id: existing.id }).update({
|
|
3380
|
+
server_name: serverName,
|
|
3381
|
+
pid,
|
|
3382
|
+
metadata: meta,
|
|
3383
|
+
service_group: serviceGroup,
|
|
3384
|
+
instance_number: instanceNumber,
|
|
3385
|
+
last_seen_at: db.fn.now()
|
|
3386
|
+
});
|
|
3387
|
+
const reg = {
|
|
3388
|
+
serviceName: serviceNameRaw,
|
|
3389
|
+
serviceGroup,
|
|
3390
|
+
queueName: options.queueName,
|
|
3391
|
+
target: options.target,
|
|
3392
|
+
rowId: String(existing.id),
|
|
3393
|
+
registryTable,
|
|
3394
|
+
instanceNumber
|
|
3395
|
+
};
|
|
3396
|
+
context.servicesRegistry = reg;
|
|
3397
|
+
context.runnerHeartbeat = reg;
|
|
3398
|
+
context.logger.info?.(
|
|
3399
|
+
`[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
3400
|
+
);
|
|
3401
|
+
return reg;
|
|
3290
3402
|
}
|
|
3291
|
-
}
|
|
3292
|
-
let inserted;
|
|
3293
|
-
for (const candidate of eachServiceNameCandidate(baseCandidates)) {
|
|
3294
3403
|
try {
|
|
3295
3404
|
const rows = await db(registryTable).insert({
|
|
3296
|
-
|
|
3297
|
-
queue: options.queue,
|
|
3405
|
+
queue_name: options.queueName,
|
|
3298
3406
|
service_group: serviceGroup,
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3407
|
+
instance_number: instanceNumber,
|
|
3408
|
+
service_name: serviceNameRaw,
|
|
3409
|
+
server_name: serverName,
|
|
3302
3410
|
pid,
|
|
3303
3411
|
metadata: meta,
|
|
3304
|
-
last_seen_at: db.fn.now()
|
|
3412
|
+
last_seen_at: db.fn.now(),
|
|
3413
|
+
created_at: db.fn.now()
|
|
3305
3414
|
}).returning(["id", "service_name"]);
|
|
3306
3415
|
const row = Array.isArray(rows) ? rows[0] : rows;
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3416
|
+
let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
|
|
3417
|
+
if (!rowId) {
|
|
3418
|
+
const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
3419
|
+
rowId = again?.id != null ? String(again.id) : "";
|
|
3310
3420
|
}
|
|
3421
|
+
if (!rowId) continue;
|
|
3422
|
+
const regNew = {
|
|
3423
|
+
serviceName: String(row?.service_name ?? serviceNameRaw),
|
|
3424
|
+
serviceGroup,
|
|
3425
|
+
queueName: options.queueName,
|
|
3426
|
+
target: options.target,
|
|
3427
|
+
rowId,
|
|
3428
|
+
registryTable,
|
|
3429
|
+
instanceNumber
|
|
3430
|
+
};
|
|
3431
|
+
context.servicesRegistry = regNew;
|
|
3432
|
+
context.runnerHeartbeat = regNew;
|
|
3433
|
+
context.logger.info?.(
|
|
3434
|
+
`[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
3435
|
+
);
|
|
3436
|
+
return regNew;
|
|
3311
3437
|
} catch (error) {
|
|
3312
3438
|
if (!isUniqueViolation(error)) {
|
|
3313
3439
|
throw error;
|
|
3314
3440
|
}
|
|
3441
|
+
context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
|
|
3315
3442
|
}
|
|
3316
3443
|
}
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
`[services-registry] could not allocate a unique service_name for group=${serviceGroup} queue=${options.queue} (too many collisions).`
|
|
3320
|
-
);
|
|
3321
|
-
}
|
|
3322
|
-
identity.serviceName = inserted.service_name;
|
|
3323
|
-
await writeIdentityFile(identityPath, identity);
|
|
3324
|
-
const regNew = {
|
|
3325
|
-
instanceId,
|
|
3326
|
-
serviceName: inserted.service_name,
|
|
3327
|
-
serviceGroup,
|
|
3328
|
-
queue: options.queue,
|
|
3329
|
-
target: options.target,
|
|
3330
|
-
rowId: inserted.id
|
|
3331
|
-
};
|
|
3332
|
-
context.servicesRegistry = regNew;
|
|
3333
|
-
context.runnerHeartbeat = regNew;
|
|
3334
|
-
context.logger.info?.(
|
|
3335
|
-
`[services-registry] registered instance_id=${instanceId} name=${inserted.service_name} group=${serviceGroup} queue=${options.queue} target=${options.target}`
|
|
3444
|
+
throw new Error(
|
|
3445
|
+
`[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
|
|
3336
3446
|
);
|
|
3337
|
-
return {
|
|
3338
|
-
instanceId,
|
|
3339
|
-
serviceName: inserted.service_name,
|
|
3340
|
-
serviceGroup,
|
|
3341
|
-
queue: options.queue,
|
|
3342
|
-
target: options.target,
|
|
3343
|
-
rowId: inserted.id,
|
|
3344
|
-
registryTable
|
|
3345
|
-
};
|
|
3346
3447
|
}
|
|
3347
3448
|
async function touchServicesRegistry(context, registration) {
|
|
3348
3449
|
const db = getDb2(context);
|
|
3349
|
-
const
|
|
3450
|
+
const serverName = os.hostname();
|
|
3350
3451
|
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
3351
|
-
await db(registration.registryTable).where({
|
|
3452
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
3352
3453
|
last_seen_at: db.fn.now(),
|
|
3353
|
-
|
|
3454
|
+
server_name: serverName,
|
|
3354
3455
|
pid
|
|
3355
3456
|
});
|
|
3356
3457
|
}
|
|
3357
3458
|
async function unregisterServicesRegistry(context, registration) {
|
|
3358
3459
|
const db = getDb2(context);
|
|
3359
|
-
await db(registration.registryTable).where({
|
|
3360
|
-
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName}
|
|
3460
|
+
await db(registration.registryTable).where({ id: registration.rowId }).delete();
|
|
3461
|
+
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
|
|
3361
3462
|
}
|
|
3362
3463
|
|
|
3363
|
-
// src/
|
|
3364
|
-
import fs3 from "fs";
|
|
3464
|
+
// src/tasks/taskLogs.js
|
|
3365
3465
|
import path4 from "path";
|
|
3366
3466
|
|
|
3367
|
-
// src/filedatabase/
|
|
3467
|
+
// src/filedatabase/index.js
|
|
3468
|
+
import fs3 from "fs";
|
|
3469
|
+
import path3 from "path";
|
|
3470
|
+
|
|
3471
|
+
// src/filedatabase/serializers.js
|
|
3368
3472
|
function detectDataType(data) {
|
|
3369
3473
|
if (Array.isArray(data)) {
|
|
3370
3474
|
return "json-array";
|
|
@@ -3396,7 +3500,7 @@ function deserializeData(rawData, dataType) {
|
|
|
3396
3500
|
}
|
|
3397
3501
|
}
|
|
3398
3502
|
|
|
3399
|
-
// src/filedatabase/index.
|
|
3503
|
+
// src/filedatabase/index.js
|
|
3400
3504
|
var FileDatabase = class _FileDatabase {
|
|
3401
3505
|
basePath;
|
|
3402
3506
|
namespace;
|
|
@@ -3482,14 +3586,14 @@ var FileDatabase = class _FileDatabase {
|
|
|
3482
3586
|
if (errors.length) {
|
|
3483
3587
|
throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
|
|
3484
3588
|
}
|
|
3485
|
-
|
|
3589
|
+
const parts = [this.basePath, this.namespace];
|
|
3486
3590
|
if (this.tableName) {
|
|
3487
3591
|
parts.push(...this.tableName.split("/"));
|
|
3488
3592
|
}
|
|
3489
3593
|
if (this.versioned && version) {
|
|
3490
3594
|
parts.push(version);
|
|
3491
3595
|
}
|
|
3492
|
-
return
|
|
3596
|
+
return path3.resolve(...parts);
|
|
3493
3597
|
}
|
|
3494
3598
|
/**
|
|
3495
3599
|
* Set current version and version folder
|
|
@@ -3526,7 +3630,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3526
3630
|
this.currentFileNumber = 0;
|
|
3527
3631
|
const versions = await this.getVersions();
|
|
3528
3632
|
while (versions.length > this.maxVersions) {
|
|
3529
|
-
const versionToDelete =
|
|
3633
|
+
const versionToDelete = path3.resolve(this.getDestinationPath(), versions.shift());
|
|
3530
3634
|
this.logger.silly?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
|
|
3531
3635
|
await fs3.promises.rm(versionToDelete, { recursive: true, force: true });
|
|
3532
3636
|
}
|
|
@@ -3545,7 +3649,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3545
3649
|
await ensurePath(destPath);
|
|
3546
3650
|
const items = await fs3.promises.readdir(destPath);
|
|
3547
3651
|
const versions = items.filter((item) => {
|
|
3548
|
-
const itemPath =
|
|
3652
|
+
const itemPath = path3.join(destPath, item);
|
|
3549
3653
|
const stat = fs3.statSync(itemPath);
|
|
3550
3654
|
return stat.isDirectory() && isTimestampFolder(item);
|
|
3551
3655
|
});
|
|
@@ -3602,7 +3706,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3602
3706
|
const items = await fs3.promises.readdir(tablePath);
|
|
3603
3707
|
if (items.includes("metadata.json")) {
|
|
3604
3708
|
const metadata = JSON.parse(
|
|
3605
|
-
await fs3.promises.readFile(
|
|
3709
|
+
await fs3.promises.readFile(path3.join(tablePath, "metadata.json"), "utf8")
|
|
3606
3710
|
);
|
|
3607
3711
|
return {
|
|
3608
3712
|
versioned: false,
|
|
@@ -3611,13 +3715,13 @@ var FileDatabase = class _FileDatabase {
|
|
|
3611
3715
|
};
|
|
3612
3716
|
}
|
|
3613
3717
|
const versionFolders = items.filter((item) => {
|
|
3614
|
-
const itemPath =
|
|
3718
|
+
const itemPath = path3.join(tablePath, item);
|
|
3615
3719
|
const stat = fs3.statSync(itemPath);
|
|
3616
3720
|
return stat.isDirectory() && isTimestampFolder(item);
|
|
3617
3721
|
});
|
|
3618
3722
|
if (versionFolders.length > 0) {
|
|
3619
3723
|
const latestVersion = versionFolders.sort().pop();
|
|
3620
|
-
const versionMetadataPath =
|
|
3724
|
+
const versionMetadataPath = path3.join(tablePath, latestVersion, "metadata.json");
|
|
3621
3725
|
return {
|
|
3622
3726
|
versioned: true,
|
|
3623
3727
|
hasMetadata: fs3.existsSync(versionMetadataPath),
|
|
@@ -3638,7 +3742,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3638
3742
|
* Load metadata from JSON file
|
|
3639
3743
|
*/
|
|
3640
3744
|
async loadMetadataJson(version) {
|
|
3641
|
-
const metadataFile =
|
|
3745
|
+
const metadataFile = path3.join(this.getDestinationPath(), version, "metadata.json");
|
|
3642
3746
|
if (fs3.existsSync(metadataFile)) {
|
|
3643
3747
|
try {
|
|
3644
3748
|
const rawData = await fs3.promises.readFile(metadataFile, "utf8");
|
|
@@ -3654,7 +3758,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3654
3758
|
* Reads all files to get accurate counts - used when synopsis calculation is needed
|
|
3655
3759
|
*/
|
|
3656
3760
|
async figureMetadataFromVersionFiles(version) {
|
|
3657
|
-
const versionPath =
|
|
3761
|
+
const versionPath = path3.join(this.getDestinationPath(), version);
|
|
3658
3762
|
if (!fs3.existsSync(versionPath)) {
|
|
3659
3763
|
return this.getDefaultMetadata();
|
|
3660
3764
|
}
|
|
@@ -3666,10 +3770,10 @@ var FileDatabase = class _FileDatabase {
|
|
|
3666
3770
|
let detectedDataType = null;
|
|
3667
3771
|
for (let i = 0; i < files.length; i++) {
|
|
3668
3772
|
const fileName = files[i];
|
|
3669
|
-
const filePath =
|
|
3773
|
+
const filePath = path3.join(versionPath, fileName);
|
|
3670
3774
|
try {
|
|
3671
3775
|
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
3672
|
-
const extension =
|
|
3776
|
+
const extension = path3.extname(fileName).toLowerCase();
|
|
3673
3777
|
let dataType = "text";
|
|
3674
3778
|
if (extension === ".json") {
|
|
3675
3779
|
dataType = "json-array";
|
|
@@ -3702,7 +3806,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3702
3806
|
* Much faster for large datasets with many files
|
|
3703
3807
|
*/
|
|
3704
3808
|
async buildMetadataOptimized(version) {
|
|
3705
|
-
const versionPath =
|
|
3809
|
+
const versionPath = path3.join(this.getDestinationPath(), version);
|
|
3706
3810
|
if (!fs3.existsSync(versionPath)) {
|
|
3707
3811
|
return this.getDefaultMetadata();
|
|
3708
3812
|
}
|
|
@@ -3718,7 +3822,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3718
3822
|
fileName
|
|
3719
3823
|
}));
|
|
3720
3824
|
const firstFile = metadata.files[0];
|
|
3721
|
-
const firstFilePath =
|
|
3825
|
+
const firstFilePath = path3.join(versionPath, firstFile.fileName);
|
|
3722
3826
|
const firstFileRaw = await fs3.promises.readFile(firstFilePath, "utf8");
|
|
3723
3827
|
let firstFileData;
|
|
3724
3828
|
try {
|
|
@@ -3735,7 +3839,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3735
3839
|
}
|
|
3736
3840
|
if (files.length > 1) {
|
|
3737
3841
|
const lastFile = metadata.files[metadata.files.length - 1];
|
|
3738
|
-
const lastFilePath =
|
|
3842
|
+
const lastFilePath = path3.join(versionPath, lastFile.fileName);
|
|
3739
3843
|
const lastFileRaw = await fs3.promises.readFile(lastFilePath, "utf8");
|
|
3740
3844
|
const lastFileData = deserializeData(lastFileRaw, metadata.dataType);
|
|
3741
3845
|
lastFile.recordsCount = Array.isArray(lastFileData) ? lastFileData.length : 1;
|
|
@@ -3786,9 +3890,9 @@ var FileDatabase = class _FileDatabase {
|
|
|
3786
3890
|
if (!this.currentVersion) {
|
|
3787
3891
|
return;
|
|
3788
3892
|
}
|
|
3789
|
-
metadataFile =
|
|
3893
|
+
metadataFile = path3.join(this.getDestinationPath(), this.currentVersion, "metadata.json");
|
|
3790
3894
|
} else {
|
|
3791
|
-
metadataFile =
|
|
3895
|
+
metadataFile = path3.join(this.getDestinationPath(), "metadata.json");
|
|
3792
3896
|
}
|
|
3793
3897
|
await fs3.promises.writeFile(metadataFile, JSON.stringify(metadataToSave, null, 4), "utf8");
|
|
3794
3898
|
}
|
|
@@ -3851,7 +3955,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3851
3955
|
}
|
|
3852
3956
|
}
|
|
3853
3957
|
if (!Array.isArray(data) && !forceNewFile) {
|
|
3854
|
-
const lastFileExtension =
|
|
3958
|
+
const lastFileExtension = path3.extname(lastFile.fileName);
|
|
3855
3959
|
const expectedExtension = `.${getFileExtension(incomingDataType)}`;
|
|
3856
3960
|
if (lastFileExtension !== expectedExtension) {
|
|
3857
3961
|
if (lastFileRecordsCount > 0) {
|
|
@@ -3861,7 +3965,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3861
3965
|
}
|
|
3862
3966
|
}
|
|
3863
3967
|
} else if (!Array.isArray(data) && forceNewFile) {
|
|
3864
|
-
const lastFileExtension =
|
|
3968
|
+
const lastFileExtension = path3.extname(lastFile.fileName);
|
|
3865
3969
|
const expectedExtension = `.${getFileExtension(incomingDataType)}`;
|
|
3866
3970
|
if (lastFileExtension !== expectedExtension) {
|
|
3867
3971
|
lastFile.fileName = `${this.currentFileNumber.toString().padStart(6, "0")}.${getFileExtension(incomingDataType)}`;
|
|
@@ -3951,7 +4055,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3951
4055
|
*/
|
|
3952
4056
|
async safeWrite(filePath, data) {
|
|
3953
4057
|
const serializedData = serializeData(data);
|
|
3954
|
-
const dir =
|
|
4058
|
+
const dir = path3.dirname(filePath);
|
|
3955
4059
|
const requiredBytes = Buffer.byteLength(serializedData, "utf8");
|
|
3956
4060
|
const freeBytes = getFreeDiskSpace(dir);
|
|
3957
4061
|
if (freeBytes !== null) {
|
|
@@ -3975,14 +4079,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
3975
4079
|
* Prepare the instance for read or write operations
|
|
3976
4080
|
* This discovers state and sets up internal members based on mode and current data
|
|
3977
4081
|
*/
|
|
3978
|
-
async prepare(
|
|
4082
|
+
async prepare(options) {
|
|
4083
|
+
const { write, read, version, deferInitialVersion } = options;
|
|
3979
4084
|
if (write) {
|
|
3980
4085
|
if (this.versioned) {
|
|
3981
4086
|
if (this.currentVersion === null) {
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
4087
|
+
if (!deferInitialVersion) {
|
|
4088
|
+
await this.makeNewVersion();
|
|
4089
|
+
this.metadata = this.getDefaultMetadata();
|
|
4090
|
+
this.metadata.version = this.currentVersion;
|
|
4091
|
+
this.makeNewFile();
|
|
4092
|
+
}
|
|
3986
4093
|
} else {
|
|
3987
4094
|
if (!this.metadata.files.length) {
|
|
3988
4095
|
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
@@ -3996,7 +4103,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3996
4103
|
} else {
|
|
3997
4104
|
await ensurePath(this.getDestinationPath());
|
|
3998
4105
|
if (this.useMetadata === true) {
|
|
3999
|
-
const metadataPath =
|
|
4106
|
+
const metadataPath = path3.join(this.getDestinationPath(), "metadata.json");
|
|
4000
4107
|
if (fs3.existsSync(metadataPath)) {
|
|
4001
4108
|
try {
|
|
4002
4109
|
const rawData = await fs3.promises.readFile(metadataPath, "utf8");
|
|
@@ -4049,7 +4156,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4049
4156
|
}
|
|
4050
4157
|
if (this.useMetadata) {
|
|
4051
4158
|
const destPath = this.getDestinationPath();
|
|
4052
|
-
const metadataPath =
|
|
4159
|
+
const metadataPath = path3.join(destPath, "metadata.json");
|
|
4053
4160
|
if (fs3.existsSync(metadataPath)) {
|
|
4054
4161
|
try {
|
|
4055
4162
|
const rawData = await fs3.promises.readFile(metadataPath, "utf8");
|
|
@@ -4085,14 +4192,14 @@ var FileDatabase = class _FileDatabase {
|
|
|
4085
4192
|
if (options.filename) {
|
|
4086
4193
|
const destPath2 = this.getDestinationPath();
|
|
4087
4194
|
await ensurePath(destPath2);
|
|
4088
|
-
const filePath =
|
|
4195
|
+
const filePath = path3.join(destPath2, options.filename);
|
|
4089
4196
|
await this.safeWrite(filePath, data);
|
|
4090
4197
|
return;
|
|
4091
4198
|
}
|
|
4092
4199
|
if (options.forceNewVersion && !this.versioned) {
|
|
4093
4200
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
4094
4201
|
}
|
|
4095
|
-
await this.prepare({ write: true });
|
|
4202
|
+
await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
|
|
4096
4203
|
const incomingDataType = detectDataType(data);
|
|
4097
4204
|
this.metadata.dataType = incomingDataType;
|
|
4098
4205
|
if (options.forceNewVersion) {
|
|
@@ -4134,11 +4241,11 @@ var FileDatabase = class _FileDatabase {
|
|
|
4134
4241
|
const forceNewFile = hasCustomMetadata && targetFileIndex === null;
|
|
4135
4242
|
let { dataToWrite, dataLeftOver, fileName } = this.figureOutDataAndFileToWrite(data, targetFileIndex, forceNewFile);
|
|
4136
4243
|
const destPath = this.getDestinationPath(this.currentVersion || void 0);
|
|
4137
|
-
await this.safeWrite(
|
|
4244
|
+
await this.safeWrite(path3.join(destPath, fileName), dataToWrite);
|
|
4138
4245
|
this.updateMetadata(dataToWrite, fileName, options.customMetadata);
|
|
4139
4246
|
while (dataLeftOver && dataLeftOver.length > 0 && targetFileIndex === null) {
|
|
4140
4247
|
const writeContext = this.figureOutDataAndFileToWrite(dataLeftOver);
|
|
4141
|
-
await this.safeWrite(
|
|
4248
|
+
await this.safeWrite(path3.join(destPath, writeContext.fileName), writeContext.dataToWrite);
|
|
4142
4249
|
this.updateMetadata(writeContext.dataToWrite, writeContext.fileName, options.customMetadata);
|
|
4143
4250
|
dataLeftOver = writeContext.dataLeftOver;
|
|
4144
4251
|
}
|
|
@@ -4154,7 +4261,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4154
4261
|
const { version, nextPage = false, pageSize, filename } = options;
|
|
4155
4262
|
if (filename) {
|
|
4156
4263
|
const destPath = this.getDestinationPath(version);
|
|
4157
|
-
const filePath =
|
|
4264
|
+
const filePath = path3.join(destPath, filename);
|
|
4158
4265
|
try {
|
|
4159
4266
|
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
4160
4267
|
return JSON.parse(rawData);
|
|
@@ -4166,7 +4273,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4166
4273
|
const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
|
|
4167
4274
|
if (isNonPaginatedData) {
|
|
4168
4275
|
const file = this.metadata.files[0];
|
|
4169
|
-
const filePath =
|
|
4276
|
+
const filePath = path3.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
|
|
4170
4277
|
try {
|
|
4171
4278
|
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
4172
4279
|
return deserializeData(rawData, this.metadata.dataType);
|
|
@@ -4204,7 +4311,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4204
4311
|
let cumulativeRecords = currentFileOffset;
|
|
4205
4312
|
for (let i = currentFileIndex; i < this.metadata.files.length && recordsRead < effectivePageSize; i++) {
|
|
4206
4313
|
const file = this.metadata.files[i];
|
|
4207
|
-
const filePath =
|
|
4314
|
+
const filePath = path3.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
|
|
4208
4315
|
try {
|
|
4209
4316
|
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
4210
4317
|
const fileData = deserializeData(rawData, this.metadata.dataType);
|
|
@@ -4248,7 +4355,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4248
4355
|
* Returns data file names (.json, .txt, .xml) excluding metadata.json.
|
|
4249
4356
|
*/
|
|
4250
4357
|
async listFilenames() {
|
|
4251
|
-
const destPath = this.versioned && this.currentVersion ?
|
|
4358
|
+
const destPath = this.versioned && this.currentVersion ? path3.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
4252
4359
|
try {
|
|
4253
4360
|
const entries = await fs3.promises.readdir(destPath, { withFileTypes: true });
|
|
4254
4361
|
return entries.filter((e) => e.isFile() && e.name !== "metadata.json" && /\.(json|txt|xml)$/i.test(e.name)).map((e) => e.name);
|
|
@@ -4262,8 +4369,8 @@ var FileDatabase = class _FileDatabase {
|
|
|
4262
4369
|
* Use with listFilenames() to manage individual files.
|
|
4263
4370
|
*/
|
|
4264
4371
|
async removeFile(filename) {
|
|
4265
|
-
const destPath = this.versioned && this.currentVersion ?
|
|
4266
|
-
const filePath =
|
|
4372
|
+
const destPath = this.versioned && this.currentVersion ? path3.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
4373
|
+
const filePath = path3.join(destPath, filename);
|
|
4267
4374
|
try {
|
|
4268
4375
|
await fs3.promises.unlink(filePath);
|
|
4269
4376
|
} catch (err) {
|
|
@@ -4289,7 +4396,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4289
4396
|
this.metadata.files.splice(idx, 1);
|
|
4290
4397
|
this.metadata.totalRecords = Math.max(0, (this.metadata.totalRecords || 0) - recordsCount);
|
|
4291
4398
|
const destPath = this.getDestinationPath();
|
|
4292
|
-
const filePath =
|
|
4399
|
+
const filePath = path3.join(destPath, filename);
|
|
4293
4400
|
try {
|
|
4294
4401
|
await fs3.promises.unlink(filePath);
|
|
4295
4402
|
} catch (err) {
|
|
@@ -4345,7 +4452,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4345
4452
|
});
|
|
4346
4453
|
if (matches) {
|
|
4347
4454
|
const destPath = this.getDestinationPath();
|
|
4348
|
-
const filePath =
|
|
4455
|
+
const filePath = path3.join(destPath, fileEntry.fileName);
|
|
4349
4456
|
const fileData = await fs3.promises.readFile(filePath, "utf8");
|
|
4350
4457
|
const data = deserializeData(fileData, metadata.dataType || "json-object");
|
|
4351
4458
|
results.push({
|
|
@@ -4368,7 +4475,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4368
4475
|
});
|
|
4369
4476
|
if (matches) {
|
|
4370
4477
|
const destPath = this.getDestinationPath(version);
|
|
4371
|
-
const filePath =
|
|
4478
|
+
const filePath = path3.join(destPath, fileEntry.fileName);
|
|
4372
4479
|
const fileData = await fs3.promises.readFile(filePath, "utf8");
|
|
4373
4480
|
const data = deserializeData(fileData, metadata.dataType || "json-object");
|
|
4374
4481
|
results.push({
|
|
@@ -4386,7 +4493,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4386
4493
|
}
|
|
4387
4494
|
};
|
|
4388
4495
|
|
|
4389
|
-
// src/tasks/taskLogs.
|
|
4496
|
+
// src/tasks/taskLogs.js
|
|
4390
4497
|
function getLogsState(context) {
|
|
4391
4498
|
const holder = context;
|
|
4392
4499
|
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
@@ -4439,33 +4546,128 @@ function getLogsState(context) {
|
|
|
4439
4546
|
holder.__tasksLogsState = state;
|
|
4440
4547
|
return state;
|
|
4441
4548
|
}
|
|
4442
|
-
function
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
if (level === "error" || level === "fatal") return true;
|
|
4447
|
-
if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
|
|
4448
|
-
return false;
|
|
4449
|
-
}
|
|
4450
|
-
if (typeof payload === "string") {
|
|
4451
|
-
return /\berror\b/i.test(payload);
|
|
4452
|
-
}
|
|
4453
|
-
return false;
|
|
4549
|
+
function ipcLogTargetKey(target) {
|
|
4550
|
+
const bp = target.basePath ?? "";
|
|
4551
|
+
const ns = target.namespace ?? "";
|
|
4552
|
+
return `${bp}::${ns}::${target.tableName}`;
|
|
4454
4553
|
}
|
|
4455
|
-
function
|
|
4456
|
-
const
|
|
4457
|
-
|
|
4458
|
-
|
|
4554
|
+
function ipcFileLogsTableNameForSourceResource(source, resource) {
|
|
4555
|
+
const seg = (s) => {
|
|
4556
|
+
const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
4557
|
+
return t.length ? t : "x";
|
|
4558
|
+
};
|
|
4559
|
+
return `${seg(source)}/${seg(resource)}`;
|
|
4560
|
+
}
|
|
4561
|
+
async function readTaskIpcLogsSnapshot(context, options) {
|
|
4562
|
+
const holder = context;
|
|
4563
|
+
const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
|
|
4564
|
+
const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
|
|
4565
|
+
const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
|
|
4566
|
+
const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
|
|
4567
|
+
const fd = new FileDatabase({
|
|
4568
|
+
basePath,
|
|
4569
|
+
namespace,
|
|
4570
|
+
tableName,
|
|
4571
|
+
versioned: true,
|
|
4572
|
+
useMetadata: true,
|
|
4573
|
+
maxVersions: 30,
|
|
4574
|
+
pageSize: 2e3,
|
|
4575
|
+
logger: holder.logger
|
|
4576
|
+
});
|
|
4577
|
+
const versions = await fd.getVersions();
|
|
4578
|
+
if (versions.length === 0) {
|
|
4579
|
+
return { records: [], latestTs: null };
|
|
4580
|
+
}
|
|
4581
|
+
const latest = versions[versions.length - 1];
|
|
4582
|
+
const raw = await fd.read({ version: latest });
|
|
4583
|
+
const arr = Array.isArray(raw) ? raw : [];
|
|
4584
|
+
let filtered = arr;
|
|
4585
|
+
if (options.afterTs && String(options.afterTs).trim()) {
|
|
4586
|
+
const cut = String(options.afterTs).trim();
|
|
4587
|
+
filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
|
|
4588
|
+
}
|
|
4589
|
+
let latestTs = null;
|
|
4590
|
+
for (const r of filtered) {
|
|
4591
|
+
const ts = typeof r?.ts === "string" ? String(r.ts) : null;
|
|
4592
|
+
if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
|
|
4593
|
+
}
|
|
4594
|
+
const incremental = !!(options.afterTs && String(options.afterTs).trim());
|
|
4595
|
+
const maxReturn = incremental ? 1e4 : tail;
|
|
4596
|
+
const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
|
|
4597
|
+
return { records: sliced, latestTs };
|
|
4598
|
+
}
|
|
4599
|
+
function getLogsStateForTarget(context, target) {
|
|
4600
|
+
const holder = context;
|
|
4601
|
+
const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
|
|
4602
|
+
const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
4603
|
+
if (!enabled) return null;
|
|
4604
|
+
if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
|
|
4605
|
+
const map = holder.__tasksLogsTargetStates;
|
|
4606
|
+
const key = ipcLogTargetKey(target);
|
|
4607
|
+
if (map.has(key)) return map.get(key);
|
|
4608
|
+
const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
|
|
4609
|
+
const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
|
|
4610
|
+
const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
|
|
4611
|
+
const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
|
|
4612
|
+
const db = new FileDatabase({
|
|
4613
|
+
basePath,
|
|
4614
|
+
namespace,
|
|
4615
|
+
tableName: target.tableName,
|
|
4616
|
+
versioned: true,
|
|
4617
|
+
useMetadata: true,
|
|
4618
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4619
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4620
|
+
logger: holder.logger
|
|
4621
|
+
});
|
|
4622
|
+
const state = {
|
|
4623
|
+
db,
|
|
4624
|
+
errorDb: null,
|
|
4625
|
+
queue: Promise.resolve(),
|
|
4626
|
+
initialized: false,
|
|
4627
|
+
errorInitialized: false
|
|
4628
|
+
};
|
|
4629
|
+
map.set(key, state);
|
|
4630
|
+
return state;
|
|
4631
|
+
}
|
|
4632
|
+
function isErrorPayload(payload) {
|
|
4633
|
+
if (!payload) return false;
|
|
4634
|
+
if (typeof payload === "object") {
|
|
4635
|
+
const level = typeof payload.level === "string" ? payload.level.toLowerCase() : "";
|
|
4636
|
+
if (level === "error" || level === "fatal") return true;
|
|
4637
|
+
if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
|
|
4638
|
+
return false;
|
|
4639
|
+
}
|
|
4640
|
+
if (typeof payload === "string") {
|
|
4641
|
+
return /\berror\b/i.test(payload);
|
|
4642
|
+
}
|
|
4643
|
+
return false;
|
|
4644
|
+
}
|
|
4645
|
+
function buildLogRecord(task, payload) {
|
|
4646
|
+
const params = task.params && typeof task.params === "object" ? task.params : {};
|
|
4647
|
+
return {
|
|
4648
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4459
4649
|
opid: task.opid ?? null,
|
|
4460
4650
|
taskId: task.id,
|
|
4461
|
-
taskName: task.
|
|
4462
|
-
target: task.
|
|
4651
|
+
taskName: task.name,
|
|
4652
|
+
target: task.service_group,
|
|
4463
4653
|
source: typeof params.source === "string" ? params.source : null,
|
|
4464
4654
|
resource: typeof params.resource === "string" ? params.resource : null,
|
|
4465
4655
|
payload
|
|
4466
4656
|
};
|
|
4467
4657
|
}
|
|
4468
|
-
function appendTaskIpcLog(context, task, payload) {
|
|
4658
|
+
function appendTaskIpcLog(context, task, payload, target) {
|
|
4659
|
+
if (target) {
|
|
4660
|
+
const state2 = getLogsStateForTarget(context, target);
|
|
4661
|
+
if (!state2?.db) return;
|
|
4662
|
+
const record2 = buildLogRecord(task, payload);
|
|
4663
|
+
state2.queue = state2.queue.then(async () => {
|
|
4664
|
+
await state2.db.write([record2], { forceNewVersion: !state2.initialized });
|
|
4665
|
+
state2.initialized = true;
|
|
4666
|
+
}).catch((error) => {
|
|
4667
|
+
context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
|
|
4668
|
+
});
|
|
4669
|
+
return;
|
|
4670
|
+
}
|
|
4469
4671
|
const state = getLogsState(context);
|
|
4470
4672
|
if (!state.db && !state.errorDb) return;
|
|
4471
4673
|
const record = buildLogRecord(task, payload);
|
|
@@ -4483,83 +4685,280 @@ function appendTaskIpcLog(context, task, payload) {
|
|
|
4483
4685
|
});
|
|
4484
4686
|
}
|
|
4485
4687
|
|
|
4486
|
-
// src/tasks/
|
|
4487
|
-
var
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
if (last < first) {
|
|
4501
|
-
[first, last] = [last, first];
|
|
4502
|
-
}
|
|
4503
|
-
const values = [];
|
|
4504
|
-
for (let i = first; i <= last; i += 1) {
|
|
4505
|
-
values.push(i);
|
|
4506
|
-
}
|
|
4507
|
-
current = current.replace(raw, values.join(","));
|
|
4508
|
-
}
|
|
4509
|
-
return current;
|
|
4510
|
-
}
|
|
4511
|
-
function resolveSteps(field) {
|
|
4512
|
-
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
4513
|
-
if (!match) return field;
|
|
4514
|
-
const base = match[1];
|
|
4515
|
-
const step = Number(match[2]);
|
|
4516
|
-
if (!Number.isFinite(step) || step <= 0) return field;
|
|
4517
|
-
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
4518
|
-
}
|
|
4519
|
-
function convertPattern(pattern) {
|
|
4520
|
-
const parts = pattern.trim().split(/\s+/);
|
|
4521
|
-
if (parts.length !== 6) {
|
|
4522
|
-
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
4523
|
-
}
|
|
4524
|
-
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
4525
|
-
}
|
|
4526
|
-
function fieldMatches(field, value) {
|
|
4527
|
-
const allowed = field.split(",").map((v) => Number(v));
|
|
4528
|
-
return allowed.includes(value);
|
|
4529
|
-
}
|
|
4530
|
-
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
4531
|
-
const parsed = convertPattern(pattern);
|
|
4532
|
-
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());
|
|
4533
|
-
}
|
|
4534
|
-
|
|
4535
|
-
// src/tasks/TaskMaster.ts
|
|
4536
|
-
var TaskMaster = class {
|
|
4537
|
-
context;
|
|
4538
|
-
task;
|
|
4688
|
+
// src/tasks/AbstractTask.js
|
|
4689
|
+
var AbstractTask = class _AbstractTask {
|
|
4690
|
+
/**
|
|
4691
|
+
* Whether `send-task` should wait for completion (and print a result
|
|
4692
|
+
* report) when no explicit `--wait` / `--noWait` flag is given. Defaults
|
|
4693
|
+
* to false; short-lived probe tasks (e.g. `ping`) override to true.
|
|
4694
|
+
*
|
|
4695
|
+
* @type {boolean}
|
|
4696
|
+
*/
|
|
4697
|
+
static defaultWaitForResult = false;
|
|
4698
|
+
/**
|
|
4699
|
+
* @param {object} context Runner context (db, logger, params, emitter...).
|
|
4700
|
+
* @param {object} task Task row as claimed from the queue.
|
|
4701
|
+
*/
|
|
4539
4702
|
constructor(context, task) {
|
|
4540
4703
|
this.context = context;
|
|
4541
4704
|
this.task = task;
|
|
4542
4705
|
}
|
|
4706
|
+
/**
|
|
4707
|
+
* Return a short reason string when the task should be deferred (e.g. "locked
|
|
4708
|
+
* by source"), or `false`/falsy when it is free to run. Default: always `false`.
|
|
4709
|
+
*
|
|
4710
|
+
* @returns {string | false | Promise<string | false>}
|
|
4711
|
+
*/
|
|
4543
4712
|
cantRunReason() {
|
|
4544
4713
|
return false;
|
|
4545
4714
|
}
|
|
4715
|
+
/**
|
|
4716
|
+
* Called by the runner when a stop has been requested. Subclasses running
|
|
4717
|
+
* long loops should flip a flag here and check it between iterations.
|
|
4718
|
+
*
|
|
4719
|
+
* @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
|
|
4720
|
+
*/
|
|
4546
4721
|
requestStop(_allowanceMs) {
|
|
4547
4722
|
}
|
|
4723
|
+
/**
|
|
4724
|
+
* Perform the task. Must be implemented by subclasses.
|
|
4725
|
+
*
|
|
4726
|
+
* @param {(progress: unknown) => Promise<void>} _reportProgress
|
|
4727
|
+
* Updates the DB `progress` column. Accepts any serializable value;
|
|
4728
|
+
* strings are stored verbatim, objects are JSON-stringified.
|
|
4729
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
4730
|
+
*/
|
|
4731
|
+
async run(_reportProgress) {
|
|
4732
|
+
throw new Error("AbstractTask.run must be implemented by subclass");
|
|
4733
|
+
}
|
|
4734
|
+
/**
|
|
4735
|
+
* Resolve a complete row payload for this task — envelope fields (queue,
|
|
4736
|
+
* priority, targeting, schedule…) plus the inner `params` blob produced by
|
|
4737
|
+
* {@link AbstractTask.resolveCustomParams}. Output shape matches
|
|
4738
|
+
* {@link enqueueTask}'s `options` argument, so the typical call is:
|
|
4739
|
+
*
|
|
4740
|
+
* const payload = await TaskClass.resolveParams(context, { name });
|
|
4741
|
+
* await enqueueTask(context, payload);
|
|
4742
|
+
*
|
|
4743
|
+
* Validation failures throw {@link ParamError} so the script aborts before
|
|
4744
|
+
* a malformed row hits the DB.
|
|
4745
|
+
*
|
|
4746
|
+
* @param {object} context
|
|
4747
|
+
* @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
|
|
4748
|
+
* Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
|
|
4749
|
+
* `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
|
|
4750
|
+
* `nextRunAt`, plus `params` (object — overlay onto inner blob).
|
|
4751
|
+
* @returns {Promise<object>}
|
|
4752
|
+
*/
|
|
4753
|
+
static async resolveParams(context, overrides = {}) {
|
|
4754
|
+
const main = _AbstractTask._resolveMainFields(context, overrides);
|
|
4755
|
+
const params = await this.resolveCustomParams(context, overrides);
|
|
4756
|
+
return { ...main, params };
|
|
4757
|
+
}
|
|
4758
|
+
/**
|
|
4759
|
+
* Resolve the inner JSON blob stored in the `params` column. Default
|
|
4760
|
+
* implementation passes through `--paramsJson` (parsed as a JSON object)
|
|
4761
|
+
* overlaid with `overrides.params` when supplied; returns `null` when
|
|
4762
|
+
* neither is provided.
|
|
4763
|
+
*
|
|
4764
|
+
* Subclasses with typed fields should override and call
|
|
4765
|
+
* {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
|
|
4766
|
+
* resolution, then validate and throw {@link ParamError} on bad input.
|
|
4767
|
+
*
|
|
4768
|
+
* @param {object} context
|
|
4769
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4770
|
+
* @returns {Promise<object|null>}
|
|
4771
|
+
*/
|
|
4772
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
4773
|
+
return _AbstractTask._defaultParamsBlob(context, overrides);
|
|
4774
|
+
}
|
|
4775
|
+
/**
|
|
4776
|
+
* Read main task envelope fields from `context.params` (CLI/env), with
|
|
4777
|
+
* any matching key on `overrides` taking precedence. Internal; called by
|
|
4778
|
+
* {@link AbstractTask.resolveParams}.
|
|
4779
|
+
*
|
|
4780
|
+
* @param {object} context
|
|
4781
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4782
|
+
* @returns {object}
|
|
4783
|
+
*/
|
|
4784
|
+
static _resolveMainFields(context, overrides = {}) {
|
|
4785
|
+
const defs2 = {
|
|
4786
|
+
queueName: "string default tasks",
|
|
4787
|
+
priority: "number default 50",
|
|
4788
|
+
serviceGroup: "string",
|
|
4789
|
+
serviceName: "string",
|
|
4790
|
+
instanceNumber: "number",
|
|
4791
|
+
serverName: "string",
|
|
4792
|
+
opid: "string",
|
|
4793
|
+
schedule: "string"
|
|
4794
|
+
};
|
|
4795
|
+
const cli = context.params.getAllForModule("task-envelope", defs2);
|
|
4796
|
+
const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
|
|
4797
|
+
if (!name) {
|
|
4798
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
4799
|
+
}
|
|
4800
|
+
let instanceNumber;
|
|
4801
|
+
const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
|
|
4802
|
+
if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
|
|
4803
|
+
const n = Number(rawInstance);
|
|
4804
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
4805
|
+
throw new ParamError("--instanceNumber must be a positive integer when set");
|
|
4806
|
+
}
|
|
4807
|
+
instanceNumber = n;
|
|
4808
|
+
} else {
|
|
4809
|
+
instanceNumber = null;
|
|
4810
|
+
}
|
|
4811
|
+
const priorityRaw = overrides.priority ?? cli.priority ?? 50;
|
|
4812
|
+
const priority = Number(priorityRaw);
|
|
4813
|
+
if (!Number.isFinite(priority)) {
|
|
4814
|
+
throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
|
|
4815
|
+
}
|
|
4816
|
+
return {
|
|
4817
|
+
name,
|
|
4818
|
+
queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
|
|
4819
|
+
priority,
|
|
4820
|
+
serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
|
|
4821
|
+
serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
|
|
4822
|
+
instanceNumber,
|
|
4823
|
+
serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
|
|
4824
|
+
opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
|
|
4825
|
+
schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
|
|
4826
|
+
nextRunAt: overrides.nextRunAt ?? null
|
|
4827
|
+
};
|
|
4828
|
+
}
|
|
4829
|
+
/**
|
|
4830
|
+
* Default inner-params resolver: parses `--paramsJson` (must be a JSON
|
|
4831
|
+
* object), then overlays `overrides.params` on top. Returns `null` when
|
|
4832
|
+
* neither is provided.
|
|
4833
|
+
*
|
|
4834
|
+
* @param {object} context
|
|
4835
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4836
|
+
* @returns {object|null}
|
|
4837
|
+
*/
|
|
4838
|
+
static _defaultParamsBlob(context, overrides = {}) {
|
|
4839
|
+
const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
|
|
4840
|
+
const fromJson = parseParamsJson(cli.paramsJson);
|
|
4841
|
+
const fromOverride = pickParamsObject(overrides);
|
|
4842
|
+
if (!fromJson && !fromOverride) return null;
|
|
4843
|
+
return { ...fromJson ?? {}, ...fromOverride ?? {} };
|
|
4844
|
+
}
|
|
4845
|
+
/**
|
|
4846
|
+
* Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
|
|
4847
|
+
* params (per `defs`) plus `--paramsJson` under a module namespace, then
|
|
4848
|
+
* merges them with explicit `overrides.params` in increasing priority:
|
|
4849
|
+
*
|
|
4850
|
+
* typed CLI flags → --paramsJson → overrides.params
|
|
4851
|
+
*
|
|
4852
|
+
* Undefined values are dropped so defaults declared in `defs` aren't
|
|
4853
|
+
* overwritten by missing-flag noise. Returns the merged object; the
|
|
4854
|
+
* caller is responsible for validation and throwing `ParamError`.
|
|
4855
|
+
*
|
|
4856
|
+
* @param {object} context
|
|
4857
|
+
* @param {string} moduleName Namespace for `--showUsedParams` grouping.
|
|
4858
|
+
* @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
|
|
4859
|
+
* @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
|
|
4860
|
+
* @returns {Record<string, unknown>}
|
|
4861
|
+
*/
|
|
4862
|
+
static _mergeTypedParams(context, moduleName, defs2, overrides = {}) {
|
|
4863
|
+
const fullDefs = { ...defs2, paramsJson: "string" };
|
|
4864
|
+
const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
|
|
4865
|
+
const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
|
|
4866
|
+
const fromCli = {};
|
|
4867
|
+
for (const [k, v] of Object.entries(cliRaw)) {
|
|
4868
|
+
if (k === "paramsJson") continue;
|
|
4869
|
+
if (v !== void 0 && v !== null) fromCli[k] = v;
|
|
4870
|
+
}
|
|
4871
|
+
const fromOverride = pickParamsObject(overrides) ?? {};
|
|
4872
|
+
return { ...fromCli, ...fromJson, ...fromOverride };
|
|
4873
|
+
}
|
|
4548
4874
|
};
|
|
4875
|
+
function emptyToUndef(s) {
|
|
4876
|
+
if (s === void 0 || s === null) return void 0;
|
|
4877
|
+
if (typeof s !== "string") return s;
|
|
4878
|
+
const t = s.trim();
|
|
4879
|
+
return t.length ? t : void 0;
|
|
4880
|
+
}
|
|
4881
|
+
function parseParamsJson(raw) {
|
|
4882
|
+
if (raw == null) return null;
|
|
4883
|
+
const t = String(raw).trim();
|
|
4884
|
+
if (!t) return null;
|
|
4885
|
+
let parsed;
|
|
4886
|
+
try {
|
|
4887
|
+
parsed = JSON.parse(t);
|
|
4888
|
+
} catch (e) {
|
|
4889
|
+
throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
|
|
4890
|
+
}
|
|
4891
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4892
|
+
throw new ParamError("--paramsJson must be a JSON object");
|
|
4893
|
+
}
|
|
4894
|
+
return parsed;
|
|
4895
|
+
}
|
|
4896
|
+
function pickParamsObject(overrides) {
|
|
4897
|
+
const p = overrides?.params;
|
|
4898
|
+
if (p && typeof p === "object" && !Array.isArray(p)) return p;
|
|
4899
|
+
return void 0;
|
|
4900
|
+
}
|
|
4549
4901
|
|
|
4550
|
-
// src/tasks/coreTasks/TaskPing.
|
|
4551
|
-
var TaskPing = class extends
|
|
4902
|
+
// src/tasks/coreTasks/TaskPing.js
|
|
4903
|
+
var TaskPing = class extends AbstractTask {
|
|
4904
|
+
/** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
|
|
4905
|
+
static defaultWaitForResult = true;
|
|
4906
|
+
/** Ping takes no params. */
|
|
4907
|
+
static async resolveCustomParams() {
|
|
4908
|
+
return null;
|
|
4909
|
+
}
|
|
4910
|
+
/**
|
|
4911
|
+
* @returns {Promise<{ success: true, results: "pong" }>}
|
|
4912
|
+
*/
|
|
4552
4913
|
async run() {
|
|
4553
4914
|
this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
|
|
4554
4915
|
return { success: true, results: "pong" };
|
|
4555
4916
|
}
|
|
4556
4917
|
};
|
|
4557
4918
|
|
|
4558
|
-
// src/tasks/coreTasks/TaskSampleProcess.
|
|
4559
|
-
var TaskSampleProcess = class extends
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4919
|
+
// src/tasks/coreTasks/TaskSampleProcess.js
|
|
4920
|
+
var TaskSampleProcess = class extends AbstractTask {
|
|
4921
|
+
/**
|
|
4922
|
+
* @param {object} context
|
|
4923
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4924
|
+
* @returns {Promise<{ total: number, delay: number, name?: string }>}
|
|
4925
|
+
*/
|
|
4926
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
4927
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
|
|
4928
|
+
total: "number default 10",
|
|
4929
|
+
delay: "number default 1000",
|
|
4930
|
+
name: "string"
|
|
4931
|
+
}, overrides);
|
|
4932
|
+
const total = Number(merged.total);
|
|
4933
|
+
const delay = Number(merged.delay);
|
|
4934
|
+
if (!Number.isInteger(total) || total <= 0) {
|
|
4935
|
+
throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
|
|
4936
|
+
}
|
|
4937
|
+
if (!Number.isInteger(delay) || delay < 0) {
|
|
4938
|
+
throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
|
|
4939
|
+
}
|
|
4940
|
+
const out = { total, delay };
|
|
4941
|
+
if (typeof merged.name === "string" && merged.name.trim()) {
|
|
4942
|
+
out.name = merged.name.trim();
|
|
4943
|
+
}
|
|
4944
|
+
return out;
|
|
4945
|
+
}
|
|
4946
|
+
/**
|
|
4947
|
+
* @param {object} context
|
|
4948
|
+
* @param {object} task
|
|
4949
|
+
*/
|
|
4950
|
+
constructor(context, task) {
|
|
4951
|
+
super(context, task);
|
|
4952
|
+
this.stopRequested = false;
|
|
4953
|
+
this.stopAllowanceMs = 0;
|
|
4954
|
+
this.stopDecisionLogged = false;
|
|
4955
|
+
}
|
|
4956
|
+
/**
|
|
4957
|
+
* Runner-facing stop signal. Records the allowance window so the main loop
|
|
4958
|
+
* can decide per-iteration whether to finish or abort early.
|
|
4959
|
+
*
|
|
4960
|
+
* @param {number} allowanceMs
|
|
4961
|
+
*/
|
|
4563
4962
|
requestStop(allowanceMs) {
|
|
4564
4963
|
this.stopRequested = true;
|
|
4565
4964
|
this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
|
|
@@ -4567,6 +4966,14 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4567
4966
|
`[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
|
|
4568
4967
|
);
|
|
4569
4968
|
}
|
|
4969
|
+
/**
|
|
4970
|
+
* Iterate `total` times, sleeping `delay` ms between ticks and reporting
|
|
4971
|
+
* progress every iteration. Validates params up front; invalid values short-
|
|
4972
|
+
* circuit to a structured failure without starting the loop.
|
|
4973
|
+
*
|
|
4974
|
+
* @param {(progress: object) => Promise<void>} reportProgress
|
|
4975
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
4976
|
+
*/
|
|
4570
4977
|
async run(reportProgress) {
|
|
4571
4978
|
const totalRaw = this.task?.params?.total ?? 10;
|
|
4572
4979
|
const delayRaw = this.task?.params?.delay ?? 1e3;
|
|
@@ -4648,7 +5055,7 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4648
5055
|
}
|
|
4649
5056
|
};
|
|
4650
5057
|
|
|
4651
|
-
// src/tasks/coreTasks/TaskShellCommand.
|
|
5058
|
+
// src/tasks/coreTasks/TaskShellCommand.js
|
|
4652
5059
|
import { spawn } from "child_process";
|
|
4653
5060
|
function runShellCommand(command, cwd) {
|
|
4654
5061
|
return new Promise((resolve2, reject) => {
|
|
@@ -4678,7 +5085,27 @@ function runShellCommand(command, cwd) {
|
|
|
4678
5085
|
});
|
|
4679
5086
|
});
|
|
4680
5087
|
}
|
|
4681
|
-
var TaskShellCommand = class extends
|
|
5088
|
+
var TaskShellCommand = class extends AbstractTask {
|
|
5089
|
+
/**
|
|
5090
|
+
* @param {object} context
|
|
5091
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5092
|
+
* @returns {Promise<{ command: string, cwd?: string }>}
|
|
5093
|
+
*/
|
|
5094
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5095
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
|
|
5096
|
+
command: "string",
|
|
5097
|
+
cwd: "string"
|
|
5098
|
+
}, overrides);
|
|
5099
|
+
const command = typeof merged.command === "string" ? merged.command.trim() : "";
|
|
5100
|
+
if (!command) {
|
|
5101
|
+
throw new ParamError('shellCommand: param "command" must be a non-empty string');
|
|
5102
|
+
}
|
|
5103
|
+
const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
|
|
5104
|
+
return cwd ? { command, cwd } : { command };
|
|
5105
|
+
}
|
|
5106
|
+
/**
|
|
5107
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5108
|
+
*/
|
|
4682
5109
|
async run() {
|
|
4683
5110
|
const params = this.task?.params;
|
|
4684
5111
|
const commandRaw = typeof params === "string" ? params : params?.command;
|
|
@@ -4727,7 +5154,7 @@ var TaskShellCommand = class extends TaskMaster {
|
|
|
4727
5154
|
}
|
|
4728
5155
|
};
|
|
4729
5156
|
|
|
4730
|
-
// src/tasks/coreTasks/TaskSystemInfo.
|
|
5157
|
+
// src/tasks/coreTasks/TaskSystemInfo.js
|
|
4731
5158
|
import os2 from "os";
|
|
4732
5159
|
import fs4 from "fs/promises";
|
|
4733
5160
|
function toGb(valueBytes) {
|
|
@@ -4747,7 +5174,16 @@ async function getDiskStats() {
|
|
|
4747
5174
|
free: toGb(free)
|
|
4748
5175
|
};
|
|
4749
5176
|
}
|
|
4750
|
-
var TaskSystemInfo = class extends
|
|
5177
|
+
var TaskSystemInfo = class extends AbstractTask {
|
|
5178
|
+
/** Same UX expectation as `ping` — short probe, print the result. */
|
|
5179
|
+
static defaultWaitForResult = true;
|
|
5180
|
+
/** systemInfo takes no params. */
|
|
5181
|
+
static async resolveCustomParams() {
|
|
5182
|
+
return null;
|
|
5183
|
+
}
|
|
5184
|
+
/**
|
|
5185
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5186
|
+
*/
|
|
4751
5187
|
async run() {
|
|
4752
5188
|
try {
|
|
4753
5189
|
const totalMemory = os2.totalmem();
|
|
@@ -4799,8 +5235,31 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
4799
5235
|
}
|
|
4800
5236
|
};
|
|
4801
5237
|
|
|
4802
|
-
// src/tasks/coreTasks/TaskSumAB.
|
|
4803
|
-
var TaskSumAB = class extends
|
|
5238
|
+
// src/tasks/coreTasks/TaskSumAB.js
|
|
5239
|
+
var TaskSumAB = class extends AbstractTask {
|
|
5240
|
+
/** Short, deterministic — wait by default so callers see the sum. */
|
|
5241
|
+
static defaultWaitForResult = true;
|
|
5242
|
+
/**
|
|
5243
|
+
* @param {object} context
|
|
5244
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5245
|
+
* @returns {Promise<{ a: number, b: number }>}
|
|
5246
|
+
*/
|
|
5247
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5248
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
|
|
5249
|
+
a: "number",
|
|
5250
|
+
b: "number"
|
|
5251
|
+
}, overrides);
|
|
5252
|
+
if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
|
|
5253
|
+
throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
|
|
5254
|
+
}
|
|
5255
|
+
if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
|
|
5256
|
+
throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
|
|
5257
|
+
}
|
|
5258
|
+
return { a: merged.a, b: merged.b };
|
|
5259
|
+
}
|
|
5260
|
+
/**
|
|
5261
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5262
|
+
*/
|
|
4804
5263
|
async run() {
|
|
4805
5264
|
const a = this.task?.params?.a;
|
|
4806
5265
|
const b = this.task?.params?.b;
|
|
@@ -4831,8 +5290,46 @@ var TaskSumAB = class extends TaskMaster {
|
|
|
4831
5290
|
}
|
|
4832
5291
|
};
|
|
4833
5292
|
|
|
4834
|
-
// src/tasks/coreTasks/TaskStopRunner.
|
|
4835
|
-
var TaskStopRunner = class extends
|
|
5293
|
+
// src/tasks/coreTasks/TaskStopRunner.js
|
|
5294
|
+
var TaskStopRunner = class extends AbstractTask {
|
|
5295
|
+
/**
|
|
5296
|
+
* Stop tasks must target a concrete instance — without `serviceName` the
|
|
5297
|
+
* row would race against any worker on the queue. Layered on top of the
|
|
5298
|
+
* envelope built by {@link AbstractTask.resolveParams}.
|
|
5299
|
+
*
|
|
5300
|
+
* @param {object} context
|
|
5301
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5302
|
+
* @returns {Promise<object>}
|
|
5303
|
+
*/
|
|
5304
|
+
static async resolveParams(context, overrides = {}) {
|
|
5305
|
+
const main = await super.resolveParams(context, overrides);
|
|
5306
|
+
if (!main.serviceName) {
|
|
5307
|
+
throw new ParamError(
|
|
5308
|
+
"stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
|
|
5309
|
+
);
|
|
5310
|
+
}
|
|
5311
|
+
return main;
|
|
5312
|
+
}
|
|
5313
|
+
/**
|
|
5314
|
+
* @param {object} context
|
|
5315
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5316
|
+
* @returns {Promise<{ allowanceMs: number }>}
|
|
5317
|
+
*/
|
|
5318
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5319
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
|
|
5320
|
+
allowanceMs: "number default 5000"
|
|
5321
|
+
}, overrides);
|
|
5322
|
+
const allowanceMs = Number(merged.allowanceMs);
|
|
5323
|
+
if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
|
|
5324
|
+
throw new ParamError(
|
|
5325
|
+
`stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
|
|
5326
|
+
);
|
|
5327
|
+
}
|
|
5328
|
+
return { allowanceMs };
|
|
5329
|
+
}
|
|
5330
|
+
/**
|
|
5331
|
+
* @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
|
|
5332
|
+
*/
|
|
4836
5333
|
async run() {
|
|
4837
5334
|
const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
|
|
4838
5335
|
this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
|
|
@@ -4847,42 +5344,200 @@ var TaskStopRunner = class extends TaskMaster {
|
|
|
4847
5344
|
}
|
|
4848
5345
|
};
|
|
4849
5346
|
|
|
4850
|
-
// src/tasks/
|
|
5347
|
+
// src/tasks/coreTasks/TaskGetLogs.js
|
|
5348
|
+
var TaskGetLogs = class extends AbstractTask {
|
|
5349
|
+
/**
|
|
5350
|
+
* @param {object} context
|
|
5351
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5352
|
+
* @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
|
|
5353
|
+
*/
|
|
5354
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5355
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
|
|
5356
|
+
source: "string",
|
|
5357
|
+
resource: "string",
|
|
5358
|
+
tail: "number default 100",
|
|
5359
|
+
afterTs: "string"
|
|
5360
|
+
}, overrides);
|
|
5361
|
+
const source = typeof merged.source === "string" ? merged.source.trim() : "";
|
|
5362
|
+
const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
|
|
5363
|
+
if (!source) throw new ParamError('getLogs: param "source" is required');
|
|
5364
|
+
if (!resource) throw new ParamError('getLogs: param "resource" is required');
|
|
5365
|
+
let tail = Number(merged.tail);
|
|
5366
|
+
if (!Number.isFinite(tail) || tail < 1) tail = 100;
|
|
5367
|
+
tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
|
|
5368
|
+
const out = { source, resource, tail };
|
|
5369
|
+
if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
|
|
5370
|
+
out.afterTs = merged.afterTs.trim();
|
|
5371
|
+
}
|
|
5372
|
+
return out;
|
|
5373
|
+
}
|
|
5374
|
+
/**
|
|
5375
|
+
* @param {unknown} _reportProgress Unused (single-shot read, no progress events).
|
|
5376
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5377
|
+
*/
|
|
5378
|
+
async run(_reportProgress) {
|
|
5379
|
+
const p = this.task.params ?? {};
|
|
5380
|
+
const source = String(p.source ?? "").trim();
|
|
5381
|
+
const resource = String(p.resource ?? "").trim();
|
|
5382
|
+
const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
|
|
5383
|
+
const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
|
|
5384
|
+
if (!source || !resource) {
|
|
5385
|
+
return {
|
|
5386
|
+
success: false,
|
|
5387
|
+
results: { error: 'getLogs requires params "source" and "resource"' }
|
|
5388
|
+
};
|
|
5389
|
+
}
|
|
5390
|
+
try {
|
|
5391
|
+
const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
|
|
5392
|
+
source,
|
|
5393
|
+
resource,
|
|
5394
|
+
tail,
|
|
5395
|
+
afterTs
|
|
5396
|
+
});
|
|
5397
|
+
return {
|
|
5398
|
+
success: true,
|
|
5399
|
+
results: { records, latestTs, source, resource }
|
|
5400
|
+
};
|
|
5401
|
+
} catch (e) {
|
|
5402
|
+
return {
|
|
5403
|
+
success: false,
|
|
5404
|
+
results: { error: e?.message ?? String(e) }
|
|
5405
|
+
};
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
};
|
|
5409
|
+
|
|
5410
|
+
// src/tasks/TasksRegistry.js
|
|
4851
5411
|
var TasksRegistry = class _TasksRegistry {
|
|
4852
|
-
|
|
5412
|
+
/**
|
|
5413
|
+
* @param {Record<string, Function>} [initial] Optional seed entries to copy in.
|
|
5414
|
+
*/
|
|
4853
5415
|
constructor(initial) {
|
|
5416
|
+
this.map = {};
|
|
4854
5417
|
if (initial) {
|
|
4855
5418
|
this.addMany(initial);
|
|
4856
5419
|
}
|
|
4857
5420
|
}
|
|
5421
|
+
/**
|
|
5422
|
+
* Build a registry pre-populated with every core task plus legacy aliases.
|
|
5423
|
+
* Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
|
|
5424
|
+
*
|
|
5425
|
+
* @returns {TasksRegistry}
|
|
5426
|
+
*/
|
|
4858
5427
|
static withCoreTasks() {
|
|
4859
|
-
return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
|
|
5428
|
+
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);
|
|
4860
5429
|
}
|
|
5430
|
+
/**
|
|
5431
|
+
* Register a single task class under a name. Overwrites any previous entry.
|
|
5432
|
+
*
|
|
5433
|
+
* @param {string} taskName
|
|
5434
|
+
* @param {Function} taskClass Subclass of `AbstractTask`.
|
|
5435
|
+
* @returns {this}
|
|
5436
|
+
*/
|
|
4861
5437
|
add(taskName, taskClass) {
|
|
4862
5438
|
this.map[taskName] = taskClass;
|
|
4863
5439
|
return this;
|
|
4864
5440
|
}
|
|
5441
|
+
/**
|
|
5442
|
+
* Bulk-register a name → class map. Later calls override earlier ones.
|
|
5443
|
+
*
|
|
5444
|
+
* @param {Record<string, Function>} entries
|
|
5445
|
+
* @returns {this}
|
|
5446
|
+
*/
|
|
4865
5447
|
addMany(entries) {
|
|
4866
5448
|
for (const [name, klass] of Object.entries(entries)) {
|
|
4867
5449
|
this.add(name, klass);
|
|
4868
5450
|
}
|
|
4869
5451
|
return this;
|
|
4870
5452
|
}
|
|
5453
|
+
/**
|
|
5454
|
+
* Look up a task class by name. Returns `undefined` when the name is unknown;
|
|
5455
|
+
* the runner treats that as "some other worker may handle this" and skips.
|
|
5456
|
+
*
|
|
5457
|
+
* @param {string} taskName
|
|
5458
|
+
* @returns {Function | undefined}
|
|
5459
|
+
*/
|
|
4871
5460
|
get(taskName) {
|
|
4872
5461
|
return this.map[taskName];
|
|
4873
5462
|
}
|
|
5463
|
+
/**
|
|
5464
|
+
* Strict variant of {@link get}: throws {@link ParamError} (with the list
|
|
5465
|
+
* of supported names) when `taskName` is unknown. Use from enqueuer code
|
|
5466
|
+
* paths where an unknown name is a hard CLI/programmer error.
|
|
5467
|
+
*
|
|
5468
|
+
* @param {string} taskName
|
|
5469
|
+
* @returns {Function}
|
|
5470
|
+
*/
|
|
5471
|
+
requireClass(taskName) {
|
|
5472
|
+
const TaskClass = taskName ? this.map[taskName] : void 0;
|
|
5473
|
+
if (!TaskClass) {
|
|
5474
|
+
const supported = this.listSupportedTasks().join(", ") || "(none)";
|
|
5475
|
+
throw new ParamError(
|
|
5476
|
+
`Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
|
|
5477
|
+
);
|
|
5478
|
+
}
|
|
5479
|
+
return TaskClass;
|
|
5480
|
+
}
|
|
5481
|
+
/**
|
|
5482
|
+
* Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
|
|
5483
|
+
* from `overrides` or `context.params`, look up the class, and delegate
|
|
5484
|
+
* to its static {@link AbstractTask.resolveParams} with `name` seeded into
|
|
5485
|
+
* the overrides. The returned object is shaped for {@link enqueueTask}.
|
|
5486
|
+
*
|
|
5487
|
+
* Validation failures (unknown task, missing required custom params, etc.)
|
|
5488
|
+
* surface as {@link ParamError} so the caller aborts cleanly before any
|
|
5489
|
+
* row is inserted.
|
|
5490
|
+
*
|
|
5491
|
+
* @param {object} context
|
|
5492
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5493
|
+
* @returns {Promise<object>}
|
|
5494
|
+
*/
|
|
5495
|
+
async resolveTaskParams(context, overrides = {}) {
|
|
5496
|
+
const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
|
|
5497
|
+
const fromCli = context.params.get("name", "string");
|
|
5498
|
+
const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
|
|
5499
|
+
const name = overrideName || cliName;
|
|
5500
|
+
if (!name) {
|
|
5501
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
5502
|
+
}
|
|
5503
|
+
const TaskClass = this.requireClass(name);
|
|
5504
|
+
return TaskClass.resolveParams(context, { ...overrides, name });
|
|
5505
|
+
}
|
|
5506
|
+
/**
|
|
5507
|
+
* Names of every registered task, sorted alphabetically (useful for CLI output
|
|
5508
|
+
* and allowlist sanity checks).
|
|
5509
|
+
*
|
|
5510
|
+
* @returns {string[]}
|
|
5511
|
+
*/
|
|
4874
5512
|
listSupportedTasks() {
|
|
4875
5513
|
return Object.keys(this.map).sort();
|
|
4876
5514
|
}
|
|
5515
|
+
/**
|
|
5516
|
+
* Shallow copy of the internal map, for handing to `addMany` on another registry
|
|
5517
|
+
* or for serialization.
|
|
5518
|
+
*
|
|
5519
|
+
* @returns {Record<string, Function>}
|
|
5520
|
+
*/
|
|
4877
5521
|
toObject() {
|
|
4878
5522
|
return { ...this.map };
|
|
4879
5523
|
}
|
|
4880
5524
|
};
|
|
4881
5525
|
|
|
4882
|
-
// src/tasks/
|
|
5526
|
+
// src/tasks/serviceTaskAllowlist.js
|
|
5527
|
+
function normalizeAllowedTasks(value) {
|
|
5528
|
+
if (!value) return void 0;
|
|
5529
|
+
if (Array.isArray(value)) {
|
|
5530
|
+
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
5531
|
+
return out2.length ? out2 : void 0;
|
|
5532
|
+
}
|
|
5533
|
+
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
5534
|
+
return out.length ? out : void 0;
|
|
5535
|
+
}
|
|
5536
|
+
|
|
5537
|
+
// src/tasks/taskScriptRunner.js
|
|
4883
5538
|
import { spawn as spawn2 } from "child_process";
|
|
4884
5539
|
|
|
4885
|
-
// src/tasks/index.
|
|
5540
|
+
// src/tasks/index.js
|
|
4886
5541
|
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
4887
5542
|
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
4888
5543
|
function getDb3(context) {
|
|
@@ -4897,15 +5552,6 @@ function normalizeRegistry(registry) {
|
|
|
4897
5552
|
if (registry instanceof TasksRegistry) return registry;
|
|
4898
5553
|
return new TasksRegistry().addMany(registry);
|
|
4899
5554
|
}
|
|
4900
|
-
function normalizeAllowedTasks(value) {
|
|
4901
|
-
if (!value) return void 0;
|
|
4902
|
-
if (Array.isArray(value)) {
|
|
4903
|
-
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
4904
|
-
return out2.length ? out2 : void 0;
|
|
4905
|
-
}
|
|
4906
|
-
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
4907
|
-
return out.length ? out : void 0;
|
|
4908
|
-
}
|
|
4909
5555
|
async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
|
|
4910
5556
|
context.logger.warn?.(`[tasks] signaling ${runningTaskInstances.size} running task(s) to stop`);
|
|
4911
5557
|
for (const [, taskInstance] of runningTaskInstances) {
|
|
@@ -4921,18 +5567,20 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
|
|
|
4921
5567
|
}
|
|
4922
5568
|
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
4923
5569
|
const db = getDb3(context);
|
|
4924
|
-
const taskName = row.
|
|
5570
|
+
const taskName = row.name;
|
|
4925
5571
|
const TaskClass = registry.get(taskName);
|
|
4926
|
-
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
4927
5572
|
if (!TaskClass) {
|
|
4928
5573
|
const err = { message: `Unknown task "${taskName}"` };
|
|
4929
|
-
await db(historyTable).insert(
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
5574
|
+
await db(historyTable).insert(
|
|
5575
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
5576
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5577
|
+
success: false,
|
|
5578
|
+
status: "failed",
|
|
5579
|
+
status_changed_at: db.fn.now(),
|
|
5580
|
+
params: toJsonColumn(row.params),
|
|
5581
|
+
results: toJsonColumn(err)
|
|
5582
|
+
})
|
|
5583
|
+
);
|
|
4936
5584
|
if (row.schedule) {
|
|
4937
5585
|
await db(tasksTable).where({ id: row.id }).update({
|
|
4938
5586
|
started_at: null,
|
|
@@ -4940,7 +5588,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4940
5588
|
success: false,
|
|
4941
5589
|
results: toJsonColumn(err),
|
|
4942
5590
|
past_due: null,
|
|
4943
|
-
|
|
5591
|
+
status: "paused",
|
|
5592
|
+
status_changed_at: db.fn.now(),
|
|
4944
5593
|
progress: LOCKED_BY_ERROR_MESSAGE
|
|
4945
5594
|
});
|
|
4946
5595
|
} else {
|
|
@@ -4967,13 +5616,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4967
5616
|
} finally {
|
|
4968
5617
|
runningTaskInstances.delete(row.id);
|
|
4969
5618
|
}
|
|
4970
|
-
await db(historyTable).insert(
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
5619
|
+
await db(historyTable).insert(
|
|
5620
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
5621
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5622
|
+
success,
|
|
5623
|
+
status: success ? "completed" : "failed",
|
|
5624
|
+
status_changed_at: db.fn.now(),
|
|
5625
|
+
params: toJsonColumn(row.params),
|
|
5626
|
+
results: toJsonColumn(results)
|
|
5627
|
+
})
|
|
5628
|
+
);
|
|
4977
5629
|
if (!success) {
|
|
4978
5630
|
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
4979
5631
|
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
@@ -4988,19 +5640,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4988
5640
|
const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
|
|
4989
5641
|
appendTaskIpcLog(context, row, {
|
|
4990
5642
|
level: "error",
|
|
4991
|
-
message: `[tasks] task failed: ${row.
|
|
5643
|
+
message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
|
|
4992
5644
|
details: results
|
|
4993
5645
|
});
|
|
4994
5646
|
}
|
|
4995
5647
|
if (row.schedule) {
|
|
4996
5648
|
if (success) {
|
|
5649
|
+
let nextRunAt = null;
|
|
5650
|
+
try {
|
|
5651
|
+
nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
|
|
5652
|
+
} catch (e) {
|
|
5653
|
+
context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
|
|
5654
|
+
}
|
|
4997
5655
|
await db(tasksTable).where({ id: row.id }).update({
|
|
4998
5656
|
started_at: null,
|
|
4999
5657
|
completed_at: /* @__PURE__ */ new Date(),
|
|
5000
5658
|
success,
|
|
5001
5659
|
results: toJsonColumn(results),
|
|
5002
5660
|
progress: null,
|
|
5003
|
-
past_due: null
|
|
5661
|
+
past_due: null,
|
|
5662
|
+
status: "idle",
|
|
5663
|
+
status_changed_at: db.fn.now(),
|
|
5664
|
+
next_run_at: nextRunAt,
|
|
5665
|
+
// Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
|
|
5666
|
+
service_name: null,
|
|
5667
|
+
server_name: null,
|
|
5668
|
+
instance_number: null
|
|
5004
5669
|
});
|
|
5005
5670
|
} else {
|
|
5006
5671
|
await db(tasksTable).where({ id: row.id }).update({
|
|
@@ -5008,7 +5673,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5008
5673
|
completed_at: /* @__PURE__ */ new Date(),
|
|
5009
5674
|
success,
|
|
5010
5675
|
results: toJsonColumn(results),
|
|
5011
|
-
|
|
5676
|
+
status: "paused",
|
|
5677
|
+
status_changed_at: db.fn.now(),
|
|
5012
5678
|
progress: LOCKED_BY_ERROR_MESSAGE,
|
|
5013
5679
|
past_due: null
|
|
5014
5680
|
});
|
|
@@ -5020,58 +5686,93 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5020
5686
|
const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
|
|
5021
5687
|
return { stopRunnerRequested, stopAllowanceMs };
|
|
5022
5688
|
}
|
|
5023
|
-
|
|
5689
|
+
function shuffleTaskRowsInPlace(rows) {
|
|
5690
|
+
for (let i = rows.length - 1; i > 0; i--) {
|
|
5691
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
5692
|
+
const t = rows[i];
|
|
5693
|
+
rows[i] = rows[j];
|
|
5694
|
+
rows[j] = t;
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5697
|
+
async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
|
|
5024
5698
|
const db = getDb3(context);
|
|
5025
|
-
let query = db(tasksTable).
|
|
5699
|
+
let query = db(tasksTable).where({ status: "idle" }).where(function() {
|
|
5700
|
+
this.whereNull("service_group").orWhere({ service_group: serviceGroup });
|
|
5701
|
+
}).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);
|
|
5026
5702
|
if (taskNames && taskNames.length > 0) {
|
|
5027
|
-
query = query.whereIn("
|
|
5703
|
+
query = query.whereIn("name", taskNames);
|
|
5704
|
+
}
|
|
5705
|
+
if (runnerIdentity) {
|
|
5706
|
+
query = query.where(function() {
|
|
5707
|
+
this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
|
|
5708
|
+
}).where(function() {
|
|
5709
|
+
this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
|
|
5710
|
+
}).where(function() {
|
|
5711
|
+
this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
|
|
5712
|
+
});
|
|
5713
|
+
} else {
|
|
5714
|
+
query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
|
|
5028
5715
|
}
|
|
5029
5716
|
const candidates = await query;
|
|
5717
|
+
shuffleTaskRowsInPlace(candidates);
|
|
5030
5718
|
for (const row of candidates) {
|
|
5031
5719
|
if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
|
|
5032
5720
|
continue;
|
|
5033
5721
|
}
|
|
5034
|
-
const TaskClass = registry.get(row.
|
|
5035
|
-
if (TaskClass) {
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5722
|
+
const TaskClass = registry.get(row.name);
|
|
5723
|
+
if (!TaskClass) {
|
|
5724
|
+
continue;
|
|
5725
|
+
}
|
|
5726
|
+
const taskInstance = new TaskClass(context, row);
|
|
5727
|
+
const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
|
|
5728
|
+
if (reason) {
|
|
5729
|
+
if (!row.past_due) {
|
|
5730
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
5731
|
+
past_due: db.fn.now(),
|
|
5732
|
+
progress: String(reason)
|
|
5733
|
+
});
|
|
5046
5734
|
}
|
|
5735
|
+
continue;
|
|
5736
|
+
}
|
|
5737
|
+
const claimPatch = {
|
|
5738
|
+
started_at: db.fn.now(),
|
|
5739
|
+
status: "running",
|
|
5740
|
+
status_changed_at: db.fn.now()
|
|
5741
|
+
};
|
|
5742
|
+
if (runnerIdentity) {
|
|
5743
|
+
claimPatch.service_name = runnerIdentity.service_name;
|
|
5744
|
+
claimPatch.server_name = runnerIdentity.server_name;
|
|
5745
|
+
claimPatch.instance_number = runnerIdentity.instance_number;
|
|
5047
5746
|
}
|
|
5048
|
-
const updated = await db(tasksTable).where({ id: row.id
|
|
5747
|
+
const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
|
|
5049
5748
|
const claimed = Array.isArray(updated) ? updated[0] : null;
|
|
5050
5749
|
if (claimed) return claimed;
|
|
5051
5750
|
}
|
|
5052
5751
|
return null;
|
|
5053
5752
|
}
|
|
5054
5753
|
async function runTasksLoop(context, options) {
|
|
5055
|
-
const
|
|
5754
|
+
const queueName = options.queueName ?? "tasks";
|
|
5056
5755
|
const target = options.target;
|
|
5057
5756
|
const pollMs = options.pollMs ?? 1e3;
|
|
5058
|
-
const
|
|
5757
|
+
const claimJitterMs = options.claimJitterMs ?? 0;
|
|
5758
|
+
const maxParallel = options.maxParallel ?? 32;
|
|
5059
5759
|
const scanLimit = options.scanLimit ?? 100;
|
|
5060
5760
|
const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5061
5761
|
const registry = normalizeRegistry(options.registry);
|
|
5062
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
5762
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
5063
5763
|
if (!target) throw new Error("runTasksLoop: target is required");
|
|
5764
|
+
context.tasksQueueName = queueName;
|
|
5064
5765
|
const runningPromises = /* @__PURE__ */ new Set();
|
|
5065
5766
|
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
5066
5767
|
let runningStopControlPromise = null;
|
|
5067
5768
|
let stopRequested = false;
|
|
5068
5769
|
let stopAllowanceMs = 5e3;
|
|
5069
|
-
context.
|
|
5770
|
+
context.tasksRunnerStop = false;
|
|
5070
5771
|
let registryReg = null;
|
|
5071
5772
|
let registryInterval = null;
|
|
5773
|
+
let runnerIdentity = null;
|
|
5072
5774
|
const hbGroup = options.runnerServiceGroup?.trim();
|
|
5073
5775
|
if (hbGroup) {
|
|
5074
|
-
const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
|
|
5075
5776
|
const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
|
|
5076
5777
|
const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
|
|
5077
5778
|
const defaultMeta = {
|
|
@@ -5079,16 +5780,21 @@ async function runTasksLoop(context, options) {
|
|
|
5079
5780
|
allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
|
|
5080
5781
|
};
|
|
5081
5782
|
registryReg = await registerInServicesRegistry(context, {
|
|
5082
|
-
|
|
5783
|
+
queueName,
|
|
5083
5784
|
target,
|
|
5084
5785
|
serviceGroup: hbGroup,
|
|
5085
5786
|
serviceName: options.runnerServiceName,
|
|
5086
|
-
|
|
5787
|
+
instanceNumber: options.runnerInstanceNumber,
|
|
5087
5788
|
staleMs,
|
|
5088
5789
|
groupMaxInstances: options.runnerGroupMaxInstances,
|
|
5089
5790
|
enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
|
|
5090
5791
|
metadata: options.runnerMetadata ?? defaultMeta
|
|
5091
5792
|
});
|
|
5793
|
+
runnerIdentity = {
|
|
5794
|
+
service_name: registryReg.serviceName,
|
|
5795
|
+
server_name: os3.hostname(),
|
|
5796
|
+
instance_number: registryReg.instanceNumber
|
|
5797
|
+
};
|
|
5092
5798
|
registryInterval = setInterval(() => {
|
|
5093
5799
|
void touchServicesRegistry(context, registryReg).catch((err) => {
|
|
5094
5800
|
context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
|
|
@@ -5096,7 +5802,7 @@ async function runTasksLoop(context, options) {
|
|
|
5096
5802
|
}, hbIntervalMs);
|
|
5097
5803
|
}
|
|
5098
5804
|
try {
|
|
5099
|
-
while (!context.isStop() && !stopRequested &&
|
|
5805
|
+
while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
|
|
5100
5806
|
if (!runningStopControlPromise) {
|
|
5101
5807
|
const claimedStopTask = await claimNextRunnableTask(
|
|
5102
5808
|
context,
|
|
@@ -5104,7 +5810,8 @@ async function runTasksLoop(context, options) {
|
|
|
5104
5810
|
target,
|
|
5105
5811
|
registry,
|
|
5106
5812
|
10,
|
|
5107
|
-
["stopRunner", "stop"]
|
|
5813
|
+
["stopRunner", "stop"],
|
|
5814
|
+
runnerIdentity
|
|
5108
5815
|
);
|
|
5109
5816
|
if (claimedStopTask) {
|
|
5110
5817
|
runningStopControlPromise = executeClaimedTask(
|
|
@@ -5118,7 +5825,7 @@ async function runTasksLoop(context, options) {
|
|
|
5118
5825
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5119
5826
|
stopRequested = true;
|
|
5120
5827
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5121
|
-
context.
|
|
5828
|
+
context.tasksRunnerStop = true;
|
|
5122
5829
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5123
5830
|
}
|
|
5124
5831
|
}).finally(() => {
|
|
@@ -5126,6 +5833,9 @@ async function runTasksLoop(context, options) {
|
|
|
5126
5833
|
});
|
|
5127
5834
|
}
|
|
5128
5835
|
}
|
|
5836
|
+
if (claimJitterMs > 0) {
|
|
5837
|
+
await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
|
|
5838
|
+
}
|
|
5129
5839
|
while (runningPromises.size < maxParallel) {
|
|
5130
5840
|
const claimed = await claimNextRunnableTask(
|
|
5131
5841
|
context,
|
|
@@ -5133,14 +5843,15 @@ async function runTasksLoop(context, options) {
|
|
|
5133
5843
|
target,
|
|
5134
5844
|
registry,
|
|
5135
5845
|
scanLimit,
|
|
5136
|
-
allowedTasks
|
|
5846
|
+
allowedTasks,
|
|
5847
|
+
runnerIdentity
|
|
5137
5848
|
);
|
|
5138
5849
|
if (!claimed) break;
|
|
5139
5850
|
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
5140
5851
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5141
5852
|
stopRequested = true;
|
|
5142
5853
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5143
|
-
context.
|
|
5854
|
+
context.tasksRunnerStop = true;
|
|
5144
5855
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5145
5856
|
}
|
|
5146
5857
|
}).finally(() => {
|
|
@@ -5148,7 +5859,16 @@ async function runTasksLoop(context, options) {
|
|
|
5148
5859
|
});
|
|
5149
5860
|
runningPromises.add(p);
|
|
5150
5861
|
}
|
|
5151
|
-
|
|
5862
|
+
const wakePromises = [...runningPromises];
|
|
5863
|
+
if (runningStopControlPromise) {
|
|
5864
|
+
wakePromises.push(runningStopControlPromise);
|
|
5865
|
+
}
|
|
5866
|
+
if (wakePromises.length === 0) {
|
|
5867
|
+
await sleepMs(pollMs);
|
|
5868
|
+
} else {
|
|
5869
|
+
const safe = wakePromises.map((p) => p.catch(() => void 0));
|
|
5870
|
+
await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
|
|
5871
|
+
}
|
|
5152
5872
|
}
|
|
5153
5873
|
if (context.isStop() && !stopRequested) {
|
|
5154
5874
|
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
@@ -5183,71 +5903,88 @@ async function runTasksLoop(context, options) {
|
|
|
5183
5903
|
}
|
|
5184
5904
|
}
|
|
5185
5905
|
var TasksManager = class _TasksManager {
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5906
|
+
/**
|
|
5907
|
+
* @param {object} context
|
|
5908
|
+
* @param {{
|
|
5909
|
+
* queueName?: string,
|
|
5910
|
+
* target?: string,
|
|
5911
|
+
* recreateTaskTables?: boolean,
|
|
5912
|
+
* pollMs?: number,
|
|
5913
|
+
* claimJitterMs?: number,
|
|
5914
|
+
* maxParallel?: number,
|
|
5915
|
+
* scanLimit?: number,
|
|
5916
|
+
* allowedTasks?: string | string[],
|
|
5917
|
+
* registry?: TasksRegistry | Record<string, Function>,
|
|
5918
|
+
* runnerServiceGroup?: string,
|
|
5919
|
+
* runnerServiceName?: string,
|
|
5920
|
+
* runnerInstanceNumber?: number,
|
|
5921
|
+
* runnerHeartbeatIntervalMs?: number,
|
|
5922
|
+
* runnerHeartbeatStaleMs?: number,
|
|
5923
|
+
* runnerGroupMaxInstances?: number,
|
|
5924
|
+
* runnerEnforceMaxInstances?: boolean,
|
|
5925
|
+
* runnerMetadata?: Record<string, unknown>,
|
|
5926
|
+
* }} [options]
|
|
5927
|
+
*/
|
|
5203
5928
|
constructor(context, options = {}) {
|
|
5204
5929
|
this.context = context;
|
|
5205
|
-
this.
|
|
5930
|
+
this.queueName = options.queueName ?? "tasks";
|
|
5206
5931
|
this.target = options.target ?? "localRunner";
|
|
5207
5932
|
this.recreateTaskTables = options.recreateTaskTables ?? false;
|
|
5208
5933
|
this.pollMs = options.pollMs ?? 1e3;
|
|
5934
|
+
this.claimJitterMs = options.claimJitterMs ?? 0;
|
|
5209
5935
|
this.maxParallel = options.maxParallel ?? 1;
|
|
5210
5936
|
this.scanLimit = options.scanLimit ?? 100;
|
|
5211
5937
|
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5212
5938
|
this.registry = normalizeRegistry(options.registry);
|
|
5213
5939
|
this.runnerServiceGroup = options.runnerServiceGroup;
|
|
5214
5940
|
this.runnerServiceName = options.runnerServiceName;
|
|
5215
|
-
this.
|
|
5941
|
+
this.runnerInstanceNumber = options.runnerInstanceNumber;
|
|
5216
5942
|
this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
|
|
5217
5943
|
this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
|
|
5218
5944
|
this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
|
|
5219
5945
|
this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
|
|
5220
5946
|
this.runnerMetadata = options.runnerMetadata;
|
|
5221
5947
|
}
|
|
5948
|
+
/**
|
|
5949
|
+
* Preferred factory: reads defaults from `context.params` (module namespace
|
|
5950
|
+
* `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
|
|
5951
|
+
* and inline options in one consistent resolver.
|
|
5952
|
+
*
|
|
5953
|
+
* @param {object} context
|
|
5954
|
+
* @param {ConstructorParameters<typeof TasksManager>[1]} [options]
|
|
5955
|
+
* @returns {TasksManager}
|
|
5956
|
+
*/
|
|
5222
5957
|
static init(context, options = {}) {
|
|
5223
5958
|
const defs2 = {
|
|
5224
5959
|
table: "string default tasks",
|
|
5225
5960
|
target: "string default localRunner",
|
|
5226
5961
|
recreateTaskTables: "boolean default false",
|
|
5227
5962
|
pollMs: "number default 1000",
|
|
5963
|
+
claimJitterMs: "number default 0",
|
|
5228
5964
|
maxParallel: "number default 1",
|
|
5229
5965
|
scanLimit: "number default 100",
|
|
5230
5966
|
allowedTasks: "string",
|
|
5231
5967
|
runnerServiceGroup: "string",
|
|
5232
5968
|
runnerServiceName: "string",
|
|
5233
|
-
|
|
5969
|
+
runnerInstanceNumber: "number",
|
|
5234
5970
|
runnerHeartbeatIntervalMs: "number default 10000",
|
|
5235
5971
|
runnerHeartbeatStaleMs: "number default 45000",
|
|
5236
5972
|
runnerGroupMaxInstances: "number",
|
|
5237
5973
|
runnerEnforceMaxInstances: "boolean default true"
|
|
5238
5974
|
};
|
|
5239
|
-
const discovered = context.params.getAllForModule(defs2);
|
|
5975
|
+
const discovered = context.params.getAllForModule("tasks", defs2);
|
|
5240
5976
|
const resolved = {
|
|
5241
|
-
|
|
5977
|
+
queueName: discovered.table,
|
|
5242
5978
|
target: discovered.target,
|
|
5243
5979
|
recreateTaskTables: discovered.recreateTaskTables,
|
|
5244
5980
|
pollMs: discovered.pollMs,
|
|
5981
|
+
claimJitterMs: discovered.claimJitterMs,
|
|
5245
5982
|
maxParallel: discovered.maxParallel,
|
|
5246
5983
|
scanLimit: discovered.scanLimit,
|
|
5247
5984
|
allowedTasks: discovered.allowedTasks,
|
|
5248
5985
|
runnerServiceGroup: discovered.runnerServiceGroup,
|
|
5249
5986
|
runnerServiceName: discovered.runnerServiceName,
|
|
5250
|
-
|
|
5987
|
+
runnerInstanceNumber: discovered.runnerInstanceNumber,
|
|
5251
5988
|
runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
|
|
5252
5989
|
runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
|
|
5253
5990
|
runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
|
|
@@ -5256,24 +5993,39 @@ var TasksManager = class _TasksManager {
|
|
|
5256
5993
|
};
|
|
5257
5994
|
return new _TasksManager(context, resolved);
|
|
5258
5995
|
}
|
|
5996
|
+
/**
|
|
5997
|
+
* Idempotently ensure the three backing tables exist for this queue.
|
|
5998
|
+
*
|
|
5999
|
+
* @param {{ recreate?: boolean }} [options]
|
|
6000
|
+
* @returns {Promise<void>}
|
|
6001
|
+
*/
|
|
5259
6002
|
async ensureTaskTables(options = {}) {
|
|
5260
6003
|
await ensureTaskTables(this.context, {
|
|
5261
|
-
|
|
6004
|
+
queueName: this.queueName,
|
|
5262
6005
|
recreate: options.recreate ?? this.recreateTaskTables
|
|
5263
6006
|
});
|
|
5264
6007
|
}
|
|
6008
|
+
/**
|
|
6009
|
+
* Start the runner loop using this manager's resolved config. Per-call
|
|
6010
|
+
* options override the stored defaults, but `runnerMetadata` still falls
|
|
6011
|
+
* through when omitted.
|
|
6012
|
+
*
|
|
6013
|
+
* @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
|
|
6014
|
+
* @returns {Promise<void>}
|
|
6015
|
+
*/
|
|
5265
6016
|
async runTasksLoop(options = {}) {
|
|
5266
6017
|
await runTasksLoop(this.context, {
|
|
5267
|
-
|
|
6018
|
+
queueName: options.queueName ?? this.queueName,
|
|
5268
6019
|
target: options.target ?? this.target,
|
|
5269
6020
|
pollMs: options.pollMs ?? this.pollMs,
|
|
6021
|
+
claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
|
|
5270
6022
|
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
5271
6023
|
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
5272
6024
|
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
5273
6025
|
registry: options.registry ?? this.registry,
|
|
5274
6026
|
runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
|
|
5275
6027
|
runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
|
|
5276
|
-
|
|
6028
|
+
runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
|
|
5277
6029
|
runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
|
|
5278
6030
|
runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
|
|
5279
6031
|
runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
|
|
@@ -5283,9 +6035,8 @@ var TasksManager = class _TasksManager {
|
|
|
5283
6035
|
}
|
|
5284
6036
|
};
|
|
5285
6037
|
|
|
5286
|
-
// src/scripts/cli-runner.
|
|
6038
|
+
// src/scripts/cli-runner.js
|
|
5287
6039
|
var defs = {
|
|
5288
|
-
dbName: "string default local",
|
|
5289
6040
|
tasksModule: "string"
|
|
5290
6041
|
};
|
|
5291
6042
|
async function loadTasksModule(modulePath) {
|
|
@@ -5297,11 +6048,8 @@ async function loadTasksModule(modulePath) {
|
|
|
5297
6048
|
return imported.tasksRegistry;
|
|
5298
6049
|
}
|
|
5299
6050
|
var flow = async (context) => {
|
|
5300
|
-
const {
|
|
5301
|
-
|
|
5302
|
-
tasksModule
|
|
5303
|
-
} = context.params.getAll(defs);
|
|
5304
|
-
const db = await dbInit(context, dbName);
|
|
6051
|
+
const { tasksModule } = context.params.getAll(defs);
|
|
6052
|
+
const db = await Db.init(context);
|
|
5305
6053
|
context.db = db;
|
|
5306
6054
|
const registry = new TasksRegistry().addMany(defaultTasksRegistry.toObject());
|
|
5307
6055
|
if (tasksModule) {
|