@nmakarov/cli-toolkit 0.18.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/args.cjs +1 -4
- package/dist/args.cjs.map +1 -1
- package/dist/args.js +1 -1
- package/dist/args.js.map +1 -1
- package/dist/cli-runner.cjs +1493 -516
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +1509 -531
- package/dist/cli-runner.js.map +1 -1
- package/dist/db.cjs +85 -157
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +84 -150
- package/dist/db.js.map +1 -1
- package/dist/errors.cjs +2 -2
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/filedatabase.cjs +19 -19
- package/dist/filedatabase.cjs.map +1 -1
- package/dist/filedatabase.js +19 -16
- package/dist/filedatabase.js.map +1 -1
- package/dist/http-client.cjs +9 -11
- package/dist/http-client.cjs.map +1 -1
- package/dist/http-client.js +10 -9
- package/dist/http-client.js.map +1 -1
- package/dist/http-client2.cjs +34 -33
- package/dist/http-client2.cjs.map +1 -1
- package/dist/http-client2.js +34 -30
- package/dist/http-client2.js.map +1 -1
- package/dist/index.cjs +2063 -658
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2063 -663
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +97 -69
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +112 -83
- package/dist/init.js.map +1 -1
- package/dist/logger.cjs +5 -5
- package/dist/logger.cjs.map +1 -1
- package/dist/logger.js +5 -4
- package/dist/logger.js.map +1 -1
- package/dist/mock-server.cjs +21 -33
- package/dist/mock-server.cjs.map +1 -1
- package/dist/mock-server.js +21 -28
- package/dist/mock-server.js.map +1 -1
- package/dist/params.cjs +22 -10
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +22 -7
- package/dist/params.js.map +1 -1
- package/dist/s3.cjs +286 -0
- package/dist/s3.cjs.map +1 -0
- package/dist/s3.js +273 -0
- package/dist/s3.js.map +1 -0
- package/dist/screen.cjs +34 -39
- package/dist/screen.cjs.map +1 -1
- package/dist/screen.js +48 -46
- package/dist/screen.js.map +1 -1
- package/dist/tasks.cjs +1640 -416
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +1614 -412
- package/dist/tasks.js.map +1 -1
- package/dist/utils.cjs +7 -8
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.js +6 -6
- package/dist/utils.js.map +1 -1
- package/package.json +36 -44
- package/scripts/ssm/parse-cli.js +35 -0
- package/scripts/ssm/ssm-admin.js +151 -0
- package/scripts/ssm/ssm-pull.js +147 -0
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ var __esm = (fn, res) => function __init() {
|
|
|
9
9
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
// src/screen/components.
|
|
12
|
+
// src/screen/components.js
|
|
13
13
|
import { createElement as h } from "react";
|
|
14
14
|
import { Box, Text } from "ink";
|
|
15
15
|
function getScreenWidth(maxWidth = null) {
|
|
@@ -84,13 +84,12 @@ function ScreenFooter({ lines, textStyle }) {
|
|
|
84
84
|
);
|
|
85
85
|
}
|
|
86
86
|
var init_components = __esm({
|
|
87
|
-
"src/screen/components.
|
|
88
|
-
"use strict";
|
|
87
|
+
"src/screen/components.js"() {
|
|
89
88
|
}
|
|
90
89
|
});
|
|
91
90
|
|
|
92
|
-
// src/screen/list-components.
|
|
93
|
-
import
|
|
91
|
+
// src/screen/list-components.js
|
|
92
|
+
import React, { useState, useEffect, useRef, createElement } from "react";
|
|
94
93
|
import { Box as Box2, Text as Text2 } from "ink";
|
|
95
94
|
function MultiColumnListComponent({ items, ctx, selectedIndexRef }) {
|
|
96
95
|
const [, forceUpdate] = useState({});
|
|
@@ -226,11 +225,11 @@ function MultiColumnListWithPreviewComponent({
|
|
|
226
225
|
const previewRows = [];
|
|
227
226
|
if (typeof previewContent === "string") {
|
|
228
227
|
previewRows.push(h2(ScreenRow, { key: "preview-string", children: h2(Text2, { bold: true }, previewContent) }));
|
|
229
|
-
} else if (typeof previewContent === "object" && !
|
|
228
|
+
} else if (typeof previewContent === "object" && !React.isValidElement(previewContent) && previewContent !== null) {
|
|
230
229
|
Object.entries(previewContent).forEach(([key, value], idx) => {
|
|
231
230
|
previewRows.push(h2(ScreenRow, { key: `preview-${key}-${idx}`, children: h2(Text2, {}, `${key}: ${value}`) }));
|
|
232
231
|
});
|
|
233
|
-
} else if (
|
|
232
|
+
} else if (React.isValidElement(previewContent)) {
|
|
234
233
|
previewRows.push(h2(ScreenRow, { key: "preview-element", children: previewContent }));
|
|
235
234
|
}
|
|
236
235
|
return h2(
|
|
@@ -243,11 +242,13 @@ function MultiColumnListWithPreviewComponent({
|
|
|
243
242
|
...previewRows
|
|
244
243
|
);
|
|
245
244
|
}
|
|
246
|
-
function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " " }) {
|
|
245
|
+
function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sortable = false, maxHeight, sortHighlightStyle, selectionMarker = " ", onSelectionChange }) {
|
|
247
246
|
const [, forceUpdate] = useState({});
|
|
248
247
|
const [sortOrder, setSortOrder] = useState("none");
|
|
249
248
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
250
249
|
const scrollStateRef = useRef({ scrollOffset: 0, maxHeight: 0, totalItems: 0 });
|
|
250
|
+
const itemsRef = useRef(items);
|
|
251
|
+
itemsRef.current = items;
|
|
251
252
|
const defaultGetTitle = (item) => {
|
|
252
253
|
return getTitle ? getTitle(item) : typeof item.value === "string" ? item.value : item.value?.title || item.name;
|
|
253
254
|
};
|
|
@@ -261,18 +262,24 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
261
262
|
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
262
263
|
}
|
|
263
264
|
}) : items;
|
|
265
|
+
const displayItemsRef = useRef(displayItems);
|
|
266
|
+
displayItemsRef.current = displayItems;
|
|
264
267
|
const effectiveMaxHeight = maxHeight || displayItems.length;
|
|
265
|
-
const
|
|
268
|
+
const _canScroll = displayItems.length > effectiveMaxHeight;
|
|
266
269
|
const maxScrollOffset = Math.max(0, displayItems.length - effectiveMaxHeight);
|
|
267
270
|
const clampedScrollOffset = Math.min(Math.max(0, scrollOffset), maxScrollOffset);
|
|
268
271
|
const visibleItems = displayItems.slice(clampedScrollOffset, clampedScrollOffset + effectiveMaxHeight);
|
|
269
272
|
const canScrollUp = clampedScrollOffset > 0;
|
|
270
273
|
const canScrollDown = clampedScrollOffset < maxScrollOffset;
|
|
271
274
|
scrollStateRef.current = { scrollOffset, maxHeight: effectiveMaxHeight, totalItems: displayItems.length };
|
|
275
|
+
const onSelectionChangeRef = useRef(onSelectionChange);
|
|
276
|
+
onSelectionChangeRef.current = onSelectionChange;
|
|
272
277
|
useEffect(() => {
|
|
273
278
|
ctx.setAction("moveUp", () => {
|
|
274
279
|
const newIndex = Math.max(0, selectedIndexRef.current - 1);
|
|
275
280
|
selectedIndexRef.current = newIndex;
|
|
281
|
+
const list = displayItemsRef.current;
|
|
282
|
+
onSelectionChangeRef.current?.(newIndex, list[newIndex]);
|
|
276
283
|
const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
|
|
277
284
|
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
278
285
|
const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
|
|
@@ -282,18 +289,11 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
282
289
|
forceUpdate({});
|
|
283
290
|
});
|
|
284
291
|
ctx.setAction("moveDown", () => {
|
|
285
|
-
const currentItems =
|
|
286
|
-
|
|
287
|
-
const titleB = titleGetter(b).toLowerCase();
|
|
288
|
-
if (sortOrder === "asc") {
|
|
289
|
-
return titleA < titleB ? -1 : titleA > titleB ? 1 : 0;
|
|
290
|
-
} else {
|
|
291
|
-
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
292
|
-
}
|
|
293
|
-
}) : items;
|
|
294
|
-
const maxIndex = currentItems.length - 1;
|
|
292
|
+
const currentItems = displayItemsRef.current;
|
|
293
|
+
const maxIndex = Math.max(0, currentItems.length - 1);
|
|
295
294
|
const newIndex = Math.min(maxIndex, selectedIndexRef.current + 1);
|
|
296
295
|
selectedIndexRef.current = newIndex;
|
|
296
|
+
onSelectionChangeRef.current?.(newIndex, currentItems[newIndex]);
|
|
297
297
|
const { scrollOffset: currentScrollOffset, maxHeight: currentMaxHeight, totalItems } = scrollStateRef.current;
|
|
298
298
|
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
299
299
|
const currentClampedScrollOffset = Math.min(Math.max(0, currentScrollOffset), currentMaxScrollOffset);
|
|
@@ -303,8 +303,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
303
303
|
forceUpdate({});
|
|
304
304
|
});
|
|
305
305
|
ctx.setAction("scrollUp", () => {
|
|
306
|
-
const { scrollOffset: currentScrollOffset
|
|
307
|
-
const currentMaxScrollOffset = Math.max(0, totalItems - currentMaxHeight);
|
|
306
|
+
const { scrollOffset: currentScrollOffset } = scrollStateRef.current;
|
|
308
307
|
const newScrollOffset = Math.max(0, currentScrollOffset - 1);
|
|
309
308
|
setScrollOffset(newScrollOffset);
|
|
310
309
|
forceUpdate({});
|
|
@@ -319,9 +318,9 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
319
318
|
if (sortable) {
|
|
320
319
|
ctx.setAction("toggleSort", () => {
|
|
321
320
|
const nextSort = sortOrder === "none" ? "asc" : sortOrder === "asc" ? "desc" : "none";
|
|
322
|
-
const currentSelectedItem =
|
|
321
|
+
const currentSelectedItem = displayItemsRef.current[selectedIndexRef.current];
|
|
323
322
|
setSortOrder(nextSort);
|
|
324
|
-
const newSortedItems = nextSort !== "none" ? [...
|
|
323
|
+
const newSortedItems = nextSort !== "none" ? [...itemsRef.current].sort((a, b) => {
|
|
325
324
|
const titleA = titleGetter(a).toLowerCase();
|
|
326
325
|
const titleB = titleGetter(b).toLowerCase();
|
|
327
326
|
if (nextSort === "asc") {
|
|
@@ -329,7 +328,7 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
329
328
|
} else {
|
|
330
329
|
return titleA > titleB ? -1 : titleA < titleB ? 1 : 0;
|
|
331
330
|
}
|
|
332
|
-
}) :
|
|
331
|
+
}) : itemsRef.current;
|
|
333
332
|
const newIndex = newSortedItems.findIndex((item) => item === currentSelectedItem);
|
|
334
333
|
if (newIndex !== -1) {
|
|
335
334
|
selectedIndexRef.current = newIndex;
|
|
@@ -467,14 +466,13 @@ function ListComponent({ items, ctx, selectedIndexRef, renderItem, getTitle, sor
|
|
|
467
466
|
}
|
|
468
467
|
var h2;
|
|
469
468
|
var init_list_components = __esm({
|
|
470
|
-
"src/screen/list-components.
|
|
471
|
-
"use strict";
|
|
469
|
+
"src/screen/list-components.js"() {
|
|
472
470
|
init_components();
|
|
473
471
|
h2 = createElement;
|
|
474
472
|
}
|
|
475
473
|
});
|
|
476
474
|
|
|
477
|
-
// src/screen/screens.
|
|
475
|
+
// src/screen/screens.js
|
|
478
476
|
import { useState as useState2, createElement as h3 } from "react";
|
|
479
477
|
import { render, useInput, Text as Text3 } from "ink";
|
|
480
478
|
function groupKeyBindings(bindings) {
|
|
@@ -552,7 +550,7 @@ async function showScreen(config2) {
|
|
|
552
550
|
let renderResult = null;
|
|
553
551
|
let initialized = false;
|
|
554
552
|
const Screen = () => {
|
|
555
|
-
const [
|
|
553
|
+
const [, setUpdateCounter] = useState2(0);
|
|
556
554
|
if (!initialized) {
|
|
557
555
|
const defaultBindings = [
|
|
558
556
|
{ key: "escape", caption: "go back", action: "back", protected: true, order: 1 },
|
|
@@ -671,7 +669,7 @@ async function showScreen(config2) {
|
|
|
671
669
|
}
|
|
672
670
|
}
|
|
673
671
|
if (matchedBinding && actions[matchedBinding.action]) {
|
|
674
|
-
|
|
672
|
+
actions[matchedBinding.action]({
|
|
675
673
|
input,
|
|
676
674
|
key,
|
|
677
675
|
binding: matchedBinding
|
|
@@ -801,8 +799,7 @@ async function showMultiColumnListWithPreviewScreen(config2) {
|
|
|
801
799
|
}
|
|
802
800
|
var showMenuScreen, showWordGridScreen;
|
|
803
801
|
var init_screens = __esm({
|
|
804
|
-
"src/screen/screens.
|
|
805
|
-
"use strict";
|
|
802
|
+
"src/screen/screens.js"() {
|
|
806
803
|
init_components();
|
|
807
804
|
init_list_components();
|
|
808
805
|
showMenuScreen = showListScreen;
|
|
@@ -810,9 +807,9 @@ var init_screens = __esm({
|
|
|
810
807
|
}
|
|
811
808
|
});
|
|
812
809
|
|
|
813
|
-
// src/screen/ui-elements.
|
|
810
|
+
// src/screen/ui-elements.js
|
|
814
811
|
import { createElement as h4 } from "react";
|
|
815
|
-
import { Box as
|
|
812
|
+
import { Box as Box3, Text as Text4 } from "ink";
|
|
816
813
|
function ListItem({
|
|
817
814
|
children,
|
|
818
815
|
isSelected = false,
|
|
@@ -822,7 +819,7 @@ function ListItem({
|
|
|
822
819
|
dimColor = false
|
|
823
820
|
}) {
|
|
824
821
|
return h4(
|
|
825
|
-
|
|
822
|
+
Box3,
|
|
826
823
|
{},
|
|
827
824
|
h4(Text4, {
|
|
828
825
|
color: isSelected ? backgroundColor || "green" : color,
|
|
@@ -837,10 +834,10 @@ function TextBlock({
|
|
|
837
834
|
color = "white",
|
|
838
835
|
dimmed = false,
|
|
839
836
|
bold = false,
|
|
840
|
-
maxWidth
|
|
837
|
+
maxWidth: _maxWidth
|
|
841
838
|
}) {
|
|
842
839
|
return h4(
|
|
843
|
-
|
|
840
|
+
Box3,
|
|
844
841
|
{},
|
|
845
842
|
h4(Text4, {
|
|
846
843
|
color,
|
|
@@ -851,7 +848,7 @@ function TextBlock({
|
|
|
851
848
|
}
|
|
852
849
|
function Divider({ character = "\u2500", width = 80 }) {
|
|
853
850
|
return h4(
|
|
854
|
-
|
|
851
|
+
Box3,
|
|
855
852
|
{ marginY: 1 },
|
|
856
853
|
h4(Text4, { dimColor: true }, character.repeat(width))
|
|
857
854
|
);
|
|
@@ -866,7 +863,7 @@ function GridCell({
|
|
|
866
863
|
align = "left"
|
|
867
864
|
}) {
|
|
868
865
|
return h4(
|
|
869
|
-
|
|
866
|
+
Box3,
|
|
870
867
|
{ width },
|
|
871
868
|
h4(Text4, {
|
|
872
869
|
color,
|
|
@@ -877,44 +874,42 @@ function GridCell({
|
|
|
877
874
|
}, children)
|
|
878
875
|
);
|
|
879
876
|
}
|
|
880
|
-
function InputField({ prompt, value, onChange, onSubmit }) {
|
|
877
|
+
function InputField({ prompt, value, onChange: _onChange, onSubmit: _onSubmit }) {
|
|
881
878
|
return h4(
|
|
882
|
-
|
|
879
|
+
Box3,
|
|
883
880
|
{ flexDirection: "column" },
|
|
884
881
|
h4(Text4, {}, prompt),
|
|
885
882
|
h4(
|
|
886
|
-
|
|
883
|
+
Box3,
|
|
887
884
|
{ marginTop: 1 },
|
|
888
885
|
h4(Text4, { color: "cyan" }, " > ", value, "_")
|
|
889
886
|
)
|
|
890
887
|
);
|
|
891
888
|
}
|
|
892
889
|
var init_ui_elements = __esm({
|
|
893
|
-
"src/screen/ui-elements.
|
|
894
|
-
"use strict";
|
|
890
|
+
"src/screen/ui-elements.js"() {
|
|
895
891
|
}
|
|
896
892
|
});
|
|
897
893
|
|
|
898
|
-
// src/screen/utils.
|
|
894
|
+
// src/screen/utils.js
|
|
899
895
|
function buildBreadcrumb(parts) {
|
|
900
896
|
if (parts.length === 0) return "";
|
|
901
897
|
if (parts.length === 1) return parts[0];
|
|
902
898
|
return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
|
|
903
899
|
}
|
|
904
|
-
function buildDetailBreadcrumb(
|
|
905
|
-
if (
|
|
906
|
-
return suffix ? `\u2190 ${suffix}` :
|
|
900
|
+
function buildDetailBreadcrumb(path5, suffix = "") {
|
|
901
|
+
if (path5.length <= 1) {
|
|
902
|
+
return suffix ? `\u2190 ${suffix}` : path5[0] || "";
|
|
907
903
|
}
|
|
908
|
-
const breadcrumb = buildBreadcrumb(
|
|
904
|
+
const breadcrumb = buildBreadcrumb(path5);
|
|
909
905
|
return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
|
|
910
906
|
}
|
|
911
907
|
var init_utils = __esm({
|
|
912
|
-
"src/screen/utils.
|
|
913
|
-
"use strict";
|
|
908
|
+
"src/screen/utils.js"() {
|
|
914
909
|
}
|
|
915
910
|
});
|
|
916
911
|
|
|
917
|
-
// src/screen/footer-builder.
|
|
912
|
+
// src/screen/footer-builder.js
|
|
918
913
|
function buildFooter(config2 = {}) {
|
|
919
914
|
const {
|
|
920
915
|
navigation = null,
|
|
@@ -965,8 +960,7 @@ function organizeFooterMessages(messages) {
|
|
|
965
960
|
}
|
|
966
961
|
var FooterPresets;
|
|
967
962
|
var init_footer_builder = __esm({
|
|
968
|
-
"src/screen/footer-builder.
|
|
969
|
-
"use strict";
|
|
963
|
+
"src/screen/footer-builder.js"() {
|
|
970
964
|
FooterPresets = {
|
|
971
965
|
/**
|
|
972
966
|
* Menu screen footer
|
|
@@ -1025,9 +1019,9 @@ var init_footer_builder = __esm({
|
|
|
1025
1019
|
}
|
|
1026
1020
|
});
|
|
1027
1021
|
|
|
1028
|
-
// src/screen/index.
|
|
1029
|
-
import
|
|
1030
|
-
import { Box as
|
|
1022
|
+
// src/screen/index.js
|
|
1023
|
+
import React2, { useState as useState3, useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useMemo, useCallback, memo, createElement as createElement2 } from "react";
|
|
1024
|
+
import { Box as Box4, Text as Text5, useInput as useInput2 } from "ink";
|
|
1031
1025
|
async function load() {
|
|
1032
1026
|
if (loadPromise) return loadPromise;
|
|
1033
1027
|
loadPromise = Promise.all([
|
|
@@ -1039,8 +1033,7 @@ async function load() {
|
|
|
1039
1033
|
}
|
|
1040
1034
|
var loadPromise;
|
|
1041
1035
|
var init_screen = __esm({
|
|
1042
|
-
"src/screen/index.
|
|
1043
|
-
"use strict";
|
|
1036
|
+
"src/screen/index.js"() {
|
|
1044
1037
|
init_screens();
|
|
1045
1038
|
init_list_components();
|
|
1046
1039
|
init_components();
|
|
@@ -1055,7 +1048,7 @@ var init_screen = __esm({
|
|
|
1055
1048
|
}
|
|
1056
1049
|
});
|
|
1057
1050
|
|
|
1058
|
-
// src/args/index.
|
|
1051
|
+
// src/args/index.js
|
|
1059
1052
|
import { readFileSync, existsSync } from "fs";
|
|
1060
1053
|
import { resolve, dirname, basename, extname, join, isAbsolute } from "path";
|
|
1061
1054
|
import { config } from "dotenv";
|
|
@@ -1542,10 +1535,10 @@ function getArgsInstance() {
|
|
|
1542
1535
|
return instance;
|
|
1543
1536
|
}
|
|
1544
1537
|
|
|
1545
|
-
// src/params/index.
|
|
1538
|
+
// src/params/index.js
|
|
1546
1539
|
import Joi from "joi";
|
|
1547
1540
|
|
|
1548
|
-
// src/errors.
|
|
1541
|
+
// src/errors.js
|
|
1549
1542
|
var FrameworkError = class extends Error {
|
|
1550
1543
|
constructor(message) {
|
|
1551
1544
|
super(message);
|
|
@@ -1565,7 +1558,7 @@ var FileDatabaseError = class extends FrameworkError {
|
|
|
1565
1558
|
}
|
|
1566
1559
|
};
|
|
1567
1560
|
|
|
1568
|
-
// src/params/custom-types.
|
|
1561
|
+
// src/params/custom-types.js
|
|
1569
1562
|
var joiEdateType = (value, helpers) => {
|
|
1570
1563
|
if (typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/.test(value)) {
|
|
1571
1564
|
const testDate = new Date(value);
|
|
@@ -1658,7 +1651,7 @@ function calculateTimeOffset(amount, unit, sign) {
|
|
|
1658
1651
|
}
|
|
1659
1652
|
return sign === "+" ? amount * multiplier : -amount * multiplier;
|
|
1660
1653
|
}
|
|
1661
|
-
var joiStringArrayType = (type) => (value,
|
|
1654
|
+
var joiStringArrayType = (type) => (value, _helpers) => {
|
|
1662
1655
|
if (value === void 0 || typeof value === "function") {
|
|
1663
1656
|
return [];
|
|
1664
1657
|
}
|
|
@@ -1684,7 +1677,7 @@ var joiStringArrayType = (type) => (value, helpers) => {
|
|
|
1684
1677
|
return arr;
|
|
1685
1678
|
};
|
|
1686
1679
|
|
|
1687
|
-
// src/params/index.
|
|
1680
|
+
// src/params/index.js
|
|
1688
1681
|
var Params = class _Params {
|
|
1689
1682
|
context;
|
|
1690
1683
|
// Partial context during initialization
|
|
@@ -1876,7 +1869,7 @@ var Params = class _Params {
|
|
|
1876
1869
|
throw new ParamError(`default value "${defValObj.value}" type mismatch`);
|
|
1877
1870
|
}
|
|
1878
1871
|
type = type.default(defValObj.value);
|
|
1879
|
-
} else if (str.match(
|
|
1872
|
+
} else if (str.match(/\s*required\s*/)) {
|
|
1880
1873
|
type = type.required();
|
|
1881
1874
|
} else {
|
|
1882
1875
|
type = type.optional();
|
|
@@ -1944,7 +1937,7 @@ var Params = class _Params {
|
|
|
1944
1937
|
definition = val;
|
|
1945
1938
|
val = val.value;
|
|
1946
1939
|
}
|
|
1947
|
-
|
|
1940
|
+
this.assignDefinition(key, definition);
|
|
1948
1941
|
if (!this.runAllRegisteredSetters(key, val)) {
|
|
1949
1942
|
this.params[key] = val;
|
|
1950
1943
|
}
|
|
@@ -1952,6 +1945,8 @@ var Params = class _Params {
|
|
|
1952
1945
|
/**
|
|
1953
1946
|
* Get all parameters from definitions (main script).
|
|
1954
1947
|
* Same as getAllForModule("script", defs). Processes left-to-right for cross-parameter references.
|
|
1948
|
+
* Libraries should use {@link getAllForModule} with an explicit module name (or {@link runWithModule}
|
|
1949
|
+
* around {@link get}) so --showUsedParams groups usage correctly.
|
|
1955
1950
|
*/
|
|
1956
1951
|
getAll(defs) {
|
|
1957
1952
|
return this.getAllForModule("script", defs);
|
|
@@ -1988,6 +1983,19 @@ var Params = class _Params {
|
|
|
1988
1983
|
this._currentModule = prev;
|
|
1989
1984
|
}
|
|
1990
1985
|
}
|
|
1986
|
+
/**
|
|
1987
|
+
* Run a callback with {@link _currentModule} set so single {@link get} calls are tracked
|
|
1988
|
+
* under the same module (for --showUsedParams / getFiguredByModule).
|
|
1989
|
+
*/
|
|
1990
|
+
runWithModule(moduleName, fn) {
|
|
1991
|
+
const prev = this._currentModule;
|
|
1992
|
+
this._currentModule = moduleName;
|
|
1993
|
+
try {
|
|
1994
|
+
return fn();
|
|
1995
|
+
} finally {
|
|
1996
|
+
this._currentModule = prev;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1991
1999
|
/**
|
|
1992
2000
|
* Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
|
|
1993
2001
|
*/
|
|
@@ -2001,9 +2009,9 @@ var Params = class _Params {
|
|
|
2001
2009
|
if (!parenMatch) continue;
|
|
2002
2010
|
const parts = parenMatch[1].split(":");
|
|
2003
2011
|
if (parts.length < 3) continue;
|
|
2004
|
-
const
|
|
2005
|
-
if (!
|
|
2006
|
-
const srcMatch =
|
|
2012
|
+
const path5 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
|
|
2013
|
+
if (!path5 || path5.includes(paramsIndexPath)) continue;
|
|
2014
|
+
const srcMatch = path5.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
|
|
2007
2015
|
if (srcMatch) return srcMatch[1];
|
|
2008
2016
|
}
|
|
2009
2017
|
return "script";
|
|
@@ -2048,14 +2056,14 @@ var Params = class _Params {
|
|
|
2048
2056
|
}
|
|
2049
2057
|
};
|
|
2050
2058
|
|
|
2051
|
-
// src/
|
|
2059
|
+
// src/index.js
|
|
2052
2060
|
init_screen();
|
|
2053
2061
|
|
|
2054
|
-
// src/filedatabase/index.
|
|
2062
|
+
// src/filedatabase/index.js
|
|
2055
2063
|
import fs3 from "fs";
|
|
2056
2064
|
import path3 from "path";
|
|
2057
2065
|
|
|
2058
|
-
// src/utils/os-utils.
|
|
2066
|
+
// src/utils/os-utils.js
|
|
2059
2067
|
import fs from "fs";
|
|
2060
2068
|
import path from "path";
|
|
2061
2069
|
import { execSync } from "child_process";
|
|
@@ -2084,7 +2092,7 @@ function getFreeDiskSpace(targetPath) {
|
|
|
2084
2092
|
}
|
|
2085
2093
|
}
|
|
2086
2094
|
|
|
2087
|
-
// src/utils/fs-utils.
|
|
2095
|
+
// src/utils/fs-utils.js
|
|
2088
2096
|
import fs2 from "fs";
|
|
2089
2097
|
import path2 from "path";
|
|
2090
2098
|
async function ensurePath(...pathParts) {
|
|
@@ -2108,7 +2116,7 @@ function getFileExtension(dataType) {
|
|
|
2108
2116
|
}
|
|
2109
2117
|
}
|
|
2110
2118
|
|
|
2111
|
-
// src/utils/format-utils.
|
|
2119
|
+
// src/utils/format-utils.js
|
|
2112
2120
|
function bytesToHumanReadable(bytes) {
|
|
2113
2121
|
if (bytes === 0) return "0 B";
|
|
2114
2122
|
const k = 1024;
|
|
@@ -2117,7 +2125,7 @@ function bytesToHumanReadable(bytes) {
|
|
|
2117
2125
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
2118
2126
|
}
|
|
2119
2127
|
|
|
2120
|
-
// src/utils/date-utils.
|
|
2128
|
+
// src/utils/date-utils.js
|
|
2121
2129
|
function isTimestampFolder(folderName) {
|
|
2122
2130
|
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
|
|
2123
2131
|
if (!isoRegex.test(folderName)) {
|
|
@@ -2127,7 +2135,7 @@ function isTimestampFolder(folderName) {
|
|
|
2127
2135
|
return !isNaN(date.getTime()) && date.getTime() > 0;
|
|
2128
2136
|
}
|
|
2129
2137
|
|
|
2130
|
-
// src/filedatabase/serializers.
|
|
2138
|
+
// src/filedatabase/serializers.js
|
|
2131
2139
|
function detectDataType(data) {
|
|
2132
2140
|
if (Array.isArray(data)) {
|
|
2133
2141
|
return "json-array";
|
|
@@ -2159,7 +2167,7 @@ function deserializeData(rawData, dataType) {
|
|
|
2159
2167
|
}
|
|
2160
2168
|
}
|
|
2161
2169
|
|
|
2162
|
-
// src/filedatabase/synopsis-functions.
|
|
2170
|
+
// src/filedatabase/synopsis-functions.js
|
|
2163
2171
|
function defaultFileSynopsisFunction(fileEntry, data) {
|
|
2164
2172
|
if (!Array.isArray(data) || data.length === 0) {
|
|
2165
2173
|
return { ...fileEntry };
|
|
@@ -2227,7 +2235,7 @@ function defaultVersionSynopsisFunction(metadata) {
|
|
|
2227
2235
|
return result;
|
|
2228
2236
|
}
|
|
2229
2237
|
|
|
2230
|
-
// src/filedatabase/index.
|
|
2238
|
+
// src/filedatabase/index.js
|
|
2231
2239
|
var FileDatabase = class _FileDatabase {
|
|
2232
2240
|
basePath;
|
|
2233
2241
|
namespace;
|
|
@@ -2313,7 +2321,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2313
2321
|
if (errors.length) {
|
|
2314
2322
|
throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
|
|
2315
2323
|
}
|
|
2316
|
-
|
|
2324
|
+
const parts = [this.basePath, this.namespace];
|
|
2317
2325
|
if (this.tableName) {
|
|
2318
2326
|
parts.push(...this.tableName.split("/"));
|
|
2319
2327
|
}
|
|
@@ -2806,14 +2814,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
2806
2814
|
* Prepare the instance for read or write operations
|
|
2807
2815
|
* This discovers state and sets up internal members based on mode and current data
|
|
2808
2816
|
*/
|
|
2809
|
-
async prepare(
|
|
2817
|
+
async prepare(options) {
|
|
2818
|
+
const { write, read, version, deferInitialVersion } = options;
|
|
2810
2819
|
if (write) {
|
|
2811
2820
|
if (this.versioned) {
|
|
2812
2821
|
if (this.currentVersion === null) {
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2822
|
+
if (!deferInitialVersion) {
|
|
2823
|
+
await this.makeNewVersion();
|
|
2824
|
+
this.metadata = this.getDefaultMetadata();
|
|
2825
|
+
this.metadata.version = this.currentVersion;
|
|
2826
|
+
this.makeNewFile();
|
|
2827
|
+
}
|
|
2817
2828
|
} else {
|
|
2818
2829
|
if (!this.metadata.files.length) {
|
|
2819
2830
|
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
@@ -2923,7 +2934,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2923
2934
|
if (options.forceNewVersion && !this.versioned) {
|
|
2924
2935
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
2925
2936
|
}
|
|
2926
|
-
await this.prepare({ write: true });
|
|
2937
|
+
await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
|
|
2927
2938
|
const incomingDataType = detectDataType(data);
|
|
2928
2939
|
this.metadata.dataType = incomingDataType;
|
|
2929
2940
|
if (options.forceNewVersion) {
|
|
@@ -3238,22 +3249,57 @@ function listSources(basePath) {
|
|
|
3238
3249
|
}
|
|
3239
3250
|
}
|
|
3240
3251
|
|
|
3241
|
-
// src/db/index.
|
|
3252
|
+
// src/db/index.js
|
|
3242
3253
|
import knex from "knex";
|
|
3254
|
+
var KNEX_DEFAULTS = {
|
|
3255
|
+
testConnection: true,
|
|
3256
|
+
pool: { min: 2, max: 10 },
|
|
3257
|
+
acquireConnectionTimeout: 1e4,
|
|
3258
|
+
ssl: { rejectUnauthorized: false }
|
|
3259
|
+
};
|
|
3243
3260
|
var Db = class {
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3261
|
+
static async init(context, options = {}) {
|
|
3262
|
+
const defs = {
|
|
3263
|
+
dbName: "string",
|
|
3264
|
+
dbConnectionString: "string",
|
|
3265
|
+
dbProfile: "boolean default false"
|
|
3266
|
+
};
|
|
3267
|
+
const discovered = context?.params?.getAllForModule?.("db", defs) ?? {};
|
|
3268
|
+
const merged = { ...discovered, ...options };
|
|
3269
|
+
let { dbName, dbConnectionString } = merged;
|
|
3270
|
+
const { dbProfile } = merged;
|
|
3271
|
+
if (!dbName && !dbConnectionString) {
|
|
3272
|
+
dbName = "local";
|
|
3273
|
+
}
|
|
3274
|
+
if (dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
|
|
3275
|
+
dbConnectionString = dbName;
|
|
3276
|
+
dbName = void 0;
|
|
3277
|
+
}
|
|
3278
|
+
if (dbName && !dbConnectionString) {
|
|
3279
|
+
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
3280
|
+
dbConnectionString = await context.params.get(paramName, "string");
|
|
3281
|
+
if (!dbConnectionString) {
|
|
3282
|
+
throw new ParamError(
|
|
3283
|
+
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
3284
|
+
);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
const config2 = {
|
|
3288
|
+
...KNEX_DEFAULTS,
|
|
3289
|
+
connectionString: dbConnectionString,
|
|
3290
|
+
name: dbName || merged.name || "default",
|
|
3291
|
+
profile: !!dbProfile,
|
|
3292
|
+
logger: context.logger
|
|
3293
|
+
};
|
|
3294
|
+
return dbConnect(context, config2);
|
|
3295
|
+
}
|
|
3253
3296
|
constructor(config2) {
|
|
3254
|
-
if (!config2.connectionString) {
|
|
3297
|
+
if (!config2 || !config2.connectionString) {
|
|
3255
3298
|
throw new ParamError("Db: connectionString is required");
|
|
3256
3299
|
}
|
|
3300
|
+
this.knexInstance = null;
|
|
3301
|
+
this.isConnected = false;
|
|
3302
|
+
this.queriesLog = [];
|
|
3257
3303
|
this.config = {
|
|
3258
3304
|
testConnection: true,
|
|
3259
3305
|
profile: false,
|
|
@@ -3266,25 +3312,23 @@ var Db = class {
|
|
|
3266
3312
|
};
|
|
3267
3313
|
this.logger = this.config.logger;
|
|
3268
3314
|
const instance2 = this;
|
|
3269
|
-
const callableWrapper = function(
|
|
3315
|
+
const callableWrapper = function() {
|
|
3270
3316
|
throw new Error("This should never be called directly");
|
|
3271
3317
|
};
|
|
3272
3318
|
callableWrapper._instance = instance2;
|
|
3273
3319
|
return new Proxy(callableWrapper, {
|
|
3274
|
-
|
|
3275
|
-
apply: (target, thisArg, argumentsList) => {
|
|
3320
|
+
apply: (target, _thisArg, argumentsList) => {
|
|
3276
3321
|
const inst = target._instance;
|
|
3277
3322
|
if (!inst.knexInstance) {
|
|
3278
3323
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
3279
3324
|
}
|
|
3280
3325
|
return inst.knexInstance(...argumentsList);
|
|
3281
3326
|
},
|
|
3282
|
-
// Intercept property access: db.schema, db.raw, etc.
|
|
3283
3327
|
get: (target, prop) => {
|
|
3284
3328
|
if (prop === "_instance") {
|
|
3285
3329
|
return target._instance;
|
|
3286
3330
|
}
|
|
3287
|
-
const
|
|
3331
|
+
const inst = target._instance;
|
|
3288
3332
|
const ownMethods = [
|
|
3289
3333
|
"connect",
|
|
3290
3334
|
"disconnect",
|
|
@@ -3297,26 +3341,26 @@ var Db = class {
|
|
|
3297
3341
|
"detectClient",
|
|
3298
3342
|
"attachProfiler"
|
|
3299
3343
|
];
|
|
3300
|
-
if (prop in
|
|
3301
|
-
const value =
|
|
3344
|
+
if (prop in inst) {
|
|
3345
|
+
const value = inst[prop];
|
|
3302
3346
|
if (typeof value === "function" && ownMethods.includes(prop)) {
|
|
3303
|
-
return value.bind(
|
|
3347
|
+
return value.bind(inst);
|
|
3304
3348
|
}
|
|
3305
3349
|
if (typeof value !== "function") {
|
|
3306
3350
|
return value;
|
|
3307
3351
|
}
|
|
3308
3352
|
}
|
|
3309
|
-
if (
|
|
3310
|
-
const knexProp =
|
|
3353
|
+
if (inst.knexInstance) {
|
|
3354
|
+
const knexProp = inst.knexInstance[prop];
|
|
3311
3355
|
if (typeof knexProp === "function") {
|
|
3312
|
-
return knexProp.bind(
|
|
3356
|
+
return knexProp.bind(inst.knexInstance);
|
|
3313
3357
|
}
|
|
3314
3358
|
return knexProp;
|
|
3315
3359
|
}
|
|
3316
|
-
if (prop in
|
|
3317
|
-
const method =
|
|
3360
|
+
if (prop in inst) {
|
|
3361
|
+
const method = inst[prop];
|
|
3318
3362
|
if (typeof method === "function") {
|
|
3319
|
-
return method.bind(
|
|
3363
|
+
return method.bind(inst);
|
|
3320
3364
|
}
|
|
3321
3365
|
return method;
|
|
3322
3366
|
}
|
|
@@ -3324,9 +3368,6 @@ var Db = class {
|
|
|
3324
3368
|
}
|
|
3325
3369
|
});
|
|
3326
3370
|
}
|
|
3327
|
-
/**
|
|
3328
|
-
* Detect database client type from connection string
|
|
3329
|
-
*/
|
|
3330
3371
|
detectClient(connectionString) {
|
|
3331
3372
|
if (connectionString.match(/^postgresql/)) {
|
|
3332
3373
|
return "pg";
|
|
@@ -3336,9 +3377,6 @@ var Db = class {
|
|
|
3336
3377
|
}
|
|
3337
3378
|
return null;
|
|
3338
3379
|
}
|
|
3339
|
-
/**
|
|
3340
|
-
* Connect to the database
|
|
3341
|
-
*/
|
|
3342
3380
|
async connect() {
|
|
3343
3381
|
if (this.isConnected && this.knexInstance) {
|
|
3344
3382
|
this.logger.warn?.("[Db] Already connected");
|
|
@@ -3347,14 +3385,13 @@ var Db = class {
|
|
|
3347
3385
|
const client = this.detectClient(this.config.connectionString);
|
|
3348
3386
|
if (!client) {
|
|
3349
3387
|
throw new ParamError(
|
|
3350
|
-
|
|
3388
|
+
"Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://"
|
|
3351
3389
|
);
|
|
3352
3390
|
}
|
|
3353
3391
|
try {
|
|
3354
3392
|
const connectionConfig = {
|
|
3355
3393
|
connectionString: this.config.connectionString,
|
|
3356
3394
|
family: 4
|
|
3357
|
-
// Force IPv4 only (disable IPv6)
|
|
3358
3395
|
};
|
|
3359
3396
|
this.knexInstance = knex({
|
|
3360
3397
|
client,
|
|
@@ -3370,7 +3407,9 @@ var Db = class {
|
|
|
3370
3407
|
await this.testConnection();
|
|
3371
3408
|
}
|
|
3372
3409
|
this.isConnected = true;
|
|
3373
|
-
this.logger.debug?.(
|
|
3410
|
+
this.logger.debug?.(
|
|
3411
|
+
`[Db] Connected to database "${this.config.name || this.config.connectionString}"`
|
|
3412
|
+
);
|
|
3374
3413
|
} catch (error) {
|
|
3375
3414
|
if (error instanceof ParamError) {
|
|
3376
3415
|
throw error;
|
|
@@ -3379,9 +3418,6 @@ var Db = class {
|
|
|
3379
3418
|
throw new ParamError(`Db: Connection failed - ${errorMsg}`);
|
|
3380
3419
|
}
|
|
3381
3420
|
}
|
|
3382
|
-
/**
|
|
3383
|
-
* Disconnect from the database
|
|
3384
|
-
*/
|
|
3385
3421
|
async disconnect() {
|
|
3386
3422
|
if (!this.knexInstance) {
|
|
3387
3423
|
return;
|
|
@@ -3391,16 +3427,15 @@ var Db = class {
|
|
|
3391
3427
|
this.knexInstance = null;
|
|
3392
3428
|
this.isConnected = false;
|
|
3393
3429
|
this.queriesLog = [];
|
|
3394
|
-
this.logger.debug?.(
|
|
3430
|
+
this.logger.debug?.(
|
|
3431
|
+
`[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`
|
|
3432
|
+
);
|
|
3395
3433
|
} catch (error) {
|
|
3396
3434
|
const errorMsg = this.getErrorMessage(error);
|
|
3397
3435
|
this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
|
|
3398
3436
|
throw error;
|
|
3399
3437
|
}
|
|
3400
3438
|
}
|
|
3401
|
-
/**
|
|
3402
|
-
* Extract error message from various error types
|
|
3403
|
-
*/
|
|
3404
3439
|
getErrorMessage(error) {
|
|
3405
3440
|
if (error instanceof AggregateError) {
|
|
3406
3441
|
const errors = error.errors || [];
|
|
@@ -3425,9 +3460,11 @@ var Db = class {
|
|
|
3425
3460
|
return `${code} (tried: ${addresses.join(", ")})`;
|
|
3426
3461
|
}
|
|
3427
3462
|
}
|
|
3428
|
-
const uniqueMessages = [
|
|
3429
|
-
|
|
3430
|
-
|
|
3463
|
+
const uniqueMessages = [
|
|
3464
|
+
...new Set(
|
|
3465
|
+
errors.map((e) => e instanceof Error ? e.message : String(e))
|
|
3466
|
+
)
|
|
3467
|
+
];
|
|
3431
3468
|
if (uniqueMessages.length === 1) {
|
|
3432
3469
|
return uniqueMessages[0];
|
|
3433
3470
|
}
|
|
@@ -3436,28 +3473,25 @@ var Db = class {
|
|
|
3436
3473
|
return error.message || "Multiple errors occurred";
|
|
3437
3474
|
}
|
|
3438
3475
|
if (error instanceof Error) {
|
|
3439
|
-
const
|
|
3440
|
-
if (
|
|
3441
|
-
return `${
|
|
3476
|
+
const code = error.code;
|
|
3477
|
+
if (code) {
|
|
3478
|
+
return `${code}: ${error.message || String(error)}`;
|
|
3442
3479
|
}
|
|
3443
3480
|
return error.message || String(error);
|
|
3444
3481
|
}
|
|
3445
3482
|
if (typeof error === "string") {
|
|
3446
3483
|
return error;
|
|
3447
3484
|
}
|
|
3448
|
-
if (error
|
|
3485
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
3449
3486
|
const msg = String(error.message);
|
|
3450
|
-
const
|
|
3451
|
-
if (
|
|
3452
|
-
return `${
|
|
3487
|
+
const code = error.code;
|
|
3488
|
+
if (code) {
|
|
3489
|
+
return `${code}: ${msg}`;
|
|
3453
3490
|
}
|
|
3454
3491
|
return msg;
|
|
3455
3492
|
}
|
|
3456
3493
|
return String(error) || "Unknown error";
|
|
3457
3494
|
}
|
|
3458
|
-
/**
|
|
3459
|
-
* Test database connection
|
|
3460
|
-
*/
|
|
3461
3495
|
async testConnection() {
|
|
3462
3496
|
if (!this.knexInstance) {
|
|
3463
3497
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -3473,9 +3507,6 @@ var Db = class {
|
|
|
3473
3507
|
throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
|
|
3474
3508
|
}
|
|
3475
3509
|
}
|
|
3476
|
-
/**
|
|
3477
|
-
* Attach query profiler to log all queries
|
|
3478
|
-
*/
|
|
3479
3510
|
attachProfiler() {
|
|
3480
3511
|
if (!this.knexInstance) {
|
|
3481
3512
|
return;
|
|
@@ -3485,7 +3516,7 @@ var Db = class {
|
|
|
3485
3516
|
this.knexInstance.on("query", (query) => {
|
|
3486
3517
|
query.__startTime = process.hrtime();
|
|
3487
3518
|
});
|
|
3488
|
-
this.knexInstance.on("query-response", (
|
|
3519
|
+
this.knexInstance.on("query-response", (_response, query) => {
|
|
3489
3520
|
const [seconds, nanoseconds] = process.hrtime(query.__startTime);
|
|
3490
3521
|
const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
|
|
3491
3522
|
const logEntry = {
|
|
@@ -3500,15 +3531,9 @@ var Db = class {
|
|
|
3500
3531
|
this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
|
|
3501
3532
|
});
|
|
3502
3533
|
}
|
|
3503
|
-
/**
|
|
3504
|
-
* Get query log (only available if profiling is enabled)
|
|
3505
|
-
*/
|
|
3506
3534
|
getQueryLog() {
|
|
3507
3535
|
return [...this.queriesLog];
|
|
3508
3536
|
}
|
|
3509
|
-
/**
|
|
3510
|
-
* Check if a table exists
|
|
3511
|
-
*/
|
|
3512
3537
|
async tableExists(tableName) {
|
|
3513
3538
|
if (!this.knexInstance) {
|
|
3514
3539
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -3520,65 +3545,28 @@ var Db = class {
|
|
|
3520
3545
|
throw error;
|
|
3521
3546
|
}
|
|
3522
3547
|
}
|
|
3523
|
-
/**
|
|
3524
|
-
* Get the underlying Knex instance (for advanced usage)
|
|
3525
|
-
*/
|
|
3526
3548
|
getKnex() {
|
|
3527
3549
|
if (!this.knexInstance) {
|
|
3528
3550
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
3529
3551
|
}
|
|
3530
3552
|
return this.knexInstance;
|
|
3531
3553
|
}
|
|
3532
|
-
/**
|
|
3533
|
-
* Get connection status
|
|
3534
|
-
*/
|
|
3535
3554
|
isConnectedToDb() {
|
|
3536
3555
|
return this.isConnected && this.knexInstance !== null;
|
|
3537
3556
|
}
|
|
3538
|
-
/**
|
|
3539
|
-
* Initialize Db with context (connects and registers disconnect cleanup).
|
|
3540
|
-
* Params are read via getAllForModule("db", defs). Same as dbInit(context, dbNameOrConnectionString).
|
|
3541
|
-
*/
|
|
3542
|
-
static async init(context, dbNameOrConnectionString) {
|
|
3543
|
-
return dbFindAndConnect(context, dbNameOrConnectionString);
|
|
3544
|
-
}
|
|
3545
3557
|
};
|
|
3546
3558
|
function capitalizeFirstLetter(str) {
|
|
3547
3559
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
3548
3560
|
}
|
|
3549
|
-
async function dbConnect(context,
|
|
3550
|
-
const defs = {
|
|
3551
|
-
testDbConnection: "boolean default true",
|
|
3552
|
-
name: "string",
|
|
3553
|
-
poolMin: "number default 2",
|
|
3554
|
-
poolMax: "number default 10",
|
|
3555
|
-
acquireConnectionTimeout: "number default 10000",
|
|
3556
|
-
sslRejectUnauthorized: "boolean default false"
|
|
3557
|
-
};
|
|
3558
|
-
const paramsConfig = context.params.getAllForModule(defs);
|
|
3559
|
-
const config2 = {
|
|
3560
|
-
connectionString,
|
|
3561
|
-
name: paramsConfig.name || name || "default",
|
|
3562
|
-
testConnection: paramsConfig.testDbConnection,
|
|
3563
|
-
profile: dbProfile ?? false,
|
|
3564
|
-
pool: {
|
|
3565
|
-
min: paramsConfig.poolMin,
|
|
3566
|
-
max: paramsConfig.poolMax
|
|
3567
|
-
},
|
|
3568
|
-
acquireConnectionTimeout: paramsConfig.acquireConnectionTimeout,
|
|
3569
|
-
ssl: {
|
|
3570
|
-
rejectUnauthorized: paramsConfig.sslRejectUnauthorized
|
|
3571
|
-
},
|
|
3572
|
-
logger: context.logger
|
|
3573
|
-
};
|
|
3561
|
+
async function dbConnect(context, config2) {
|
|
3574
3562
|
try {
|
|
3575
3563
|
const db = new Db(config2);
|
|
3576
3564
|
context.registerCleanup(async () => {
|
|
3577
3565
|
await db.disconnect();
|
|
3578
|
-
context.logger.debug(`[Db] instance "${name
|
|
3566
|
+
context.logger.debug?.(`[Db] instance "${config2.name}" disconnected`);
|
|
3579
3567
|
});
|
|
3580
3568
|
await db.connect();
|
|
3581
|
-
context.logger.debug(`[Db] instance "${name
|
|
3569
|
+
context.logger.debug?.(`[Db] instance "${config2.name}" initialized`);
|
|
3582
3570
|
return db;
|
|
3583
3571
|
} catch (error) {
|
|
3584
3572
|
if (error instanceof ParamError) {
|
|
@@ -3588,52 +3576,266 @@ async function dbConnect(context, connectionString, name, dbProfile) {
|
|
|
3588
3576
|
throw new ParamError(`[Db] connect error: ${errorMsg}`);
|
|
3589
3577
|
}
|
|
3590
3578
|
}
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3579
|
+
|
|
3580
|
+
// src/s3/index.js
|
|
3581
|
+
import {
|
|
3582
|
+
S3Client,
|
|
3583
|
+
HeadBucketCommand,
|
|
3584
|
+
HeadObjectCommand,
|
|
3585
|
+
GetObjectCommand,
|
|
3586
|
+
PutObjectCommand,
|
|
3587
|
+
DeleteObjectCommand,
|
|
3588
|
+
CopyObjectCommand,
|
|
3589
|
+
ListObjectsV2Command,
|
|
3590
|
+
PutObjectTaggingCommand,
|
|
3591
|
+
GetObjectTaggingCommand
|
|
3592
|
+
} from "@aws-sdk/client-s3";
|
|
3593
|
+
var DEFAULT_PROFILE = "local";
|
|
3594
|
+
function capitalize(str) {
|
|
3595
|
+
if (!str) return str;
|
|
3596
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
3597
|
+
}
|
|
3598
|
+
var S3 = class _S3 {
|
|
3599
|
+
/**
|
|
3600
|
+
* Build an S3 instance. Reads bucket profile from --bucket (default "local"),
|
|
3601
|
+
* then resolves per-profile params, builds the SDK client, and returns the
|
|
3602
|
+
* instance. Optionally pings the bucket once to verify reachability.
|
|
3603
|
+
*/
|
|
3604
|
+
static async init(context, options = {}) {
|
|
3605
|
+
const profileDef = { bucket: "string" };
|
|
3606
|
+
const discovered = context?.params?.getAllForModule?.("s3", profileDef) ?? {};
|
|
3607
|
+
const profile = options.bucket ?? discovered.bucket ?? DEFAULT_PROFILE;
|
|
3608
|
+
const cap = capitalize(profile);
|
|
3609
|
+
const config2 = {
|
|
3610
|
+
profile,
|
|
3611
|
+
bucketName: options.bucketName ?? await context.params.get(`s3Bucket${cap}`, "string"),
|
|
3612
|
+
region: options.region ?? await context.params.get(`s3Region${cap}`, "string default us-east-1"),
|
|
3613
|
+
endpoint: options.endpoint ?? await context.params.get(`s3Endpoint${cap}`, "string"),
|
|
3614
|
+
forcePathStyle: options.forcePathStyle ?? await context.params.get(`s3ForcePathStyle${cap}`, "boolean default false"),
|
|
3615
|
+
accessKeyId: options.accessKeyId ?? await context.params.get(`s3AccessKeyId${cap}`, "string"),
|
|
3616
|
+
secretAccessKey: options.secretAccessKey ?? await context.params.get(`s3SecretAccessKey${cap}`, "string")
|
|
3607
3617
|
};
|
|
3608
|
-
|
|
3609
|
-
dbName = paramsConfig.dbName;
|
|
3610
|
-
dbConnectionString = paramsConfig.dbConnectionString;
|
|
3611
|
-
dbProfile = paramsConfig.dbProfile;
|
|
3612
|
-
}
|
|
3613
|
-
if (!dbName && !dbConnectionString) {
|
|
3614
|
-
throw new ParamError("Db: either dbName or dbConnectionString must be specified");
|
|
3615
|
-
}
|
|
3616
|
-
if (dbName) {
|
|
3617
|
-
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
3618
|
-
dbConnectionString = await context.params.get(paramName, "string");
|
|
3619
|
-
if (!dbConnectionString) {
|
|
3618
|
+
if (!config2.bucketName) {
|
|
3620
3619
|
throw new ParamError(
|
|
3621
|
-
`
|
|
3620
|
+
`S3: bucket name not configured for profile "${profile}" (set s3Bucket${cap} or S3_BUCKET_${profile.toUpperCase()})`
|
|
3622
3621
|
);
|
|
3623
3622
|
}
|
|
3623
|
+
const s3 = new _S3(context, config2);
|
|
3624
|
+
if (options.testBucket !== false) {
|
|
3625
|
+
try {
|
|
3626
|
+
await s3.bucketExists();
|
|
3627
|
+
context.logger?.debug?.(
|
|
3628
|
+
`[S3] profile="${profile}" bucket="${config2.bucketName}" reachable`
|
|
3629
|
+
);
|
|
3630
|
+
} catch (err) {
|
|
3631
|
+
context.logger?.warn?.(
|
|
3632
|
+
`[S3] profile="${profile}" bucket="${config2.bucketName}" reachability test failed: ${err?.message ?? err}`
|
|
3633
|
+
);
|
|
3634
|
+
}
|
|
3635
|
+
}
|
|
3636
|
+
return s3;
|
|
3637
|
+
}
|
|
3638
|
+
constructor(context, config2) {
|
|
3639
|
+
this.logger = context?.logger ?? console;
|
|
3640
|
+
this.profile = config2.profile;
|
|
3641
|
+
this.bucketName = config2.bucketName;
|
|
3642
|
+
this.region = config2.region;
|
|
3643
|
+
this.endpoint = config2.endpoint || null;
|
|
3644
|
+
const clientConfig = { region: config2.region };
|
|
3645
|
+
if (config2.endpoint) clientConfig.endpoint = config2.endpoint;
|
|
3646
|
+
if (config2.forcePathStyle) clientConfig.forcePathStyle = true;
|
|
3647
|
+
if (config2.accessKeyId && config2.secretAccessKey) {
|
|
3648
|
+
clientConfig.credentials = {
|
|
3649
|
+
accessKeyId: config2.accessKeyId,
|
|
3650
|
+
secretAccessKey: config2.secretAccessKey
|
|
3651
|
+
};
|
|
3652
|
+
}
|
|
3653
|
+
this.client = new S3Client(clientConfig);
|
|
3624
3654
|
}
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3655
|
+
// ── info ────────────────────────────────────────────────────────────────
|
|
3656
|
+
getBucketName() {
|
|
3657
|
+
return this.bucketName;
|
|
3658
|
+
}
|
|
3659
|
+
getProfile() {
|
|
3660
|
+
return this.profile;
|
|
3661
|
+
}
|
|
3662
|
+
getRegion() {
|
|
3663
|
+
return this.region;
|
|
3664
|
+
}
|
|
3665
|
+
getEndpoint() {
|
|
3666
|
+
return this.endpoint;
|
|
3667
|
+
}
|
|
3668
|
+
// ── reachability ────────────────────────────────────────────────────────
|
|
3669
|
+
async bucketExists() {
|
|
3670
|
+
await this.client.send(new HeadBucketCommand({ Bucket: this.bucketName }));
|
|
3671
|
+
return true;
|
|
3672
|
+
}
|
|
3673
|
+
// ── HEAD / GET ──────────────────────────────────────────────────────────
|
|
3674
|
+
/** Returns null on 404; never throws for "missing". Other errors throw. */
|
|
3675
|
+
async headObject(key) {
|
|
3676
|
+
try {
|
|
3677
|
+
const out = await this.client.send(new HeadObjectCommand({
|
|
3678
|
+
Bucket: this.bucketName,
|
|
3679
|
+
Key: key
|
|
3680
|
+
}));
|
|
3681
|
+
return {
|
|
3682
|
+
etag: out.ETag,
|
|
3683
|
+
size: out.ContentLength,
|
|
3684
|
+
contentType: out.ContentType,
|
|
3685
|
+
lastModified: out.LastModified,
|
|
3686
|
+
metadata: out.Metadata,
|
|
3687
|
+
storageClass: out.StorageClass
|
|
3688
|
+
};
|
|
3689
|
+
} catch (err) {
|
|
3690
|
+
if (this._isNotFound(err)) return null;
|
|
3691
|
+
throw err;
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
/** Returns { body: Readable, contentType, contentLength, etag, ... } or null on 404. */
|
|
3695
|
+
async getObject(key) {
|
|
3696
|
+
try {
|
|
3697
|
+
const out = await this.client.send(new GetObjectCommand({
|
|
3698
|
+
Bucket: this.bucketName,
|
|
3699
|
+
Key: key
|
|
3700
|
+
}));
|
|
3701
|
+
return {
|
|
3702
|
+
body: out.Body,
|
|
3703
|
+
contentType: out.ContentType,
|
|
3704
|
+
contentLength: out.ContentLength,
|
|
3705
|
+
etag: out.ETag,
|
|
3706
|
+
lastModified: out.LastModified,
|
|
3707
|
+
metadata: out.Metadata
|
|
3708
|
+
};
|
|
3709
|
+
} catch (err) {
|
|
3710
|
+
if (this._isNotFound(err)) return null;
|
|
3711
|
+
throw err;
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
/** Buffers the whole object. Use only for small objects (manifests, JSON). */
|
|
3715
|
+
async getObjectBytes(key) {
|
|
3716
|
+
const obj = await this.getObject(key);
|
|
3717
|
+
if (!obj) return null;
|
|
3718
|
+
const chunks = [];
|
|
3719
|
+
for await (const chunk of obj.body) chunks.push(chunk);
|
|
3720
|
+
return { ...obj, body: Buffer.concat(chunks) };
|
|
3721
|
+
}
|
|
3722
|
+
/** Convenience for JSON manifests. Returns parsed object or null on 404. */
|
|
3723
|
+
async getJson(key) {
|
|
3724
|
+
const obj = await this.getObjectBytes(key);
|
|
3725
|
+
if (!obj) return null;
|
|
3726
|
+
return JSON.parse(obj.body.toString("utf8"));
|
|
3727
|
+
}
|
|
3728
|
+
// ── PUT ─────────────────────────────────────────────────────────────────
|
|
3729
|
+
async putObject({ key, body, contentType, contentLength, tags, metadata }) {
|
|
3730
|
+
const cmd = new PutObjectCommand({
|
|
3731
|
+
Bucket: this.bucketName,
|
|
3732
|
+
Key: key,
|
|
3733
|
+
Body: body,
|
|
3734
|
+
...contentType && { ContentType: contentType },
|
|
3735
|
+
...contentLength != null && { ContentLength: contentLength },
|
|
3736
|
+
...metadata && { Metadata: metadata },
|
|
3737
|
+
...tags && { Tagging: this._tagsToQuery(tags) }
|
|
3738
|
+
});
|
|
3739
|
+
return this.client.send(cmd);
|
|
3740
|
+
}
|
|
3741
|
+
/** Convenience for JSON manifests. */
|
|
3742
|
+
async putJson(key, value, opts = {}) {
|
|
3743
|
+
const json = JSON.stringify(value, null, opts.pretty ? 2 : 0);
|
|
3744
|
+
const body = Buffer.from(json, "utf8");
|
|
3745
|
+
return this.putObject({
|
|
3746
|
+
key,
|
|
3747
|
+
body,
|
|
3748
|
+
contentType: "application/json",
|
|
3749
|
+
contentLength: body.length,
|
|
3750
|
+
tags: opts.tags,
|
|
3751
|
+
metadata: opts.metadata
|
|
3752
|
+
});
|
|
3753
|
+
}
|
|
3754
|
+
// ── DELETE ──────────────────────────────────────────────────────────────
|
|
3755
|
+
async deleteObject(key) {
|
|
3756
|
+
return this.client.send(new DeleteObjectCommand({
|
|
3757
|
+
Bucket: this.bucketName,
|
|
3758
|
+
Key: key
|
|
3759
|
+
}));
|
|
3760
|
+
}
|
|
3761
|
+
// ── COPY (for migration: legacy → new bucket, or intra-bucket "rename") ─
|
|
3762
|
+
async copyObject({ sourceBucket, sourceKey, key, contentType, metadata, tags }) {
|
|
3763
|
+
const src = sourceBucket || this.bucketName;
|
|
3764
|
+
const cmd = new CopyObjectCommand({
|
|
3765
|
+
Bucket: this.bucketName,
|
|
3766
|
+
Key: key,
|
|
3767
|
+
CopySource: encodeURIComponent(`${src}/${sourceKey}`),
|
|
3768
|
+
...contentType && {
|
|
3769
|
+
ContentType: contentType,
|
|
3770
|
+
MetadataDirective: "REPLACE"
|
|
3771
|
+
},
|
|
3772
|
+
...metadata && {
|
|
3773
|
+
Metadata: metadata,
|
|
3774
|
+
MetadataDirective: "REPLACE"
|
|
3775
|
+
},
|
|
3776
|
+
...tags && {
|
|
3777
|
+
Tagging: this._tagsToQuery(tags),
|
|
3778
|
+
TaggingDirective: "REPLACE"
|
|
3779
|
+
}
|
|
3780
|
+
});
|
|
3781
|
+
return this.client.send(cmd);
|
|
3782
|
+
}
|
|
3783
|
+
// ── LIST ────────────────────────────────────────────────────────────────
|
|
3784
|
+
async listObjects(prefix, { keysOnly = false, maxKeys = 1e3, continuationToken } = {}) {
|
|
3785
|
+
const out = await this.client.send(new ListObjectsV2Command({
|
|
3786
|
+
Bucket: this.bucketName,
|
|
3787
|
+
Prefix: prefix,
|
|
3788
|
+
MaxKeys: maxKeys,
|
|
3789
|
+
ContinuationToken: continuationToken
|
|
3790
|
+
}));
|
|
3791
|
+
const items = (out.Contents ?? []).map((o) => ({
|
|
3792
|
+
key: o.Key,
|
|
3793
|
+
size: o.Size,
|
|
3794
|
+
etag: o.ETag,
|
|
3795
|
+
lastModified: o.LastModified,
|
|
3796
|
+
storageClass: o.StorageClass
|
|
3797
|
+
}));
|
|
3798
|
+
return {
|
|
3799
|
+
items: keysOnly ? items.map((i) => i.key) : items,
|
|
3800
|
+
isTruncated: !!out.IsTruncated,
|
|
3801
|
+
nextContinuationToken: out.NextContinuationToken
|
|
3802
|
+
};
|
|
3803
|
+
}
|
|
3804
|
+
// ── TAGS (used for lifecycle rules, e.g. status=closed → Glacier IR) ────
|
|
3805
|
+
async putObjectTagging(key, tags) {
|
|
3806
|
+
return this.client.send(new PutObjectTaggingCommand({
|
|
3807
|
+
Bucket: this.bucketName,
|
|
3808
|
+
Key: key,
|
|
3809
|
+
Tagging: { TagSet: this._tagsToTagSet(tags) }
|
|
3810
|
+
}));
|
|
3811
|
+
}
|
|
3812
|
+
async getObjectTagging(key) {
|
|
3813
|
+
const out = await this.client.send(new GetObjectTaggingCommand({
|
|
3814
|
+
Bucket: this.bucketName,
|
|
3815
|
+
Key: key
|
|
3816
|
+
}));
|
|
3817
|
+
const tags = {};
|
|
3818
|
+
for (const t of out.TagSet ?? []) tags[t.Key] = t.Value;
|
|
3819
|
+
return tags;
|
|
3820
|
+
}
|
|
3821
|
+
// ── internals ───────────────────────────────────────────────────────────
|
|
3822
|
+
_isNotFound(err) {
|
|
3823
|
+
const status = err?.$metadata?.httpStatusCode;
|
|
3824
|
+
return status === 404 || err?.name === "NotFound" || err?.name === "NoSuchKey" || err?.Code === "NoSuchKey";
|
|
3825
|
+
}
|
|
3826
|
+
_tagsToQuery(tags) {
|
|
3827
|
+
return Object.entries(tags).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
|
|
3828
|
+
}
|
|
3829
|
+
_tagsToTagSet(tags) {
|
|
3830
|
+
return Object.entries(tags).map(([Key, Value]) => ({ Key, Value: String(Value) }));
|
|
3831
|
+
}
|
|
3832
|
+
};
|
|
3631
3833
|
|
|
3632
|
-
// src/logger/index.
|
|
3834
|
+
// src/logger/index.js
|
|
3633
3835
|
import chalk from "chalk";
|
|
3634
3836
|
import util from "util";
|
|
3635
3837
|
|
|
3636
|
-
// src/logger/transports.
|
|
3838
|
+
// src/logger/transports.js
|
|
3637
3839
|
var ConsoleTransport = class {
|
|
3638
3840
|
write(payload) {
|
|
3639
3841
|
console.info(payload);
|
|
@@ -3653,7 +3855,7 @@ var ParentProcessTransport = class {
|
|
|
3653
3855
|
}
|
|
3654
3856
|
};
|
|
3655
3857
|
|
|
3656
|
-
// src/logger/index.
|
|
3858
|
+
// src/logger/index.js
|
|
3657
3859
|
var ALL_LEVELS = [
|
|
3658
3860
|
"silly",
|
|
3659
3861
|
"debug",
|
|
@@ -3726,6 +3928,7 @@ var Logger = class _Logger {
|
|
|
3726
3928
|
}
|
|
3727
3929
|
/**
|
|
3728
3930
|
* Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
|
|
3931
|
+
* Params are tracked under the `logger` module for --showUsedParams.
|
|
3729
3932
|
*/
|
|
3730
3933
|
static init(context, options) {
|
|
3731
3934
|
const paramDefs = {
|
|
@@ -3739,7 +3942,7 @@ var Logger = class _Logger {
|
|
|
3739
3942
|
progressWithTimes: "boolean default false",
|
|
3740
3943
|
progressThrottleMs: "number"
|
|
3741
3944
|
};
|
|
3742
|
-
const discovered = context.params.getAllForModule(paramDefs);
|
|
3945
|
+
const discovered = context.params.getAllForModule("logger", paramDefs);
|
|
3743
3946
|
const config2 = { ...discovered, ...options };
|
|
3744
3947
|
const logger = new _Logger(context, config2);
|
|
3745
3948
|
context.logger = logger;
|
|
@@ -3935,9 +4138,9 @@ var Logger = class _Logger {
|
|
|
3935
4138
|
}
|
|
3936
4139
|
};
|
|
3937
4140
|
|
|
3938
|
-
// src/init/index.
|
|
4141
|
+
// src/init/index.js
|
|
3939
4142
|
import { EventEmitter } from "events";
|
|
3940
|
-
function extractComponentOptions(opts,
|
|
4143
|
+
function extractComponentOptions(opts, _componentName) {
|
|
3941
4144
|
const reservedKeys = ["overrides", "defaults", "modules"];
|
|
3942
4145
|
const componentOptions = {};
|
|
3943
4146
|
for (const [key, value] of Object.entries(opts)) {
|
|
@@ -3982,7 +4185,10 @@ function setupContext(opts = {}) {
|
|
|
3982
4185
|
return setup(opts);
|
|
3983
4186
|
}
|
|
3984
4187
|
|
|
3985
|
-
// src/
|
|
4188
|
+
// src/tasks/index.js
|
|
4189
|
+
import os3 from "os";
|
|
4190
|
+
|
|
4191
|
+
// src/utils/core-utils.js
|
|
3986
4192
|
function sleepMs(ms) {
|
|
3987
4193
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3988
4194
|
}
|
|
@@ -3991,8 +4197,82 @@ function toJsonColumn(value) {
|
|
|
3991
4197
|
return JSON.stringify(value);
|
|
3992
4198
|
}
|
|
3993
4199
|
|
|
3994
|
-
// src/tasks/
|
|
4200
|
+
// src/tasks/servicesRegistry.js
|
|
4201
|
+
import os from "os";
|
|
4202
|
+
|
|
4203
|
+
// src/tasks/taskUtils.js
|
|
3995
4204
|
import { randomUUID } from "crypto";
|
|
4205
|
+
|
|
4206
|
+
// src/tasks/time-matcher.js
|
|
4207
|
+
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
4208
|
+
function resolveAsterisks(field, range) {
|
|
4209
|
+
return field.includes("*") ? field.replace("*", range) : field;
|
|
4210
|
+
}
|
|
4211
|
+
function resolveRanges(field) {
|
|
4212
|
+
const regex = /(\d+)-(\d+)/;
|
|
4213
|
+
let current = field;
|
|
4214
|
+
while (true) {
|
|
4215
|
+
const match = regex.exec(current);
|
|
4216
|
+
if (!match) break;
|
|
4217
|
+
const raw = match[0];
|
|
4218
|
+
let first = Number(match[1]);
|
|
4219
|
+
let last = Number(match[2]);
|
|
4220
|
+
if (last < first) {
|
|
4221
|
+
[first, last] = [last, first];
|
|
4222
|
+
}
|
|
4223
|
+
const values = [];
|
|
4224
|
+
for (let i = first; i <= last; i += 1) {
|
|
4225
|
+
values.push(i);
|
|
4226
|
+
}
|
|
4227
|
+
current = current.replace(raw, values.join(","));
|
|
4228
|
+
}
|
|
4229
|
+
return current;
|
|
4230
|
+
}
|
|
4231
|
+
function resolveSteps(field) {
|
|
4232
|
+
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
4233
|
+
if (!match) return field;
|
|
4234
|
+
const base = match[1];
|
|
4235
|
+
const step = Number(match[2]);
|
|
4236
|
+
if (!Number.isFinite(step) || step <= 0) return field;
|
|
4237
|
+
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
4238
|
+
}
|
|
4239
|
+
function convertPattern(pattern) {
|
|
4240
|
+
const parts = pattern.trim().split(/\s+/);
|
|
4241
|
+
if (parts.length !== 6) {
|
|
4242
|
+
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
4243
|
+
}
|
|
4244
|
+
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
4245
|
+
}
|
|
4246
|
+
function fieldMatches(field, value) {
|
|
4247
|
+
const allowed = field.split(",").map((v) => Number(v));
|
|
4248
|
+
return allowed.includes(value);
|
|
4249
|
+
}
|
|
4250
|
+
function matchesParsedPattern(parsed, date) {
|
|
4251
|
+
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());
|
|
4252
|
+
}
|
|
4253
|
+
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
4254
|
+
const parsed = convertPattern(pattern);
|
|
4255
|
+
return matchesParsedPattern(parsed, date);
|
|
4256
|
+
}
|
|
4257
|
+
var MS_PER_SECOND = 1e3;
|
|
4258
|
+
var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
|
|
4259
|
+
function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
|
|
4260
|
+
const parsed = convertPattern(pattern);
|
|
4261
|
+
let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
|
|
4262
|
+
const end = t + maxSearchMs;
|
|
4263
|
+
while (t <= end) {
|
|
4264
|
+
const date = new Date(t);
|
|
4265
|
+
if (matchesParsedPattern(parsed, date)) {
|
|
4266
|
+
return date;
|
|
4267
|
+
}
|
|
4268
|
+
t += MS_PER_SECOND;
|
|
4269
|
+
}
|
|
4270
|
+
throw new Error(
|
|
4271
|
+
`nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
|
|
4272
|
+
);
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
// src/tasks/taskUtils.js
|
|
3996
4276
|
function getDb(context) {
|
|
3997
4277
|
const db = context.db;
|
|
3998
4278
|
if (!db) {
|
|
@@ -4000,105 +4280,117 @@ function getDb(context) {
|
|
|
4000
4280
|
}
|
|
4001
4281
|
return db;
|
|
4002
4282
|
}
|
|
4003
|
-
function queueToTableNames(
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4283
|
+
function queueToTableNames(queueName) {
|
|
4284
|
+
return {
|
|
4285
|
+
tasksTable: queueName,
|
|
4286
|
+
historyTable: `${queueName}_history`,
|
|
4287
|
+
registryTable: `${queueName}_services_registry`
|
|
4288
|
+
};
|
|
4289
|
+
}
|
|
4290
|
+
function defineTasksTable(t, db, tableNameForIndex) {
|
|
4291
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
4292
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
4293
|
+
t.timestamp("started_at");
|
|
4294
|
+
t.timestamp("completed_at");
|
|
4295
|
+
t.integer("priority").notNullable().defaultTo(50);
|
|
4296
|
+
t.text("schedule");
|
|
4297
|
+
t.timestamp("next_run_at").defaultTo(null);
|
|
4298
|
+
t.timestamp("past_due").defaultTo(null);
|
|
4299
|
+
t.text("name").notNullable();
|
|
4300
|
+
t.text("opid");
|
|
4301
|
+
t.json("params");
|
|
4302
|
+
t.text("service_group");
|
|
4303
|
+
t.integer("instance_number");
|
|
4304
|
+
t.text("service_name");
|
|
4305
|
+
t.text("server_name");
|
|
4306
|
+
t.text("status").notNullable().defaultTo("idle");
|
|
4307
|
+
t.timestamp("status_changed_at").defaultTo(null);
|
|
4308
|
+
t.text("progress");
|
|
4309
|
+
t.boolean("success");
|
|
4310
|
+
t.json("results");
|
|
4311
|
+
t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
|
|
4312
|
+
t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
|
|
4313
|
+
}
|
|
4314
|
+
function taskHistoryInsertFromQueueRow(row, overrides) {
|
|
4315
|
+
const { id, ...snapshot } = row;
|
|
4316
|
+
void id;
|
|
4007
4317
|
return {
|
|
4008
|
-
|
|
4009
|
-
|
|
4318
|
+
...snapshot,
|
|
4319
|
+
...overrides
|
|
4010
4320
|
};
|
|
4011
4321
|
}
|
|
4012
4322
|
async function ensureTaskTables(context, options = {}) {
|
|
4013
|
-
const
|
|
4323
|
+
const queueName = options.queueName ?? "tasks";
|
|
4014
4324
|
const recreate = options.recreate ?? false;
|
|
4015
4325
|
const db = getDb(context);
|
|
4016
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
4326
|
+
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
4017
4327
|
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
4018
4328
|
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
4329
|
+
const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
|
|
4019
4330
|
if (recreate) {
|
|
4020
4331
|
await db.schema.dropTableIfExists(historyTable);
|
|
4021
4332
|
await db.schema.dropTableIfExists(tasksTable);
|
|
4333
|
+
await db.schema.dropTableIfExists(registryTable);
|
|
4022
4334
|
}
|
|
4023
4335
|
if (needsTasks) {
|
|
4024
4336
|
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
4025
4337
|
await db.schema.createTable(tasksTable, (t) => {
|
|
4026
|
-
t
|
|
4027
|
-
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
4028
|
-
t.timestamp("started_at");
|
|
4029
|
-
t.timestamp("completed_at");
|
|
4030
|
-
t.integer("priority").notNullable().defaultTo(0);
|
|
4031
|
-
t.text("schedule");
|
|
4032
|
-
t.timestamp("past_due").defaultTo(null);
|
|
4033
|
-
t.text("target").notNullable();
|
|
4034
|
-
t.text("task").notNullable();
|
|
4035
|
-
t.json("params");
|
|
4036
|
-
t.text("opid");
|
|
4037
|
-
t.timestamp("paused_at").defaultTo(null);
|
|
4038
|
-
t.text("progress");
|
|
4039
|
-
t.boolean("success");
|
|
4040
|
-
t.json("results");
|
|
4041
|
-
});
|
|
4042
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
4043
|
-
t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
|
|
4044
|
-
t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
|
|
4045
|
-
t.index(["target", "task"], `${tasksTable}_target_task_idx`);
|
|
4046
|
-
});
|
|
4047
|
-
}
|
|
4048
|
-
const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
|
|
4049
|
-
if (!tasksHasOpid) {
|
|
4050
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
4051
|
-
t.text("opid");
|
|
4052
|
-
});
|
|
4053
|
-
}
|
|
4054
|
-
const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
|
|
4055
|
-
if (!tasksHasPausedAt) {
|
|
4056
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
4057
|
-
t.timestamp("paused_at").defaultTo(null);
|
|
4338
|
+
defineTasksTable(t, db, tasksTable);
|
|
4058
4339
|
});
|
|
4059
4340
|
}
|
|
4060
4341
|
if (needsHistory) {
|
|
4061
4342
|
await db.schema.createTable(historyTable, (t) => {
|
|
4062
|
-
t
|
|
4063
|
-
t.timestamp("created_at").notNullable();
|
|
4064
|
-
t.timestamp("started_at");
|
|
4065
|
-
t.timestamp("completed_at");
|
|
4066
|
-
t.integer("priority").notNullable().defaultTo(0);
|
|
4067
|
-
t.text("schedule");
|
|
4068
|
-
t.timestamp("past_due").defaultTo(null);
|
|
4069
|
-
t.text("target").notNullable();
|
|
4070
|
-
t.text("task").notNullable();
|
|
4071
|
-
t.json("params");
|
|
4072
|
-
t.text("opid");
|
|
4073
|
-
t.text("progress");
|
|
4074
|
-
t.boolean("success");
|
|
4075
|
-
t.json("results");
|
|
4076
|
-
});
|
|
4077
|
-
await db.schema.alterTable(historyTable, (t) => {
|
|
4078
|
-
t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
|
|
4079
|
-
t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
|
|
4343
|
+
defineTasksTable(t, db, historyTable);
|
|
4080
4344
|
});
|
|
4081
4345
|
}
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
t.text("
|
|
4346
|
+
if (needsRegistry) {
|
|
4347
|
+
await db.schema.createTable(registryTable, (t) => {
|
|
4348
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
4349
|
+
t.text("queue_name").notNullable();
|
|
4350
|
+
t.text("service_group").notNullable();
|
|
4351
|
+
t.integer("instance_number").notNullable().defaultTo(1);
|
|
4352
|
+
t.text("service_name").notNullable();
|
|
4353
|
+
t.text("server_name").notNullable();
|
|
4354
|
+
t.integer("pid");
|
|
4355
|
+
t.json("metadata");
|
|
4356
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
4357
|
+
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
4358
|
+
t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
|
|
4359
|
+
t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
|
|
4360
|
+
t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
|
|
4086
4361
|
});
|
|
4087
4362
|
}
|
|
4088
4363
|
}
|
|
4089
4364
|
async function enqueueTask(context, options) {
|
|
4090
4365
|
const db = getDb(context);
|
|
4091
|
-
const
|
|
4092
|
-
const { tasksTable } = queueToTableNames(
|
|
4366
|
+
const queueName = options.queueName ?? "tasks";
|
|
4367
|
+
const { tasksTable } = queueToTableNames(queueName);
|
|
4093
4368
|
const id = randomUUID();
|
|
4369
|
+
const name = options.name ?? options.task;
|
|
4370
|
+
if (!name) {
|
|
4371
|
+
throw new Error("enqueueTask: name (or task) is required");
|
|
4372
|
+
}
|
|
4373
|
+
const schedule = options.schedule?.trim() ? options.schedule : null;
|
|
4374
|
+
let nextRunAt = null;
|
|
4375
|
+
if (options.nextRunAt !== void 0) {
|
|
4376
|
+
nextRunAt = options.nextRunAt == null ? null : new Date(options.nextRunAt);
|
|
4377
|
+
} else if (schedule) {
|
|
4378
|
+
nextRunAt = nextTimeMatch(schedule, /* @__PURE__ */ new Date());
|
|
4379
|
+
}
|
|
4094
4380
|
await db(tasksTable).insert({
|
|
4095
4381
|
id,
|
|
4096
|
-
|
|
4097
|
-
task: options.task,
|
|
4382
|
+
name,
|
|
4098
4383
|
params: toJsonColumn(options.params ?? null),
|
|
4099
4384
|
opid: options.opid ?? null,
|
|
4100
|
-
priority: options.priority ??
|
|
4101
|
-
schedule
|
|
4385
|
+
priority: options.priority ?? 50,
|
|
4386
|
+
schedule,
|
|
4387
|
+
next_run_at: nextRunAt,
|
|
4388
|
+
service_group: options.serviceGroup ?? null,
|
|
4389
|
+
instance_number: options.instanceNumber ?? null,
|
|
4390
|
+
service_name: options.serviceName ?? null,
|
|
4391
|
+
server_name: options.serverName ?? null,
|
|
4392
|
+
status: "idle",
|
|
4393
|
+
status_changed_at: db.fn.now()
|
|
4102
4394
|
});
|
|
4103
4395
|
return id;
|
|
4104
4396
|
}
|
|
@@ -4109,57 +4401,395 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
|
4109
4401
|
});
|
|
4110
4402
|
}
|
|
4111
4403
|
|
|
4112
|
-
// src/tasks/
|
|
4113
|
-
function
|
|
4114
|
-
const
|
|
4115
|
-
if (
|
|
4116
|
-
|
|
4117
|
-
const namespace = holder.params?.get?.("tasksLogsNamespace") || "tasks-logs";
|
|
4118
|
-
const tableName = holder.params?.get?.("tasksLogsTable") || "runner";
|
|
4119
|
-
const errorTableName = holder.params?.get?.("tasksErrorLogsTable") || `${tableName}-errors`;
|
|
4120
|
-
const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
|
|
4121
|
-
const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
|
|
4122
|
-
const errorDb = new FileDatabase({
|
|
4123
|
-
basePath,
|
|
4124
|
-
namespace,
|
|
4125
|
-
tableName: errorTableName,
|
|
4126
|
-
versioned: true,
|
|
4127
|
-
useMetadata: true,
|
|
4128
|
-
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4129
|
-
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4130
|
-
logger: holder.logger
|
|
4131
|
-
});
|
|
4132
|
-
const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
|
|
4133
|
-
const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
4134
|
-
if (!enabled) {
|
|
4135
|
-
const disabledState = {
|
|
4136
|
-
db: null,
|
|
4137
|
-
errorDb,
|
|
4138
|
-
queue: Promise.resolve(),
|
|
4139
|
-
initialized: true,
|
|
4140
|
-
errorInitialized: false
|
|
4141
|
-
};
|
|
4142
|
-
holder.__tasksLogsState = disabledState;
|
|
4143
|
-
return disabledState;
|
|
4404
|
+
// src/tasks/servicesRegistry.js
|
|
4405
|
+
function getDb2(context) {
|
|
4406
|
+
const db = context.db;
|
|
4407
|
+
if (!db) {
|
|
4408
|
+
throw new Error("Services registry requires context.db");
|
|
4144
4409
|
}
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4410
|
+
return db;
|
|
4411
|
+
}
|
|
4412
|
+
function parseMetadataColumn(value) {
|
|
4413
|
+
if (!value) return {};
|
|
4414
|
+
if (typeof value === "object" && !Array.isArray(value)) return value;
|
|
4415
|
+
if (typeof value === "string") {
|
|
4416
|
+
try {
|
|
4417
|
+
const p = JSON.parse(value);
|
|
4418
|
+
return p && typeof p === "object" && !Array.isArray(value) ? p : {};
|
|
4419
|
+
} catch {
|
|
4420
|
+
return {};
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
return {};
|
|
4424
|
+
}
|
|
4425
|
+
var DEFAULT_GROUP_MAX_INSTANCES = {
|
|
4426
|
+
intake: 1,
|
|
4427
|
+
harvest: 1,
|
|
4428
|
+
harvester: 0,
|
|
4429
|
+
loader: 0,
|
|
4430
|
+
photos: 0,
|
|
4431
|
+
photosprocessor: 0,
|
|
4432
|
+
ingest: 0
|
|
4433
|
+
};
|
|
4434
|
+
function sanitizeNamePart(raw) {
|
|
4435
|
+
const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
4436
|
+
return s.slice(0, 80) || "runner";
|
|
4437
|
+
}
|
|
4438
|
+
function resolveMaxInstances(serviceGroup, override) {
|
|
4439
|
+
if (override !== void 0 && Number.isFinite(override)) {
|
|
4440
|
+
return Math.max(0, Math.floor(Number(override)));
|
|
4441
|
+
}
|
|
4442
|
+
const g = serviceGroup.trim().toLowerCase();
|
|
4443
|
+
return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
|
|
4444
|
+
}
|
|
4445
|
+
async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
|
|
4446
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
4447
|
+
let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
|
|
4448
|
+
if (excludeRowId) {
|
|
4449
|
+
q = q.whereNot("id", excludeRowId);
|
|
4450
|
+
}
|
|
4451
|
+
const row = await q.count("id as count").first();
|
|
4452
|
+
return Number(row?.count ?? 0);
|
|
4453
|
+
}
|
|
4454
|
+
async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
|
|
4455
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
4456
|
+
const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
|
|
4457
|
+
const set = /* @__PURE__ */ new Set();
|
|
4458
|
+
for (const r of rows) {
|
|
4459
|
+
const n = Number(r.instance_number);
|
|
4460
|
+
if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
|
|
4461
|
+
}
|
|
4462
|
+
return set;
|
|
4463
|
+
}
|
|
4464
|
+
function isUniqueViolation(error) {
|
|
4465
|
+
const code = error?.code ?? error?.errno;
|
|
4466
|
+
return code === "23505" || String(error?.message || "").includes("duplicate key");
|
|
4467
|
+
}
|
|
4468
|
+
function buildMetadata(options) {
|
|
4469
|
+
const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
|
|
4470
|
+
if (options.target) {
|
|
4471
|
+
base.runnerTarget = options.target;
|
|
4472
|
+
}
|
|
4473
|
+
return toJsonColumn(Object.keys(base).length ? base : null);
|
|
4474
|
+
}
|
|
4475
|
+
function allocateInstanceNumber(occupied, explicit, maxSlots) {
|
|
4476
|
+
if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
|
|
4477
|
+
const e = Math.max(1, Math.floor(Number(explicit)));
|
|
4478
|
+
if (occupied.has(e)) {
|
|
4479
|
+
throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
|
|
4480
|
+
}
|
|
4481
|
+
if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
|
|
4482
|
+
throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
|
|
4483
|
+
}
|
|
4484
|
+
return e;
|
|
4485
|
+
}
|
|
4486
|
+
const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
|
|
4487
|
+
for (let n = 1; n <= cap; n++) {
|
|
4488
|
+
if (!occupied.has(n)) return n;
|
|
4489
|
+
}
|
|
4490
|
+
throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
|
|
4491
|
+
}
|
|
4492
|
+
function defaultServiceName(groupBase, hostBase, instanceNumber) {
|
|
4493
|
+
return `${groupBase}-${hostBase}-${instanceNumber}`;
|
|
4494
|
+
}
|
|
4495
|
+
async function registerInServicesRegistry(context, options) {
|
|
4496
|
+
const db = getDb2(context);
|
|
4497
|
+
const registryTable = queueToTableNames(options.queueName).registryTable;
|
|
4498
|
+
const serviceGroup = options.serviceGroup.trim();
|
|
4499
|
+
if (!serviceGroup) {
|
|
4500
|
+
throw new Error("registerInServicesRegistry: serviceGroup is required");
|
|
4501
|
+
}
|
|
4502
|
+
const serverName = os.hostname();
|
|
4503
|
+
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
4504
|
+
const meta = buildMetadata(options);
|
|
4505
|
+
const groupBase = sanitizeNamePart(serviceGroup);
|
|
4506
|
+
const hostBase = sanitizeNamePart(serverName);
|
|
4507
|
+
const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
|
|
4508
|
+
const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
|
|
4509
|
+
if (maxAllowed > 0 && aliveCount >= maxAllowed) {
|
|
4510
|
+
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
|
|
4511
|
+
if (options.enforceMaxInstances) {
|
|
4512
|
+
throw new Error(msg);
|
|
4513
|
+
}
|
|
4514
|
+
context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
|
|
4515
|
+
}
|
|
4516
|
+
const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
|
|
4517
|
+
const cutoff = new Date(Date.now() - options.staleMs);
|
|
4518
|
+
const MAX_ATTEMPTS = 8;
|
|
4519
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
4520
|
+
const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
|
|
4521
|
+
const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
|
|
4522
|
+
const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
|
|
4523
|
+
const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
4524
|
+
if (existing) {
|
|
4525
|
+
const lastSeen = new Date(existing.last_seen_at);
|
|
4526
|
+
const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
|
|
4527
|
+
if (isAlive) {
|
|
4528
|
+
if (options.serviceName?.trim()) {
|
|
4529
|
+
throw new Error(
|
|
4530
|
+
`[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
|
|
4531
|
+
);
|
|
4532
|
+
}
|
|
4533
|
+
context.logger.warn?.(
|
|
4534
|
+
`[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
|
|
4535
|
+
);
|
|
4536
|
+
if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
|
|
4537
|
+
throw new Error(
|
|
4538
|
+
`[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
|
|
4539
|
+
);
|
|
4540
|
+
}
|
|
4541
|
+
await new Promise((r) => setTimeout(r, 50 + attempt * 30));
|
|
4542
|
+
continue;
|
|
4543
|
+
}
|
|
4544
|
+
await db(registryTable).where({ id: existing.id }).update({
|
|
4545
|
+
server_name: serverName,
|
|
4546
|
+
pid,
|
|
4547
|
+
metadata: meta,
|
|
4548
|
+
service_group: serviceGroup,
|
|
4549
|
+
instance_number: instanceNumber,
|
|
4550
|
+
last_seen_at: db.fn.now()
|
|
4551
|
+
});
|
|
4552
|
+
const reg = {
|
|
4553
|
+
serviceName: serviceNameRaw,
|
|
4554
|
+
serviceGroup,
|
|
4555
|
+
queueName: options.queueName,
|
|
4556
|
+
target: options.target,
|
|
4557
|
+
rowId: String(existing.id),
|
|
4558
|
+
registryTable,
|
|
4559
|
+
instanceNumber
|
|
4560
|
+
};
|
|
4561
|
+
context.servicesRegistry = reg;
|
|
4562
|
+
context.runnerHeartbeat = reg;
|
|
4563
|
+
context.logger.info?.(
|
|
4564
|
+
`[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
4565
|
+
);
|
|
4566
|
+
return reg;
|
|
4567
|
+
}
|
|
4568
|
+
try {
|
|
4569
|
+
const rows = await db(registryTable).insert({
|
|
4570
|
+
queue_name: options.queueName,
|
|
4571
|
+
service_group: serviceGroup,
|
|
4572
|
+
instance_number: instanceNumber,
|
|
4573
|
+
service_name: serviceNameRaw,
|
|
4574
|
+
server_name: serverName,
|
|
4575
|
+
pid,
|
|
4576
|
+
metadata: meta,
|
|
4577
|
+
last_seen_at: db.fn.now(),
|
|
4578
|
+
created_at: db.fn.now()
|
|
4579
|
+
}).returning(["id", "service_name"]);
|
|
4580
|
+
const row = Array.isArray(rows) ? rows[0] : rows;
|
|
4581
|
+
let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
|
|
4582
|
+
if (!rowId) {
|
|
4583
|
+
const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
4584
|
+
rowId = again?.id != null ? String(again.id) : "";
|
|
4585
|
+
}
|
|
4586
|
+
if (!rowId) continue;
|
|
4587
|
+
const regNew = {
|
|
4588
|
+
serviceName: String(row?.service_name ?? serviceNameRaw),
|
|
4589
|
+
serviceGroup,
|
|
4590
|
+
queueName: options.queueName,
|
|
4591
|
+
target: options.target,
|
|
4592
|
+
rowId,
|
|
4593
|
+
registryTable,
|
|
4594
|
+
instanceNumber
|
|
4595
|
+
};
|
|
4596
|
+
context.servicesRegistry = regNew;
|
|
4597
|
+
context.runnerHeartbeat = regNew;
|
|
4598
|
+
context.logger.info?.(
|
|
4599
|
+
`[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
4600
|
+
);
|
|
4601
|
+
return regNew;
|
|
4602
|
+
} catch (error) {
|
|
4603
|
+
if (!isUniqueViolation(error)) {
|
|
4604
|
+
throw error;
|
|
4605
|
+
}
|
|
4606
|
+
context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
throw new Error(
|
|
4610
|
+
`[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
|
|
4611
|
+
);
|
|
4612
|
+
}
|
|
4613
|
+
async function touchServicesRegistry(context, registration) {
|
|
4614
|
+
const db = getDb2(context);
|
|
4615
|
+
const serverName = os.hostname();
|
|
4616
|
+
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
4617
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
4618
|
+
last_seen_at: db.fn.now(),
|
|
4619
|
+
server_name: serverName,
|
|
4620
|
+
pid
|
|
4621
|
+
});
|
|
4622
|
+
}
|
|
4623
|
+
async function updateServicesRegistryMetadata(context, registration, patch) {
|
|
4624
|
+
const db = getDb2(context);
|
|
4625
|
+
const row = await db(registration.registryTable).where({ id: registration.rowId }).first();
|
|
4626
|
+
const prev = parseMetadataColumn(row?.metadata);
|
|
4627
|
+
const merged = { ...prev, ...patch };
|
|
4628
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
4629
|
+
metadata: toJsonColumn(merged),
|
|
4630
|
+
last_seen_at: db.fn.now()
|
|
4631
|
+
});
|
|
4632
|
+
context.logger.info?.(`[services-registry] metadata updated for ${registration.serviceName}`);
|
|
4633
|
+
}
|
|
4634
|
+
async function unregisterServicesRegistry(context, registration) {
|
|
4635
|
+
const db = getDb2(context);
|
|
4636
|
+
await db(registration.registryTable).where({ id: registration.rowId }).delete();
|
|
4637
|
+
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
|
|
4638
|
+
}
|
|
4639
|
+
async function listServicesRegistry(context, options = { queueName: "tasks" }) {
|
|
4640
|
+
const db = getDb2(context);
|
|
4641
|
+
const staleMs = options.staleMs ?? 6e4;
|
|
4642
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
4643
|
+
const table = queueToTableNames(options.queueName).registryTable;
|
|
4644
|
+
let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
|
|
4645
|
+
if (options.serviceGroup?.trim()) {
|
|
4646
|
+
q = q.where({ service_group: options.serviceGroup.trim() });
|
|
4647
|
+
}
|
|
4648
|
+
return await q;
|
|
4649
|
+
}
|
|
4650
|
+
|
|
4651
|
+
// src/tasks/taskLogs.js
|
|
4652
|
+
import path4 from "path";
|
|
4653
|
+
function getLogsState(context) {
|
|
4654
|
+
const holder = context;
|
|
4655
|
+
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
4656
|
+
const basePath = holder.params?.get?.("tasksLogsBasePath") || "./data";
|
|
4657
|
+
const namespace = holder.params?.get?.("tasksLogsNamespace") || "tasks-logs";
|
|
4658
|
+
const tableName = holder.params?.get?.("tasksLogsTable") || "runner";
|
|
4659
|
+
const errorTableName = holder.params?.get?.("tasksErrorLogsTable") || `${tableName}-errors`;
|
|
4660
|
+
const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
|
|
4661
|
+
const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
|
|
4662
|
+
const errorDb = new FileDatabase({
|
|
4663
|
+
basePath,
|
|
4664
|
+
namespace,
|
|
4665
|
+
tableName: errorTableName,
|
|
4666
|
+
versioned: true,
|
|
4667
|
+
useMetadata: true,
|
|
4668
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4669
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4670
|
+
logger: holder.logger
|
|
4671
|
+
});
|
|
4672
|
+
const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
|
|
4673
|
+
const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
4674
|
+
if (!enabled) {
|
|
4675
|
+
const disabledState = {
|
|
4676
|
+
db: null,
|
|
4677
|
+
errorDb,
|
|
4678
|
+
queue: Promise.resolve(),
|
|
4679
|
+
initialized: true,
|
|
4680
|
+
errorInitialized: false
|
|
4681
|
+
};
|
|
4682
|
+
holder.__tasksLogsState = disabledState;
|
|
4683
|
+
return disabledState;
|
|
4684
|
+
}
|
|
4685
|
+
const db = new FileDatabase({
|
|
4686
|
+
basePath,
|
|
4687
|
+
namespace,
|
|
4688
|
+
tableName,
|
|
4689
|
+
versioned: true,
|
|
4690
|
+
useMetadata: true,
|
|
4691
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4692
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4693
|
+
logger: holder.logger
|
|
4694
|
+
});
|
|
4695
|
+
const state = {
|
|
4696
|
+
db,
|
|
4697
|
+
errorDb,
|
|
4698
|
+
queue: Promise.resolve(),
|
|
4699
|
+
initialized: false,
|
|
4700
|
+
errorInitialized: false
|
|
4701
|
+
};
|
|
4702
|
+
holder.__tasksLogsState = state;
|
|
4703
|
+
return state;
|
|
4704
|
+
}
|
|
4705
|
+
function ipcLogTargetKey(target) {
|
|
4706
|
+
const bp = target.basePath ?? "";
|
|
4707
|
+
const ns = target.namespace ?? "";
|
|
4708
|
+
return `${bp}::${ns}::${target.tableName}`;
|
|
4709
|
+
}
|
|
4710
|
+
function ipcFileLogsTableNameForSourceResource(source, resource) {
|
|
4711
|
+
const seg = (s) => {
|
|
4712
|
+
const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
4713
|
+
return t.length ? t : "x";
|
|
4714
|
+
};
|
|
4715
|
+
return `${seg(source)}/${seg(resource)}`;
|
|
4716
|
+
}
|
|
4717
|
+
async function readTaskIpcLogsSnapshot(context, options) {
|
|
4718
|
+
const holder = context;
|
|
4719
|
+
const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
|
|
4720
|
+
const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
|
|
4721
|
+
const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
|
|
4722
|
+
const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
|
|
4723
|
+
const fd = new FileDatabase({
|
|
4724
|
+
basePath,
|
|
4725
|
+
namespace,
|
|
4726
|
+
tableName,
|
|
4727
|
+
versioned: true,
|
|
4728
|
+
useMetadata: true,
|
|
4729
|
+
maxVersions: 30,
|
|
4730
|
+
pageSize: 2e3,
|
|
4731
|
+
logger: holder.logger
|
|
4732
|
+
});
|
|
4733
|
+
const versions = await fd.getVersions();
|
|
4734
|
+
if (versions.length === 0) {
|
|
4735
|
+
return { records: [], latestTs: null };
|
|
4736
|
+
}
|
|
4737
|
+
const latest = versions[versions.length - 1];
|
|
4738
|
+
const raw = await fd.read({ version: latest });
|
|
4739
|
+
const arr = Array.isArray(raw) ? raw : [];
|
|
4740
|
+
let filtered = arr;
|
|
4741
|
+
if (options.afterTs && String(options.afterTs).trim()) {
|
|
4742
|
+
const cut = String(options.afterTs).trim();
|
|
4743
|
+
filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
|
|
4744
|
+
}
|
|
4745
|
+
let latestTs = null;
|
|
4746
|
+
for (const r of filtered) {
|
|
4747
|
+
const ts = typeof r?.ts === "string" ? String(r.ts) : null;
|
|
4748
|
+
if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
|
|
4749
|
+
}
|
|
4750
|
+
const incremental = !!(options.afterTs && String(options.afterTs).trim());
|
|
4751
|
+
const maxReturn = incremental ? 1e4 : tail;
|
|
4752
|
+
const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
|
|
4753
|
+
return { records: sliced, latestTs };
|
|
4754
|
+
}
|
|
4755
|
+
function resolveIpcFileLogsDir(context, target) {
|
|
4756
|
+
const holder = context;
|
|
4757
|
+
const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
|
|
4758
|
+
const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
|
|
4759
|
+
const segments = target.tableName.split("/").filter(Boolean);
|
|
4760
|
+
return path4.resolve(basePath, namespace, ...segments);
|
|
4761
|
+
}
|
|
4762
|
+
function getLogsStateForTarget(context, target) {
|
|
4763
|
+
const holder = context;
|
|
4764
|
+
const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
|
|
4765
|
+
const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
4766
|
+
if (!enabled) return null;
|
|
4767
|
+
if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
|
|
4768
|
+
const map = holder.__tasksLogsTargetStates;
|
|
4769
|
+
const key = ipcLogTargetKey(target);
|
|
4770
|
+
if (map.has(key)) return map.get(key);
|
|
4771
|
+
const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
|
|
4772
|
+
const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
|
|
4773
|
+
const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
|
|
4774
|
+
const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
|
|
4775
|
+
const db = new FileDatabase({
|
|
4776
|
+
basePath,
|
|
4777
|
+
namespace,
|
|
4778
|
+
tableName: target.tableName,
|
|
4779
|
+
versioned: true,
|
|
4780
|
+
useMetadata: true,
|
|
4781
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4782
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4783
|
+
logger: holder.logger
|
|
4784
|
+
});
|
|
4785
|
+
const state = {
|
|
4786
|
+
db,
|
|
4787
|
+
errorDb: null,
|
|
4788
|
+
queue: Promise.resolve(),
|
|
4789
|
+
initialized: false,
|
|
4790
|
+
errorInitialized: false
|
|
4791
|
+
};
|
|
4792
|
+
map.set(key, state);
|
|
4163
4793
|
return state;
|
|
4164
4794
|
}
|
|
4165
4795
|
function isErrorPayload(payload) {
|
|
@@ -4181,14 +4811,26 @@ function buildLogRecord(task, payload) {
|
|
|
4181
4811
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4182
4812
|
opid: task.opid ?? null,
|
|
4183
4813
|
taskId: task.id,
|
|
4184
|
-
taskName: task.
|
|
4185
|
-
target: task.
|
|
4814
|
+
taskName: task.name,
|
|
4815
|
+
target: task.service_group,
|
|
4186
4816
|
source: typeof params.source === "string" ? params.source : null,
|
|
4187
4817
|
resource: typeof params.resource === "string" ? params.resource : null,
|
|
4188
4818
|
payload
|
|
4189
4819
|
};
|
|
4190
4820
|
}
|
|
4191
|
-
function appendTaskIpcLog(context, task, payload) {
|
|
4821
|
+
function appendTaskIpcLog(context, task, payload, target) {
|
|
4822
|
+
if (target) {
|
|
4823
|
+
const state2 = getLogsStateForTarget(context, target);
|
|
4824
|
+
if (!state2?.db) return;
|
|
4825
|
+
const record2 = buildLogRecord(task, payload);
|
|
4826
|
+
state2.queue = state2.queue.then(async () => {
|
|
4827
|
+
await state2.db.write([record2], { forceNewVersion: !state2.initialized });
|
|
4828
|
+
state2.initialized = true;
|
|
4829
|
+
}).catch((error) => {
|
|
4830
|
+
context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
|
|
4831
|
+
});
|
|
4832
|
+
return;
|
|
4833
|
+
}
|
|
4192
4834
|
const state = getLogsState(context);
|
|
4193
4835
|
if (!state.db && !state.errorDb) return;
|
|
4194
4836
|
const record = buildLogRecord(task, payload);
|
|
@@ -4205,84 +4847,293 @@ function appendTaskIpcLog(context, task, payload) {
|
|
|
4205
4847
|
context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
|
|
4206
4848
|
});
|
|
4207
4849
|
}
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
let current = field;
|
|
4217
|
-
while (true) {
|
|
4218
|
-
const match = regex.exec(current);
|
|
4219
|
-
if (!match) break;
|
|
4220
|
-
const raw = match[0];
|
|
4221
|
-
let first = Number(match[1]);
|
|
4222
|
-
let last = Number(match[2]);
|
|
4223
|
-
if (last < first) {
|
|
4224
|
-
[first, last] = [last, first];
|
|
4225
|
-
}
|
|
4226
|
-
const values = [];
|
|
4227
|
-
for (let i = first; i <= last; i += 1) {
|
|
4228
|
-
values.push(i);
|
|
4850
|
+
async function flushTaskIpcLogs(context) {
|
|
4851
|
+
const holder = context;
|
|
4852
|
+
const promises = [];
|
|
4853
|
+
if (holder.__tasksLogsState?.queue) promises.push(holder.__tasksLogsState.queue);
|
|
4854
|
+
const map = holder.__tasksLogsTargetStates;
|
|
4855
|
+
if (map) {
|
|
4856
|
+
for (const s of map.values()) {
|
|
4857
|
+
if (s.queue) promises.push(s.queue);
|
|
4229
4858
|
}
|
|
4230
|
-
current = current.replace(raw, values.join(","));
|
|
4231
4859
|
}
|
|
4232
|
-
|
|
4233
|
-
}
|
|
4234
|
-
function resolveSteps(field) {
|
|
4235
|
-
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
4236
|
-
if (!match) return field;
|
|
4237
|
-
const base = match[1];
|
|
4238
|
-
const step = Number(match[2]);
|
|
4239
|
-
if (!Number.isFinite(step) || step <= 0) return field;
|
|
4240
|
-
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
4241
|
-
}
|
|
4242
|
-
function convertPattern(pattern) {
|
|
4243
|
-
const parts = pattern.trim().split(/\s+/);
|
|
4244
|
-
if (parts.length !== 6) {
|
|
4245
|
-
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
4246
|
-
}
|
|
4247
|
-
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
4248
|
-
}
|
|
4249
|
-
function fieldMatches(field, value) {
|
|
4250
|
-
const allowed = field.split(",").map((v) => Number(v));
|
|
4251
|
-
return allowed.includes(value);
|
|
4252
|
-
}
|
|
4253
|
-
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
4254
|
-
const parsed = convertPattern(pattern);
|
|
4255
|
-
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());
|
|
4860
|
+
await Promise.all(promises);
|
|
4256
4861
|
}
|
|
4257
4862
|
|
|
4258
|
-
// src/tasks/
|
|
4259
|
-
var
|
|
4260
|
-
|
|
4261
|
-
|
|
4863
|
+
// src/tasks/AbstractTask.js
|
|
4864
|
+
var AbstractTask = class _AbstractTask {
|
|
4865
|
+
/**
|
|
4866
|
+
* Whether `send-task` should wait for completion (and print a result
|
|
4867
|
+
* report) when no explicit `--wait` / `--noWait` flag is given. Defaults
|
|
4868
|
+
* to false; short-lived probe tasks (e.g. `ping`) override to true.
|
|
4869
|
+
*
|
|
4870
|
+
* @type {boolean}
|
|
4871
|
+
*/
|
|
4872
|
+
static defaultWaitForResult = false;
|
|
4873
|
+
/**
|
|
4874
|
+
* @param {object} context Runner context (db, logger, params, emitter...).
|
|
4875
|
+
* @param {object} task Task row as claimed from the queue.
|
|
4876
|
+
*/
|
|
4262
4877
|
constructor(context, task) {
|
|
4263
4878
|
this.context = context;
|
|
4264
4879
|
this.task = task;
|
|
4265
4880
|
}
|
|
4881
|
+
/**
|
|
4882
|
+
* Return a short reason string when the task should be deferred (e.g. "locked
|
|
4883
|
+
* by source"), or `false`/falsy when it is free to run. Default: always `false`.
|
|
4884
|
+
*
|
|
4885
|
+
* @returns {string | false | Promise<string | false>}
|
|
4886
|
+
*/
|
|
4266
4887
|
cantRunReason() {
|
|
4267
4888
|
return false;
|
|
4268
4889
|
}
|
|
4890
|
+
/**
|
|
4891
|
+
* Called by the runner when a stop has been requested. Subclasses running
|
|
4892
|
+
* long loops should flip a flag here and check it between iterations.
|
|
4893
|
+
*
|
|
4894
|
+
* @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
|
|
4895
|
+
*/
|
|
4269
4896
|
requestStop(_allowanceMs) {
|
|
4270
4897
|
}
|
|
4898
|
+
/**
|
|
4899
|
+
* Perform the task. Must be implemented by subclasses.
|
|
4900
|
+
*
|
|
4901
|
+
* @param {(progress: unknown) => Promise<void>} _reportProgress
|
|
4902
|
+
* Updates the DB `progress` column. Accepts any serializable value;
|
|
4903
|
+
* strings are stored verbatim, objects are JSON-stringified.
|
|
4904
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
4905
|
+
*/
|
|
4906
|
+
async run(_reportProgress) {
|
|
4907
|
+
throw new Error("AbstractTask.run must be implemented by subclass");
|
|
4908
|
+
}
|
|
4909
|
+
/**
|
|
4910
|
+
* Resolve a complete row payload for this task — envelope fields (queue,
|
|
4911
|
+
* priority, targeting, schedule…) plus the inner `params` blob produced by
|
|
4912
|
+
* {@link AbstractTask.resolveCustomParams}. Output shape matches
|
|
4913
|
+
* {@link enqueueTask}'s `options` argument, so the typical call is:
|
|
4914
|
+
*
|
|
4915
|
+
* const payload = await TaskClass.resolveParams(context, { name });
|
|
4916
|
+
* await enqueueTask(context, payload);
|
|
4917
|
+
*
|
|
4918
|
+
* Validation failures throw {@link ParamError} so the script aborts before
|
|
4919
|
+
* a malformed row hits the DB.
|
|
4920
|
+
*
|
|
4921
|
+
* @param {object} context
|
|
4922
|
+
* @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
|
|
4923
|
+
* Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
|
|
4924
|
+
* `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
|
|
4925
|
+
* `nextRunAt`, plus `params` (object — overlay onto inner blob).
|
|
4926
|
+
* @returns {Promise<object>}
|
|
4927
|
+
*/
|
|
4928
|
+
static async resolveParams(context, overrides = {}) {
|
|
4929
|
+
const main = _AbstractTask._resolveMainFields(context, overrides);
|
|
4930
|
+
const params = await this.resolveCustomParams(context, overrides);
|
|
4931
|
+
return { ...main, params };
|
|
4932
|
+
}
|
|
4933
|
+
/**
|
|
4934
|
+
* Resolve the inner JSON blob stored in the `params` column. Default
|
|
4935
|
+
* implementation passes through `--paramsJson` (parsed as a JSON object)
|
|
4936
|
+
* overlaid with `overrides.params` when supplied; returns `null` when
|
|
4937
|
+
* neither is provided.
|
|
4938
|
+
*
|
|
4939
|
+
* Subclasses with typed fields should override and call
|
|
4940
|
+
* {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
|
|
4941
|
+
* resolution, then validate and throw {@link ParamError} on bad input.
|
|
4942
|
+
*
|
|
4943
|
+
* @param {object} context
|
|
4944
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4945
|
+
* @returns {Promise<object|null>}
|
|
4946
|
+
*/
|
|
4947
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
4948
|
+
return _AbstractTask._defaultParamsBlob(context, overrides);
|
|
4949
|
+
}
|
|
4950
|
+
/**
|
|
4951
|
+
* Read main task envelope fields from `context.params` (CLI/env), with
|
|
4952
|
+
* any matching key on `overrides` taking precedence. Internal; called by
|
|
4953
|
+
* {@link AbstractTask.resolveParams}.
|
|
4954
|
+
*
|
|
4955
|
+
* @param {object} context
|
|
4956
|
+
* @param {Record<string, unknown>} [overrides]
|
|
4957
|
+
* @returns {object}
|
|
4958
|
+
*/
|
|
4959
|
+
static _resolveMainFields(context, overrides = {}) {
|
|
4960
|
+
const defs = {
|
|
4961
|
+
queueName: "string default tasks",
|
|
4962
|
+
priority: "number default 50",
|
|
4963
|
+
serviceGroup: "string",
|
|
4964
|
+
serviceName: "string",
|
|
4965
|
+
instanceNumber: "number",
|
|
4966
|
+
serverName: "string",
|
|
4967
|
+
opid: "string",
|
|
4968
|
+
schedule: "string"
|
|
4969
|
+
};
|
|
4970
|
+
const cli = context.params.getAllForModule("task-envelope", defs);
|
|
4971
|
+
const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
|
|
4972
|
+
if (!name) {
|
|
4973
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
4974
|
+
}
|
|
4975
|
+
let instanceNumber;
|
|
4976
|
+
const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
|
|
4977
|
+
if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
|
|
4978
|
+
const n = Number(rawInstance);
|
|
4979
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
4980
|
+
throw new ParamError("--instanceNumber must be a positive integer when set");
|
|
4981
|
+
}
|
|
4982
|
+
instanceNumber = n;
|
|
4983
|
+
} else {
|
|
4984
|
+
instanceNumber = null;
|
|
4985
|
+
}
|
|
4986
|
+
const priorityRaw = overrides.priority ?? cli.priority ?? 50;
|
|
4987
|
+
const priority = Number(priorityRaw);
|
|
4988
|
+
if (!Number.isFinite(priority)) {
|
|
4989
|
+
throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
|
|
4990
|
+
}
|
|
4991
|
+
return {
|
|
4992
|
+
name,
|
|
4993
|
+
queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
|
|
4994
|
+
priority,
|
|
4995
|
+
serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
|
|
4996
|
+
serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
|
|
4997
|
+
instanceNumber,
|
|
4998
|
+
serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
|
|
4999
|
+
opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
|
|
5000
|
+
schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
|
|
5001
|
+
nextRunAt: overrides.nextRunAt ?? null
|
|
5002
|
+
};
|
|
5003
|
+
}
|
|
5004
|
+
/**
|
|
5005
|
+
* Default inner-params resolver: parses `--paramsJson` (must be a JSON
|
|
5006
|
+
* object), then overlays `overrides.params` on top. Returns `null` when
|
|
5007
|
+
* neither is provided.
|
|
5008
|
+
*
|
|
5009
|
+
* @param {object} context
|
|
5010
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5011
|
+
* @returns {object|null}
|
|
5012
|
+
*/
|
|
5013
|
+
static _defaultParamsBlob(context, overrides = {}) {
|
|
5014
|
+
const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
|
|
5015
|
+
const fromJson = parseParamsJson(cli.paramsJson);
|
|
5016
|
+
const fromOverride = pickParamsObject(overrides);
|
|
5017
|
+
if (!fromJson && !fromOverride) return null;
|
|
5018
|
+
return { ...fromJson ?? {}, ...fromOverride ?? {} };
|
|
5019
|
+
}
|
|
5020
|
+
/**
|
|
5021
|
+
* Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
|
|
5022
|
+
* params (per `defs`) plus `--paramsJson` under a module namespace, then
|
|
5023
|
+
* merges them with explicit `overrides.params` in increasing priority:
|
|
5024
|
+
*
|
|
5025
|
+
* typed CLI flags → --paramsJson → overrides.params
|
|
5026
|
+
*
|
|
5027
|
+
* Undefined values are dropped so defaults declared in `defs` aren't
|
|
5028
|
+
* overwritten by missing-flag noise. Returns the merged object; the
|
|
5029
|
+
* caller is responsible for validation and throwing `ParamError`.
|
|
5030
|
+
*
|
|
5031
|
+
* @param {object} context
|
|
5032
|
+
* @param {string} moduleName Namespace for `--showUsedParams` grouping.
|
|
5033
|
+
* @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
|
|
5034
|
+
* @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
|
|
5035
|
+
* @returns {Record<string, unknown>}
|
|
5036
|
+
*/
|
|
5037
|
+
static _mergeTypedParams(context, moduleName, defs, overrides = {}) {
|
|
5038
|
+
const fullDefs = { ...defs, paramsJson: "string" };
|
|
5039
|
+
const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
|
|
5040
|
+
const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
|
|
5041
|
+
const fromCli = {};
|
|
5042
|
+
for (const [k, v] of Object.entries(cliRaw)) {
|
|
5043
|
+
if (k === "paramsJson") continue;
|
|
5044
|
+
if (v !== void 0 && v !== null) fromCli[k] = v;
|
|
5045
|
+
}
|
|
5046
|
+
const fromOverride = pickParamsObject(overrides) ?? {};
|
|
5047
|
+
return { ...fromCli, ...fromJson, ...fromOverride };
|
|
5048
|
+
}
|
|
4271
5049
|
};
|
|
5050
|
+
function emptyToUndef(s) {
|
|
5051
|
+
if (s === void 0 || s === null) return void 0;
|
|
5052
|
+
if (typeof s !== "string") return s;
|
|
5053
|
+
const t = s.trim();
|
|
5054
|
+
return t.length ? t : void 0;
|
|
5055
|
+
}
|
|
5056
|
+
function parseParamsJson(raw) {
|
|
5057
|
+
if (raw == null) return null;
|
|
5058
|
+
const t = String(raw).trim();
|
|
5059
|
+
if (!t) return null;
|
|
5060
|
+
let parsed;
|
|
5061
|
+
try {
|
|
5062
|
+
parsed = JSON.parse(t);
|
|
5063
|
+
} catch (e) {
|
|
5064
|
+
throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
|
|
5065
|
+
}
|
|
5066
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5067
|
+
throw new ParamError("--paramsJson must be a JSON object");
|
|
5068
|
+
}
|
|
5069
|
+
return parsed;
|
|
5070
|
+
}
|
|
5071
|
+
function pickParamsObject(overrides) {
|
|
5072
|
+
const p = overrides?.params;
|
|
5073
|
+
if (p && typeof p === "object" && !Array.isArray(p)) return p;
|
|
5074
|
+
return void 0;
|
|
5075
|
+
}
|
|
4272
5076
|
|
|
4273
|
-
// src/tasks/coreTasks/TaskPing.
|
|
4274
|
-
var TaskPing = class extends
|
|
5077
|
+
// src/tasks/coreTasks/TaskPing.js
|
|
5078
|
+
var TaskPing = class extends AbstractTask {
|
|
5079
|
+
/** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
|
|
5080
|
+
static defaultWaitForResult = true;
|
|
5081
|
+
/** Ping takes no params. */
|
|
5082
|
+
static async resolveCustomParams() {
|
|
5083
|
+
return null;
|
|
5084
|
+
}
|
|
5085
|
+
/**
|
|
5086
|
+
* @returns {Promise<{ success: true, results: "pong" }>}
|
|
5087
|
+
*/
|
|
4275
5088
|
async run() {
|
|
4276
5089
|
this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
|
|
4277
5090
|
return { success: true, results: "pong" };
|
|
4278
5091
|
}
|
|
4279
5092
|
};
|
|
4280
5093
|
|
|
4281
|
-
// src/tasks/coreTasks/TaskSampleProcess.
|
|
4282
|
-
var TaskSampleProcess = class extends
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
5094
|
+
// src/tasks/coreTasks/TaskSampleProcess.js
|
|
5095
|
+
var TaskSampleProcess = class extends AbstractTask {
|
|
5096
|
+
/**
|
|
5097
|
+
* @param {object} context
|
|
5098
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5099
|
+
* @returns {Promise<{ total: number, delay: number, name?: string }>}
|
|
5100
|
+
*/
|
|
5101
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5102
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
|
|
5103
|
+
total: "number default 10",
|
|
5104
|
+
delay: "number default 1000",
|
|
5105
|
+
name: "string"
|
|
5106
|
+
}, overrides);
|
|
5107
|
+
const total = Number(merged.total);
|
|
5108
|
+
const delay = Number(merged.delay);
|
|
5109
|
+
if (!Number.isInteger(total) || total <= 0) {
|
|
5110
|
+
throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
|
|
5111
|
+
}
|
|
5112
|
+
if (!Number.isInteger(delay) || delay < 0) {
|
|
5113
|
+
throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
|
|
5114
|
+
}
|
|
5115
|
+
const out = { total, delay };
|
|
5116
|
+
if (typeof merged.name === "string" && merged.name.trim()) {
|
|
5117
|
+
out.name = merged.name.trim();
|
|
5118
|
+
}
|
|
5119
|
+
return out;
|
|
5120
|
+
}
|
|
5121
|
+
/**
|
|
5122
|
+
* @param {object} context
|
|
5123
|
+
* @param {object} task
|
|
5124
|
+
*/
|
|
5125
|
+
constructor(context, task) {
|
|
5126
|
+
super(context, task);
|
|
5127
|
+
this.stopRequested = false;
|
|
5128
|
+
this.stopAllowanceMs = 0;
|
|
5129
|
+
this.stopDecisionLogged = false;
|
|
5130
|
+
}
|
|
5131
|
+
/**
|
|
5132
|
+
* Runner-facing stop signal. Records the allowance window so the main loop
|
|
5133
|
+
* can decide per-iteration whether to finish or abort early.
|
|
5134
|
+
*
|
|
5135
|
+
* @param {number} allowanceMs
|
|
5136
|
+
*/
|
|
4286
5137
|
requestStop(allowanceMs) {
|
|
4287
5138
|
this.stopRequested = true;
|
|
4288
5139
|
this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
|
|
@@ -4290,6 +5141,14 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4290
5141
|
`[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
|
|
4291
5142
|
);
|
|
4292
5143
|
}
|
|
5144
|
+
/**
|
|
5145
|
+
* Iterate `total` times, sleeping `delay` ms between ticks and reporting
|
|
5146
|
+
* progress every iteration. Validates params up front; invalid values short-
|
|
5147
|
+
* circuit to a structured failure without starting the loop.
|
|
5148
|
+
*
|
|
5149
|
+
* @param {(progress: object) => Promise<void>} reportProgress
|
|
5150
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5151
|
+
*/
|
|
4293
5152
|
async run(reportProgress) {
|
|
4294
5153
|
const totalRaw = this.task?.params?.total ?? 10;
|
|
4295
5154
|
const delayRaw = this.task?.params?.delay ?? 1e3;
|
|
@@ -4371,7 +5230,7 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4371
5230
|
}
|
|
4372
5231
|
};
|
|
4373
5232
|
|
|
4374
|
-
// src/tasks/coreTasks/TaskShellCommand.
|
|
5233
|
+
// src/tasks/coreTasks/TaskShellCommand.js
|
|
4375
5234
|
import { spawn } from "child_process";
|
|
4376
5235
|
function runShellCommand(command, cwd) {
|
|
4377
5236
|
return new Promise((resolve2, reject) => {
|
|
@@ -4401,7 +5260,27 @@ function runShellCommand(command, cwd) {
|
|
|
4401
5260
|
});
|
|
4402
5261
|
});
|
|
4403
5262
|
}
|
|
4404
|
-
var TaskShellCommand = class extends
|
|
5263
|
+
var TaskShellCommand = class extends AbstractTask {
|
|
5264
|
+
/**
|
|
5265
|
+
* @param {object} context
|
|
5266
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5267
|
+
* @returns {Promise<{ command: string, cwd?: string }>}
|
|
5268
|
+
*/
|
|
5269
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5270
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
|
|
5271
|
+
command: "string",
|
|
5272
|
+
cwd: "string"
|
|
5273
|
+
}, overrides);
|
|
5274
|
+
const command = typeof merged.command === "string" ? merged.command.trim() : "";
|
|
5275
|
+
if (!command) {
|
|
5276
|
+
throw new ParamError('shellCommand: param "command" must be a non-empty string');
|
|
5277
|
+
}
|
|
5278
|
+
const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
|
|
5279
|
+
return cwd ? { command, cwd } : { command };
|
|
5280
|
+
}
|
|
5281
|
+
/**
|
|
5282
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5283
|
+
*/
|
|
4405
5284
|
async run() {
|
|
4406
5285
|
const params = this.task?.params;
|
|
4407
5286
|
const commandRaw = typeof params === "string" ? params : params?.command;
|
|
@@ -4450,8 +5329,8 @@ var TaskShellCommand = class extends TaskMaster {
|
|
|
4450
5329
|
}
|
|
4451
5330
|
};
|
|
4452
5331
|
|
|
4453
|
-
// src/tasks/coreTasks/TaskSystemInfo.
|
|
4454
|
-
import
|
|
5332
|
+
// src/tasks/coreTasks/TaskSystemInfo.js
|
|
5333
|
+
import os2 from "os";
|
|
4455
5334
|
import fs4 from "fs/promises";
|
|
4456
5335
|
function toGb(valueBytes) {
|
|
4457
5336
|
return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
|
|
@@ -4470,13 +5349,22 @@ async function getDiskStats() {
|
|
|
4470
5349
|
free: toGb(free)
|
|
4471
5350
|
};
|
|
4472
5351
|
}
|
|
4473
|
-
var TaskSystemInfo = class extends
|
|
5352
|
+
var TaskSystemInfo = class extends AbstractTask {
|
|
5353
|
+
/** Same UX expectation as `ping` — short probe, print the result. */
|
|
5354
|
+
static defaultWaitForResult = true;
|
|
5355
|
+
/** systemInfo takes no params. */
|
|
5356
|
+
static async resolveCustomParams() {
|
|
5357
|
+
return null;
|
|
5358
|
+
}
|
|
5359
|
+
/**
|
|
5360
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5361
|
+
*/
|
|
4474
5362
|
async run() {
|
|
4475
5363
|
try {
|
|
4476
|
-
const totalMemory =
|
|
4477
|
-
const freeMemory =
|
|
5364
|
+
const totalMemory = os2.totalmem();
|
|
5365
|
+
const freeMemory = os2.freemem();
|
|
4478
5366
|
const usedMemory = totalMemory - freeMemory;
|
|
4479
|
-
const cpus =
|
|
5367
|
+
const cpus = os2.cpus();
|
|
4480
5368
|
const cpuUtilization = cpus.map((cpu) => {
|
|
4481
5369
|
const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
|
|
4482
5370
|
const usage = (total - cpu.times.idle) / total * 100;
|
|
@@ -4502,10 +5390,10 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
4502
5390
|
utilization: cpuUtilization
|
|
4503
5391
|
},
|
|
4504
5392
|
runtime: {
|
|
4505
|
-
platform:
|
|
4506
|
-
arch:
|
|
4507
|
-
uptimeSec:
|
|
4508
|
-
hostname:
|
|
5393
|
+
platform: os2.platform(),
|
|
5394
|
+
arch: os2.arch(),
|
|
5395
|
+
uptimeSec: os2.uptime(),
|
|
5396
|
+
hostname: os2.hostname()
|
|
4509
5397
|
}
|
|
4510
5398
|
};
|
|
4511
5399
|
this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
|
|
@@ -4522,8 +5410,31 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
4522
5410
|
}
|
|
4523
5411
|
};
|
|
4524
5412
|
|
|
4525
|
-
// src/tasks/coreTasks/TaskSumAB.
|
|
4526
|
-
var TaskSumAB = class extends
|
|
5413
|
+
// src/tasks/coreTasks/TaskSumAB.js
|
|
5414
|
+
var TaskSumAB = class extends AbstractTask {
|
|
5415
|
+
/** Short, deterministic — wait by default so callers see the sum. */
|
|
5416
|
+
static defaultWaitForResult = true;
|
|
5417
|
+
/**
|
|
5418
|
+
* @param {object} context
|
|
5419
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5420
|
+
* @returns {Promise<{ a: number, b: number }>}
|
|
5421
|
+
*/
|
|
5422
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5423
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
|
|
5424
|
+
a: "number",
|
|
5425
|
+
b: "number"
|
|
5426
|
+
}, overrides);
|
|
5427
|
+
if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
|
|
5428
|
+
throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
|
|
5429
|
+
}
|
|
5430
|
+
if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
|
|
5431
|
+
throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
|
|
5432
|
+
}
|
|
5433
|
+
return { a: merged.a, b: merged.b };
|
|
5434
|
+
}
|
|
5435
|
+
/**
|
|
5436
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5437
|
+
*/
|
|
4527
5438
|
async run() {
|
|
4528
5439
|
const a = this.task?.params?.a;
|
|
4529
5440
|
const b = this.task?.params?.b;
|
|
@@ -4554,8 +5465,46 @@ var TaskSumAB = class extends TaskMaster {
|
|
|
4554
5465
|
}
|
|
4555
5466
|
};
|
|
4556
5467
|
|
|
4557
|
-
// src/tasks/coreTasks/TaskStopRunner.
|
|
4558
|
-
var TaskStopRunner = class extends
|
|
5468
|
+
// src/tasks/coreTasks/TaskStopRunner.js
|
|
5469
|
+
var TaskStopRunner = class extends AbstractTask {
|
|
5470
|
+
/**
|
|
5471
|
+
* Stop tasks must target a concrete instance — without `serviceName` the
|
|
5472
|
+
* row would race against any worker on the queue. Layered on top of the
|
|
5473
|
+
* envelope built by {@link AbstractTask.resolveParams}.
|
|
5474
|
+
*
|
|
5475
|
+
* @param {object} context
|
|
5476
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5477
|
+
* @returns {Promise<object>}
|
|
5478
|
+
*/
|
|
5479
|
+
static async resolveParams(context, overrides = {}) {
|
|
5480
|
+
const main = await super.resolveParams(context, overrides);
|
|
5481
|
+
if (!main.serviceName) {
|
|
5482
|
+
throw new ParamError(
|
|
5483
|
+
"stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
|
|
5484
|
+
);
|
|
5485
|
+
}
|
|
5486
|
+
return main;
|
|
5487
|
+
}
|
|
5488
|
+
/**
|
|
5489
|
+
* @param {object} context
|
|
5490
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5491
|
+
* @returns {Promise<{ allowanceMs: number }>}
|
|
5492
|
+
*/
|
|
5493
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5494
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
|
|
5495
|
+
allowanceMs: "number default 5000"
|
|
5496
|
+
}, overrides);
|
|
5497
|
+
const allowanceMs = Number(merged.allowanceMs);
|
|
5498
|
+
if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
|
|
5499
|
+
throw new ParamError(
|
|
5500
|
+
`stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
|
|
5501
|
+
);
|
|
5502
|
+
}
|
|
5503
|
+
return { allowanceMs };
|
|
5504
|
+
}
|
|
5505
|
+
/**
|
|
5506
|
+
* @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
|
|
5507
|
+
*/
|
|
4559
5508
|
async run() {
|
|
4560
5509
|
const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
|
|
4561
5510
|
this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
|
|
@@ -4570,175 +5519,399 @@ var TaskStopRunner = class extends TaskMaster {
|
|
|
4570
5519
|
}
|
|
4571
5520
|
};
|
|
4572
5521
|
|
|
4573
|
-
// src/tasks/
|
|
5522
|
+
// src/tasks/coreTasks/TaskGetLogs.js
|
|
5523
|
+
var TaskGetLogs = class extends AbstractTask {
|
|
5524
|
+
/**
|
|
5525
|
+
* @param {object} context
|
|
5526
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5527
|
+
* @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
|
|
5528
|
+
*/
|
|
5529
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5530
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
|
|
5531
|
+
source: "string",
|
|
5532
|
+
resource: "string",
|
|
5533
|
+
tail: "number default 100",
|
|
5534
|
+
afterTs: "string"
|
|
5535
|
+
}, overrides);
|
|
5536
|
+
const source = typeof merged.source === "string" ? merged.source.trim() : "";
|
|
5537
|
+
const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
|
|
5538
|
+
if (!source) throw new ParamError('getLogs: param "source" is required');
|
|
5539
|
+
if (!resource) throw new ParamError('getLogs: param "resource" is required');
|
|
5540
|
+
let tail = Number(merged.tail);
|
|
5541
|
+
if (!Number.isFinite(tail) || tail < 1) tail = 100;
|
|
5542
|
+
tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
|
|
5543
|
+
const out = { source, resource, tail };
|
|
5544
|
+
if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
|
|
5545
|
+
out.afterTs = merged.afterTs.trim();
|
|
5546
|
+
}
|
|
5547
|
+
return out;
|
|
5548
|
+
}
|
|
5549
|
+
/**
|
|
5550
|
+
* @param {unknown} _reportProgress Unused (single-shot read, no progress events).
|
|
5551
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5552
|
+
*/
|
|
5553
|
+
async run(_reportProgress) {
|
|
5554
|
+
const p = this.task.params ?? {};
|
|
5555
|
+
const source = String(p.source ?? "").trim();
|
|
5556
|
+
const resource = String(p.resource ?? "").trim();
|
|
5557
|
+
const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
|
|
5558
|
+
const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
|
|
5559
|
+
if (!source || !resource) {
|
|
5560
|
+
return {
|
|
5561
|
+
success: false,
|
|
5562
|
+
results: { error: 'getLogs requires params "source" and "resource"' }
|
|
5563
|
+
};
|
|
5564
|
+
}
|
|
5565
|
+
try {
|
|
5566
|
+
const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
|
|
5567
|
+
source,
|
|
5568
|
+
resource,
|
|
5569
|
+
tail,
|
|
5570
|
+
afterTs
|
|
5571
|
+
});
|
|
5572
|
+
return {
|
|
5573
|
+
success: true,
|
|
5574
|
+
results: { records, latestTs, source, resource }
|
|
5575
|
+
};
|
|
5576
|
+
} catch (e) {
|
|
5577
|
+
return {
|
|
5578
|
+
success: false,
|
|
5579
|
+
results: { error: e?.message ?? String(e) }
|
|
5580
|
+
};
|
|
5581
|
+
}
|
|
5582
|
+
}
|
|
5583
|
+
};
|
|
5584
|
+
|
|
5585
|
+
// src/tasks/TasksRegistry.js
|
|
4574
5586
|
var TasksRegistry = class _TasksRegistry {
|
|
4575
|
-
|
|
5587
|
+
/**
|
|
5588
|
+
* @param {Record<string, Function>} [initial] Optional seed entries to copy in.
|
|
5589
|
+
*/
|
|
4576
5590
|
constructor(initial) {
|
|
5591
|
+
this.map = {};
|
|
4577
5592
|
if (initial) {
|
|
4578
5593
|
this.addMany(initial);
|
|
4579
5594
|
}
|
|
4580
5595
|
}
|
|
5596
|
+
/**
|
|
5597
|
+
* Build a registry pre-populated with every core task plus legacy aliases.
|
|
5598
|
+
* Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
|
|
5599
|
+
*
|
|
5600
|
+
* @returns {TasksRegistry}
|
|
5601
|
+
*/
|
|
4581
5602
|
static withCoreTasks() {
|
|
4582
|
-
return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
|
|
5603
|
+
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);
|
|
4583
5604
|
}
|
|
5605
|
+
/**
|
|
5606
|
+
* Register a single task class under a name. Overwrites any previous entry.
|
|
5607
|
+
*
|
|
5608
|
+
* @param {string} taskName
|
|
5609
|
+
* @param {Function} taskClass Subclass of `AbstractTask`.
|
|
5610
|
+
* @returns {this}
|
|
5611
|
+
*/
|
|
4584
5612
|
add(taskName, taskClass) {
|
|
4585
5613
|
this.map[taskName] = taskClass;
|
|
4586
5614
|
return this;
|
|
4587
5615
|
}
|
|
5616
|
+
/**
|
|
5617
|
+
* Bulk-register a name → class map. Later calls override earlier ones.
|
|
5618
|
+
*
|
|
5619
|
+
* @param {Record<string, Function>} entries
|
|
5620
|
+
* @returns {this}
|
|
5621
|
+
*/
|
|
4588
5622
|
addMany(entries) {
|
|
4589
5623
|
for (const [name, klass] of Object.entries(entries)) {
|
|
4590
5624
|
this.add(name, klass);
|
|
4591
5625
|
}
|
|
4592
5626
|
return this;
|
|
4593
5627
|
}
|
|
5628
|
+
/**
|
|
5629
|
+
* Look up a task class by name. Returns `undefined` when the name is unknown;
|
|
5630
|
+
* the runner treats that as "some other worker may handle this" and skips.
|
|
5631
|
+
*
|
|
5632
|
+
* @param {string} taskName
|
|
5633
|
+
* @returns {Function | undefined}
|
|
5634
|
+
*/
|
|
4594
5635
|
get(taskName) {
|
|
4595
5636
|
return this.map[taskName];
|
|
4596
5637
|
}
|
|
5638
|
+
/**
|
|
5639
|
+
* Strict variant of {@link get}: throws {@link ParamError} (with the list
|
|
5640
|
+
* of supported names) when `taskName` is unknown. Use from enqueuer code
|
|
5641
|
+
* paths where an unknown name is a hard CLI/programmer error.
|
|
5642
|
+
*
|
|
5643
|
+
* @param {string} taskName
|
|
5644
|
+
* @returns {Function}
|
|
5645
|
+
*/
|
|
5646
|
+
requireClass(taskName) {
|
|
5647
|
+
const TaskClass = taskName ? this.map[taskName] : void 0;
|
|
5648
|
+
if (!TaskClass) {
|
|
5649
|
+
const supported = this.listSupportedTasks().join(", ") || "(none)";
|
|
5650
|
+
throw new ParamError(
|
|
5651
|
+
`Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
|
|
5652
|
+
);
|
|
5653
|
+
}
|
|
5654
|
+
return TaskClass;
|
|
5655
|
+
}
|
|
5656
|
+
/**
|
|
5657
|
+
* Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
|
|
5658
|
+
* from `overrides` or `context.params`, look up the class, and delegate
|
|
5659
|
+
* to its static {@link AbstractTask.resolveParams} with `name` seeded into
|
|
5660
|
+
* the overrides. The returned object is shaped for {@link enqueueTask}.
|
|
5661
|
+
*
|
|
5662
|
+
* Validation failures (unknown task, missing required custom params, etc.)
|
|
5663
|
+
* surface as {@link ParamError} so the caller aborts cleanly before any
|
|
5664
|
+
* row is inserted.
|
|
5665
|
+
*
|
|
5666
|
+
* @param {object} context
|
|
5667
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5668
|
+
* @returns {Promise<object>}
|
|
5669
|
+
*/
|
|
5670
|
+
async resolveTaskParams(context, overrides = {}) {
|
|
5671
|
+
const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
|
|
5672
|
+
const fromCli = context.params.get("name", "string");
|
|
5673
|
+
const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
|
|
5674
|
+
const name = overrideName || cliName;
|
|
5675
|
+
if (!name) {
|
|
5676
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
5677
|
+
}
|
|
5678
|
+
const TaskClass = this.requireClass(name);
|
|
5679
|
+
return TaskClass.resolveParams(context, { ...overrides, name });
|
|
5680
|
+
}
|
|
5681
|
+
/**
|
|
5682
|
+
* Names of every registered task, sorted alphabetically (useful for CLI output
|
|
5683
|
+
* and allowlist sanity checks).
|
|
5684
|
+
*
|
|
5685
|
+
* @returns {string[]}
|
|
5686
|
+
*/
|
|
4597
5687
|
listSupportedTasks() {
|
|
4598
5688
|
return Object.keys(this.map).sort();
|
|
4599
5689
|
}
|
|
5690
|
+
/**
|
|
5691
|
+
* Shallow copy of the internal map, for handing to `addMany` on another registry
|
|
5692
|
+
* or for serialization.
|
|
5693
|
+
*
|
|
5694
|
+
* @returns {Record<string, Function>}
|
|
5695
|
+
*/
|
|
4600
5696
|
toObject() {
|
|
4601
5697
|
return { ...this.map };
|
|
4602
5698
|
}
|
|
4603
5699
|
};
|
|
4604
5700
|
|
|
4605
|
-
// src/tasks/
|
|
5701
|
+
// src/tasks/serviceTaskAllowlist.js
|
|
5702
|
+
var SERVICE_TASK_NAMES = [
|
|
5703
|
+
"ping",
|
|
5704
|
+
"stop",
|
|
5705
|
+
"stopRunner",
|
|
5706
|
+
"shellCommand",
|
|
5707
|
+
"systemInfo",
|
|
5708
|
+
"info",
|
|
5709
|
+
"getLogs"
|
|
5710
|
+
];
|
|
5711
|
+
function normalizeAllowedTasks(value) {
|
|
5712
|
+
if (!value) return void 0;
|
|
5713
|
+
if (Array.isArray(value)) {
|
|
5714
|
+
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
5715
|
+
return out2.length ? out2 : void 0;
|
|
5716
|
+
}
|
|
5717
|
+
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
5718
|
+
return out.length ? out : void 0;
|
|
5719
|
+
}
|
|
5720
|
+
function mergeAllowedTasksWithServiceTasks(names) {
|
|
5721
|
+
const set = /* @__PURE__ */ new Set([...SERVICE_TASK_NAMES, ...names ?? []]);
|
|
5722
|
+
return Array.from(set).sort();
|
|
5723
|
+
}
|
|
5724
|
+
|
|
5725
|
+
// src/tasks/taskScriptRunner.js
|
|
4606
5726
|
import { spawn as spawn2 } from "child_process";
|
|
5727
|
+
var MAX_PROGRESS_TEXT_LEN = 4e3;
|
|
4607
5728
|
function toCliArgs(args = []) {
|
|
4608
5729
|
return args.filter((a) => typeof a === "string" && a.length > 0);
|
|
4609
5730
|
}
|
|
4610
5731
|
function formatChildLogPrefix(task) {
|
|
4611
|
-
return `${task.
|
|
5732
|
+
return `${task.name}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
|
|
4612
5733
|
}
|
|
4613
|
-
|
|
4614
|
-
|
|
5734
|
+
function isProgressPayload(payload) {
|
|
5735
|
+
if (!payload || typeof payload !== "object") return false;
|
|
5736
|
+
if (payload.level !== "progress") return false;
|
|
5737
|
+
const count = Number(payload.count);
|
|
5738
|
+
const total = Number(payload.total);
|
|
5739
|
+
return Number.isFinite(count) && Number.isFinite(total) && total > 0;
|
|
5740
|
+
}
|
|
5741
|
+
function formatProgressText(payload, fallbackPrefix) {
|
|
5742
|
+
const pfx = payload.prefix ? `${payload.prefix} ` : fallbackPrefix ? `${fallbackPrefix} ` : "";
|
|
5743
|
+
const label = typeof payload.message === "string" && payload.message ? `${payload.message} ` : "";
|
|
5744
|
+
return `${pfx}${label}${payload.count}/${payload.total}`;
|
|
5745
|
+
}
|
|
5746
|
+
function forwardChildLogToParent(context, prefix, message) {
|
|
5747
|
+
if (!message || typeof message !== "object") return;
|
|
5748
|
+
const text = typeof message.message === "string" ? message.message : null;
|
|
5749
|
+
if (!text) return;
|
|
5750
|
+
const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
|
|
5751
|
+
const line = `[child:${prefix}] ${text}`;
|
|
5752
|
+
const logger = context.logger;
|
|
5753
|
+
switch (level) {
|
|
5754
|
+
case "error":
|
|
5755
|
+
case "fatal":
|
|
5756
|
+
logger.error?.(line);
|
|
5757
|
+
return;
|
|
5758
|
+
case "warn":
|
|
5759
|
+
case "warning":
|
|
5760
|
+
logger.warn?.(line);
|
|
5761
|
+
return;
|
|
5762
|
+
case "debug":
|
|
5763
|
+
logger.debug?.(line);
|
|
5764
|
+
return;
|
|
5765
|
+
case "info":
|
|
5766
|
+
default:
|
|
5767
|
+
logger.info?.(line);
|
|
5768
|
+
}
|
|
5769
|
+
}
|
|
5770
|
+
function buildNodeArgs(scriptPath, cliArgs) {
|
|
4615
5771
|
const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
|
|
4616
5772
|
const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
}
|
|
4630
|
-
}
|
|
5773
|
+
return hasTsRuntimeInParent ? [...inheritedExecArgs, scriptPath, ...cliArgs] : ["--import", "tsx", scriptPath, ...cliArgs];
|
|
5774
|
+
}
|
|
5775
|
+
function resolveTasksTableName(context) {
|
|
5776
|
+
return context.tasksQueueName || context.params?.get?.("table") || "tasks";
|
|
5777
|
+
}
|
|
5778
|
+
function announceIpcFileLogsTarget(context, options) {
|
|
5779
|
+
if (!options.ipcFileLogs) return;
|
|
5780
|
+
const logsDir = resolveIpcFileLogsDir(context, options.ipcFileLogs);
|
|
5781
|
+
const enabledRaw = context.params?.get?.("tasksLogsEnabled");
|
|
5782
|
+
const logsEnabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
5783
|
+
context.logger.info?.(
|
|
5784
|
+
`[tasks] IPC file logs: ${logsDir}` + (logsEnabled ? "" : " (tasksLogsEnabled=false; not persisted)")
|
|
4631
5785
|
);
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
let
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
|
|
4645
|
-
}).catch((error) => {
|
|
4646
|
-
context.logger.warn?.(
|
|
4647
|
-
`[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
4648
|
-
);
|
|
4649
|
-
});
|
|
4650
|
-
if (options.onProgress) {
|
|
4651
|
-
progressCallbackChain = progressCallbackChain.then(async () => {
|
|
4652
|
-
await options.onProgress?.(text.slice(0, 4e3));
|
|
4653
|
-
}).catch((error) => {
|
|
4654
|
-
context.logger.warn?.(
|
|
4655
|
-
`[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
4656
|
-
);
|
|
5786
|
+
}
|
|
5787
|
+
function createSerializedQueue() {
|
|
5788
|
+
let chain = Promise.resolve();
|
|
5789
|
+
return {
|
|
5790
|
+
push(fn) {
|
|
5791
|
+
chain = chain.then(fn, () => {
|
|
5792
|
+
}).catch(() => {
|
|
5793
|
+
});
|
|
5794
|
+
return chain;
|
|
5795
|
+
},
|
|
5796
|
+
drain() {
|
|
5797
|
+
return chain.catch(() => {
|
|
4657
5798
|
});
|
|
4658
5799
|
}
|
|
4659
5800
|
};
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
5801
|
+
}
|
|
5802
|
+
async function runNodeTaskScript(context, options) {
|
|
5803
|
+
const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
|
|
5804
|
+
const nodeArgs = buildNodeArgs(options.scriptPath, cliArgs);
|
|
5805
|
+
const child = spawn2(process.execPath, nodeArgs, {
|
|
5806
|
+
cwd: options.cwd || process.cwd(),
|
|
5807
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
5808
|
+
env: {
|
|
5809
|
+
...process.env,
|
|
5810
|
+
TASK_ID: options.task.id,
|
|
5811
|
+
TASK_NAME: options.task.name,
|
|
5812
|
+
TASK_OPID: options.task.opid || ""
|
|
5813
|
+
}
|
|
5814
|
+
});
|
|
5815
|
+
announceIpcFileLogsTarget(context, options);
|
|
5816
|
+
const prefix = formatChildLogPrefix(options.task);
|
|
5817
|
+
const tasksTable = resolveTasksTableName(context);
|
|
5818
|
+
const progressQueue = createSerializedQueue();
|
|
5819
|
+
const state = {
|
|
5820
|
+
stdout: "",
|
|
5821
|
+
stderr: "",
|
|
5822
|
+
workerResult: null,
|
|
5823
|
+
hadErrorMessage: false
|
|
5824
|
+
};
|
|
5825
|
+
const writeProgress = (text) => {
|
|
5826
|
+
const trimmed = typeof text === "string" ? text.slice(0, MAX_PROGRESS_TEXT_LEN) : "";
|
|
5827
|
+
if (!trimmed) return;
|
|
5828
|
+
progressQueue.push(async () => {
|
|
5829
|
+
const db = context.db;
|
|
5830
|
+
if (db) {
|
|
5831
|
+
try {
|
|
5832
|
+
await db(tasksTable).where({ id: options.task.id }).update({ progress: trimmed });
|
|
5833
|
+
} catch (error) {
|
|
5834
|
+
context.logger.warn?.(
|
|
5835
|
+
`[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
5836
|
+
);
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
5839
|
+
if (options.onProgress) {
|
|
5840
|
+
try {
|
|
5841
|
+
await options.onProgress(trimmed);
|
|
5842
|
+
} catch (error) {
|
|
5843
|
+
context.logger.warn?.(
|
|
5844
|
+
`[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
5845
|
+
);
|
|
5846
|
+
}
|
|
5847
|
+
}
|
|
5848
|
+
});
|
|
4673
5849
|
};
|
|
4674
|
-
child.stdout
|
|
5850
|
+
child.stdout?.on("data", (chunk) => {
|
|
4675
5851
|
const text = String(chunk);
|
|
4676
|
-
stdout += text;
|
|
5852
|
+
state.stdout += text;
|
|
4677
5853
|
if (text.trim()) {
|
|
4678
5854
|
context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
4679
|
-
updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
|
|
4680
5855
|
}
|
|
4681
5856
|
});
|
|
4682
|
-
child.stderr
|
|
5857
|
+
child.stderr?.on("data", (chunk) => {
|
|
4683
5858
|
const text = String(chunk);
|
|
4684
|
-
stderr += text;
|
|
5859
|
+
state.stderr += text;
|
|
4685
5860
|
if (text.trim()) {
|
|
4686
5861
|
context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
4687
5862
|
}
|
|
4688
5863
|
});
|
|
4689
5864
|
child.on("message", (message) => {
|
|
4690
5865
|
if (message && typeof message === "object" && "__taskWorkerResult" in message) {
|
|
4691
|
-
workerResult = message.__taskWorkerResult;
|
|
5866
|
+
state.workerResult = message.__taskWorkerResult;
|
|
4692
5867
|
return;
|
|
4693
5868
|
}
|
|
4694
5869
|
if (message && typeof message === "object") {
|
|
4695
5870
|
const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
|
|
4696
5871
|
if (level === "error" || level === "fatal") {
|
|
4697
|
-
hadErrorMessage = true;
|
|
5872
|
+
state.hadErrorMessage = true;
|
|
4698
5873
|
}
|
|
4699
5874
|
}
|
|
4700
|
-
appendTaskIpcLog(context, options.task, message);
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
const countNum = Number(String(message.count).trim());
|
|
4706
|
-
const totalNum = Number(message.total);
|
|
4707
|
-
if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
|
|
4708
|
-
context.logger.progress(message.message || "progress", {
|
|
4709
|
-
prefix: message.prefix || prefix,
|
|
4710
|
-
count: countNum,
|
|
4711
|
-
total: totalNum
|
|
4712
|
-
});
|
|
4713
|
-
} else {
|
|
4714
|
-
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
4715
|
-
}
|
|
4716
|
-
} else {
|
|
4717
|
-
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
4718
|
-
}
|
|
5875
|
+
appendTaskIpcLog(context, options.task, message, options.ipcFileLogs);
|
|
5876
|
+
try {
|
|
5877
|
+
options.onChildIpcMessage?.(message);
|
|
5878
|
+
} catch (e) {
|
|
5879
|
+
context.logger.warn?.(`[tasks] onChildIpcMessage failed: ${e?.message ?? String(e)}`);
|
|
4719
5880
|
}
|
|
5881
|
+
if (isProgressPayload(message)) {
|
|
5882
|
+
context.logger.progress(message.message || "progress", {
|
|
5883
|
+
prefix: message.prefix || prefix,
|
|
5884
|
+
count: Number(message.count),
|
|
5885
|
+
total: Number(message.total)
|
|
5886
|
+
});
|
|
5887
|
+
writeProgress(formatProgressText(message, prefix));
|
|
5888
|
+
return;
|
|
5889
|
+
}
|
|
5890
|
+
forwardChildLogToParent(context, prefix, message);
|
|
4720
5891
|
});
|
|
4721
5892
|
return await new Promise((resolve2, reject) => {
|
|
4722
5893
|
child.on("error", (error) => reject(error));
|
|
4723
5894
|
child.on("close", (exitCode, signal) => {
|
|
4724
|
-
|
|
5895
|
+
void (async () => {
|
|
5896
|
+
await flushTaskIpcLogs(context);
|
|
5897
|
+
await progressQueue.drain();
|
|
4725
5898
|
resolve2({
|
|
4726
5899
|
exitCode,
|
|
4727
5900
|
signal,
|
|
4728
|
-
stdout: stdout.trim(),
|
|
4729
|
-
stderr: stderr.trim(),
|
|
4730
|
-
workerResult,
|
|
4731
|
-
hadErrorMessage
|
|
5901
|
+
stdout: state.stdout.trim(),
|
|
5902
|
+
stderr: state.stderr.trim(),
|
|
5903
|
+
workerResult: state.workerResult,
|
|
5904
|
+
hadErrorMessage: state.hadErrorMessage
|
|
4732
5905
|
});
|
|
4733
|
-
});
|
|
5906
|
+
})();
|
|
4734
5907
|
});
|
|
4735
5908
|
});
|
|
4736
5909
|
}
|
|
4737
5910
|
|
|
4738
|
-
// src/tasks/index.
|
|
5911
|
+
// src/tasks/index.js
|
|
4739
5912
|
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
4740
5913
|
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
4741
|
-
function
|
|
5914
|
+
function getDb3(context) {
|
|
4742
5915
|
const db = context.db;
|
|
4743
5916
|
if (!db) {
|
|
4744
5917
|
throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
|
|
@@ -4750,22 +5923,13 @@ function normalizeRegistry(registry) {
|
|
|
4750
5923
|
if (registry instanceof TasksRegistry) return registry;
|
|
4751
5924
|
return new TasksRegistry().addMany(registry);
|
|
4752
5925
|
}
|
|
4753
|
-
function
|
|
4754
|
-
if (!value) return void 0;
|
|
4755
|
-
if (Array.isArray(value)) {
|
|
4756
|
-
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
4757
|
-
return out2.length ? out2 : void 0;
|
|
4758
|
-
}
|
|
4759
|
-
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
4760
|
-
return out.length ? out : void 0;
|
|
4761
|
-
}
|
|
4762
|
-
async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
|
|
5926
|
+
async function enqueueStopTask(context, serviceGroup, queueName = "tasks", allowanceMs = 5e3) {
|
|
4763
5927
|
return enqueueTask(context, {
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
task: "stopRunner",
|
|
5928
|
+
queueName,
|
|
5929
|
+
name: "stopRunner",
|
|
4767
5930
|
params: { allowanceMs },
|
|
4768
|
-
priority:
|
|
5931
|
+
priority: 0,
|
|
5932
|
+
serviceGroup
|
|
4769
5933
|
});
|
|
4770
5934
|
}
|
|
4771
5935
|
async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
|
|
@@ -4782,19 +5946,21 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
|
|
|
4782
5946
|
context.emitter.emit("stop", allowanceMs);
|
|
4783
5947
|
}
|
|
4784
5948
|
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
4785
|
-
const db =
|
|
4786
|
-
const taskName = row.
|
|
5949
|
+
const db = getDb3(context);
|
|
5950
|
+
const taskName = row.name;
|
|
4787
5951
|
const TaskClass = registry.get(taskName);
|
|
4788
|
-
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
4789
5952
|
if (!TaskClass) {
|
|
4790
5953
|
const err = { message: `Unknown task "${taskName}"` };
|
|
4791
|
-
await db(historyTable).insert(
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
5954
|
+
await db(historyTable).insert(
|
|
5955
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
5956
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
5957
|
+
success: false,
|
|
5958
|
+
status: "failed",
|
|
5959
|
+
status_changed_at: db.fn.now(),
|
|
5960
|
+
params: toJsonColumn(row.params),
|
|
5961
|
+
results: toJsonColumn(err)
|
|
5962
|
+
})
|
|
5963
|
+
);
|
|
4798
5964
|
if (row.schedule) {
|
|
4799
5965
|
await db(tasksTable).where({ id: row.id }).update({
|
|
4800
5966
|
started_at: null,
|
|
@@ -4802,7 +5968,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4802
5968
|
success: false,
|
|
4803
5969
|
results: toJsonColumn(err),
|
|
4804
5970
|
past_due: null,
|
|
4805
|
-
|
|
5971
|
+
status: "paused",
|
|
5972
|
+
status_changed_at: db.fn.now(),
|
|
4806
5973
|
progress: LOCKED_BY_ERROR_MESSAGE
|
|
4807
5974
|
});
|
|
4808
5975
|
} else {
|
|
@@ -4829,13 +5996,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4829
5996
|
} finally {
|
|
4830
5997
|
runningTaskInstances.delete(row.id);
|
|
4831
5998
|
}
|
|
4832
|
-
await db(historyTable).insert(
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
5999
|
+
await db(historyTable).insert(
|
|
6000
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
6001
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
6002
|
+
success,
|
|
6003
|
+
status: success ? "completed" : "failed",
|
|
6004
|
+
status_changed_at: db.fn.now(),
|
|
6005
|
+
params: toJsonColumn(row.params),
|
|
6006
|
+
results: toJsonColumn(results)
|
|
6007
|
+
})
|
|
6008
|
+
);
|
|
4839
6009
|
if (!success) {
|
|
4840
6010
|
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
4841
6011
|
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
@@ -4850,19 +6020,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4850
6020
|
const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
|
|
4851
6021
|
appendTaskIpcLog(context, row, {
|
|
4852
6022
|
level: "error",
|
|
4853
|
-
message: `[tasks] task failed: ${row.
|
|
6023
|
+
message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
|
|
4854
6024
|
details: results
|
|
4855
6025
|
});
|
|
4856
6026
|
}
|
|
4857
6027
|
if (row.schedule) {
|
|
4858
6028
|
if (success) {
|
|
6029
|
+
let nextRunAt = null;
|
|
6030
|
+
try {
|
|
6031
|
+
nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
|
|
6032
|
+
} catch (e) {
|
|
6033
|
+
context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
|
|
6034
|
+
}
|
|
4859
6035
|
await db(tasksTable).where({ id: row.id }).update({
|
|
4860
6036
|
started_at: null,
|
|
4861
6037
|
completed_at: /* @__PURE__ */ new Date(),
|
|
4862
6038
|
success,
|
|
4863
6039
|
results: toJsonColumn(results),
|
|
4864
6040
|
progress: null,
|
|
4865
|
-
past_due: null
|
|
6041
|
+
past_due: null,
|
|
6042
|
+
status: "idle",
|
|
6043
|
+
status_changed_at: db.fn.now(),
|
|
6044
|
+
next_run_at: nextRunAt,
|
|
6045
|
+
// Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
|
|
6046
|
+
service_name: null,
|
|
6047
|
+
server_name: null,
|
|
6048
|
+
instance_number: null
|
|
4866
6049
|
});
|
|
4867
6050
|
} else {
|
|
4868
6051
|
await db(tasksTable).where({ id: row.id }).update({
|
|
@@ -4870,7 +6053,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4870
6053
|
completed_at: /* @__PURE__ */ new Date(),
|
|
4871
6054
|
success,
|
|
4872
6055
|
results: toJsonColumn(results),
|
|
4873
|
-
|
|
6056
|
+
status: "paused",
|
|
6057
|
+
status_changed_at: db.fn.now(),
|
|
4874
6058
|
progress: LOCKED_BY_ERROR_MESSAGE,
|
|
4875
6059
|
past_due: null
|
|
4876
6060
|
});
|
|
@@ -4882,209 +6066,401 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
4882
6066
|
const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
|
|
4883
6067
|
return { stopRunnerRequested, stopAllowanceMs };
|
|
4884
6068
|
}
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
6069
|
+
function shuffleTaskRowsInPlace(rows) {
|
|
6070
|
+
for (let i = rows.length - 1; i > 0; i--) {
|
|
6071
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
6072
|
+
const t = rows[i];
|
|
6073
|
+
rows[i] = rows[j];
|
|
6074
|
+
rows[j] = t;
|
|
6075
|
+
}
|
|
6076
|
+
}
|
|
6077
|
+
async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
|
|
6078
|
+
const db = getDb3(context);
|
|
6079
|
+
let query = db(tasksTable).where({ status: "idle" }).where(function() {
|
|
6080
|
+
this.whereNull("service_group").orWhere({ service_group: serviceGroup });
|
|
6081
|
+
}).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);
|
|
4888
6082
|
if (taskNames && taskNames.length > 0) {
|
|
4889
|
-
query = query.whereIn("
|
|
6083
|
+
query = query.whereIn("name", taskNames);
|
|
6084
|
+
}
|
|
6085
|
+
if (runnerIdentity) {
|
|
6086
|
+
query = query.where(function() {
|
|
6087
|
+
this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
|
|
6088
|
+
}).where(function() {
|
|
6089
|
+
this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
|
|
6090
|
+
}).where(function() {
|
|
6091
|
+
this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
|
|
6092
|
+
});
|
|
6093
|
+
} else {
|
|
6094
|
+
query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
|
|
4890
6095
|
}
|
|
4891
6096
|
const candidates = await query;
|
|
6097
|
+
shuffleTaskRowsInPlace(candidates);
|
|
4892
6098
|
for (const row of candidates) {
|
|
4893
6099
|
if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
|
|
4894
6100
|
continue;
|
|
4895
6101
|
}
|
|
4896
|
-
const TaskClass = registry.get(row.
|
|
4897
|
-
if (TaskClass) {
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
6102
|
+
const TaskClass = registry.get(row.name);
|
|
6103
|
+
if (!TaskClass) {
|
|
6104
|
+
continue;
|
|
6105
|
+
}
|
|
6106
|
+
const taskInstance = new TaskClass(context, row);
|
|
6107
|
+
const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
|
|
6108
|
+
if (reason) {
|
|
6109
|
+
if (!row.past_due) {
|
|
6110
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
6111
|
+
past_due: db.fn.now(),
|
|
6112
|
+
progress: String(reason)
|
|
6113
|
+
});
|
|
4908
6114
|
}
|
|
6115
|
+
continue;
|
|
6116
|
+
}
|
|
6117
|
+
const claimPatch = {
|
|
6118
|
+
started_at: db.fn.now(),
|
|
6119
|
+
status: "running",
|
|
6120
|
+
status_changed_at: db.fn.now()
|
|
6121
|
+
};
|
|
6122
|
+
if (runnerIdentity) {
|
|
6123
|
+
claimPatch.service_name = runnerIdentity.service_name;
|
|
6124
|
+
claimPatch.server_name = runnerIdentity.server_name;
|
|
6125
|
+
claimPatch.instance_number = runnerIdentity.instance_number;
|
|
4909
6126
|
}
|
|
4910
|
-
const updated = await db(tasksTable).where({ id: row.id
|
|
6127
|
+
const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
|
|
4911
6128
|
const claimed = Array.isArray(updated) ? updated[0] : null;
|
|
4912
6129
|
if (claimed) return claimed;
|
|
4913
6130
|
}
|
|
4914
6131
|
return null;
|
|
4915
6132
|
}
|
|
4916
6133
|
async function runTasksLoop(context, options) {
|
|
4917
|
-
const
|
|
6134
|
+
const queueName = options.queueName ?? "tasks";
|
|
4918
6135
|
const target = options.target;
|
|
4919
6136
|
const pollMs = options.pollMs ?? 1e3;
|
|
4920
|
-
const
|
|
6137
|
+
const claimJitterMs = options.claimJitterMs ?? 0;
|
|
6138
|
+
const maxParallel = options.maxParallel ?? 32;
|
|
4921
6139
|
const scanLimit = options.scanLimit ?? 100;
|
|
4922
6140
|
const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
4923
6141
|
const registry = normalizeRegistry(options.registry);
|
|
4924
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
6142
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
4925
6143
|
if (!target) throw new Error("runTasksLoop: target is required");
|
|
6144
|
+
context.tasksQueueName = queueName;
|
|
4926
6145
|
const runningPromises = /* @__PURE__ */ new Set();
|
|
4927
6146
|
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
4928
6147
|
let runningStopControlPromise = null;
|
|
4929
6148
|
let stopRequested = false;
|
|
4930
6149
|
let stopAllowanceMs = 5e3;
|
|
4931
|
-
context.
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
)
|
|
4942
|
-
|
|
4943
|
-
|
|
6150
|
+
context.tasksRunnerStop = false;
|
|
6151
|
+
let registryReg = null;
|
|
6152
|
+
let registryInterval = null;
|
|
6153
|
+
let runnerIdentity = null;
|
|
6154
|
+
const hbGroup = options.runnerServiceGroup?.trim();
|
|
6155
|
+
if (hbGroup) {
|
|
6156
|
+
const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
|
|
6157
|
+
const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
|
|
6158
|
+
const defaultMeta = {
|
|
6159
|
+
component: "tasks-runner",
|
|
6160
|
+
allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
|
|
6161
|
+
};
|
|
6162
|
+
registryReg = await registerInServicesRegistry(context, {
|
|
6163
|
+
queueName,
|
|
6164
|
+
target,
|
|
6165
|
+
serviceGroup: hbGroup,
|
|
6166
|
+
serviceName: options.runnerServiceName,
|
|
6167
|
+
instanceNumber: options.runnerInstanceNumber,
|
|
6168
|
+
staleMs,
|
|
6169
|
+
groupMaxInstances: options.runnerGroupMaxInstances,
|
|
6170
|
+
enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
|
|
6171
|
+
metadata: options.runnerMetadata ?? defaultMeta
|
|
6172
|
+
});
|
|
6173
|
+
runnerIdentity = {
|
|
6174
|
+
service_name: registryReg.serviceName,
|
|
6175
|
+
server_name: os3.hostname(),
|
|
6176
|
+
instance_number: registryReg.instanceNumber
|
|
6177
|
+
};
|
|
6178
|
+
registryInterval = setInterval(() => {
|
|
6179
|
+
void touchServicesRegistry(context, registryReg).catch((err) => {
|
|
6180
|
+
context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
|
|
6181
|
+
});
|
|
6182
|
+
}, hbIntervalMs);
|
|
6183
|
+
}
|
|
6184
|
+
try {
|
|
6185
|
+
while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
|
|
6186
|
+
if (!runningStopControlPromise) {
|
|
6187
|
+
const claimedStopTask = await claimNextRunnableTask(
|
|
4944
6188
|
context,
|
|
4945
6189
|
tasksTable,
|
|
4946
|
-
|
|
4947
|
-
claimedStopTask,
|
|
6190
|
+
target,
|
|
4948
6191
|
registry,
|
|
4949
|
-
|
|
4950
|
-
|
|
6192
|
+
10,
|
|
6193
|
+
["stopRunner", "stop"],
|
|
6194
|
+
runnerIdentity
|
|
6195
|
+
);
|
|
6196
|
+
if (claimedStopTask) {
|
|
6197
|
+
runningStopControlPromise = executeClaimedTask(
|
|
6198
|
+
context,
|
|
6199
|
+
tasksTable,
|
|
6200
|
+
historyTable,
|
|
6201
|
+
claimedStopTask,
|
|
6202
|
+
registry,
|
|
6203
|
+
runningTaskInstances
|
|
6204
|
+
).then(async (outcome) => {
|
|
6205
|
+
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
6206
|
+
stopRequested = true;
|
|
6207
|
+
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
6208
|
+
context.tasksRunnerStop = true;
|
|
6209
|
+
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
6210
|
+
}
|
|
6211
|
+
}).finally(() => {
|
|
6212
|
+
runningStopControlPromise = null;
|
|
6213
|
+
});
|
|
6214
|
+
}
|
|
6215
|
+
}
|
|
6216
|
+
if (claimJitterMs > 0) {
|
|
6217
|
+
await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
|
|
6218
|
+
}
|
|
6219
|
+
while (runningPromises.size < maxParallel) {
|
|
6220
|
+
const claimed = await claimNextRunnableTask(
|
|
6221
|
+
context,
|
|
6222
|
+
tasksTable,
|
|
6223
|
+
target,
|
|
6224
|
+
registry,
|
|
6225
|
+
scanLimit,
|
|
6226
|
+
allowedTasks,
|
|
6227
|
+
runnerIdentity
|
|
6228
|
+
);
|
|
6229
|
+
if (!claimed) break;
|
|
6230
|
+
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
4951
6231
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
4952
6232
|
stopRequested = true;
|
|
4953
6233
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
4954
|
-
context.
|
|
6234
|
+
context.tasksRunnerStop = true;
|
|
4955
6235
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
4956
6236
|
}
|
|
4957
6237
|
}).finally(() => {
|
|
4958
|
-
|
|
6238
|
+
runningPromises.delete(p);
|
|
4959
6239
|
});
|
|
6240
|
+
runningPromises.add(p);
|
|
6241
|
+
}
|
|
6242
|
+
const wakePromises = [...runningPromises];
|
|
6243
|
+
if (runningStopControlPromise) {
|
|
6244
|
+
wakePromises.push(runningStopControlPromise);
|
|
6245
|
+
}
|
|
6246
|
+
if (wakePromises.length === 0) {
|
|
6247
|
+
await sleepMs(pollMs);
|
|
6248
|
+
} else {
|
|
6249
|
+
const safe = wakePromises.map((p) => p.catch(() => void 0));
|
|
6250
|
+
await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
|
|
4960
6251
|
}
|
|
4961
6252
|
}
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
context,
|
|
4965
|
-
tasksTable,
|
|
4966
|
-
target,
|
|
4967
|
-
registry,
|
|
4968
|
-
scanLimit,
|
|
4969
|
-
allowedTasks
|
|
4970
|
-
);
|
|
4971
|
-
if (!claimed) break;
|
|
4972
|
-
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
4973
|
-
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
4974
|
-
stopRequested = true;
|
|
4975
|
-
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
4976
|
-
context.__tasksRunnerStop = true;
|
|
4977
|
-
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
4978
|
-
}
|
|
4979
|
-
}).finally(() => {
|
|
4980
|
-
runningPromises.delete(p);
|
|
4981
|
-
});
|
|
4982
|
-
runningPromises.add(p);
|
|
6253
|
+
if (context.isStop() && !stopRequested) {
|
|
6254
|
+
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
4983
6255
|
}
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
6256
|
+
if (runningPromises.size > 0) {
|
|
6257
|
+
if (stopRequested) {
|
|
6258
|
+
await Promise.race([
|
|
6259
|
+
Promise.allSettled(Array.from(runningPromises)),
|
|
6260
|
+
sleepMs(stopAllowanceMs).then(() => {
|
|
6261
|
+
context.logger.warn?.(
|
|
6262
|
+
`[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
|
|
6263
|
+
);
|
|
6264
|
+
})
|
|
6265
|
+
]);
|
|
6266
|
+
} else {
|
|
6267
|
+
await Promise.allSettled(Array.from(runningPromises));
|
|
6268
|
+
}
|
|
6269
|
+
}
|
|
6270
|
+
} finally {
|
|
6271
|
+
if (registryInterval) {
|
|
6272
|
+
clearInterval(registryInterval);
|
|
6273
|
+
registryInterval = null;
|
|
6274
|
+
}
|
|
6275
|
+
if (registryReg) {
|
|
6276
|
+
await unregisterServicesRegistry(context, registryReg).catch((err) => {
|
|
6277
|
+
context.logger.warn?.(`[services-registry] unregister failed: ${err?.message ?? String(err)}`);
|
|
6278
|
+
});
|
|
6279
|
+
registryReg = null;
|
|
6280
|
+
delete context.servicesRegistry;
|
|
6281
|
+
delete context.runnerHeartbeat;
|
|
5001
6282
|
}
|
|
5002
6283
|
}
|
|
5003
6284
|
}
|
|
5004
6285
|
async function waitForTaskResult(context, taskId, options = {}) {
|
|
5005
|
-
const db =
|
|
5006
|
-
const
|
|
6286
|
+
const db = getDb3(context);
|
|
6287
|
+
const queueName = options.queueName ?? "tasks";
|
|
5007
6288
|
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
5008
6289
|
const pollMs = options.pollMs ?? 500;
|
|
5009
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
6290
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
5010
6291
|
const deadline = Date.now() + timeoutMs;
|
|
6292
|
+
const waitStartedAt = /* @__PURE__ */ new Date();
|
|
6293
|
+
let cachedNameOpid = null;
|
|
6294
|
+
async function historySinceWait(name, opid) {
|
|
6295
|
+
let q = db(historyTable).where({ name }).where("completed_at", ">=", waitStartedAt);
|
|
6296
|
+
if (opid == null || opid === "") {
|
|
6297
|
+
q = q.whereNull("opid");
|
|
6298
|
+
} else {
|
|
6299
|
+
q = q.where({ opid });
|
|
6300
|
+
}
|
|
6301
|
+
return await q.orderBy("completed_at", "desc").first();
|
|
6302
|
+
}
|
|
5011
6303
|
while (Date.now() <= deadline) {
|
|
5012
|
-
const
|
|
5013
|
-
if (
|
|
6304
|
+
const legacy = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
|
|
6305
|
+
if (legacy) {
|
|
6306
|
+
return legacy;
|
|
6307
|
+
}
|
|
5014
6308
|
const pending = await db(tasksTable).where({ id: taskId }).first();
|
|
5015
|
-
if (
|
|
5016
|
-
|
|
5017
|
-
|
|
6309
|
+
if (pending) {
|
|
6310
|
+
cachedNameOpid = { name: pending.name, opid: pending.opid };
|
|
6311
|
+
const done = await historySinceWait(pending.name, pending.opid);
|
|
6312
|
+
if (done) {
|
|
6313
|
+
return done;
|
|
6314
|
+
}
|
|
6315
|
+
} else if (cachedNameOpid) {
|
|
6316
|
+
const done = await historySinceWait(cachedNameOpid.name, cachedNameOpid.opid);
|
|
6317
|
+
if (done) {
|
|
6318
|
+
return done;
|
|
6319
|
+
}
|
|
6320
|
+
return null;
|
|
6321
|
+
} else {
|
|
6322
|
+
return null;
|
|
5018
6323
|
}
|
|
5019
6324
|
await sleepMs(pollMs);
|
|
5020
6325
|
}
|
|
5021
6326
|
return null;
|
|
5022
6327
|
}
|
|
5023
6328
|
var TasksManager = class _TasksManager {
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
6329
|
+
/**
|
|
6330
|
+
* @param {object} context
|
|
6331
|
+
* @param {{
|
|
6332
|
+
* queueName?: string,
|
|
6333
|
+
* target?: string,
|
|
6334
|
+
* recreateTaskTables?: boolean,
|
|
6335
|
+
* pollMs?: number,
|
|
6336
|
+
* claimJitterMs?: number,
|
|
6337
|
+
* maxParallel?: number,
|
|
6338
|
+
* scanLimit?: number,
|
|
6339
|
+
* allowedTasks?: string | string[],
|
|
6340
|
+
* registry?: TasksRegistry | Record<string, Function>,
|
|
6341
|
+
* runnerServiceGroup?: string,
|
|
6342
|
+
* runnerServiceName?: string,
|
|
6343
|
+
* runnerInstanceNumber?: number,
|
|
6344
|
+
* runnerHeartbeatIntervalMs?: number,
|
|
6345
|
+
* runnerHeartbeatStaleMs?: number,
|
|
6346
|
+
* runnerGroupMaxInstances?: number,
|
|
6347
|
+
* runnerEnforceMaxInstances?: boolean,
|
|
6348
|
+
* runnerMetadata?: Record<string, unknown>,
|
|
6349
|
+
* }} [options]
|
|
6350
|
+
*/
|
|
5033
6351
|
constructor(context, options = {}) {
|
|
5034
6352
|
this.context = context;
|
|
5035
|
-
this.
|
|
6353
|
+
this.queueName = options.queueName ?? "tasks";
|
|
5036
6354
|
this.target = options.target ?? "localRunner";
|
|
5037
6355
|
this.recreateTaskTables = options.recreateTaskTables ?? false;
|
|
5038
6356
|
this.pollMs = options.pollMs ?? 1e3;
|
|
6357
|
+
this.claimJitterMs = options.claimJitterMs ?? 0;
|
|
5039
6358
|
this.maxParallel = options.maxParallel ?? 1;
|
|
5040
6359
|
this.scanLimit = options.scanLimit ?? 100;
|
|
5041
6360
|
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5042
6361
|
this.registry = normalizeRegistry(options.registry);
|
|
6362
|
+
this.runnerServiceGroup = options.runnerServiceGroup;
|
|
6363
|
+
this.runnerServiceName = options.runnerServiceName;
|
|
6364
|
+
this.runnerInstanceNumber = options.runnerInstanceNumber;
|
|
6365
|
+
this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
|
|
6366
|
+
this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
|
|
6367
|
+
this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
|
|
6368
|
+
this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
|
|
6369
|
+
this.runnerMetadata = options.runnerMetadata;
|
|
5043
6370
|
}
|
|
6371
|
+
/**
|
|
6372
|
+
* Preferred factory: reads defaults from `context.params` (module namespace
|
|
6373
|
+
* `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
|
|
6374
|
+
* and inline options in one consistent resolver.
|
|
6375
|
+
*
|
|
6376
|
+
* @param {object} context
|
|
6377
|
+
* @param {ConstructorParameters<typeof TasksManager>[1]} [options]
|
|
6378
|
+
* @returns {TasksManager}
|
|
6379
|
+
*/
|
|
5044
6380
|
static init(context, options = {}) {
|
|
5045
6381
|
const defs = {
|
|
5046
6382
|
table: "string default tasks",
|
|
5047
6383
|
target: "string default localRunner",
|
|
5048
6384
|
recreateTaskTables: "boolean default false",
|
|
5049
6385
|
pollMs: "number default 1000",
|
|
6386
|
+
claimJitterMs: "number default 0",
|
|
5050
6387
|
maxParallel: "number default 1",
|
|
5051
6388
|
scanLimit: "number default 100",
|
|
5052
|
-
allowedTasks: "string"
|
|
6389
|
+
allowedTasks: "string",
|
|
6390
|
+
runnerServiceGroup: "string",
|
|
6391
|
+
runnerServiceName: "string",
|
|
6392
|
+
runnerInstanceNumber: "number",
|
|
6393
|
+
runnerHeartbeatIntervalMs: "number default 10000",
|
|
6394
|
+
runnerHeartbeatStaleMs: "number default 45000",
|
|
6395
|
+
runnerGroupMaxInstances: "number",
|
|
6396
|
+
runnerEnforceMaxInstances: "boolean default true"
|
|
5053
6397
|
};
|
|
5054
|
-
const discovered = context.params.getAllForModule(defs);
|
|
6398
|
+
const discovered = context.params.getAllForModule("tasks", defs);
|
|
5055
6399
|
const resolved = {
|
|
5056
|
-
|
|
6400
|
+
queueName: discovered.table,
|
|
5057
6401
|
target: discovered.target,
|
|
5058
6402
|
recreateTaskTables: discovered.recreateTaskTables,
|
|
5059
6403
|
pollMs: discovered.pollMs,
|
|
6404
|
+
claimJitterMs: discovered.claimJitterMs,
|
|
5060
6405
|
maxParallel: discovered.maxParallel,
|
|
5061
6406
|
scanLimit: discovered.scanLimit,
|
|
5062
6407
|
allowedTasks: discovered.allowedTasks,
|
|
6408
|
+
runnerServiceGroup: discovered.runnerServiceGroup,
|
|
6409
|
+
runnerServiceName: discovered.runnerServiceName,
|
|
6410
|
+
runnerInstanceNumber: discovered.runnerInstanceNumber,
|
|
6411
|
+
runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
|
|
6412
|
+
runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
|
|
6413
|
+
runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
|
|
6414
|
+
runnerEnforceMaxInstances: discovered.runnerEnforceMaxInstances,
|
|
5063
6415
|
...options
|
|
5064
6416
|
};
|
|
5065
6417
|
return new _TasksManager(context, resolved);
|
|
5066
6418
|
}
|
|
6419
|
+
/**
|
|
6420
|
+
* Idempotently ensure the three backing tables exist for this queue.
|
|
6421
|
+
*
|
|
6422
|
+
* @param {{ recreate?: boolean }} [options]
|
|
6423
|
+
* @returns {Promise<void>}
|
|
6424
|
+
*/
|
|
5067
6425
|
async ensureTaskTables(options = {}) {
|
|
5068
6426
|
await ensureTaskTables(this.context, {
|
|
5069
|
-
|
|
6427
|
+
queueName: this.queueName,
|
|
5070
6428
|
recreate: options.recreate ?? this.recreateTaskTables
|
|
5071
6429
|
});
|
|
5072
6430
|
}
|
|
6431
|
+
/**
|
|
6432
|
+
* Start the runner loop using this manager's resolved config. Per-call
|
|
6433
|
+
* options override the stored defaults, but `runnerMetadata` still falls
|
|
6434
|
+
* through when omitted.
|
|
6435
|
+
*
|
|
6436
|
+
* @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
|
|
6437
|
+
* @returns {Promise<void>}
|
|
6438
|
+
*/
|
|
5073
6439
|
async runTasksLoop(options = {}) {
|
|
5074
6440
|
await runTasksLoop(this.context, {
|
|
5075
|
-
|
|
6441
|
+
queueName: options.queueName ?? this.queueName,
|
|
5076
6442
|
target: options.target ?? this.target,
|
|
5077
6443
|
pollMs: options.pollMs ?? this.pollMs,
|
|
6444
|
+
claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
|
|
5078
6445
|
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
5079
6446
|
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
5080
6447
|
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
5081
|
-
registry: options.registry ?? this.registry
|
|
6448
|
+
registry: options.registry ?? this.registry,
|
|
6449
|
+
runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
|
|
6450
|
+
runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
|
|
6451
|
+
runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
|
|
6452
|
+
runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
|
|
6453
|
+
runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
|
|
6454
|
+
runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
|
|
6455
|
+
runnerEnforceMaxInstances: options.runnerEnforceMaxInstances ?? this.runnerEnforceMaxInstances,
|
|
6456
|
+
runnerMetadata: options.runnerMetadata ?? this.runnerMetadata
|
|
5082
6457
|
});
|
|
5083
6458
|
}
|
|
5084
6459
|
};
|
|
5085
6460
|
export {
|
|
6461
|
+
AbstractTask,
|
|
5086
6462
|
Args,
|
|
5087
|
-
|
|
6463
|
+
Box4 as Box,
|
|
5088
6464
|
Db,
|
|
5089
6465
|
Divider,
|
|
5090
6466
|
FileDatabase,
|
|
@@ -5097,14 +6473,16 @@ export {
|
|
|
5097
6473
|
MultiColumnListComponent,
|
|
5098
6474
|
MultiColumnListWithPreviewComponent,
|
|
5099
6475
|
Params,
|
|
5100
|
-
|
|
6476
|
+
React2 as React,
|
|
6477
|
+
S3,
|
|
6478
|
+
SERVICE_TASK_NAMES,
|
|
5101
6479
|
ScreenBody,
|
|
5102
6480
|
ScreenContainer,
|
|
5103
6481
|
ScreenDivider,
|
|
5104
6482
|
ScreenFooter,
|
|
5105
6483
|
ScreenRow,
|
|
5106
6484
|
ScreenTitle,
|
|
5107
|
-
|
|
6485
|
+
TaskGetLogs,
|
|
5108
6486
|
TaskPing,
|
|
5109
6487
|
TaskSampleProcess,
|
|
5110
6488
|
TaskShellCommand,
|
|
@@ -5119,24 +6497,38 @@ export {
|
|
|
5119
6497
|
buildBreadcrumb,
|
|
5120
6498
|
buildDetailBreadcrumb,
|
|
5121
6499
|
buildFooter,
|
|
5122
|
-
|
|
5123
|
-
dbFindAndConnect,
|
|
5124
|
-
dbInit,
|
|
6500
|
+
convertPattern,
|
|
5125
6501
|
defaultFileSynopsisFunction,
|
|
5126
6502
|
defaultTasksRegistry,
|
|
5127
6503
|
defaultVersionSynopsisFunction,
|
|
5128
6504
|
enqueueStopTask,
|
|
5129
6505
|
enqueueTask,
|
|
5130
6506
|
ensureTaskTables,
|
|
6507
|
+
flushTaskIpcLogs,
|
|
5131
6508
|
getArgsInstance,
|
|
5132
6509
|
createElement2 as h,
|
|
6510
|
+
ipcFileLogsTableNameForSourceResource,
|
|
5133
6511
|
joiEdateType,
|
|
5134
6512
|
joiStringArrayType,
|
|
6513
|
+
listServicesRegistry as listAliveRunnerHeartbeats,
|
|
6514
|
+
listServicesRegistry,
|
|
5135
6515
|
listSources,
|
|
5136
6516
|
listTables,
|
|
5137
6517
|
load,
|
|
6518
|
+
matchesParsedPattern,
|
|
6519
|
+
memo,
|
|
6520
|
+
mergeAllowedTasksWithServiceTasks,
|
|
6521
|
+
nextTimeMatch,
|
|
6522
|
+
normalizeAllowedTasks,
|
|
5138
6523
|
organizeFooterMessages,
|
|
5139
6524
|
queueToTableNames,
|
|
6525
|
+
readTaskIpcLogsSnapshot,
|
|
6526
|
+
registerInServicesRegistry,
|
|
6527
|
+
registerInServicesRegistry as registerRunnerHeartbeat,
|
|
6528
|
+
resolveAsterisks,
|
|
6529
|
+
resolveIpcFileLogsDir,
|
|
6530
|
+
resolveRanges,
|
|
6531
|
+
resolveSteps,
|
|
5140
6532
|
runNodeTaskScript,
|
|
5141
6533
|
runTasksLoop,
|
|
5142
6534
|
setupContext,
|
|
@@ -5146,12 +6538,20 @@ export {
|
|
|
5146
6538
|
showMultiColumnListWithPreviewScreen,
|
|
5147
6539
|
showScreen,
|
|
5148
6540
|
showWordGridScreen,
|
|
6541
|
+
taskHistoryInsertFromQueueRow,
|
|
6542
|
+
timeMatcher,
|
|
6543
|
+
touchServicesRegistry as touchRunnerHeartbeat,
|
|
6544
|
+
touchServicesRegistry,
|
|
6545
|
+
unregisterServicesRegistry as unregisterRunnerHeartbeat,
|
|
6546
|
+
unregisterServicesRegistry,
|
|
6547
|
+
updateServicesRegistryMetadata,
|
|
5149
6548
|
updateTaskProgress,
|
|
5150
6549
|
useCallback,
|
|
5151
|
-
|
|
6550
|
+
useEffect2 as useEffect,
|
|
5152
6551
|
useInput2 as useInput,
|
|
6552
|
+
useLayoutEffect,
|
|
5153
6553
|
useMemo,
|
|
5154
|
-
|
|
6554
|
+
useRef2 as useRef,
|
|
5155
6555
|
useState3 as useState,
|
|
5156
6556
|
waitForTaskResult
|
|
5157
6557
|
};
|