@nmakarov/cli-toolkit 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/args.cjs +1 -4
- package/dist/args.cjs.map +1 -1
- package/dist/args.js +1 -1
- package/dist/args.js.map +1 -1
- package/dist/cli-runner.cjs +1252 -604
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +1299 -650
- package/dist/cli-runner.js.map +1 -1
- package/dist/db.cjs +85 -157
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +84 -150
- package/dist/db.js.map +1 -1
- package/dist/errors.cjs +2 -2
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/filedatabase.cjs +19 -19
- package/dist/filedatabase.cjs.map +1 -1
- package/dist/filedatabase.js +19 -16
- package/dist/filedatabase.js.map +1 -1
- package/dist/http-client.cjs +9 -11
- package/dist/http-client.cjs.map +1 -1
- package/dist/http-client.js +10 -9
- package/dist/http-client.js.map +1 -1
- package/dist/http-client2.cjs +34 -37
- package/dist/http-client2.cjs.map +1 -1
- package/dist/http-client2.js +34 -34
- package/dist/http-client2.js.map +1 -1
- package/dist/index.cjs +1739 -720
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1746 -721
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +81 -68
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +96 -82
- package/dist/init.js.map +1 -1
- package/dist/logger.cjs +5 -5
- package/dist/logger.cjs.map +1 -1
- package/dist/logger.js +5 -4
- package/dist/logger.js.map +1 -1
- package/dist/mock-server.cjs +21 -33
- package/dist/mock-server.cjs.map +1 -1
- package/dist/mock-server.js +21 -28
- package/dist/mock-server.js.map +1 -1
- package/dist/params.cjs +6 -9
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +6 -6
- package/dist/params.js.map +1 -1
- package/dist/s3.cjs +286 -0
- package/dist/s3.cjs.map +1 -0
- package/dist/s3.js +273 -0
- package/dist/s3.js.map +1 -0
- package/dist/screen.cjs +34 -39
- package/dist/screen.cjs.map +1 -1
- package/dist/screen.js +48 -46
- package/dist/screen.js.map +1 -1
- package/dist/tasks.cjs +1354 -501
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +1369 -527
- package/dist/tasks.js.map +1 -1
- package/dist/utils.cjs +7 -8
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.js +6 -6
- package/dist/utils.js.map +1 -1
- package/package.json +32 -47
- package/scripts/ssm/{parse-cli.ts → parse-cli.js} +4 -4
- package/scripts/ssm/{ssm-admin.ts → ssm-admin.js} +12 -12
- package/scripts/ssm/{ssm-pull.ts → ssm-pull.js} +10 -13
package/dist/cli-runner.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
|
}
|
|
@@ -2114,11 +2109,11 @@ var Params = class _Params {
|
|
|
2114
2109
|
}
|
|
2115
2110
|
};
|
|
2116
2111
|
|
|
2117
|
-
// src/logger/index.
|
|
2112
|
+
// src/logger/index.js
|
|
2118
2113
|
import chalk from "chalk";
|
|
2119
2114
|
import util from "util";
|
|
2120
2115
|
|
|
2121
|
-
// src/logger/transports.
|
|
2116
|
+
// src/logger/transports.js
|
|
2122
2117
|
var ConsoleTransport = class {
|
|
2123
2118
|
write(payload) {
|
|
2124
2119
|
console.info(payload);
|
|
@@ -2138,7 +2133,7 @@ var ParentProcessTransport = class {
|
|
|
2138
2133
|
}
|
|
2139
2134
|
};
|
|
2140
2135
|
|
|
2141
|
-
// src/logger/index.
|
|
2136
|
+
// src/logger/index.js
|
|
2142
2137
|
var ALL_LEVELS = [
|
|
2143
2138
|
"silly",
|
|
2144
2139
|
"debug",
|
|
@@ -2211,6 +2206,7 @@ var Logger = class _Logger {
|
|
|
2211
2206
|
}
|
|
2212
2207
|
/**
|
|
2213
2208
|
* Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
|
|
2209
|
+
* Params are tracked under the `logger` module for --showUsedParams.
|
|
2214
2210
|
*/
|
|
2215
2211
|
static init(context, options) {
|
|
2216
2212
|
const paramDefs = {
|
|
@@ -2224,7 +2220,7 @@ var Logger = class _Logger {
|
|
|
2224
2220
|
progressWithTimes: "boolean default false",
|
|
2225
2221
|
progressThrottleMs: "number"
|
|
2226
2222
|
};
|
|
2227
|
-
const discovered = context.params.getAllForModule(paramDefs);
|
|
2223
|
+
const discovered = context.params.getAllForModule("logger", paramDefs);
|
|
2228
2224
|
const config2 = { ...discovered, ...options };
|
|
2229
2225
|
const logger = new _Logger(context, config2);
|
|
2230
2226
|
context.logger = logger;
|
|
@@ -2420,9 +2416,9 @@ var Logger = class _Logger {
|
|
|
2420
2416
|
}
|
|
2421
2417
|
};
|
|
2422
2418
|
|
|
2423
|
-
// src/init/index.
|
|
2419
|
+
// src/init/index.js
|
|
2424
2420
|
import { EventEmitter } from "events";
|
|
2425
|
-
function extractComponentOptions(opts,
|
|
2421
|
+
function extractComponentOptions(opts, _componentName) {
|
|
2426
2422
|
const reservedKeys = ["overrides", "defaults", "modules"];
|
|
2427
2423
|
const componentOptions = {};
|
|
2428
2424
|
for (const [key, value] of Object.entries(opts)) {
|
|
@@ -2489,6 +2485,19 @@ function printAllParameters(context) {
|
|
|
2489
2485
|
async function init(flow2, opts = {}) {
|
|
2490
2486
|
let stop = false;
|
|
2491
2487
|
let context = null;
|
|
2488
|
+
let cleanupRan = false;
|
|
2489
|
+
const runRegisteredCleanups = async (ctx) => {
|
|
2490
|
+
if (cleanupRan) return;
|
|
2491
|
+
cleanupRan = true;
|
|
2492
|
+
const fns = [...ctx.cleanupFunctions].reverse();
|
|
2493
|
+
for (const fn of fns) {
|
|
2494
|
+
try {
|
|
2495
|
+
await fn(ctx);
|
|
2496
|
+
} catch (error) {
|
|
2497
|
+
ctx.logger.warn("[cleanup] error in cleanup function:", error);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
};
|
|
2492
2501
|
try {
|
|
2493
2502
|
try {
|
|
2494
2503
|
const screenModule = await Promise.resolve().then(() => (init_screen(), screen_exports));
|
|
@@ -2511,13 +2520,24 @@ async function init(flow2, opts = {}) {
|
|
|
2511
2520
|
printAllParameters(context);
|
|
2512
2521
|
process.exit(0);
|
|
2513
2522
|
}
|
|
2523
|
+
let sigintCount = 0;
|
|
2514
2524
|
process.on("SIGINT", async () => {
|
|
2515
|
-
if (
|
|
2516
|
-
|
|
2517
|
-
|
|
2525
|
+
if (!context) return;
|
|
2526
|
+
sigintCount += 1;
|
|
2527
|
+
if (sigintCount === 1) {
|
|
2528
|
+
stop = true;
|
|
2529
|
+
context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
|
|
2530
|
+
context.emitter.emit("stop", stopAllowance);
|
|
2531
|
+
return;
|
|
2518
2532
|
}
|
|
2533
|
+
context.logger.warn("[process] second SIGINT: running cleanup then exit");
|
|
2534
|
+
await runRegisteredCleanups(context);
|
|
2535
|
+
process.exit(2);
|
|
2536
|
+
});
|
|
2537
|
+
process.on("SIGTERM", () => {
|
|
2538
|
+
if (!context || stop) return;
|
|
2519
2539
|
stop = true;
|
|
2520
|
-
context.logger.info(`>> emitting stop with allowance ${stopAllowance}`);
|
|
2540
|
+
context.logger.info(`>> SIGTERM: emitting stop with allowance ${stopAllowance}`);
|
|
2521
2541
|
context.emitter.emit("stop", stopAllowance);
|
|
2522
2542
|
});
|
|
2523
2543
|
await flow2(context);
|
|
@@ -2542,33 +2562,62 @@ async function init(flow2, opts = {}) {
|
|
|
2542
2562
|
}
|
|
2543
2563
|
} finally {
|
|
2544
2564
|
if (context) {
|
|
2545
|
-
|
|
2546
|
-
try {
|
|
2547
|
-
await fn(context);
|
|
2548
|
-
} catch (error) {
|
|
2549
|
-
context.logger.warn("[cleanup] error in cleanup function:", error);
|
|
2550
|
-
}
|
|
2551
|
-
}
|
|
2565
|
+
await runRegisteredCleanups(context);
|
|
2552
2566
|
}
|
|
2553
2567
|
}
|
|
2554
2568
|
}
|
|
2555
2569
|
|
|
2556
|
-
// src/db/index.
|
|
2570
|
+
// src/db/index.js
|
|
2557
2571
|
import knex from "knex";
|
|
2572
|
+
var KNEX_DEFAULTS = {
|
|
2573
|
+
testConnection: true,
|
|
2574
|
+
pool: { min: 2, max: 10 },
|
|
2575
|
+
acquireConnectionTimeout: 1e4,
|
|
2576
|
+
ssl: { rejectUnauthorized: false }
|
|
2577
|
+
};
|
|
2558
2578
|
var Db = class {
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2579
|
+
static async init(context, options = {}) {
|
|
2580
|
+
const defs2 = {
|
|
2581
|
+
dbName: "string",
|
|
2582
|
+
dbConnectionString: "string",
|
|
2583
|
+
dbProfile: "boolean default false"
|
|
2584
|
+
};
|
|
2585
|
+
const discovered = context?.params?.getAllForModule?.("db", defs2) ?? {};
|
|
2586
|
+
const merged = { ...discovered, ...options };
|
|
2587
|
+
let { dbName, dbConnectionString } = merged;
|
|
2588
|
+
const { dbProfile } = merged;
|
|
2589
|
+
if (!dbName && !dbConnectionString) {
|
|
2590
|
+
dbName = "local";
|
|
2591
|
+
}
|
|
2592
|
+
if (dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
|
|
2593
|
+
dbConnectionString = dbName;
|
|
2594
|
+
dbName = void 0;
|
|
2595
|
+
}
|
|
2596
|
+
if (dbName && !dbConnectionString) {
|
|
2597
|
+
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
2598
|
+
dbConnectionString = await context.params.get(paramName, "string");
|
|
2599
|
+
if (!dbConnectionString) {
|
|
2600
|
+
throw new ParamError(
|
|
2601
|
+
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
const config2 = {
|
|
2606
|
+
...KNEX_DEFAULTS,
|
|
2607
|
+
connectionString: dbConnectionString,
|
|
2608
|
+
name: dbName || merged.name || "default",
|
|
2609
|
+
profile: !!dbProfile,
|
|
2610
|
+
logger: context.logger
|
|
2611
|
+
};
|
|
2612
|
+
return dbConnect(context, config2);
|
|
2613
|
+
}
|
|
2568
2614
|
constructor(config2) {
|
|
2569
|
-
if (!config2.connectionString) {
|
|
2615
|
+
if (!config2 || !config2.connectionString) {
|
|
2570
2616
|
throw new ParamError("Db: connectionString is required");
|
|
2571
2617
|
}
|
|
2618
|
+
this.knexInstance = null;
|
|
2619
|
+
this.isConnected = false;
|
|
2620
|
+
this.queriesLog = [];
|
|
2572
2621
|
this.config = {
|
|
2573
2622
|
testConnection: true,
|
|
2574
2623
|
profile: false,
|
|
@@ -2581,25 +2630,23 @@ var Db = class {
|
|
|
2581
2630
|
};
|
|
2582
2631
|
this.logger = this.config.logger;
|
|
2583
2632
|
const instance = this;
|
|
2584
|
-
const callableWrapper = function(
|
|
2633
|
+
const callableWrapper = function() {
|
|
2585
2634
|
throw new Error("This should never be called directly");
|
|
2586
2635
|
};
|
|
2587
2636
|
callableWrapper._instance = instance;
|
|
2588
2637
|
return new Proxy(callableWrapper, {
|
|
2589
|
-
|
|
2590
|
-
apply: (target, thisArg, argumentsList) => {
|
|
2638
|
+
apply: (target, _thisArg, argumentsList) => {
|
|
2591
2639
|
const inst = target._instance;
|
|
2592
2640
|
if (!inst.knexInstance) {
|
|
2593
2641
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
2594
2642
|
}
|
|
2595
2643
|
return inst.knexInstance(...argumentsList);
|
|
2596
2644
|
},
|
|
2597
|
-
// Intercept property access: db.schema, db.raw, etc.
|
|
2598
2645
|
get: (target, prop) => {
|
|
2599
2646
|
if (prop === "_instance") {
|
|
2600
2647
|
return target._instance;
|
|
2601
2648
|
}
|
|
2602
|
-
const
|
|
2649
|
+
const inst = target._instance;
|
|
2603
2650
|
const ownMethods = [
|
|
2604
2651
|
"connect",
|
|
2605
2652
|
"disconnect",
|
|
@@ -2612,26 +2659,26 @@ var Db = class {
|
|
|
2612
2659
|
"detectClient",
|
|
2613
2660
|
"attachProfiler"
|
|
2614
2661
|
];
|
|
2615
|
-
if (prop in
|
|
2616
|
-
const value =
|
|
2662
|
+
if (prop in inst) {
|
|
2663
|
+
const value = inst[prop];
|
|
2617
2664
|
if (typeof value === "function" && ownMethods.includes(prop)) {
|
|
2618
|
-
return value.bind(
|
|
2665
|
+
return value.bind(inst);
|
|
2619
2666
|
}
|
|
2620
2667
|
if (typeof value !== "function") {
|
|
2621
2668
|
return value;
|
|
2622
2669
|
}
|
|
2623
2670
|
}
|
|
2624
|
-
if (
|
|
2625
|
-
const knexProp =
|
|
2671
|
+
if (inst.knexInstance) {
|
|
2672
|
+
const knexProp = inst.knexInstance[prop];
|
|
2626
2673
|
if (typeof knexProp === "function") {
|
|
2627
|
-
return knexProp.bind(
|
|
2674
|
+
return knexProp.bind(inst.knexInstance);
|
|
2628
2675
|
}
|
|
2629
2676
|
return knexProp;
|
|
2630
2677
|
}
|
|
2631
|
-
if (prop in
|
|
2632
|
-
const method =
|
|
2678
|
+
if (prop in inst) {
|
|
2679
|
+
const method = inst[prop];
|
|
2633
2680
|
if (typeof method === "function") {
|
|
2634
|
-
return method.bind(
|
|
2681
|
+
return method.bind(inst);
|
|
2635
2682
|
}
|
|
2636
2683
|
return method;
|
|
2637
2684
|
}
|
|
@@ -2639,9 +2686,6 @@ var Db = class {
|
|
|
2639
2686
|
}
|
|
2640
2687
|
});
|
|
2641
2688
|
}
|
|
2642
|
-
/**
|
|
2643
|
-
* Detect database client type from connection string
|
|
2644
|
-
*/
|
|
2645
2689
|
detectClient(connectionString) {
|
|
2646
2690
|
if (connectionString.match(/^postgresql/)) {
|
|
2647
2691
|
return "pg";
|
|
@@ -2651,9 +2695,6 @@ var Db = class {
|
|
|
2651
2695
|
}
|
|
2652
2696
|
return null;
|
|
2653
2697
|
}
|
|
2654
|
-
/**
|
|
2655
|
-
* Connect to the database
|
|
2656
|
-
*/
|
|
2657
2698
|
async connect() {
|
|
2658
2699
|
if (this.isConnected && this.knexInstance) {
|
|
2659
2700
|
this.logger.warn?.("[Db] Already connected");
|
|
@@ -2662,14 +2703,13 @@ var Db = class {
|
|
|
2662
2703
|
const client = this.detectClient(this.config.connectionString);
|
|
2663
2704
|
if (!client) {
|
|
2664
2705
|
throw new ParamError(
|
|
2665
|
-
|
|
2706
|
+
"Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://"
|
|
2666
2707
|
);
|
|
2667
2708
|
}
|
|
2668
2709
|
try {
|
|
2669
2710
|
const connectionConfig = {
|
|
2670
2711
|
connectionString: this.config.connectionString,
|
|
2671
2712
|
family: 4
|
|
2672
|
-
// Force IPv4 only (disable IPv6)
|
|
2673
2713
|
};
|
|
2674
2714
|
this.knexInstance = knex({
|
|
2675
2715
|
client,
|
|
@@ -2685,7 +2725,9 @@ var Db = class {
|
|
|
2685
2725
|
await this.testConnection();
|
|
2686
2726
|
}
|
|
2687
2727
|
this.isConnected = true;
|
|
2688
|
-
this.logger.debug?.(
|
|
2728
|
+
this.logger.debug?.(
|
|
2729
|
+
`[Db] Connected to database "${this.config.name || this.config.connectionString}"`
|
|
2730
|
+
);
|
|
2689
2731
|
} catch (error) {
|
|
2690
2732
|
if (error instanceof ParamError) {
|
|
2691
2733
|
throw error;
|
|
@@ -2694,9 +2736,6 @@ var Db = class {
|
|
|
2694
2736
|
throw new ParamError(`Db: Connection failed - ${errorMsg}`);
|
|
2695
2737
|
}
|
|
2696
2738
|
}
|
|
2697
|
-
/**
|
|
2698
|
-
* Disconnect from the database
|
|
2699
|
-
*/
|
|
2700
2739
|
async disconnect() {
|
|
2701
2740
|
if (!this.knexInstance) {
|
|
2702
2741
|
return;
|
|
@@ -2706,16 +2745,15 @@ var Db = class {
|
|
|
2706
2745
|
this.knexInstance = null;
|
|
2707
2746
|
this.isConnected = false;
|
|
2708
2747
|
this.queriesLog = [];
|
|
2709
|
-
this.logger.debug?.(
|
|
2748
|
+
this.logger.debug?.(
|
|
2749
|
+
`[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`
|
|
2750
|
+
);
|
|
2710
2751
|
} catch (error) {
|
|
2711
2752
|
const errorMsg = this.getErrorMessage(error);
|
|
2712
2753
|
this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
|
|
2713
2754
|
throw error;
|
|
2714
2755
|
}
|
|
2715
2756
|
}
|
|
2716
|
-
/**
|
|
2717
|
-
* Extract error message from various error types
|
|
2718
|
-
*/
|
|
2719
2757
|
getErrorMessage(error) {
|
|
2720
2758
|
if (error instanceof AggregateError) {
|
|
2721
2759
|
const errors = error.errors || [];
|
|
@@ -2740,9 +2778,11 @@ var Db = class {
|
|
|
2740
2778
|
return `${code} (tried: ${addresses.join(", ")})`;
|
|
2741
2779
|
}
|
|
2742
2780
|
}
|
|
2743
|
-
const uniqueMessages = [
|
|
2744
|
-
|
|
2745
|
-
|
|
2781
|
+
const uniqueMessages = [
|
|
2782
|
+
...new Set(
|
|
2783
|
+
errors.map((e) => e instanceof Error ? e.message : String(e))
|
|
2784
|
+
)
|
|
2785
|
+
];
|
|
2746
2786
|
if (uniqueMessages.length === 1) {
|
|
2747
2787
|
return uniqueMessages[0];
|
|
2748
2788
|
}
|
|
@@ -2751,28 +2791,25 @@ var Db = class {
|
|
|
2751
2791
|
return error.message || "Multiple errors occurred";
|
|
2752
2792
|
}
|
|
2753
2793
|
if (error instanceof Error) {
|
|
2754
|
-
const
|
|
2755
|
-
if (
|
|
2756
|
-
return `${
|
|
2794
|
+
const code = error.code;
|
|
2795
|
+
if (code) {
|
|
2796
|
+
return `${code}: ${error.message || String(error)}`;
|
|
2757
2797
|
}
|
|
2758
2798
|
return error.message || String(error);
|
|
2759
2799
|
}
|
|
2760
2800
|
if (typeof error === "string") {
|
|
2761
2801
|
return error;
|
|
2762
2802
|
}
|
|
2763
|
-
if (error
|
|
2803
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
2764
2804
|
const msg = String(error.message);
|
|
2765
|
-
const
|
|
2766
|
-
if (
|
|
2767
|
-
return `${
|
|
2805
|
+
const code = error.code;
|
|
2806
|
+
if (code) {
|
|
2807
|
+
return `${code}: ${msg}`;
|
|
2768
2808
|
}
|
|
2769
2809
|
return msg;
|
|
2770
2810
|
}
|
|
2771
2811
|
return String(error) || "Unknown error";
|
|
2772
2812
|
}
|
|
2773
|
-
/**
|
|
2774
|
-
* Test database connection
|
|
2775
|
-
*/
|
|
2776
2813
|
async testConnection() {
|
|
2777
2814
|
if (!this.knexInstance) {
|
|
2778
2815
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -2788,9 +2825,6 @@ var Db = class {
|
|
|
2788
2825
|
throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
|
|
2789
2826
|
}
|
|
2790
2827
|
}
|
|
2791
|
-
/**
|
|
2792
|
-
* Attach query profiler to log all queries
|
|
2793
|
-
*/
|
|
2794
2828
|
attachProfiler() {
|
|
2795
2829
|
if (!this.knexInstance) {
|
|
2796
2830
|
return;
|
|
@@ -2800,7 +2834,7 @@ var Db = class {
|
|
|
2800
2834
|
this.knexInstance.on("query", (query) => {
|
|
2801
2835
|
query.__startTime = process.hrtime();
|
|
2802
2836
|
});
|
|
2803
|
-
this.knexInstance.on("query-response", (
|
|
2837
|
+
this.knexInstance.on("query-response", (_response, query) => {
|
|
2804
2838
|
const [seconds, nanoseconds] = process.hrtime(query.__startTime);
|
|
2805
2839
|
const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
|
|
2806
2840
|
const logEntry = {
|
|
@@ -2815,15 +2849,9 @@ var Db = class {
|
|
|
2815
2849
|
this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
|
|
2816
2850
|
});
|
|
2817
2851
|
}
|
|
2818
|
-
/**
|
|
2819
|
-
* Get query log (only available if profiling is enabled)
|
|
2820
|
-
*/
|
|
2821
2852
|
getQueryLog() {
|
|
2822
2853
|
return [...this.queriesLog];
|
|
2823
2854
|
}
|
|
2824
|
-
/**
|
|
2825
|
-
* Check if a table exists
|
|
2826
|
-
*/
|
|
2827
2855
|
async tableExists(tableName) {
|
|
2828
2856
|
if (!this.knexInstance) {
|
|
2829
2857
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -2835,65 +2863,28 @@ var Db = class {
|
|
|
2835
2863
|
throw error;
|
|
2836
2864
|
}
|
|
2837
2865
|
}
|
|
2838
|
-
/**
|
|
2839
|
-
* Get the underlying Knex instance (for advanced usage)
|
|
2840
|
-
*/
|
|
2841
2866
|
getKnex() {
|
|
2842
2867
|
if (!this.knexInstance) {
|
|
2843
2868
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
2844
2869
|
}
|
|
2845
2870
|
return this.knexInstance;
|
|
2846
2871
|
}
|
|
2847
|
-
/**
|
|
2848
|
-
* Get connection status
|
|
2849
|
-
*/
|
|
2850
2872
|
isConnectedToDb() {
|
|
2851
2873
|
return this.isConnected && this.knexInstance !== null;
|
|
2852
2874
|
}
|
|
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
2875
|
};
|
|
2861
2876
|
function capitalizeFirstLetter(str) {
|
|
2862
2877
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
2863
2878
|
}
|
|
2864
|
-
async function dbConnect(context,
|
|
2865
|
-
const defs2 = {
|
|
2866
|
-
testDbConnection: "boolean default true",
|
|
2867
|
-
name: "string",
|
|
2868
|
-
poolMin: "number default 2",
|
|
2869
|
-
poolMax: "number default 10",
|
|
2870
|
-
acquireConnectionTimeout: "number default 10000",
|
|
2871
|
-
sslRejectUnauthorized: "boolean default false"
|
|
2872
|
-
};
|
|
2873
|
-
const paramsConfig = context.params.getAllForModule(defs2);
|
|
2874
|
-
const config2 = {
|
|
2875
|
-
connectionString,
|
|
2876
|
-
name: paramsConfig.name || name || "default",
|
|
2877
|
-
testConnection: paramsConfig.testDbConnection,
|
|
2878
|
-
profile: dbProfile ?? false,
|
|
2879
|
-
pool: {
|
|
2880
|
-
min: paramsConfig.poolMin,
|
|
2881
|
-
max: paramsConfig.poolMax
|
|
2882
|
-
},
|
|
2883
|
-
acquireConnectionTimeout: paramsConfig.acquireConnectionTimeout,
|
|
2884
|
-
ssl: {
|
|
2885
|
-
rejectUnauthorized: paramsConfig.sslRejectUnauthorized
|
|
2886
|
-
},
|
|
2887
|
-
logger: context.logger
|
|
2888
|
-
};
|
|
2879
|
+
async function dbConnect(context, config2) {
|
|
2889
2880
|
try {
|
|
2890
2881
|
const db = new Db(config2);
|
|
2891
2882
|
context.registerCleanup(async () => {
|
|
2892
2883
|
await db.disconnect();
|
|
2893
|
-
context.logger.debug(`[Db] instance "${name
|
|
2884
|
+
context.logger.debug?.(`[Db] instance "${config2.name}" disconnected`);
|
|
2894
2885
|
});
|
|
2895
2886
|
await db.connect();
|
|
2896
|
-
context.logger.debug(`[Db] instance "${name
|
|
2887
|
+
context.logger.debug?.(`[Db] instance "${config2.name}" initialized`);
|
|
2897
2888
|
return db;
|
|
2898
2889
|
} catch (error) {
|
|
2899
2890
|
if (error instanceof ParamError) {
|
|
@@ -2903,48 +2894,11 @@ async function dbConnect(context, connectionString, name, dbProfile) {
|
|
|
2903
2894
|
throw new ParamError(`[Db] connect error: ${errorMsg}`);
|
|
2904
2895
|
}
|
|
2905
2896
|
}
|
|
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
2897
|
|
|
2947
|
-
// src/
|
|
2898
|
+
// src/tasks/index.js
|
|
2899
|
+
import os3 from "os";
|
|
2900
|
+
|
|
2901
|
+
// src/utils/date-utils.js
|
|
2948
2902
|
function isTimestampFolder(folderName) {
|
|
2949
2903
|
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
|
|
2950
2904
|
if (!isoRegex.test(folderName)) {
|
|
@@ -2954,7 +2908,7 @@ function isTimestampFolder(folderName) {
|
|
|
2954
2908
|
return !isNaN(date.getTime()) && date.getTime() > 0;
|
|
2955
2909
|
}
|
|
2956
2910
|
|
|
2957
|
-
// src/utils/fs-utils.
|
|
2911
|
+
// src/utils/fs-utils.js
|
|
2958
2912
|
import fs from "fs";
|
|
2959
2913
|
import path from "path";
|
|
2960
2914
|
async function ensurePath(...pathParts) {
|
|
@@ -2978,7 +2932,7 @@ function getFileExtension(dataType) {
|
|
|
2978
2932
|
}
|
|
2979
2933
|
}
|
|
2980
2934
|
|
|
2981
|
-
// src/utils/os-utils.
|
|
2935
|
+
// src/utils/os-utils.js
|
|
2982
2936
|
import fs2 from "fs";
|
|
2983
2937
|
import path2 from "path";
|
|
2984
2938
|
import { execSync } from "child_process";
|
|
@@ -3007,7 +2961,7 @@ function getFreeDiskSpace(targetPath) {
|
|
|
3007
2961
|
}
|
|
3008
2962
|
}
|
|
3009
2963
|
|
|
3010
|
-
// src/utils/format-utils.
|
|
2964
|
+
// src/utils/format-utils.js
|
|
3011
2965
|
function bytesToHumanReadable(bytes) {
|
|
3012
2966
|
if (bytes === 0) return "0 B";
|
|
3013
2967
|
const k = 1024;
|
|
@@ -3016,7 +2970,7 @@ function bytesToHumanReadable(bytes) {
|
|
|
3016
2970
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
3017
2971
|
}
|
|
3018
2972
|
|
|
3019
|
-
// src/utils/core-utils.
|
|
2973
|
+
// src/utils/core-utils.js
|
|
3020
2974
|
function sleepMs(ms) {
|
|
3021
2975
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3022
2976
|
}
|
|
@@ -3025,14 +2979,82 @@ function toJsonColumn(value) {
|
|
|
3025
2979
|
return JSON.stringify(value);
|
|
3026
2980
|
}
|
|
3027
2981
|
|
|
3028
|
-
// src/tasks/servicesRegistry.
|
|
3029
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
3030
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
2982
|
+
// src/tasks/servicesRegistry.js
|
|
3031
2983
|
import os from "os";
|
|
3032
|
-
import path3 from "path";
|
|
3033
2984
|
|
|
3034
|
-
// src/tasks/taskUtils.
|
|
2985
|
+
// src/tasks/taskUtils.js
|
|
3035
2986
|
import { randomUUID } from "crypto";
|
|
2987
|
+
|
|
2988
|
+
// src/tasks/time-matcher.js
|
|
2989
|
+
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
2990
|
+
function resolveAsterisks(field, range) {
|
|
2991
|
+
return field.includes("*") ? field.replace("*", range) : field;
|
|
2992
|
+
}
|
|
2993
|
+
function resolveRanges(field) {
|
|
2994
|
+
const regex = /(\d+)-(\d+)/;
|
|
2995
|
+
let current = field;
|
|
2996
|
+
while (true) {
|
|
2997
|
+
const match = regex.exec(current);
|
|
2998
|
+
if (!match) break;
|
|
2999
|
+
const raw = match[0];
|
|
3000
|
+
let first = Number(match[1]);
|
|
3001
|
+
let last = Number(match[2]);
|
|
3002
|
+
if (last < first) {
|
|
3003
|
+
[first, last] = [last, first];
|
|
3004
|
+
}
|
|
3005
|
+
const values = [];
|
|
3006
|
+
for (let i = first; i <= last; i += 1) {
|
|
3007
|
+
values.push(i);
|
|
3008
|
+
}
|
|
3009
|
+
current = current.replace(raw, values.join(","));
|
|
3010
|
+
}
|
|
3011
|
+
return current;
|
|
3012
|
+
}
|
|
3013
|
+
function resolveSteps(field) {
|
|
3014
|
+
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
3015
|
+
if (!match) return field;
|
|
3016
|
+
const base = match[1];
|
|
3017
|
+
const step = Number(match[2]);
|
|
3018
|
+
if (!Number.isFinite(step) || step <= 0) return field;
|
|
3019
|
+
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
3020
|
+
}
|
|
3021
|
+
function convertPattern(pattern) {
|
|
3022
|
+
const parts = pattern.trim().split(/\s+/);
|
|
3023
|
+
if (parts.length !== 6) {
|
|
3024
|
+
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
3025
|
+
}
|
|
3026
|
+
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
3027
|
+
}
|
|
3028
|
+
function fieldMatches(field, value) {
|
|
3029
|
+
const allowed = field.split(",").map((v) => Number(v));
|
|
3030
|
+
return allowed.includes(value);
|
|
3031
|
+
}
|
|
3032
|
+
function matchesParsedPattern(parsed, date) {
|
|
3033
|
+
return fieldMatches(parsed[0], date.getSeconds()) && fieldMatches(parsed[1], date.getMinutes()) && fieldMatches(parsed[2], date.getHours()) && fieldMatches(parsed[3], date.getDate()) && fieldMatches(parsed[4], date.getMonth() + 1) && fieldMatches(parsed[5], date.getDay());
|
|
3034
|
+
}
|
|
3035
|
+
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
3036
|
+
const parsed = convertPattern(pattern);
|
|
3037
|
+
return matchesParsedPattern(parsed, date);
|
|
3038
|
+
}
|
|
3039
|
+
var MS_PER_SECOND = 1e3;
|
|
3040
|
+
var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
|
|
3041
|
+
function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
|
|
3042
|
+
const parsed = convertPattern(pattern);
|
|
3043
|
+
let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
|
|
3044
|
+
const end = t + maxSearchMs;
|
|
3045
|
+
while (t <= end) {
|
|
3046
|
+
const date = new Date(t);
|
|
3047
|
+
if (matchesParsedPattern(parsed, date)) {
|
|
3048
|
+
return date;
|
|
3049
|
+
}
|
|
3050
|
+
t += MS_PER_SECOND;
|
|
3051
|
+
}
|
|
3052
|
+
throw new Error(
|
|
3053
|
+
`nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
|
|
3054
|
+
);
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
// src/tasks/taskUtils.js
|
|
3036
3058
|
function getDb(context) {
|
|
3037
3059
|
const db = context.db;
|
|
3038
3060
|
if (!db) {
|
|
@@ -3040,97 +3062,84 @@ function getDb(context) {
|
|
|
3040
3062
|
}
|
|
3041
3063
|
return db;
|
|
3042
3064
|
}
|
|
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
|
-
}
|
|
3065
|
+
function queueToTableNames(queueName) {
|
|
3047
3066
|
return {
|
|
3048
|
-
tasksTable:
|
|
3049
|
-
historyTable: `${
|
|
3067
|
+
tasksTable: queueName,
|
|
3068
|
+
historyTable: `${queueName}_history`,
|
|
3069
|
+
registryTable: `${queueName}_services_registry`
|
|
3050
3070
|
};
|
|
3051
3071
|
}
|
|
3052
|
-
function
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3072
|
+
function defineTasksTable(t, db, tableNameForIndex) {
|
|
3073
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
3074
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
3075
|
+
t.timestamp("started_at");
|
|
3076
|
+
t.timestamp("completed_at");
|
|
3077
|
+
t.integer("priority").notNullable().defaultTo(50);
|
|
3078
|
+
t.text("schedule");
|
|
3079
|
+
t.timestamp("next_run_at").defaultTo(null);
|
|
3080
|
+
t.timestamp("past_due").defaultTo(null);
|
|
3081
|
+
t.text("name").notNullable();
|
|
3082
|
+
t.text("opid");
|
|
3083
|
+
t.json("params");
|
|
3084
|
+
t.text("service_group");
|
|
3085
|
+
t.integer("instance_number");
|
|
3086
|
+
t.text("service_name");
|
|
3087
|
+
t.text("server_name");
|
|
3088
|
+
t.text("status").notNullable().defaultTo("idle");
|
|
3089
|
+
t.timestamp("status_changed_at").defaultTo(null);
|
|
3090
|
+
t.text("progress");
|
|
3091
|
+
t.boolean("success");
|
|
3092
|
+
t.json("results");
|
|
3093
|
+
t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
|
|
3094
|
+
t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
|
|
3095
|
+
}
|
|
3096
|
+
function taskHistoryInsertFromQueueRow(row, overrides) {
|
|
3097
|
+
const { id, ...snapshot } = row;
|
|
3098
|
+
void id;
|
|
3099
|
+
return {
|
|
3100
|
+
...snapshot,
|
|
3101
|
+
...overrides
|
|
3102
|
+
};
|
|
3057
3103
|
}
|
|
3058
3104
|
async function ensureTaskTables(context, options = {}) {
|
|
3059
|
-
const
|
|
3105
|
+
const queueName = options.queueName ?? "tasks";
|
|
3060
3106
|
const recreate = options.recreate ?? false;
|
|
3061
3107
|
const db = getDb(context);
|
|
3062
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
3108
|
+
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
3063
3109
|
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
3064
3110
|
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
3111
|
+
const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
|
|
3065
3112
|
if (recreate) {
|
|
3066
3113
|
await db.schema.dropTableIfExists(historyTable);
|
|
3067
3114
|
await db.schema.dropTableIfExists(tasksTable);
|
|
3115
|
+
await db.schema.dropTableIfExists(registryTable);
|
|
3068
3116
|
}
|
|
3069
3117
|
if (needsTasks) {
|
|
3070
3118
|
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
3071
3119
|
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`);
|
|
3120
|
+
defineTasksTable(t, db, tasksTable);
|
|
3092
3121
|
});
|
|
3093
3122
|
}
|
|
3094
3123
|
if (needsHistory) {
|
|
3095
3124
|
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`);
|
|
3125
|
+
defineTasksTable(t, db, historyTable);
|
|
3114
3126
|
});
|
|
3115
3127
|
}
|
|
3116
|
-
const registryTable = servicesRegistryTable(queue);
|
|
3117
|
-
const needsRegistry = !await db.tableExists(registryTable);
|
|
3118
3128
|
if (needsRegistry) {
|
|
3119
3129
|
await db.schema.createTable(registryTable, (t) => {
|
|
3120
3130
|
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
3121
|
-
t.
|
|
3122
|
-
t.text("queue").notNullable();
|
|
3131
|
+
t.text("queue_name").notNullable();
|
|
3123
3132
|
t.text("service_group").notNullable();
|
|
3133
|
+
t.integer("instance_number").notNullable().defaultTo(1);
|
|
3124
3134
|
t.text("service_name").notNullable();
|
|
3125
|
-
t.text("
|
|
3126
|
-
t.text("hostname");
|
|
3135
|
+
t.text("server_name").notNullable();
|
|
3127
3136
|
t.integer("pid");
|
|
3128
3137
|
t.json("metadata");
|
|
3129
3138
|
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
3130
3139
|
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
3131
|
-
t.unique(["
|
|
3132
|
-
t.index(["
|
|
3133
|
-
t.index(["
|
|
3140
|
+
t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
|
|
3141
|
+
t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
|
|
3142
|
+
t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
|
|
3134
3143
|
});
|
|
3135
3144
|
}
|
|
3136
3145
|
}
|
|
@@ -3141,7 +3150,7 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
|
3141
3150
|
});
|
|
3142
3151
|
}
|
|
3143
3152
|
|
|
3144
|
-
// src/tasks/servicesRegistry.
|
|
3153
|
+
// src/tasks/servicesRegistry.js
|
|
3145
3154
|
function getDb2(context) {
|
|
3146
3155
|
const db = context.db;
|
|
3147
3156
|
if (!db) {
|
|
@@ -3152,6 +3161,7 @@ function getDb2(context) {
|
|
|
3152
3161
|
var DEFAULT_GROUP_MAX_INSTANCES = {
|
|
3153
3162
|
intake: 1,
|
|
3154
3163
|
harvest: 1,
|
|
3164
|
+
harvester: 0,
|
|
3155
3165
|
loader: 0,
|
|
3156
3166
|
photos: 0,
|
|
3157
3167
|
photosprocessor: 0,
|
|
@@ -3161,25 +3171,6 @@ function sanitizeNamePart(raw) {
|
|
|
3161
3171
|
const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3162
3172
|
return s.slice(0, 80) || "runner";
|
|
3163
3173
|
}
|
|
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
3174
|
function resolveMaxInstances(serviceGroup, override) {
|
|
3184
3175
|
if (override !== void 0 && Number.isFinite(override)) {
|
|
3185
3176
|
return Math.max(0, Math.floor(Number(override)));
|
|
@@ -3187,184 +3178,198 @@ function resolveMaxInstances(serviceGroup, override) {
|
|
|
3187
3178
|
const g = serviceGroup.trim().toLowerCase();
|
|
3188
3179
|
return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
|
|
3189
3180
|
}
|
|
3190
|
-
async function countAliveInGroup(db, registryTable,
|
|
3181
|
+
async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
|
|
3191
3182
|
const cutoff = new Date(Date.now() - staleMs);
|
|
3192
|
-
let q = db(registryTable).where({
|
|
3193
|
-
if (
|
|
3194
|
-
q = q.whereNot("
|
|
3183
|
+
let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
|
|
3184
|
+
if (excludeRowId) {
|
|
3185
|
+
q = q.whereNot("id", excludeRowId);
|
|
3195
3186
|
}
|
|
3196
3187
|
const row = await q.count("id as count").first();
|
|
3197
3188
|
return Number(row?.count ?? 0);
|
|
3198
3189
|
}
|
|
3190
|
+
async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
|
|
3191
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
3192
|
+
const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
|
|
3193
|
+
const set = /* @__PURE__ */ new Set();
|
|
3194
|
+
for (const r of rows) {
|
|
3195
|
+
const n = Number(r.instance_number);
|
|
3196
|
+
if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
|
|
3197
|
+
}
|
|
3198
|
+
return set;
|
|
3199
|
+
}
|
|
3199
3200
|
function isUniqueViolation(error) {
|
|
3200
3201
|
const code = error?.code ?? error?.errno;
|
|
3201
3202
|
return code === "23505" || String(error?.message || "").includes("duplicate key");
|
|
3202
3203
|
}
|
|
3204
|
+
function buildMetadata(options) {
|
|
3205
|
+
const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
|
|
3206
|
+
if (options.target) {
|
|
3207
|
+
base.runnerTarget = options.target;
|
|
3208
|
+
}
|
|
3209
|
+
return toJsonColumn(Object.keys(base).length ? base : null);
|
|
3210
|
+
}
|
|
3211
|
+
function allocateInstanceNumber(occupied, explicit, maxSlots) {
|
|
3212
|
+
if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
|
|
3213
|
+
const e = Math.max(1, Math.floor(Number(explicit)));
|
|
3214
|
+
if (occupied.has(e)) {
|
|
3215
|
+
throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
|
|
3216
|
+
}
|
|
3217
|
+
if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
|
|
3218
|
+
throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
|
|
3219
|
+
}
|
|
3220
|
+
return e;
|
|
3221
|
+
}
|
|
3222
|
+
const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
|
|
3223
|
+
for (let n = 1; n <= cap; n++) {
|
|
3224
|
+
if (!occupied.has(n)) return n;
|
|
3225
|
+
}
|
|
3226
|
+
throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
|
|
3227
|
+
}
|
|
3228
|
+
function defaultServiceName(groupBase, hostBase, instanceNumber) {
|
|
3229
|
+
return `${groupBase}-${hostBase}-${instanceNumber}`;
|
|
3230
|
+
}
|
|
3203
3231
|
async function registerInServicesRegistry(context, options) {
|
|
3204
3232
|
const db = getDb2(context);
|
|
3205
|
-
const registryTable =
|
|
3233
|
+
const registryTable = queueToTableNames(options.queueName).registryTable;
|
|
3206
3234
|
const serviceGroup = options.serviceGroup.trim();
|
|
3207
3235
|
if (!serviceGroup) {
|
|
3208
3236
|
throw new Error("registerInServicesRegistry: serviceGroup is required");
|
|
3209
3237
|
}
|
|
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();
|
|
3238
|
+
const serverName = os.hostname();
|
|
3216
3239
|
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
|
-
}
|
|
3240
|
+
const meta = buildMetadata(options);
|
|
3241
|
+
const groupBase = sanitizeNamePart(serviceGroup);
|
|
3242
|
+
const hostBase = sanitizeNamePart(serverName);
|
|
3253
3243
|
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}).`;
|
|
3244
|
+
const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
|
|
3245
|
+
if (maxAllowed > 0 && aliveCount >= maxAllowed) {
|
|
3246
|
+
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
|
|
3264
3247
|
if (options.enforceMaxInstances) {
|
|
3265
3248
|
throw new Error(msg);
|
|
3266
3249
|
}
|
|
3267
3250
|
context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
|
|
3268
3251
|
}
|
|
3269
|
-
const
|
|
3270
|
-
const
|
|
3271
|
-
const
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3252
|
+
const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
|
|
3253
|
+
const cutoff = new Date(Date.now() - options.staleMs);
|
|
3254
|
+
const MAX_ATTEMPTS = 8;
|
|
3255
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
3256
|
+
const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
|
|
3257
|
+
const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
|
|
3258
|
+
const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
|
|
3259
|
+
const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
3260
|
+
if (existing) {
|
|
3261
|
+
const lastSeen = new Date(existing.last_seen_at);
|
|
3262
|
+
const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
|
|
3263
|
+
if (isAlive) {
|
|
3264
|
+
if (options.serviceName?.trim()) {
|
|
3265
|
+
throw new Error(
|
|
3266
|
+
`[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
|
|
3267
|
+
);
|
|
3268
|
+
}
|
|
3269
|
+
context.logger.warn?.(
|
|
3270
|
+
`[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
|
|
3271
|
+
);
|
|
3272
|
+
if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
|
|
3273
|
+
throw new Error(
|
|
3274
|
+
`[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
|
|
3275
|
+
);
|
|
3276
|
+
}
|
|
3277
|
+
await new Promise((r) => setTimeout(r, 50 + attempt * 30));
|
|
3278
|
+
continue;
|
|
3289
3279
|
}
|
|
3280
|
+
await db(registryTable).where({ id: existing.id }).update({
|
|
3281
|
+
server_name: serverName,
|
|
3282
|
+
pid,
|
|
3283
|
+
metadata: meta,
|
|
3284
|
+
service_group: serviceGroup,
|
|
3285
|
+
instance_number: instanceNumber,
|
|
3286
|
+
last_seen_at: db.fn.now()
|
|
3287
|
+
});
|
|
3288
|
+
const reg = {
|
|
3289
|
+
serviceName: serviceNameRaw,
|
|
3290
|
+
serviceGroup,
|
|
3291
|
+
queueName: options.queueName,
|
|
3292
|
+
target: options.target,
|
|
3293
|
+
rowId: String(existing.id),
|
|
3294
|
+
registryTable,
|
|
3295
|
+
instanceNumber
|
|
3296
|
+
};
|
|
3297
|
+
context.servicesRegistry = reg;
|
|
3298
|
+
context.runnerHeartbeat = reg;
|
|
3299
|
+
context.logger.info?.(
|
|
3300
|
+
`[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
3301
|
+
);
|
|
3302
|
+
return reg;
|
|
3290
3303
|
}
|
|
3291
|
-
}
|
|
3292
|
-
let inserted;
|
|
3293
|
-
for (const candidate of eachServiceNameCandidate(baseCandidates)) {
|
|
3294
3304
|
try {
|
|
3295
3305
|
const rows = await db(registryTable).insert({
|
|
3296
|
-
|
|
3297
|
-
queue: options.queue,
|
|
3306
|
+
queue_name: options.queueName,
|
|
3298
3307
|
service_group: serviceGroup,
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3308
|
+
instance_number: instanceNumber,
|
|
3309
|
+
service_name: serviceNameRaw,
|
|
3310
|
+
server_name: serverName,
|
|
3302
3311
|
pid,
|
|
3303
3312
|
metadata: meta,
|
|
3304
|
-
last_seen_at: db.fn.now()
|
|
3313
|
+
last_seen_at: db.fn.now(),
|
|
3314
|
+
created_at: db.fn.now()
|
|
3305
3315
|
}).returning(["id", "service_name"]);
|
|
3306
3316
|
const row = Array.isArray(rows) ? rows[0] : rows;
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3317
|
+
let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
|
|
3318
|
+
if (!rowId) {
|
|
3319
|
+
const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
3320
|
+
rowId = again?.id != null ? String(again.id) : "";
|
|
3310
3321
|
}
|
|
3322
|
+
if (!rowId) continue;
|
|
3323
|
+
const regNew = {
|
|
3324
|
+
serviceName: String(row?.service_name ?? serviceNameRaw),
|
|
3325
|
+
serviceGroup,
|
|
3326
|
+
queueName: options.queueName,
|
|
3327
|
+
target: options.target,
|
|
3328
|
+
rowId,
|
|
3329
|
+
registryTable,
|
|
3330
|
+
instanceNumber
|
|
3331
|
+
};
|
|
3332
|
+
context.servicesRegistry = regNew;
|
|
3333
|
+
context.runnerHeartbeat = regNew;
|
|
3334
|
+
context.logger.info?.(
|
|
3335
|
+
`[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
3336
|
+
);
|
|
3337
|
+
return regNew;
|
|
3311
3338
|
} catch (error) {
|
|
3312
3339
|
if (!isUniqueViolation(error)) {
|
|
3313
3340
|
throw error;
|
|
3314
3341
|
}
|
|
3342
|
+
context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
|
|
3315
3343
|
}
|
|
3316
3344
|
}
|
|
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}`
|
|
3345
|
+
throw new Error(
|
|
3346
|
+
`[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
|
|
3336
3347
|
);
|
|
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
3348
|
}
|
|
3347
3349
|
async function touchServicesRegistry(context, registration) {
|
|
3348
3350
|
const db = getDb2(context);
|
|
3349
|
-
const
|
|
3351
|
+
const serverName = os.hostname();
|
|
3350
3352
|
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
3351
|
-
await db(registration.registryTable).where({
|
|
3353
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
3352
3354
|
last_seen_at: db.fn.now(),
|
|
3353
|
-
|
|
3355
|
+
server_name: serverName,
|
|
3354
3356
|
pid
|
|
3355
3357
|
});
|
|
3356
3358
|
}
|
|
3357
3359
|
async function unregisterServicesRegistry(context, registration) {
|
|
3358
3360
|
const db = getDb2(context);
|
|
3359
|
-
await db(registration.registryTable).where({
|
|
3360
|
-
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName}
|
|
3361
|
+
await db(registration.registryTable).where({ id: registration.rowId }).delete();
|
|
3362
|
+
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
|
|
3361
3363
|
}
|
|
3362
3364
|
|
|
3363
|
-
// src/
|
|
3364
|
-
import fs3 from "fs";
|
|
3365
|
+
// src/tasks/taskLogs.js
|
|
3365
3366
|
import path4 from "path";
|
|
3366
3367
|
|
|
3367
|
-
// src/filedatabase/
|
|
3368
|
+
// src/filedatabase/index.js
|
|
3369
|
+
import fs3 from "fs";
|
|
3370
|
+
import path3 from "path";
|
|
3371
|
+
|
|
3372
|
+
// src/filedatabase/serializers.js
|
|
3368
3373
|
function detectDataType(data) {
|
|
3369
3374
|
if (Array.isArray(data)) {
|
|
3370
3375
|
return "json-array";
|
|
@@ -3396,7 +3401,7 @@ function deserializeData(rawData, dataType) {
|
|
|
3396
3401
|
}
|
|
3397
3402
|
}
|
|
3398
3403
|
|
|
3399
|
-
// src/filedatabase/index.
|
|
3404
|
+
// src/filedatabase/index.js
|
|
3400
3405
|
var FileDatabase = class _FileDatabase {
|
|
3401
3406
|
basePath;
|
|
3402
3407
|
namespace;
|
|
@@ -3482,14 +3487,14 @@ var FileDatabase = class _FileDatabase {
|
|
|
3482
3487
|
if (errors.length) {
|
|
3483
3488
|
throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
|
|
3484
3489
|
}
|
|
3485
|
-
|
|
3490
|
+
const parts = [this.basePath, this.namespace];
|
|
3486
3491
|
if (this.tableName) {
|
|
3487
3492
|
parts.push(...this.tableName.split("/"));
|
|
3488
3493
|
}
|
|
3489
3494
|
if (this.versioned && version) {
|
|
3490
3495
|
parts.push(version);
|
|
3491
3496
|
}
|
|
3492
|
-
return
|
|
3497
|
+
return path3.resolve(...parts);
|
|
3493
3498
|
}
|
|
3494
3499
|
/**
|
|
3495
3500
|
* Set current version and version folder
|
|
@@ -3526,7 +3531,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3526
3531
|
this.currentFileNumber = 0;
|
|
3527
3532
|
const versions = await this.getVersions();
|
|
3528
3533
|
while (versions.length > this.maxVersions) {
|
|
3529
|
-
const versionToDelete =
|
|
3534
|
+
const versionToDelete = path3.resolve(this.getDestinationPath(), versions.shift());
|
|
3530
3535
|
this.logger.silly?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
|
|
3531
3536
|
await fs3.promises.rm(versionToDelete, { recursive: true, force: true });
|
|
3532
3537
|
}
|
|
@@ -3545,7 +3550,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3545
3550
|
await ensurePath(destPath);
|
|
3546
3551
|
const items = await fs3.promises.readdir(destPath);
|
|
3547
3552
|
const versions = items.filter((item) => {
|
|
3548
|
-
const itemPath =
|
|
3553
|
+
const itemPath = path3.join(destPath, item);
|
|
3549
3554
|
const stat = fs3.statSync(itemPath);
|
|
3550
3555
|
return stat.isDirectory() && isTimestampFolder(item);
|
|
3551
3556
|
});
|
|
@@ -3602,7 +3607,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3602
3607
|
const items = await fs3.promises.readdir(tablePath);
|
|
3603
3608
|
if (items.includes("metadata.json")) {
|
|
3604
3609
|
const metadata = JSON.parse(
|
|
3605
|
-
await fs3.promises.readFile(
|
|
3610
|
+
await fs3.promises.readFile(path3.join(tablePath, "metadata.json"), "utf8")
|
|
3606
3611
|
);
|
|
3607
3612
|
return {
|
|
3608
3613
|
versioned: false,
|
|
@@ -3611,13 +3616,13 @@ var FileDatabase = class _FileDatabase {
|
|
|
3611
3616
|
};
|
|
3612
3617
|
}
|
|
3613
3618
|
const versionFolders = items.filter((item) => {
|
|
3614
|
-
const itemPath =
|
|
3619
|
+
const itemPath = path3.join(tablePath, item);
|
|
3615
3620
|
const stat = fs3.statSync(itemPath);
|
|
3616
3621
|
return stat.isDirectory() && isTimestampFolder(item);
|
|
3617
3622
|
});
|
|
3618
3623
|
if (versionFolders.length > 0) {
|
|
3619
3624
|
const latestVersion = versionFolders.sort().pop();
|
|
3620
|
-
const versionMetadataPath =
|
|
3625
|
+
const versionMetadataPath = path3.join(tablePath, latestVersion, "metadata.json");
|
|
3621
3626
|
return {
|
|
3622
3627
|
versioned: true,
|
|
3623
3628
|
hasMetadata: fs3.existsSync(versionMetadataPath),
|
|
@@ -3638,7 +3643,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3638
3643
|
* Load metadata from JSON file
|
|
3639
3644
|
*/
|
|
3640
3645
|
async loadMetadataJson(version) {
|
|
3641
|
-
const metadataFile =
|
|
3646
|
+
const metadataFile = path3.join(this.getDestinationPath(), version, "metadata.json");
|
|
3642
3647
|
if (fs3.existsSync(metadataFile)) {
|
|
3643
3648
|
try {
|
|
3644
3649
|
const rawData = await fs3.promises.readFile(metadataFile, "utf8");
|
|
@@ -3654,7 +3659,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3654
3659
|
* Reads all files to get accurate counts - used when synopsis calculation is needed
|
|
3655
3660
|
*/
|
|
3656
3661
|
async figureMetadataFromVersionFiles(version) {
|
|
3657
|
-
const versionPath =
|
|
3662
|
+
const versionPath = path3.join(this.getDestinationPath(), version);
|
|
3658
3663
|
if (!fs3.existsSync(versionPath)) {
|
|
3659
3664
|
return this.getDefaultMetadata();
|
|
3660
3665
|
}
|
|
@@ -3666,10 +3671,10 @@ var FileDatabase = class _FileDatabase {
|
|
|
3666
3671
|
let detectedDataType = null;
|
|
3667
3672
|
for (let i = 0; i < files.length; i++) {
|
|
3668
3673
|
const fileName = files[i];
|
|
3669
|
-
const filePath =
|
|
3674
|
+
const filePath = path3.join(versionPath, fileName);
|
|
3670
3675
|
try {
|
|
3671
3676
|
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
3672
|
-
const extension =
|
|
3677
|
+
const extension = path3.extname(fileName).toLowerCase();
|
|
3673
3678
|
let dataType = "text";
|
|
3674
3679
|
if (extension === ".json") {
|
|
3675
3680
|
dataType = "json-array";
|
|
@@ -3702,7 +3707,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3702
3707
|
* Much faster for large datasets with many files
|
|
3703
3708
|
*/
|
|
3704
3709
|
async buildMetadataOptimized(version) {
|
|
3705
|
-
const versionPath =
|
|
3710
|
+
const versionPath = path3.join(this.getDestinationPath(), version);
|
|
3706
3711
|
if (!fs3.existsSync(versionPath)) {
|
|
3707
3712
|
return this.getDefaultMetadata();
|
|
3708
3713
|
}
|
|
@@ -3718,7 +3723,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3718
3723
|
fileName
|
|
3719
3724
|
}));
|
|
3720
3725
|
const firstFile = metadata.files[0];
|
|
3721
|
-
const firstFilePath =
|
|
3726
|
+
const firstFilePath = path3.join(versionPath, firstFile.fileName);
|
|
3722
3727
|
const firstFileRaw = await fs3.promises.readFile(firstFilePath, "utf8");
|
|
3723
3728
|
let firstFileData;
|
|
3724
3729
|
try {
|
|
@@ -3735,7 +3740,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3735
3740
|
}
|
|
3736
3741
|
if (files.length > 1) {
|
|
3737
3742
|
const lastFile = metadata.files[metadata.files.length - 1];
|
|
3738
|
-
const lastFilePath =
|
|
3743
|
+
const lastFilePath = path3.join(versionPath, lastFile.fileName);
|
|
3739
3744
|
const lastFileRaw = await fs3.promises.readFile(lastFilePath, "utf8");
|
|
3740
3745
|
const lastFileData = deserializeData(lastFileRaw, metadata.dataType);
|
|
3741
3746
|
lastFile.recordsCount = Array.isArray(lastFileData) ? lastFileData.length : 1;
|
|
@@ -3786,9 +3791,9 @@ var FileDatabase = class _FileDatabase {
|
|
|
3786
3791
|
if (!this.currentVersion) {
|
|
3787
3792
|
return;
|
|
3788
3793
|
}
|
|
3789
|
-
metadataFile =
|
|
3794
|
+
metadataFile = path3.join(this.getDestinationPath(), this.currentVersion, "metadata.json");
|
|
3790
3795
|
} else {
|
|
3791
|
-
metadataFile =
|
|
3796
|
+
metadataFile = path3.join(this.getDestinationPath(), "metadata.json");
|
|
3792
3797
|
}
|
|
3793
3798
|
await fs3.promises.writeFile(metadataFile, JSON.stringify(metadataToSave, null, 4), "utf8");
|
|
3794
3799
|
}
|
|
@@ -3851,7 +3856,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3851
3856
|
}
|
|
3852
3857
|
}
|
|
3853
3858
|
if (!Array.isArray(data) && !forceNewFile) {
|
|
3854
|
-
const lastFileExtension =
|
|
3859
|
+
const lastFileExtension = path3.extname(lastFile.fileName);
|
|
3855
3860
|
const expectedExtension = `.${getFileExtension(incomingDataType)}`;
|
|
3856
3861
|
if (lastFileExtension !== expectedExtension) {
|
|
3857
3862
|
if (lastFileRecordsCount > 0) {
|
|
@@ -3861,7 +3866,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3861
3866
|
}
|
|
3862
3867
|
}
|
|
3863
3868
|
} else if (!Array.isArray(data) && forceNewFile) {
|
|
3864
|
-
const lastFileExtension =
|
|
3869
|
+
const lastFileExtension = path3.extname(lastFile.fileName);
|
|
3865
3870
|
const expectedExtension = `.${getFileExtension(incomingDataType)}`;
|
|
3866
3871
|
if (lastFileExtension !== expectedExtension) {
|
|
3867
3872
|
lastFile.fileName = `${this.currentFileNumber.toString().padStart(6, "0")}.${getFileExtension(incomingDataType)}`;
|
|
@@ -3951,7 +3956,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3951
3956
|
*/
|
|
3952
3957
|
async safeWrite(filePath, data) {
|
|
3953
3958
|
const serializedData = serializeData(data);
|
|
3954
|
-
const dir =
|
|
3959
|
+
const dir = path3.dirname(filePath);
|
|
3955
3960
|
const requiredBytes = Buffer.byteLength(serializedData, "utf8");
|
|
3956
3961
|
const freeBytes = getFreeDiskSpace(dir);
|
|
3957
3962
|
if (freeBytes !== null) {
|
|
@@ -3975,14 +3980,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
3975
3980
|
* Prepare the instance for read or write operations
|
|
3976
3981
|
* This discovers state and sets up internal members based on mode and current data
|
|
3977
3982
|
*/
|
|
3978
|
-
async prepare(
|
|
3983
|
+
async prepare(options) {
|
|
3984
|
+
const { write, read, version, deferInitialVersion } = options;
|
|
3979
3985
|
if (write) {
|
|
3980
3986
|
if (this.versioned) {
|
|
3981
3987
|
if (this.currentVersion === null) {
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3988
|
+
if (!deferInitialVersion) {
|
|
3989
|
+
await this.makeNewVersion();
|
|
3990
|
+
this.metadata = this.getDefaultMetadata();
|
|
3991
|
+
this.metadata.version = this.currentVersion;
|
|
3992
|
+
this.makeNewFile();
|
|
3993
|
+
}
|
|
3986
3994
|
} else {
|
|
3987
3995
|
if (!this.metadata.files.length) {
|
|
3988
3996
|
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
@@ -3996,7 +4004,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
3996
4004
|
} else {
|
|
3997
4005
|
await ensurePath(this.getDestinationPath());
|
|
3998
4006
|
if (this.useMetadata === true) {
|
|
3999
|
-
const metadataPath =
|
|
4007
|
+
const metadataPath = path3.join(this.getDestinationPath(), "metadata.json");
|
|
4000
4008
|
if (fs3.existsSync(metadataPath)) {
|
|
4001
4009
|
try {
|
|
4002
4010
|
const rawData = await fs3.promises.readFile(metadataPath, "utf8");
|
|
@@ -4049,7 +4057,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4049
4057
|
}
|
|
4050
4058
|
if (this.useMetadata) {
|
|
4051
4059
|
const destPath = this.getDestinationPath();
|
|
4052
|
-
const metadataPath =
|
|
4060
|
+
const metadataPath = path3.join(destPath, "metadata.json");
|
|
4053
4061
|
if (fs3.existsSync(metadataPath)) {
|
|
4054
4062
|
try {
|
|
4055
4063
|
const rawData = await fs3.promises.readFile(metadataPath, "utf8");
|
|
@@ -4085,14 +4093,14 @@ var FileDatabase = class _FileDatabase {
|
|
|
4085
4093
|
if (options.filename) {
|
|
4086
4094
|
const destPath2 = this.getDestinationPath();
|
|
4087
4095
|
await ensurePath(destPath2);
|
|
4088
|
-
const filePath =
|
|
4096
|
+
const filePath = path3.join(destPath2, options.filename);
|
|
4089
4097
|
await this.safeWrite(filePath, data);
|
|
4090
4098
|
return;
|
|
4091
4099
|
}
|
|
4092
4100
|
if (options.forceNewVersion && !this.versioned) {
|
|
4093
4101
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
4094
4102
|
}
|
|
4095
|
-
await this.prepare({ write: true });
|
|
4103
|
+
await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
|
|
4096
4104
|
const incomingDataType = detectDataType(data);
|
|
4097
4105
|
this.metadata.dataType = incomingDataType;
|
|
4098
4106
|
if (options.forceNewVersion) {
|
|
@@ -4134,11 +4142,11 @@ var FileDatabase = class _FileDatabase {
|
|
|
4134
4142
|
const forceNewFile = hasCustomMetadata && targetFileIndex === null;
|
|
4135
4143
|
let { dataToWrite, dataLeftOver, fileName } = this.figureOutDataAndFileToWrite(data, targetFileIndex, forceNewFile);
|
|
4136
4144
|
const destPath = this.getDestinationPath(this.currentVersion || void 0);
|
|
4137
|
-
await this.safeWrite(
|
|
4145
|
+
await this.safeWrite(path3.join(destPath, fileName), dataToWrite);
|
|
4138
4146
|
this.updateMetadata(dataToWrite, fileName, options.customMetadata);
|
|
4139
4147
|
while (dataLeftOver && dataLeftOver.length > 0 && targetFileIndex === null) {
|
|
4140
4148
|
const writeContext = this.figureOutDataAndFileToWrite(dataLeftOver);
|
|
4141
|
-
await this.safeWrite(
|
|
4149
|
+
await this.safeWrite(path3.join(destPath, writeContext.fileName), writeContext.dataToWrite);
|
|
4142
4150
|
this.updateMetadata(writeContext.dataToWrite, writeContext.fileName, options.customMetadata);
|
|
4143
4151
|
dataLeftOver = writeContext.dataLeftOver;
|
|
4144
4152
|
}
|
|
@@ -4154,7 +4162,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4154
4162
|
const { version, nextPage = false, pageSize, filename } = options;
|
|
4155
4163
|
if (filename) {
|
|
4156
4164
|
const destPath = this.getDestinationPath(version);
|
|
4157
|
-
const filePath =
|
|
4165
|
+
const filePath = path3.join(destPath, filename);
|
|
4158
4166
|
try {
|
|
4159
4167
|
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
4160
4168
|
return JSON.parse(rawData);
|
|
@@ -4166,7 +4174,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4166
4174
|
const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
|
|
4167
4175
|
if (isNonPaginatedData) {
|
|
4168
4176
|
const file = this.metadata.files[0];
|
|
4169
|
-
const filePath =
|
|
4177
|
+
const filePath = path3.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
|
|
4170
4178
|
try {
|
|
4171
4179
|
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
4172
4180
|
return deserializeData(rawData, this.metadata.dataType);
|
|
@@ -4204,7 +4212,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4204
4212
|
let cumulativeRecords = currentFileOffset;
|
|
4205
4213
|
for (let i = currentFileIndex; i < this.metadata.files.length && recordsRead < effectivePageSize; i++) {
|
|
4206
4214
|
const file = this.metadata.files[i];
|
|
4207
|
-
const filePath =
|
|
4215
|
+
const filePath = path3.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
|
|
4208
4216
|
try {
|
|
4209
4217
|
const rawData = await fs3.promises.readFile(filePath, "utf8");
|
|
4210
4218
|
const fileData = deserializeData(rawData, this.metadata.dataType);
|
|
@@ -4248,7 +4256,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4248
4256
|
* Returns data file names (.json, .txt, .xml) excluding metadata.json.
|
|
4249
4257
|
*/
|
|
4250
4258
|
async listFilenames() {
|
|
4251
|
-
const destPath = this.versioned && this.currentVersion ?
|
|
4259
|
+
const destPath = this.versioned && this.currentVersion ? path3.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
4252
4260
|
try {
|
|
4253
4261
|
const entries = await fs3.promises.readdir(destPath, { withFileTypes: true });
|
|
4254
4262
|
return entries.filter((e) => e.isFile() && e.name !== "metadata.json" && /\.(json|txt|xml)$/i.test(e.name)).map((e) => e.name);
|
|
@@ -4262,8 +4270,8 @@ var FileDatabase = class _FileDatabase {
|
|
|
4262
4270
|
* Use with listFilenames() to manage individual files.
|
|
4263
4271
|
*/
|
|
4264
4272
|
async removeFile(filename) {
|
|
4265
|
-
const destPath = this.versioned && this.currentVersion ?
|
|
4266
|
-
const filePath =
|
|
4273
|
+
const destPath = this.versioned && this.currentVersion ? path3.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
|
|
4274
|
+
const filePath = path3.join(destPath, filename);
|
|
4267
4275
|
try {
|
|
4268
4276
|
await fs3.promises.unlink(filePath);
|
|
4269
4277
|
} catch (err) {
|
|
@@ -4289,7 +4297,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4289
4297
|
this.metadata.files.splice(idx, 1);
|
|
4290
4298
|
this.metadata.totalRecords = Math.max(0, (this.metadata.totalRecords || 0) - recordsCount);
|
|
4291
4299
|
const destPath = this.getDestinationPath();
|
|
4292
|
-
const filePath =
|
|
4300
|
+
const filePath = path3.join(destPath, filename);
|
|
4293
4301
|
try {
|
|
4294
4302
|
await fs3.promises.unlink(filePath);
|
|
4295
4303
|
} catch (err) {
|
|
@@ -4345,7 +4353,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4345
4353
|
});
|
|
4346
4354
|
if (matches) {
|
|
4347
4355
|
const destPath = this.getDestinationPath();
|
|
4348
|
-
const filePath =
|
|
4356
|
+
const filePath = path3.join(destPath, fileEntry.fileName);
|
|
4349
4357
|
const fileData = await fs3.promises.readFile(filePath, "utf8");
|
|
4350
4358
|
const data = deserializeData(fileData, metadata.dataType || "json-object");
|
|
4351
4359
|
results.push({
|
|
@@ -4368,7 +4376,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4368
4376
|
});
|
|
4369
4377
|
if (matches) {
|
|
4370
4378
|
const destPath = this.getDestinationPath(version);
|
|
4371
|
-
const filePath =
|
|
4379
|
+
const filePath = path3.join(destPath, fileEntry.fileName);
|
|
4372
4380
|
const fileData = await fs3.promises.readFile(filePath, "utf8");
|
|
4373
4381
|
const data = deserializeData(fileData, metadata.dataType || "json-object");
|
|
4374
4382
|
results.push({
|
|
@@ -4386,7 +4394,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
4386
4394
|
}
|
|
4387
4395
|
};
|
|
4388
4396
|
|
|
4389
|
-
// src/tasks/taskLogs.
|
|
4397
|
+
// src/tasks/taskLogs.js
|
|
4390
4398
|
function getLogsState(context) {
|
|
4391
4399
|
const holder = context;
|
|
4392
4400
|
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
@@ -4439,6 +4447,89 @@ function getLogsState(context) {
|
|
|
4439
4447
|
holder.__tasksLogsState = state;
|
|
4440
4448
|
return state;
|
|
4441
4449
|
}
|
|
4450
|
+
function ipcLogTargetKey(target) {
|
|
4451
|
+
const bp = target.basePath ?? "";
|
|
4452
|
+
const ns = target.namespace ?? "";
|
|
4453
|
+
return `${bp}::${ns}::${target.tableName}`;
|
|
4454
|
+
}
|
|
4455
|
+
function ipcFileLogsTableNameForSourceResource(source, resource) {
|
|
4456
|
+
const seg = (s) => {
|
|
4457
|
+
const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
4458
|
+
return t.length ? t : "x";
|
|
4459
|
+
};
|
|
4460
|
+
return `${seg(source)}/${seg(resource)}`;
|
|
4461
|
+
}
|
|
4462
|
+
async function readTaskIpcLogsSnapshot(context, options) {
|
|
4463
|
+
const holder = context;
|
|
4464
|
+
const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
|
|
4465
|
+
const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
|
|
4466
|
+
const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
|
|
4467
|
+
const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
|
|
4468
|
+
const fd = new FileDatabase({
|
|
4469
|
+
basePath,
|
|
4470
|
+
namespace,
|
|
4471
|
+
tableName,
|
|
4472
|
+
versioned: true,
|
|
4473
|
+
useMetadata: true,
|
|
4474
|
+
maxVersions: 30,
|
|
4475
|
+
pageSize: 2e3,
|
|
4476
|
+
logger: holder.logger
|
|
4477
|
+
});
|
|
4478
|
+
const versions = await fd.getVersions();
|
|
4479
|
+
if (versions.length === 0) {
|
|
4480
|
+
return { records: [], latestTs: null };
|
|
4481
|
+
}
|
|
4482
|
+
const latest = versions[versions.length - 1];
|
|
4483
|
+
const raw = await fd.read({ version: latest });
|
|
4484
|
+
const arr = Array.isArray(raw) ? raw : [];
|
|
4485
|
+
let filtered = arr;
|
|
4486
|
+
if (options.afterTs && String(options.afterTs).trim()) {
|
|
4487
|
+
const cut = String(options.afterTs).trim();
|
|
4488
|
+
filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
|
|
4489
|
+
}
|
|
4490
|
+
let latestTs = null;
|
|
4491
|
+
for (const r of filtered) {
|
|
4492
|
+
const ts = typeof r?.ts === "string" ? String(r.ts) : null;
|
|
4493
|
+
if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
|
|
4494
|
+
}
|
|
4495
|
+
const incremental = !!(options.afterTs && String(options.afterTs).trim());
|
|
4496
|
+
const maxReturn = incremental ? 1e4 : tail;
|
|
4497
|
+
const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
|
|
4498
|
+
return { records: sliced, latestTs };
|
|
4499
|
+
}
|
|
4500
|
+
function getLogsStateForTarget(context, target) {
|
|
4501
|
+
const holder = context;
|
|
4502
|
+
const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
|
|
4503
|
+
const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
4504
|
+
if (!enabled) return null;
|
|
4505
|
+
if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
|
|
4506
|
+
const map = holder.__tasksLogsTargetStates;
|
|
4507
|
+
const key = ipcLogTargetKey(target);
|
|
4508
|
+
if (map.has(key)) return map.get(key);
|
|
4509
|
+
const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
|
|
4510
|
+
const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
|
|
4511
|
+
const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
|
|
4512
|
+
const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
|
|
4513
|
+
const db = new FileDatabase({
|
|
4514
|
+
basePath,
|
|
4515
|
+
namespace,
|
|
4516
|
+
tableName: target.tableName,
|
|
4517
|
+
versioned: true,
|
|
4518
|
+
useMetadata: true,
|
|
4519
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4520
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4521
|
+
logger: holder.logger
|
|
4522
|
+
});
|
|
4523
|
+
const state = {
|
|
4524
|
+
db,
|
|
4525
|
+
errorDb: null,
|
|
4526
|
+
queue: Promise.resolve(),
|
|
4527
|
+
initialized: false,
|
|
4528
|
+
errorInitialized: false
|
|
4529
|
+
};
|
|
4530
|
+
map.set(key, state);
|
|
4531
|
+
return state;
|
|
4532
|
+
}
|
|
4442
4533
|
function isErrorPayload(payload) {
|
|
4443
4534
|
if (!payload) return false;
|
|
4444
4535
|
if (typeof payload === "object") {
|
|
@@ -4458,14 +4549,26 @@ function buildLogRecord(task, payload) {
|
|
|
4458
4549
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4459
4550
|
opid: task.opid ?? null,
|
|
4460
4551
|
taskId: task.id,
|
|
4461
|
-
taskName: task.
|
|
4462
|
-
target: task.
|
|
4552
|
+
taskName: task.name,
|
|
4553
|
+
target: task.service_group,
|
|
4463
4554
|
source: typeof params.source === "string" ? params.source : null,
|
|
4464
4555
|
resource: typeof params.resource === "string" ? params.resource : null,
|
|
4465
4556
|
payload
|
|
4466
4557
|
};
|
|
4467
4558
|
}
|
|
4468
|
-
function appendTaskIpcLog(context, task, payload) {
|
|
4559
|
+
function appendTaskIpcLog(context, task, payload, target) {
|
|
4560
|
+
if (target) {
|
|
4561
|
+
const state2 = getLogsStateForTarget(context, target);
|
|
4562
|
+
if (!state2?.db) return;
|
|
4563
|
+
const record2 = buildLogRecord(task, payload);
|
|
4564
|
+
state2.queue = state2.queue.then(async () => {
|
|
4565
|
+
await state2.db.write([record2], { forceNewVersion: !state2.initialized });
|
|
4566
|
+
state2.initialized = true;
|
|
4567
|
+
}).catch((error) => {
|
|
4568
|
+
context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
|
|
4569
|
+
});
|
|
4570
|
+
return;
|
|
4571
|
+
}
|
|
4469
4572
|
const state = getLogsState(context);
|
|
4470
4573
|
if (!state.db && !state.errorDb) return;
|
|
4471
4574
|
const record = buildLogRecord(task, payload);
|
|
@@ -4483,83 +4586,280 @@ function appendTaskIpcLog(context, task, payload) {
|
|
|
4483
4586
|
});
|
|
4484
4587
|
}
|
|
4485
4588
|
|
|
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;
|
|
4589
|
+
// src/tasks/AbstractTask.js
|
|
4590
|
+
var AbstractTask = class _AbstractTask {
|
|
4591
|
+
/**
|
|
4592
|
+
* Whether `send-task` should wait for completion (and print a result
|
|
4593
|
+
* report) when no explicit `--wait` / `--noWait` flag is given. Defaults
|
|
4594
|
+
* to false; short-lived probe tasks (e.g. `ping`) override to true.
|
|
4595
|
+
*
|
|
4596
|
+
* @type {boolean}
|
|
4597
|
+
*/
|
|
4598
|
+
static defaultWaitForResult = false;
|
|
4599
|
+
/**
|
|
4600
|
+
* @param {object} context Runner context (db, logger, params, emitter...).
|
|
4601
|
+
* @param {object} task Task row as claimed from the queue.
|
|
4602
|
+
*/
|
|
4539
4603
|
constructor(context, task) {
|
|
4540
4604
|
this.context = context;
|
|
4541
4605
|
this.task = task;
|
|
4542
4606
|
}
|
|
4607
|
+
/**
|
|
4608
|
+
* Return a short reason string when the task should be deferred (e.g. "locked
|
|
4609
|
+
* by source"), or `false`/falsy when it is free to run. Default: always `false`.
|
|
4610
|
+
*
|
|
4611
|
+
* @returns {string | false | Promise<string | false>}
|
|
4612
|
+
*/
|
|
4543
4613
|
cantRunReason() {
|
|
4544
4614
|
return false;
|
|
4545
4615
|
}
|
|
4616
|
+
/**
|
|
4617
|
+
* Called by the runner when a stop has been requested. Subclasses running
|
|
4618
|
+
* long loops should flip a flag here and check it between iterations.
|
|
4619
|
+
*
|
|
4620
|
+
* @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
|
|
4621
|
+
*/
|
|
4546
4622
|
requestStop(_allowanceMs) {
|
|
4547
4623
|
}
|
|
4624
|
+
/**
|
|
4625
|
+
* Perform the task. Must be implemented by subclasses.
|
|
4626
|
+
*
|
|
4627
|
+
* @param {(progress: unknown) => Promise<void>} _reportProgress
|
|
4628
|
+
* Updates the DB `progress` column. Accepts any serializable value;
|
|
4629
|
+
* strings are stored verbatim, objects are JSON-stringified.
|
|
4630
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
4631
|
+
*/
|
|
4632
|
+
async run(_reportProgress) {
|
|
4633
|
+
throw new Error("AbstractTask.run must be implemented by subclass");
|
|
4634
|
+
}
|
|
4635
|
+
/**
|
|
4636
|
+
* Resolve a complete row payload for this task — envelope fields (queue,
|
|
4637
|
+
* priority, targeting, schedule…) plus the inner `params` blob produced by
|
|
4638
|
+
* {@link AbstractTask.resolveCustomParams}. Output shape matches
|
|
4639
|
+
* {@link enqueueTask}'s `options` argument, so the typical call is:
|
|
4640
|
+
*
|
|
4641
|
+
* const payload = await TaskClass.resolveParams(context, { name });
|
|
4642
|
+
* await enqueueTask(context, payload);
|
|
4643
|
+
*
|
|
4644
|
+
* Validation failures throw {@link ParamError} so the script aborts before
|
|
4645
|
+
* a malformed row hits the DB.
|
|
4646
|
+
*
|
|
4647
|
+
* @param {object} context
|
|
4648
|
+
* @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
|
|
4649
|
+
* Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
|
|
4650
|
+
* `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
|
|
4651
|
+
* `nextRunAt`, plus `params` (object — overlay onto inner blob).
|
|
4652
|
+
* @returns {Promise<object>}
|
|
4653
|
+
*/
|
|
4654
|
+
static async resolveParams(context, overrides = {}) {
|
|
4655
|
+
const main = _AbstractTask._resolveMainFields(context, overrides);
|
|
4656
|
+
const params = await this.resolveCustomParams(context, overrides);
|
|
4657
|
+
return { ...main, params };
|
|
4658
|
+
}
|
|
4659
|
+
/**
|
|
4660
|
+
* Resolve the inner JSON blob stored in the `params` column. Default
|
|
4661
|
+
* implementation passes through `--paramsJson` (parsed as a JSON object)
|
|
4662
|
+
* overlaid with `overrides.params` when supplied; returns `null` when
|
|
4663
|
+
* neither is provided.
|
|
4664
|
+
*
|
|
4665
|
+
* Subclasses with typed fields should override and call
|
|
4666
|
+
* {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
|
|
4667
|
+
* resolution, then validate and throw {@link ParamError} on bad input.
|
|
4668
|
+
*
|
|
4669
|
+
* @param {object} context
|
|
4670
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4671
|
+
* @returns {Promise<object|null>}
|
|
4672
|
+
*/
|
|
4673
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
4674
|
+
return _AbstractTask._defaultParamsBlob(context, overrides);
|
|
4675
|
+
}
|
|
4676
|
+
/**
|
|
4677
|
+
* Read main task envelope fields from `context.params` (CLI/env), with
|
|
4678
|
+
* any matching key on `overrides` taking precedence. Internal; called by
|
|
4679
|
+
* {@link AbstractTask.resolveParams}.
|
|
4680
|
+
*
|
|
4681
|
+
* @param {object} context
|
|
4682
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4683
|
+
* @returns {object}
|
|
4684
|
+
*/
|
|
4685
|
+
static _resolveMainFields(context, overrides = {}) {
|
|
4686
|
+
const defs2 = {
|
|
4687
|
+
queueName: "string default tasks",
|
|
4688
|
+
priority: "number default 50",
|
|
4689
|
+
serviceGroup: "string",
|
|
4690
|
+
serviceName: "string",
|
|
4691
|
+
instanceNumber: "number",
|
|
4692
|
+
serverName: "string",
|
|
4693
|
+
opid: "string",
|
|
4694
|
+
schedule: "string"
|
|
4695
|
+
};
|
|
4696
|
+
const cli = context.params.getAllForModule("task-envelope", defs2);
|
|
4697
|
+
const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
|
|
4698
|
+
if (!name) {
|
|
4699
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
4700
|
+
}
|
|
4701
|
+
let instanceNumber;
|
|
4702
|
+
const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
|
|
4703
|
+
if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
|
|
4704
|
+
const n = Number(rawInstance);
|
|
4705
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
4706
|
+
throw new ParamError("--instanceNumber must be a positive integer when set");
|
|
4707
|
+
}
|
|
4708
|
+
instanceNumber = n;
|
|
4709
|
+
} else {
|
|
4710
|
+
instanceNumber = null;
|
|
4711
|
+
}
|
|
4712
|
+
const priorityRaw = overrides.priority ?? cli.priority ?? 50;
|
|
4713
|
+
const priority = Number(priorityRaw);
|
|
4714
|
+
if (!Number.isFinite(priority)) {
|
|
4715
|
+
throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
|
|
4716
|
+
}
|
|
4717
|
+
return {
|
|
4718
|
+
name,
|
|
4719
|
+
queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
|
|
4720
|
+
priority,
|
|
4721
|
+
serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
|
|
4722
|
+
serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
|
|
4723
|
+
instanceNumber,
|
|
4724
|
+
serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
|
|
4725
|
+
opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
|
|
4726
|
+
schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
|
|
4727
|
+
nextRunAt: overrides.nextRunAt ?? null
|
|
4728
|
+
};
|
|
4729
|
+
}
|
|
4730
|
+
/**
|
|
4731
|
+
* Default inner-params resolver: parses `--paramsJson` (must be a JSON
|
|
4732
|
+
* object), then overlays `overrides.params` on top. Returns `null` when
|
|
4733
|
+
* neither is provided.
|
|
4734
|
+
*
|
|
4735
|
+
* @param {object} context
|
|
4736
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4737
|
+
* @returns {object|null}
|
|
4738
|
+
*/
|
|
4739
|
+
static _defaultParamsBlob(context, overrides = {}) {
|
|
4740
|
+
const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
|
|
4741
|
+
const fromJson = parseParamsJson(cli.paramsJson);
|
|
4742
|
+
const fromOverride = pickParamsObject(overrides);
|
|
4743
|
+
if (!fromJson && !fromOverride) return null;
|
|
4744
|
+
return { ...fromJson ?? {}, ...fromOverride ?? {} };
|
|
4745
|
+
}
|
|
4746
|
+
/**
|
|
4747
|
+
* Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
|
|
4748
|
+
* params (per `defs`) plus `--paramsJson` under a module namespace, then
|
|
4749
|
+
* merges them with explicit `overrides.params` in increasing priority:
|
|
4750
|
+
*
|
|
4751
|
+
* typed CLI flags → --paramsJson → overrides.params
|
|
4752
|
+
*
|
|
4753
|
+
* Undefined values are dropped so defaults declared in `defs` aren't
|
|
4754
|
+
* overwritten by missing-flag noise. Returns the merged object; the
|
|
4755
|
+
* caller is responsible for validation and throwing `ParamError`.
|
|
4756
|
+
*
|
|
4757
|
+
* @param {object} context
|
|
4758
|
+
* @param {string} moduleName Namespace for `--showUsedParams` grouping.
|
|
4759
|
+
* @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
|
|
4760
|
+
* @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
|
|
4761
|
+
* @returns {Record<string, unknown>}
|
|
4762
|
+
*/
|
|
4763
|
+
static _mergeTypedParams(context, moduleName, defs2, overrides = {}) {
|
|
4764
|
+
const fullDefs = { ...defs2, paramsJson: "string" };
|
|
4765
|
+
const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
|
|
4766
|
+
const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
|
|
4767
|
+
const fromCli = {};
|
|
4768
|
+
for (const [k, v] of Object.entries(cliRaw)) {
|
|
4769
|
+
if (k === "paramsJson") continue;
|
|
4770
|
+
if (v !== void 0 && v !== null) fromCli[k] = v;
|
|
4771
|
+
}
|
|
4772
|
+
const fromOverride = pickParamsObject(overrides) ?? {};
|
|
4773
|
+
return { ...fromCli, ...fromJson, ...fromOverride };
|
|
4774
|
+
}
|
|
4548
4775
|
};
|
|
4776
|
+
function emptyToUndef(s) {
|
|
4777
|
+
if (s === void 0 || s === null) return void 0;
|
|
4778
|
+
if (typeof s !== "string") return s;
|
|
4779
|
+
const t = s.trim();
|
|
4780
|
+
return t.length ? t : void 0;
|
|
4781
|
+
}
|
|
4782
|
+
function parseParamsJson(raw) {
|
|
4783
|
+
if (raw == null) return null;
|
|
4784
|
+
const t = String(raw).trim();
|
|
4785
|
+
if (!t) return null;
|
|
4786
|
+
let parsed;
|
|
4787
|
+
try {
|
|
4788
|
+
parsed = JSON.parse(t);
|
|
4789
|
+
} catch (e) {
|
|
4790
|
+
throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
|
|
4791
|
+
}
|
|
4792
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4793
|
+
throw new ParamError("--paramsJson must be a JSON object");
|
|
4794
|
+
}
|
|
4795
|
+
return parsed;
|
|
4796
|
+
}
|
|
4797
|
+
function pickParamsObject(overrides) {
|
|
4798
|
+
const p = overrides?.params;
|
|
4799
|
+
if (p && typeof p === "object" && !Array.isArray(p)) return p;
|
|
4800
|
+
return void 0;
|
|
4801
|
+
}
|
|
4549
4802
|
|
|
4550
|
-
// src/tasks/coreTasks/TaskPing.
|
|
4551
|
-
var TaskPing = class extends
|
|
4803
|
+
// src/tasks/coreTasks/TaskPing.js
|
|
4804
|
+
var TaskPing = class extends AbstractTask {
|
|
4805
|
+
/** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
|
|
4806
|
+
static defaultWaitForResult = true;
|
|
4807
|
+
/** Ping takes no params. */
|
|
4808
|
+
static async resolveCustomParams() {
|
|
4809
|
+
return null;
|
|
4810
|
+
}
|
|
4811
|
+
/**
|
|
4812
|
+
* @returns {Promise<{ success: true, results: "pong" }>}
|
|
4813
|
+
*/
|
|
4552
4814
|
async run() {
|
|
4553
4815
|
this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
|
|
4554
4816
|
return { success: true, results: "pong" };
|
|
4555
4817
|
}
|
|
4556
4818
|
};
|
|
4557
4819
|
|
|
4558
|
-
// src/tasks/coreTasks/TaskSampleProcess.
|
|
4559
|
-
var TaskSampleProcess = class extends
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4820
|
+
// src/tasks/coreTasks/TaskSampleProcess.js
|
|
4821
|
+
var TaskSampleProcess = class extends AbstractTask {
|
|
4822
|
+
/**
|
|
4823
|
+
* @param {object} context
|
|
4824
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4825
|
+
* @returns {Promise<{ total: number, delay: number, name?: string }>}
|
|
4826
|
+
*/
|
|
4827
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
4828
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
|
|
4829
|
+
total: "number default 10",
|
|
4830
|
+
delay: "number default 1000",
|
|
4831
|
+
name: "string"
|
|
4832
|
+
}, overrides);
|
|
4833
|
+
const total = Number(merged.total);
|
|
4834
|
+
const delay = Number(merged.delay);
|
|
4835
|
+
if (!Number.isInteger(total) || total <= 0) {
|
|
4836
|
+
throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
|
|
4837
|
+
}
|
|
4838
|
+
if (!Number.isInteger(delay) || delay < 0) {
|
|
4839
|
+
throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
|
|
4840
|
+
}
|
|
4841
|
+
const out = { total, delay };
|
|
4842
|
+
if (typeof merged.name === "string" && merged.name.trim()) {
|
|
4843
|
+
out.name = merged.name.trim();
|
|
4844
|
+
}
|
|
4845
|
+
return out;
|
|
4846
|
+
}
|
|
4847
|
+
/**
|
|
4848
|
+
* @param {object} context
|
|
4849
|
+
* @param {object} task
|
|
4850
|
+
*/
|
|
4851
|
+
constructor(context, task) {
|
|
4852
|
+
super(context, task);
|
|
4853
|
+
this.stopRequested = false;
|
|
4854
|
+
this.stopAllowanceMs = 0;
|
|
4855
|
+
this.stopDecisionLogged = false;
|
|
4856
|
+
}
|
|
4857
|
+
/**
|
|
4858
|
+
* Runner-facing stop signal. Records the allowance window so the main loop
|
|
4859
|
+
* can decide per-iteration whether to finish or abort early.
|
|
4860
|
+
*
|
|
4861
|
+
* @param {number} allowanceMs
|
|
4862
|
+
*/
|
|
4563
4863
|
requestStop(allowanceMs) {
|
|
4564
4864
|
this.stopRequested = true;
|
|
4565
4865
|
this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
|
|
@@ -4567,6 +4867,14 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4567
4867
|
`[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
|
|
4568
4868
|
);
|
|
4569
4869
|
}
|
|
4870
|
+
/**
|
|
4871
|
+
* Iterate `total` times, sleeping `delay` ms between ticks and reporting
|
|
4872
|
+
* progress every iteration. Validates params up front; invalid values short-
|
|
4873
|
+
* circuit to a structured failure without starting the loop.
|
|
4874
|
+
*
|
|
4875
|
+
* @param {(progress: object) => Promise<void>} reportProgress
|
|
4876
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
4877
|
+
*/
|
|
4570
4878
|
async run(reportProgress) {
|
|
4571
4879
|
const totalRaw = this.task?.params?.total ?? 10;
|
|
4572
4880
|
const delayRaw = this.task?.params?.delay ?? 1e3;
|
|
@@ -4648,7 +4956,7 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4648
4956
|
}
|
|
4649
4957
|
};
|
|
4650
4958
|
|
|
4651
|
-
// src/tasks/coreTasks/TaskShellCommand.
|
|
4959
|
+
// src/tasks/coreTasks/TaskShellCommand.js
|
|
4652
4960
|
import { spawn } from "child_process";
|
|
4653
4961
|
function runShellCommand(command, cwd) {
|
|
4654
4962
|
return new Promise((resolve2, reject) => {
|
|
@@ -4678,7 +4986,27 @@ function runShellCommand(command, cwd) {
|
|
|
4678
4986
|
});
|
|
4679
4987
|
});
|
|
4680
4988
|
}
|
|
4681
|
-
var TaskShellCommand = class extends
|
|
4989
|
+
var TaskShellCommand = class extends AbstractTask {
|
|
4990
|
+
/**
|
|
4991
|
+
* @param {object} context
|
|
4992
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4993
|
+
* @returns {Promise<{ command: string, cwd?: string }>}
|
|
4994
|
+
*/
|
|
4995
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
4996
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
|
|
4997
|
+
command: "string",
|
|
4998
|
+
cwd: "string"
|
|
4999
|
+
}, overrides);
|
|
5000
|
+
const command = typeof merged.command === "string" ? merged.command.trim() : "";
|
|
5001
|
+
if (!command) {
|
|
5002
|
+
throw new ParamError('shellCommand: param "command" must be a non-empty string');
|
|
5003
|
+
}
|
|
5004
|
+
const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
|
|
5005
|
+
return cwd ? { command, cwd } : { command };
|
|
5006
|
+
}
|
|
5007
|
+
/**
|
|
5008
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5009
|
+
*/
|
|
4682
5010
|
async run() {
|
|
4683
5011
|
const params = this.task?.params;
|
|
4684
5012
|
const commandRaw = typeof params === "string" ? params : params?.command;
|
|
@@ -4727,7 +5055,7 @@ var TaskShellCommand = class extends TaskMaster {
|
|
|
4727
5055
|
}
|
|
4728
5056
|
};
|
|
4729
5057
|
|
|
4730
|
-
// src/tasks/coreTasks/TaskSystemInfo.
|
|
5058
|
+
// src/tasks/coreTasks/TaskSystemInfo.js
|
|
4731
5059
|
import os2 from "os";
|
|
4732
5060
|
import fs4 from "fs/promises";
|
|
4733
5061
|
function toGb(valueBytes) {
|
|
@@ -4747,7 +5075,16 @@ async function getDiskStats() {
|
|
|
4747
5075
|
free: toGb(free)
|
|
4748
5076
|
};
|
|
4749
5077
|
}
|
|
4750
|
-
var TaskSystemInfo = class extends
|
|
5078
|
+
var TaskSystemInfo = class extends AbstractTask {
|
|
5079
|
+
/** Same UX expectation as `ping` — short probe, print the result. */
|
|
5080
|
+
static defaultWaitForResult = true;
|
|
5081
|
+
/** systemInfo takes no params. */
|
|
5082
|
+
static async resolveCustomParams() {
|
|
5083
|
+
return null;
|
|
5084
|
+
}
|
|
5085
|
+
/**
|
|
5086
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5087
|
+
*/
|
|
4751
5088
|
async run() {
|
|
4752
5089
|
try {
|
|
4753
5090
|
const totalMemory = os2.totalmem();
|
|
@@ -4799,8 +5136,31 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
4799
5136
|
}
|
|
4800
5137
|
};
|
|
4801
5138
|
|
|
4802
|
-
// src/tasks/coreTasks/TaskSumAB.
|
|
4803
|
-
var TaskSumAB = class extends
|
|
5139
|
+
// src/tasks/coreTasks/TaskSumAB.js
|
|
5140
|
+
var TaskSumAB = class extends AbstractTask {
|
|
5141
|
+
/** Short, deterministic — wait by default so callers see the sum. */
|
|
5142
|
+
static defaultWaitForResult = true;
|
|
5143
|
+
/**
|
|
5144
|
+
* @param {object} context
|
|
5145
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5146
|
+
* @returns {Promise<{ a: number, b: number }>}
|
|
5147
|
+
*/
|
|
5148
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5149
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
|
|
5150
|
+
a: "number",
|
|
5151
|
+
b: "number"
|
|
5152
|
+
}, overrides);
|
|
5153
|
+
if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
|
|
5154
|
+
throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
|
|
5155
|
+
}
|
|
5156
|
+
if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
|
|
5157
|
+
throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
|
|
5158
|
+
}
|
|
5159
|
+
return { a: merged.a, b: merged.b };
|
|
5160
|
+
}
|
|
5161
|
+
/**
|
|
5162
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5163
|
+
*/
|
|
4804
5164
|
async run() {
|
|
4805
5165
|
const a = this.task?.params?.a;
|
|
4806
5166
|
const b = this.task?.params?.b;
|
|
@@ -4831,8 +5191,46 @@ var TaskSumAB = class extends TaskMaster {
|
|
|
4831
5191
|
}
|
|
4832
5192
|
};
|
|
4833
5193
|
|
|
4834
|
-
// src/tasks/coreTasks/TaskStopRunner.
|
|
4835
|
-
var TaskStopRunner = class extends
|
|
5194
|
+
// src/tasks/coreTasks/TaskStopRunner.js
|
|
5195
|
+
var TaskStopRunner = class extends AbstractTask {
|
|
5196
|
+
/**
|
|
5197
|
+
* Stop tasks must target a concrete instance — without `serviceName` the
|
|
5198
|
+
* row would race against any worker on the queue. Layered on top of the
|
|
5199
|
+
* envelope built by {@link AbstractTask.resolveParams}.
|
|
5200
|
+
*
|
|
5201
|
+
* @param {object} context
|
|
5202
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5203
|
+
* @returns {Promise<object>}
|
|
5204
|
+
*/
|
|
5205
|
+
static async resolveParams(context, overrides = {}) {
|
|
5206
|
+
const main = await super.resolveParams(context, overrides);
|
|
5207
|
+
if (!main.serviceName) {
|
|
5208
|
+
throw new ParamError(
|
|
5209
|
+
"stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
|
|
5210
|
+
);
|
|
5211
|
+
}
|
|
5212
|
+
return main;
|
|
5213
|
+
}
|
|
5214
|
+
/**
|
|
5215
|
+
* @param {object} context
|
|
5216
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5217
|
+
* @returns {Promise<{ allowanceMs: number }>}
|
|
5218
|
+
*/
|
|
5219
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5220
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
|
|
5221
|
+
allowanceMs: "number default 5000"
|
|
5222
|
+
}, overrides);
|
|
5223
|
+
const allowanceMs = Number(merged.allowanceMs);
|
|
5224
|
+
if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
|
|
5225
|
+
throw new ParamError(
|
|
5226
|
+
`stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
|
|
5227
|
+
);
|
|
5228
|
+
}
|
|
5229
|
+
return { allowanceMs };
|
|
5230
|
+
}
|
|
5231
|
+
/**
|
|
5232
|
+
* @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
|
|
5233
|
+
*/
|
|
4836
5234
|
async run() {
|
|
4837
5235
|
const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
|
|
4838
5236
|
this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
|
|
@@ -4847,42 +5245,200 @@ var TaskStopRunner = class extends TaskMaster {
|
|
|
4847
5245
|
}
|
|
4848
5246
|
};
|
|
4849
5247
|
|
|
4850
|
-
// src/tasks/
|
|
5248
|
+
// src/tasks/coreTasks/TaskGetLogs.js
|
|
5249
|
+
var TaskGetLogs = class extends AbstractTask {
|
|
5250
|
+
/**
|
|
5251
|
+
* @param {object} context
|
|
5252
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5253
|
+
* @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
|
|
5254
|
+
*/
|
|
5255
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5256
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
|
|
5257
|
+
source: "string",
|
|
5258
|
+
resource: "string",
|
|
5259
|
+
tail: "number default 100",
|
|
5260
|
+
afterTs: "string"
|
|
5261
|
+
}, overrides);
|
|
5262
|
+
const source = typeof merged.source === "string" ? merged.source.trim() : "";
|
|
5263
|
+
const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
|
|
5264
|
+
if (!source) throw new ParamError('getLogs: param "source" is required');
|
|
5265
|
+
if (!resource) throw new ParamError('getLogs: param "resource" is required');
|
|
5266
|
+
let tail = Number(merged.tail);
|
|
5267
|
+
if (!Number.isFinite(tail) || tail < 1) tail = 100;
|
|
5268
|
+
tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
|
|
5269
|
+
const out = { source, resource, tail };
|
|
5270
|
+
if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
|
|
5271
|
+
out.afterTs = merged.afterTs.trim();
|
|
5272
|
+
}
|
|
5273
|
+
return out;
|
|
5274
|
+
}
|
|
5275
|
+
/**
|
|
5276
|
+
* @param {unknown} _reportProgress Unused (single-shot read, no progress events).
|
|
5277
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5278
|
+
*/
|
|
5279
|
+
async run(_reportProgress) {
|
|
5280
|
+
const p = this.task.params ?? {};
|
|
5281
|
+
const source = String(p.source ?? "").trim();
|
|
5282
|
+
const resource = String(p.resource ?? "").trim();
|
|
5283
|
+
const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
|
|
5284
|
+
const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
|
|
5285
|
+
if (!source || !resource) {
|
|
5286
|
+
return {
|
|
5287
|
+
success: false,
|
|
5288
|
+
results: { error: 'getLogs requires params "source" and "resource"' }
|
|
5289
|
+
};
|
|
5290
|
+
}
|
|
5291
|
+
try {
|
|
5292
|
+
const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
|
|
5293
|
+
source,
|
|
5294
|
+
resource,
|
|
5295
|
+
tail,
|
|
5296
|
+
afterTs
|
|
5297
|
+
});
|
|
5298
|
+
return {
|
|
5299
|
+
success: true,
|
|
5300
|
+
results: { records, latestTs, source, resource }
|
|
5301
|
+
};
|
|
5302
|
+
} catch (e) {
|
|
5303
|
+
return {
|
|
5304
|
+
success: false,
|
|
5305
|
+
results: { error: e?.message ?? String(e) }
|
|
5306
|
+
};
|
|
5307
|
+
}
|
|
5308
|
+
}
|
|
5309
|
+
};
|
|
5310
|
+
|
|
5311
|
+
// src/tasks/TasksRegistry.js
|
|
4851
5312
|
var TasksRegistry = class _TasksRegistry {
|
|
4852
|
-
|
|
5313
|
+
/**
|
|
5314
|
+
* @param {Record<string, Function>} [initial] Optional seed entries to copy in.
|
|
5315
|
+
*/
|
|
4853
5316
|
constructor(initial) {
|
|
5317
|
+
this.map = {};
|
|
4854
5318
|
if (initial) {
|
|
4855
5319
|
this.addMany(initial);
|
|
4856
5320
|
}
|
|
4857
5321
|
}
|
|
5322
|
+
/**
|
|
5323
|
+
* Build a registry pre-populated with every core task plus legacy aliases.
|
|
5324
|
+
* Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
|
|
5325
|
+
*
|
|
5326
|
+
* @returns {TasksRegistry}
|
|
5327
|
+
*/
|
|
4858
5328
|
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);
|
|
5329
|
+
return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("info", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner).add("getLogs", TaskGetLogs);
|
|
4860
5330
|
}
|
|
5331
|
+
/**
|
|
5332
|
+
* Register a single task class under a name. Overwrites any previous entry.
|
|
5333
|
+
*
|
|
5334
|
+
* @param {string} taskName
|
|
5335
|
+
* @param {Function} taskClass Subclass of `AbstractTask`.
|
|
5336
|
+
* @returns {this}
|
|
5337
|
+
*/
|
|
4861
5338
|
add(taskName, taskClass) {
|
|
4862
5339
|
this.map[taskName] = taskClass;
|
|
4863
5340
|
return this;
|
|
4864
5341
|
}
|
|
5342
|
+
/**
|
|
5343
|
+
* Bulk-register a name → class map. Later calls override earlier ones.
|
|
5344
|
+
*
|
|
5345
|
+
* @param {Record<string, Function>} entries
|
|
5346
|
+
* @returns {this}
|
|
5347
|
+
*/
|
|
4865
5348
|
addMany(entries) {
|
|
4866
5349
|
for (const [name, klass] of Object.entries(entries)) {
|
|
4867
5350
|
this.add(name, klass);
|
|
4868
5351
|
}
|
|
4869
5352
|
return this;
|
|
4870
5353
|
}
|
|
5354
|
+
/**
|
|
5355
|
+
* Look up a task class by name. Returns `undefined` when the name is unknown;
|
|
5356
|
+
* the runner treats that as "some other worker may handle this" and skips.
|
|
5357
|
+
*
|
|
5358
|
+
* @param {string} taskName
|
|
5359
|
+
* @returns {Function | undefined}
|
|
5360
|
+
*/
|
|
4871
5361
|
get(taskName) {
|
|
4872
5362
|
return this.map[taskName];
|
|
4873
5363
|
}
|
|
5364
|
+
/**
|
|
5365
|
+
* Strict variant of {@link get}: throws {@link ParamError} (with the list
|
|
5366
|
+
* of supported names) when `taskName` is unknown. Use from enqueuer code
|
|
5367
|
+
* paths where an unknown name is a hard CLI/programmer error.
|
|
5368
|
+
*
|
|
5369
|
+
* @param {string} taskName
|
|
5370
|
+
* @returns {Function}
|
|
5371
|
+
*/
|
|
5372
|
+
requireClass(taskName) {
|
|
5373
|
+
const TaskClass = taskName ? this.map[taskName] : void 0;
|
|
5374
|
+
if (!TaskClass) {
|
|
5375
|
+
const supported = this.listSupportedTasks().join(", ") || "(none)";
|
|
5376
|
+
throw new ParamError(
|
|
5377
|
+
`Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
|
|
5378
|
+
);
|
|
5379
|
+
}
|
|
5380
|
+
return TaskClass;
|
|
5381
|
+
}
|
|
5382
|
+
/**
|
|
5383
|
+
* Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
|
|
5384
|
+
* from `overrides` or `context.params`, look up the class, and delegate
|
|
5385
|
+
* to its static {@link AbstractTask.resolveParams} with `name` seeded into
|
|
5386
|
+
* the overrides. The returned object is shaped for {@link enqueueTask}.
|
|
5387
|
+
*
|
|
5388
|
+
* Validation failures (unknown task, missing required custom params, etc.)
|
|
5389
|
+
* surface as {@link ParamError} so the caller aborts cleanly before any
|
|
5390
|
+
* row is inserted.
|
|
5391
|
+
*
|
|
5392
|
+
* @param {object} context
|
|
5393
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5394
|
+
* @returns {Promise<object>}
|
|
5395
|
+
*/
|
|
5396
|
+
async resolveTaskParams(context, overrides = {}) {
|
|
5397
|
+
const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
|
|
5398
|
+
const fromCli = context.params.get("name", "string");
|
|
5399
|
+
const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
|
|
5400
|
+
const name = overrideName || cliName;
|
|
5401
|
+
if (!name) {
|
|
5402
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
5403
|
+
}
|
|
5404
|
+
const TaskClass = this.requireClass(name);
|
|
5405
|
+
return TaskClass.resolveParams(context, { ...overrides, name });
|
|
5406
|
+
}
|
|
5407
|
+
/**
|
|
5408
|
+
* Names of every registered task, sorted alphabetically (useful for CLI output
|
|
5409
|
+
* and allowlist sanity checks).
|
|
5410
|
+
*
|
|
5411
|
+
* @returns {string[]}
|
|
5412
|
+
*/
|
|
4874
5413
|
listSupportedTasks() {
|
|
4875
5414
|
return Object.keys(this.map).sort();
|
|
4876
5415
|
}
|
|
5416
|
+
/**
|
|
5417
|
+
* Shallow copy of the internal map, for handing to `addMany` on another registry
|
|
5418
|
+
* or for serialization.
|
|
5419
|
+
*
|
|
5420
|
+
* @returns {Record<string, Function>}
|
|
5421
|
+
*/
|
|
4877
5422
|
toObject() {
|
|
4878
5423
|
return { ...this.map };
|
|
4879
5424
|
}
|
|
4880
5425
|
};
|
|
4881
5426
|
|
|
4882
|
-
// src/tasks/
|
|
5427
|
+
// src/tasks/serviceTaskAllowlist.js
|
|
5428
|
+
function normalizeAllowedTasks(value) {
|
|
5429
|
+
if (!value) return void 0;
|
|
5430
|
+
if (Array.isArray(value)) {
|
|
5431
|
+
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
5432
|
+
return out2.length ? out2 : void 0;
|
|
5433
|
+
}
|
|
5434
|
+
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
5435
|
+
return out.length ? out : void 0;
|
|
5436
|
+
}
|
|
5437
|
+
|
|
5438
|
+
// src/tasks/taskScriptRunner.js
|
|
4883
5439
|
import { spawn as spawn2 } from "child_process";
|
|
4884
5440
|
|
|
4885
|
-
// src/tasks/index.
|
|
5441
|
+
// src/tasks/index.js
|
|
4886
5442
|
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
4887
5443
|
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
4888
5444
|
function getDb3(context) {
|
|
@@ -4897,15 +5453,6 @@ function normalizeRegistry(registry) {
|
|
|
4897
5453
|
if (registry instanceof TasksRegistry) return registry;
|
|
4898
5454
|
return new TasksRegistry().addMany(registry);
|
|
4899
5455
|
}
|
|
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
5456
|
async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
|
|
4910
5457
|
context.logger.warn?.(`[tasks] signaling ${runningTaskInstances.size} running task(s) to stop`);
|
|
4911
5458
|
for (const [, taskInstance] of runningTaskInstances) {
|
|
@@ -4921,18 +5468,20 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
|
|
|
4921
5468
|
}
|
|
4922
5469
|
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
4923
5470
|
const db = getDb3(context);
|
|
4924
|
-
const taskName = row.
|
|
5471
|
+
const taskName = row.name;
|
|
4925
5472
|
const TaskClass = registry.get(taskName);
|
|
4926
|
-
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
4927
5473
|
if (!TaskClass) {
|
|
4928
5474
|
const err = { message: `Unknown task "${taskName}"` };
|
|
4929
|
-
await db(historyTable).insert(
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
5475
|
+
await db(historyTable).insert(
|
|
5476
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
5477
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5478
|
+
success: false,
|
|
5479
|
+
status: "failed",
|
|
5480
|
+
status_changed_at: db.fn.now(),
|
|
5481
|
+
params: toJsonColumn(row.params),
|
|
5482
|
+
results: toJsonColumn(err)
|
|
5483
|
+
})
|
|
5484
|
+
);
|
|
4936
5485
|
if (row.schedule) {
|
|
4937
5486
|
await db(tasksTable).where({ id: row.id }).update({
|
|
4938
5487
|
started_at: null,
|
|
@@ -4940,7 +5489,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4940
5489
|
success: false,
|
|
4941
5490
|
results: toJsonColumn(err),
|
|
4942
5491
|
past_due: null,
|
|
4943
|
-
|
|
5492
|
+
status: "paused",
|
|
5493
|
+
status_changed_at: db.fn.now(),
|
|
4944
5494
|
progress: LOCKED_BY_ERROR_MESSAGE
|
|
4945
5495
|
});
|
|
4946
5496
|
} else {
|
|
@@ -4967,13 +5517,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4967
5517
|
} finally {
|
|
4968
5518
|
runningTaskInstances.delete(row.id);
|
|
4969
5519
|
}
|
|
4970
|
-
await db(historyTable).insert(
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
5520
|
+
await db(historyTable).insert(
|
|
5521
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
5522
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5523
|
+
success,
|
|
5524
|
+
status: success ? "completed" : "failed",
|
|
5525
|
+
status_changed_at: db.fn.now(),
|
|
5526
|
+
params: toJsonColumn(row.params),
|
|
5527
|
+
results: toJsonColumn(results)
|
|
5528
|
+
})
|
|
5529
|
+
);
|
|
4977
5530
|
if (!success) {
|
|
4978
5531
|
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
4979
5532
|
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
@@ -4988,19 +5541,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4988
5541
|
const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
|
|
4989
5542
|
appendTaskIpcLog(context, row, {
|
|
4990
5543
|
level: "error",
|
|
4991
|
-
message: `[tasks] task failed: ${row.
|
|
5544
|
+
message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
|
|
4992
5545
|
details: results
|
|
4993
5546
|
});
|
|
4994
5547
|
}
|
|
4995
5548
|
if (row.schedule) {
|
|
4996
5549
|
if (success) {
|
|
5550
|
+
let nextRunAt = null;
|
|
5551
|
+
try {
|
|
5552
|
+
nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
|
|
5553
|
+
} catch (e) {
|
|
5554
|
+
context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
|
|
5555
|
+
}
|
|
4997
5556
|
await db(tasksTable).where({ id: row.id }).update({
|
|
4998
5557
|
started_at: null,
|
|
4999
5558
|
completed_at: /* @__PURE__ */ new Date(),
|
|
5000
5559
|
success,
|
|
5001
5560
|
results: toJsonColumn(results),
|
|
5002
5561
|
progress: null,
|
|
5003
|
-
past_due: null
|
|
5562
|
+
past_due: null,
|
|
5563
|
+
status: "idle",
|
|
5564
|
+
status_changed_at: db.fn.now(),
|
|
5565
|
+
next_run_at: nextRunAt,
|
|
5566
|
+
// Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
|
|
5567
|
+
service_name: null,
|
|
5568
|
+
server_name: null,
|
|
5569
|
+
instance_number: null
|
|
5004
5570
|
});
|
|
5005
5571
|
} else {
|
|
5006
5572
|
await db(tasksTable).where({ id: row.id }).update({
|
|
@@ -5008,7 +5574,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5008
5574
|
completed_at: /* @__PURE__ */ new Date(),
|
|
5009
5575
|
success,
|
|
5010
5576
|
results: toJsonColumn(results),
|
|
5011
|
-
|
|
5577
|
+
status: "paused",
|
|
5578
|
+
status_changed_at: db.fn.now(),
|
|
5012
5579
|
progress: LOCKED_BY_ERROR_MESSAGE,
|
|
5013
5580
|
past_due: null
|
|
5014
5581
|
});
|
|
@@ -5020,58 +5587,93 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5020
5587
|
const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
|
|
5021
5588
|
return { stopRunnerRequested, stopAllowanceMs };
|
|
5022
5589
|
}
|
|
5023
|
-
|
|
5590
|
+
function shuffleTaskRowsInPlace(rows) {
|
|
5591
|
+
for (let i = rows.length - 1; i > 0; i--) {
|
|
5592
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
5593
|
+
const t = rows[i];
|
|
5594
|
+
rows[i] = rows[j];
|
|
5595
|
+
rows[j] = t;
|
|
5596
|
+
}
|
|
5597
|
+
}
|
|
5598
|
+
async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
|
|
5024
5599
|
const db = getDb3(context);
|
|
5025
|
-
let query = db(tasksTable).
|
|
5600
|
+
let query = db(tasksTable).where({ status: "idle" }).where(function() {
|
|
5601
|
+
this.whereNull("service_group").orWhere({ service_group: serviceGroup });
|
|
5602
|
+
}).orderByRaw("CASE WHEN past_due IS NULL THEN 1 ELSE 0 END ASC").orderBy([{ column: "priority", order: "asc" }]).orderByRaw("CASE WHEN completed_at IS NULL THEN 0 ELSE 1 END ASC").orderBy([{ column: "completed_at", order: "asc" }, { column: "created_at", order: "asc" }]).limit(scanLimit);
|
|
5026
5603
|
if (taskNames && taskNames.length > 0) {
|
|
5027
|
-
query = query.whereIn("
|
|
5604
|
+
query = query.whereIn("name", taskNames);
|
|
5605
|
+
}
|
|
5606
|
+
if (runnerIdentity) {
|
|
5607
|
+
query = query.where(function() {
|
|
5608
|
+
this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
|
|
5609
|
+
}).where(function() {
|
|
5610
|
+
this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
|
|
5611
|
+
}).where(function() {
|
|
5612
|
+
this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
|
|
5613
|
+
});
|
|
5614
|
+
} else {
|
|
5615
|
+
query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
|
|
5028
5616
|
}
|
|
5029
5617
|
const candidates = await query;
|
|
5618
|
+
shuffleTaskRowsInPlace(candidates);
|
|
5030
5619
|
for (const row of candidates) {
|
|
5031
5620
|
if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
|
|
5032
5621
|
continue;
|
|
5033
5622
|
}
|
|
5034
|
-
const TaskClass = registry.get(row.
|
|
5035
|
-
if (TaskClass) {
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5623
|
+
const TaskClass = registry.get(row.name);
|
|
5624
|
+
if (!TaskClass) {
|
|
5625
|
+
continue;
|
|
5626
|
+
}
|
|
5627
|
+
const taskInstance = new TaskClass(context, row);
|
|
5628
|
+
const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
|
|
5629
|
+
if (reason) {
|
|
5630
|
+
if (!row.past_due) {
|
|
5631
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
5632
|
+
past_due: db.fn.now(),
|
|
5633
|
+
progress: String(reason)
|
|
5634
|
+
});
|
|
5046
5635
|
}
|
|
5636
|
+
continue;
|
|
5637
|
+
}
|
|
5638
|
+
const claimPatch = {
|
|
5639
|
+
started_at: db.fn.now(),
|
|
5640
|
+
status: "running",
|
|
5641
|
+
status_changed_at: db.fn.now()
|
|
5642
|
+
};
|
|
5643
|
+
if (runnerIdentity) {
|
|
5644
|
+
claimPatch.service_name = runnerIdentity.service_name;
|
|
5645
|
+
claimPatch.server_name = runnerIdentity.server_name;
|
|
5646
|
+
claimPatch.instance_number = runnerIdentity.instance_number;
|
|
5047
5647
|
}
|
|
5048
|
-
const updated = await db(tasksTable).where({ id: row.id
|
|
5648
|
+
const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
|
|
5049
5649
|
const claimed = Array.isArray(updated) ? updated[0] : null;
|
|
5050
5650
|
if (claimed) return claimed;
|
|
5051
5651
|
}
|
|
5052
5652
|
return null;
|
|
5053
5653
|
}
|
|
5054
5654
|
async function runTasksLoop(context, options) {
|
|
5055
|
-
const
|
|
5655
|
+
const queueName = options.queueName ?? "tasks";
|
|
5056
5656
|
const target = options.target;
|
|
5057
5657
|
const pollMs = options.pollMs ?? 1e3;
|
|
5058
|
-
const
|
|
5658
|
+
const claimJitterMs = options.claimJitterMs ?? 0;
|
|
5659
|
+
const maxParallel = options.maxParallel ?? 32;
|
|
5059
5660
|
const scanLimit = options.scanLimit ?? 100;
|
|
5060
5661
|
const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5061
5662
|
const registry = normalizeRegistry(options.registry);
|
|
5062
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
5663
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
5063
5664
|
if (!target) throw new Error("runTasksLoop: target is required");
|
|
5665
|
+
context.tasksQueueName = queueName;
|
|
5064
5666
|
const runningPromises = /* @__PURE__ */ new Set();
|
|
5065
5667
|
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
5066
5668
|
let runningStopControlPromise = null;
|
|
5067
5669
|
let stopRequested = false;
|
|
5068
5670
|
let stopAllowanceMs = 5e3;
|
|
5069
|
-
context.
|
|
5671
|
+
context.tasksRunnerStop = false;
|
|
5070
5672
|
let registryReg = null;
|
|
5071
5673
|
let registryInterval = null;
|
|
5674
|
+
let runnerIdentity = null;
|
|
5072
5675
|
const hbGroup = options.runnerServiceGroup?.trim();
|
|
5073
5676
|
if (hbGroup) {
|
|
5074
|
-
const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
|
|
5075
5677
|
const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
|
|
5076
5678
|
const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
|
|
5077
5679
|
const defaultMeta = {
|
|
@@ -5079,16 +5681,21 @@ async function runTasksLoop(context, options) {
|
|
|
5079
5681
|
allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
|
|
5080
5682
|
};
|
|
5081
5683
|
registryReg = await registerInServicesRegistry(context, {
|
|
5082
|
-
|
|
5684
|
+
queueName,
|
|
5083
5685
|
target,
|
|
5084
5686
|
serviceGroup: hbGroup,
|
|
5085
5687
|
serviceName: options.runnerServiceName,
|
|
5086
|
-
|
|
5688
|
+
instanceNumber: options.runnerInstanceNumber,
|
|
5087
5689
|
staleMs,
|
|
5088
5690
|
groupMaxInstances: options.runnerGroupMaxInstances,
|
|
5089
5691
|
enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
|
|
5090
5692
|
metadata: options.runnerMetadata ?? defaultMeta
|
|
5091
5693
|
});
|
|
5694
|
+
runnerIdentity = {
|
|
5695
|
+
service_name: registryReg.serviceName,
|
|
5696
|
+
server_name: os3.hostname(),
|
|
5697
|
+
instance_number: registryReg.instanceNumber
|
|
5698
|
+
};
|
|
5092
5699
|
registryInterval = setInterval(() => {
|
|
5093
5700
|
void touchServicesRegistry(context, registryReg).catch((err) => {
|
|
5094
5701
|
context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
|
|
@@ -5096,7 +5703,7 @@ async function runTasksLoop(context, options) {
|
|
|
5096
5703
|
}, hbIntervalMs);
|
|
5097
5704
|
}
|
|
5098
5705
|
try {
|
|
5099
|
-
while (!context.isStop() && !stopRequested &&
|
|
5706
|
+
while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
|
|
5100
5707
|
if (!runningStopControlPromise) {
|
|
5101
5708
|
const claimedStopTask = await claimNextRunnableTask(
|
|
5102
5709
|
context,
|
|
@@ -5104,7 +5711,8 @@ async function runTasksLoop(context, options) {
|
|
|
5104
5711
|
target,
|
|
5105
5712
|
registry,
|
|
5106
5713
|
10,
|
|
5107
|
-
["stopRunner", "stop"]
|
|
5714
|
+
["stopRunner", "stop"],
|
|
5715
|
+
runnerIdentity
|
|
5108
5716
|
);
|
|
5109
5717
|
if (claimedStopTask) {
|
|
5110
5718
|
runningStopControlPromise = executeClaimedTask(
|
|
@@ -5118,7 +5726,7 @@ async function runTasksLoop(context, options) {
|
|
|
5118
5726
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5119
5727
|
stopRequested = true;
|
|
5120
5728
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5121
|
-
context.
|
|
5729
|
+
context.tasksRunnerStop = true;
|
|
5122
5730
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5123
5731
|
}
|
|
5124
5732
|
}).finally(() => {
|
|
@@ -5126,6 +5734,9 @@ async function runTasksLoop(context, options) {
|
|
|
5126
5734
|
});
|
|
5127
5735
|
}
|
|
5128
5736
|
}
|
|
5737
|
+
if (claimJitterMs > 0) {
|
|
5738
|
+
await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
|
|
5739
|
+
}
|
|
5129
5740
|
while (runningPromises.size < maxParallel) {
|
|
5130
5741
|
const claimed = await claimNextRunnableTask(
|
|
5131
5742
|
context,
|
|
@@ -5133,14 +5744,15 @@ async function runTasksLoop(context, options) {
|
|
|
5133
5744
|
target,
|
|
5134
5745
|
registry,
|
|
5135
5746
|
scanLimit,
|
|
5136
|
-
allowedTasks
|
|
5747
|
+
allowedTasks,
|
|
5748
|
+
runnerIdentity
|
|
5137
5749
|
);
|
|
5138
5750
|
if (!claimed) break;
|
|
5139
5751
|
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
5140
5752
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5141
5753
|
stopRequested = true;
|
|
5142
5754
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5143
|
-
context.
|
|
5755
|
+
context.tasksRunnerStop = true;
|
|
5144
5756
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5145
5757
|
}
|
|
5146
5758
|
}).finally(() => {
|
|
@@ -5148,7 +5760,16 @@ async function runTasksLoop(context, options) {
|
|
|
5148
5760
|
});
|
|
5149
5761
|
runningPromises.add(p);
|
|
5150
5762
|
}
|
|
5151
|
-
|
|
5763
|
+
const wakePromises = [...runningPromises];
|
|
5764
|
+
if (runningStopControlPromise) {
|
|
5765
|
+
wakePromises.push(runningStopControlPromise);
|
|
5766
|
+
}
|
|
5767
|
+
if (wakePromises.length === 0) {
|
|
5768
|
+
await sleepMs(pollMs);
|
|
5769
|
+
} else {
|
|
5770
|
+
const safe = wakePromises.map((p) => p.catch(() => void 0));
|
|
5771
|
+
await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
|
|
5772
|
+
}
|
|
5152
5773
|
}
|
|
5153
5774
|
if (context.isStop() && !stopRequested) {
|
|
5154
5775
|
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
@@ -5183,71 +5804,88 @@ async function runTasksLoop(context, options) {
|
|
|
5183
5804
|
}
|
|
5184
5805
|
}
|
|
5185
5806
|
var TasksManager = class _TasksManager {
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5807
|
+
/**
|
|
5808
|
+
* @param {object} context
|
|
5809
|
+
* @param {{
|
|
5810
|
+
* queueName?: string,
|
|
5811
|
+
* target?: string,
|
|
5812
|
+
* recreateTaskTables?: boolean,
|
|
5813
|
+
* pollMs?: number,
|
|
5814
|
+
* claimJitterMs?: number,
|
|
5815
|
+
* maxParallel?: number,
|
|
5816
|
+
* scanLimit?: number,
|
|
5817
|
+
* allowedTasks?: string | string[],
|
|
5818
|
+
* registry?: TasksRegistry | Record<string, Function>,
|
|
5819
|
+
* runnerServiceGroup?: string,
|
|
5820
|
+
* runnerServiceName?: string,
|
|
5821
|
+
* runnerInstanceNumber?: number,
|
|
5822
|
+
* runnerHeartbeatIntervalMs?: number,
|
|
5823
|
+
* runnerHeartbeatStaleMs?: number,
|
|
5824
|
+
* runnerGroupMaxInstances?: number,
|
|
5825
|
+
* runnerEnforceMaxInstances?: boolean,
|
|
5826
|
+
* runnerMetadata?: Record<string, unknown>,
|
|
5827
|
+
* }} [options]
|
|
5828
|
+
*/
|
|
5203
5829
|
constructor(context, options = {}) {
|
|
5204
5830
|
this.context = context;
|
|
5205
|
-
this.
|
|
5831
|
+
this.queueName = options.queueName ?? "tasks";
|
|
5206
5832
|
this.target = options.target ?? "localRunner";
|
|
5207
5833
|
this.recreateTaskTables = options.recreateTaskTables ?? false;
|
|
5208
5834
|
this.pollMs = options.pollMs ?? 1e3;
|
|
5835
|
+
this.claimJitterMs = options.claimJitterMs ?? 0;
|
|
5209
5836
|
this.maxParallel = options.maxParallel ?? 1;
|
|
5210
5837
|
this.scanLimit = options.scanLimit ?? 100;
|
|
5211
5838
|
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5212
5839
|
this.registry = normalizeRegistry(options.registry);
|
|
5213
5840
|
this.runnerServiceGroup = options.runnerServiceGroup;
|
|
5214
5841
|
this.runnerServiceName = options.runnerServiceName;
|
|
5215
|
-
this.
|
|
5842
|
+
this.runnerInstanceNumber = options.runnerInstanceNumber;
|
|
5216
5843
|
this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
|
|
5217
5844
|
this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
|
|
5218
5845
|
this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
|
|
5219
5846
|
this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
|
|
5220
5847
|
this.runnerMetadata = options.runnerMetadata;
|
|
5221
5848
|
}
|
|
5849
|
+
/**
|
|
5850
|
+
* Preferred factory: reads defaults from `context.params` (module namespace
|
|
5851
|
+
* `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
|
|
5852
|
+
* and inline options in one consistent resolver.
|
|
5853
|
+
*
|
|
5854
|
+
* @param {object} context
|
|
5855
|
+
* @param {ConstructorParameters<typeof TasksManager>[1]} [options]
|
|
5856
|
+
* @returns {TasksManager}
|
|
5857
|
+
*/
|
|
5222
5858
|
static init(context, options = {}) {
|
|
5223
5859
|
const defs2 = {
|
|
5224
5860
|
table: "string default tasks",
|
|
5225
5861
|
target: "string default localRunner",
|
|
5226
5862
|
recreateTaskTables: "boolean default false",
|
|
5227
5863
|
pollMs: "number default 1000",
|
|
5864
|
+
claimJitterMs: "number default 0",
|
|
5228
5865
|
maxParallel: "number default 1",
|
|
5229
5866
|
scanLimit: "number default 100",
|
|
5230
5867
|
allowedTasks: "string",
|
|
5231
5868
|
runnerServiceGroup: "string",
|
|
5232
5869
|
runnerServiceName: "string",
|
|
5233
|
-
|
|
5870
|
+
runnerInstanceNumber: "number",
|
|
5234
5871
|
runnerHeartbeatIntervalMs: "number default 10000",
|
|
5235
5872
|
runnerHeartbeatStaleMs: "number default 45000",
|
|
5236
5873
|
runnerGroupMaxInstances: "number",
|
|
5237
5874
|
runnerEnforceMaxInstances: "boolean default true"
|
|
5238
5875
|
};
|
|
5239
|
-
const discovered = context.params.getAllForModule(defs2);
|
|
5876
|
+
const discovered = context.params.getAllForModule("tasks", defs2);
|
|
5240
5877
|
const resolved = {
|
|
5241
|
-
|
|
5878
|
+
queueName: discovered.table,
|
|
5242
5879
|
target: discovered.target,
|
|
5243
5880
|
recreateTaskTables: discovered.recreateTaskTables,
|
|
5244
5881
|
pollMs: discovered.pollMs,
|
|
5882
|
+
claimJitterMs: discovered.claimJitterMs,
|
|
5245
5883
|
maxParallel: discovered.maxParallel,
|
|
5246
5884
|
scanLimit: discovered.scanLimit,
|
|
5247
5885
|
allowedTasks: discovered.allowedTasks,
|
|
5248
5886
|
runnerServiceGroup: discovered.runnerServiceGroup,
|
|
5249
5887
|
runnerServiceName: discovered.runnerServiceName,
|
|
5250
|
-
|
|
5888
|
+
runnerInstanceNumber: discovered.runnerInstanceNumber,
|
|
5251
5889
|
runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
|
|
5252
5890
|
runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
|
|
5253
5891
|
runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
|
|
@@ -5256,24 +5894,39 @@ var TasksManager = class _TasksManager {
|
|
|
5256
5894
|
};
|
|
5257
5895
|
return new _TasksManager(context, resolved);
|
|
5258
5896
|
}
|
|
5897
|
+
/**
|
|
5898
|
+
* Idempotently ensure the three backing tables exist for this queue.
|
|
5899
|
+
*
|
|
5900
|
+
* @param {{ recreate?: boolean }} [options]
|
|
5901
|
+
* @returns {Promise<void>}
|
|
5902
|
+
*/
|
|
5259
5903
|
async ensureTaskTables(options = {}) {
|
|
5260
5904
|
await ensureTaskTables(this.context, {
|
|
5261
|
-
|
|
5905
|
+
queueName: this.queueName,
|
|
5262
5906
|
recreate: options.recreate ?? this.recreateTaskTables
|
|
5263
5907
|
});
|
|
5264
5908
|
}
|
|
5909
|
+
/**
|
|
5910
|
+
* Start the runner loop using this manager's resolved config. Per-call
|
|
5911
|
+
* options override the stored defaults, but `runnerMetadata` still falls
|
|
5912
|
+
* through when omitted.
|
|
5913
|
+
*
|
|
5914
|
+
* @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
|
|
5915
|
+
* @returns {Promise<void>}
|
|
5916
|
+
*/
|
|
5265
5917
|
async runTasksLoop(options = {}) {
|
|
5266
5918
|
await runTasksLoop(this.context, {
|
|
5267
|
-
|
|
5919
|
+
queueName: options.queueName ?? this.queueName,
|
|
5268
5920
|
target: options.target ?? this.target,
|
|
5269
5921
|
pollMs: options.pollMs ?? this.pollMs,
|
|
5922
|
+
claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
|
|
5270
5923
|
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
5271
5924
|
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
5272
5925
|
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
5273
5926
|
registry: options.registry ?? this.registry,
|
|
5274
5927
|
runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
|
|
5275
5928
|
runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
|
|
5276
|
-
|
|
5929
|
+
runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
|
|
5277
5930
|
runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
|
|
5278
5931
|
runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
|
|
5279
5932
|
runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
|
|
@@ -5283,9 +5936,8 @@ var TasksManager = class _TasksManager {
|
|
|
5283
5936
|
}
|
|
5284
5937
|
};
|
|
5285
5938
|
|
|
5286
|
-
// src/scripts/cli-runner.
|
|
5939
|
+
// src/scripts/cli-runner.js
|
|
5287
5940
|
var defs = {
|
|
5288
|
-
dbName: "string default local",
|
|
5289
5941
|
tasksModule: "string"
|
|
5290
5942
|
};
|
|
5291
5943
|
async function loadTasksModule(modulePath) {
|
|
@@ -5297,11 +5949,8 @@ async function loadTasksModule(modulePath) {
|
|
|
5297
5949
|
return imported.tasksRegistry;
|
|
5298
5950
|
}
|
|
5299
5951
|
var flow = async (context) => {
|
|
5300
|
-
const {
|
|
5301
|
-
|
|
5302
|
-
tasksModule
|
|
5303
|
-
} = context.params.getAll(defs);
|
|
5304
|
-
const db = await dbInit(context, dbName);
|
|
5952
|
+
const { tasksModule } = context.params.getAll(defs);
|
|
5953
|
+
const db = await Db.init(context);
|
|
5305
5954
|
context.db = db;
|
|
5306
5955
|
const registry = new TasksRegistry().addMany(defaultTasksRegistry.toObject());
|
|
5307
5956
|
if (tasksModule) {
|