@nmakarov/cli-toolkit 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/args.cjs +1 -4
- package/dist/args.cjs.map +1 -1
- package/dist/args.js +1 -1
- package/dist/args.js.map +1 -1
- package/dist/cli-runner.cjs +1252 -604
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +1299 -650
- package/dist/cli-runner.js.map +1 -1
- package/dist/db.cjs +85 -157
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +84 -150
- package/dist/db.js.map +1 -1
- package/dist/errors.cjs +2 -2
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/filedatabase.cjs +19 -19
- package/dist/filedatabase.cjs.map +1 -1
- package/dist/filedatabase.js +19 -16
- package/dist/filedatabase.js.map +1 -1
- package/dist/http-client.cjs +9 -11
- package/dist/http-client.cjs.map +1 -1
- package/dist/http-client.js +10 -9
- package/dist/http-client.js.map +1 -1
- package/dist/http-client2.cjs +34 -37
- package/dist/http-client2.cjs.map +1 -1
- package/dist/http-client2.js +34 -34
- package/dist/http-client2.js.map +1 -1
- package/dist/index.cjs +1739 -720
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1746 -721
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +81 -68
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +96 -82
- package/dist/init.js.map +1 -1
- package/dist/logger.cjs +5 -5
- package/dist/logger.cjs.map +1 -1
- package/dist/logger.js +5 -4
- package/dist/logger.js.map +1 -1
- package/dist/mock-server.cjs +21 -33
- package/dist/mock-server.cjs.map +1 -1
- package/dist/mock-server.js +21 -28
- package/dist/mock-server.js.map +1 -1
- package/dist/params.cjs +6 -9
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +6 -6
- package/dist/params.js.map +1 -1
- package/dist/s3.cjs +286 -0
- package/dist/s3.cjs.map +1 -0
- package/dist/s3.js +273 -0
- package/dist/s3.js.map +1 -0
- package/dist/screen.cjs +34 -39
- package/dist/screen.cjs.map +1 -1
- package/dist/screen.js +48 -46
- package/dist/screen.js.map +1 -1
- package/dist/tasks.cjs +1354 -501
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +1369 -527
- package/dist/tasks.js.map +1 -1
- package/dist/utils.cjs +7 -8
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.js +6 -6
- package/dist/utils.js.map +1 -1
- package/package.json +32 -47
- package/scripts/ssm/{parse-cli.ts → parse-cli.js} +4 -4
- package/scripts/ssm/{ssm-admin.ts → ssm-admin.js} +12 -12
- package/scripts/ssm/{ssm-pull.ts → ssm-pull.js} +10 -13
package/dist/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,25 +874,24 @@ 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];
|
|
@@ -909,12 +905,11 @@ function buildDetailBreadcrumb(path5, suffix = "") {
|
|
|
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
|
|
@@ -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
|
}
|
|
@@ -2063,14 +2056,14 @@ var Params = class _Params {
|
|
|
2063
2056
|
}
|
|
2064
2057
|
};
|
|
2065
2058
|
|
|
2066
|
-
// src/
|
|
2059
|
+
// src/index.js
|
|
2067
2060
|
init_screen();
|
|
2068
2061
|
|
|
2069
|
-
// src/filedatabase/index.
|
|
2062
|
+
// src/filedatabase/index.js
|
|
2070
2063
|
import fs3 from "fs";
|
|
2071
2064
|
import path3 from "path";
|
|
2072
2065
|
|
|
2073
|
-
// src/utils/os-utils.
|
|
2066
|
+
// src/utils/os-utils.js
|
|
2074
2067
|
import fs from "fs";
|
|
2075
2068
|
import path from "path";
|
|
2076
2069
|
import { execSync } from "child_process";
|
|
@@ -2099,7 +2092,7 @@ function getFreeDiskSpace(targetPath) {
|
|
|
2099
2092
|
}
|
|
2100
2093
|
}
|
|
2101
2094
|
|
|
2102
|
-
// src/utils/fs-utils.
|
|
2095
|
+
// src/utils/fs-utils.js
|
|
2103
2096
|
import fs2 from "fs";
|
|
2104
2097
|
import path2 from "path";
|
|
2105
2098
|
async function ensurePath(...pathParts) {
|
|
@@ -2123,7 +2116,7 @@ function getFileExtension(dataType) {
|
|
|
2123
2116
|
}
|
|
2124
2117
|
}
|
|
2125
2118
|
|
|
2126
|
-
// src/utils/format-utils.
|
|
2119
|
+
// src/utils/format-utils.js
|
|
2127
2120
|
function bytesToHumanReadable(bytes) {
|
|
2128
2121
|
if (bytes === 0) return "0 B";
|
|
2129
2122
|
const k = 1024;
|
|
@@ -2132,7 +2125,7 @@ function bytesToHumanReadable(bytes) {
|
|
|
2132
2125
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
2133
2126
|
}
|
|
2134
2127
|
|
|
2135
|
-
// src/utils/date-utils.
|
|
2128
|
+
// src/utils/date-utils.js
|
|
2136
2129
|
function isTimestampFolder(folderName) {
|
|
2137
2130
|
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
|
|
2138
2131
|
if (!isoRegex.test(folderName)) {
|
|
@@ -2142,7 +2135,7 @@ function isTimestampFolder(folderName) {
|
|
|
2142
2135
|
return !isNaN(date.getTime()) && date.getTime() > 0;
|
|
2143
2136
|
}
|
|
2144
2137
|
|
|
2145
|
-
// src/filedatabase/serializers.
|
|
2138
|
+
// src/filedatabase/serializers.js
|
|
2146
2139
|
function detectDataType(data) {
|
|
2147
2140
|
if (Array.isArray(data)) {
|
|
2148
2141
|
return "json-array";
|
|
@@ -2174,7 +2167,7 @@ function deserializeData(rawData, dataType) {
|
|
|
2174
2167
|
}
|
|
2175
2168
|
}
|
|
2176
2169
|
|
|
2177
|
-
// src/filedatabase/synopsis-functions.
|
|
2170
|
+
// src/filedatabase/synopsis-functions.js
|
|
2178
2171
|
function defaultFileSynopsisFunction(fileEntry, data) {
|
|
2179
2172
|
if (!Array.isArray(data) || data.length === 0) {
|
|
2180
2173
|
return { ...fileEntry };
|
|
@@ -2242,7 +2235,7 @@ function defaultVersionSynopsisFunction(metadata) {
|
|
|
2242
2235
|
return result;
|
|
2243
2236
|
}
|
|
2244
2237
|
|
|
2245
|
-
// src/filedatabase/index.
|
|
2238
|
+
// src/filedatabase/index.js
|
|
2246
2239
|
var FileDatabase = class _FileDatabase {
|
|
2247
2240
|
basePath;
|
|
2248
2241
|
namespace;
|
|
@@ -2328,7 +2321,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2328
2321
|
if (errors.length) {
|
|
2329
2322
|
throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
|
|
2330
2323
|
}
|
|
2331
|
-
|
|
2324
|
+
const parts = [this.basePath, this.namespace];
|
|
2332
2325
|
if (this.tableName) {
|
|
2333
2326
|
parts.push(...this.tableName.split("/"));
|
|
2334
2327
|
}
|
|
@@ -2821,14 +2814,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
2821
2814
|
* Prepare the instance for read or write operations
|
|
2822
2815
|
* This discovers state and sets up internal members based on mode and current data
|
|
2823
2816
|
*/
|
|
2824
|
-
async prepare(
|
|
2817
|
+
async prepare(options) {
|
|
2818
|
+
const { write, read, version, deferInitialVersion } = options;
|
|
2825
2819
|
if (write) {
|
|
2826
2820
|
if (this.versioned) {
|
|
2827
2821
|
if (this.currentVersion === null) {
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2822
|
+
if (!deferInitialVersion) {
|
|
2823
|
+
await this.makeNewVersion();
|
|
2824
|
+
this.metadata = this.getDefaultMetadata();
|
|
2825
|
+
this.metadata.version = this.currentVersion;
|
|
2826
|
+
this.makeNewFile();
|
|
2827
|
+
}
|
|
2832
2828
|
} else {
|
|
2833
2829
|
if (!this.metadata.files.length) {
|
|
2834
2830
|
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
@@ -2938,7 +2934,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2938
2934
|
if (options.forceNewVersion && !this.versioned) {
|
|
2939
2935
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
2940
2936
|
}
|
|
2941
|
-
await this.prepare({ write: true });
|
|
2937
|
+
await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
|
|
2942
2938
|
const incomingDataType = detectDataType(data);
|
|
2943
2939
|
this.metadata.dataType = incomingDataType;
|
|
2944
2940
|
if (options.forceNewVersion) {
|
|
@@ -3253,22 +3249,57 @@ function listSources(basePath) {
|
|
|
3253
3249
|
}
|
|
3254
3250
|
}
|
|
3255
3251
|
|
|
3256
|
-
// src/db/index.
|
|
3252
|
+
// src/db/index.js
|
|
3257
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
|
+
};
|
|
3258
3260
|
var Db = class {
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
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
|
+
}
|
|
3268
3296
|
constructor(config2) {
|
|
3269
|
-
if (!config2.connectionString) {
|
|
3297
|
+
if (!config2 || !config2.connectionString) {
|
|
3270
3298
|
throw new ParamError("Db: connectionString is required");
|
|
3271
3299
|
}
|
|
3300
|
+
this.knexInstance = null;
|
|
3301
|
+
this.isConnected = false;
|
|
3302
|
+
this.queriesLog = [];
|
|
3272
3303
|
this.config = {
|
|
3273
3304
|
testConnection: true,
|
|
3274
3305
|
profile: false,
|
|
@@ -3281,25 +3312,23 @@ var Db = class {
|
|
|
3281
3312
|
};
|
|
3282
3313
|
this.logger = this.config.logger;
|
|
3283
3314
|
const instance2 = this;
|
|
3284
|
-
const callableWrapper = function(
|
|
3315
|
+
const callableWrapper = function() {
|
|
3285
3316
|
throw new Error("This should never be called directly");
|
|
3286
3317
|
};
|
|
3287
3318
|
callableWrapper._instance = instance2;
|
|
3288
3319
|
return new Proxy(callableWrapper, {
|
|
3289
|
-
|
|
3290
|
-
apply: (target, thisArg, argumentsList) => {
|
|
3320
|
+
apply: (target, _thisArg, argumentsList) => {
|
|
3291
3321
|
const inst = target._instance;
|
|
3292
3322
|
if (!inst.knexInstance) {
|
|
3293
3323
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
3294
3324
|
}
|
|
3295
3325
|
return inst.knexInstance(...argumentsList);
|
|
3296
3326
|
},
|
|
3297
|
-
// Intercept property access: db.schema, db.raw, etc.
|
|
3298
3327
|
get: (target, prop) => {
|
|
3299
3328
|
if (prop === "_instance") {
|
|
3300
3329
|
return target._instance;
|
|
3301
3330
|
}
|
|
3302
|
-
const
|
|
3331
|
+
const inst = target._instance;
|
|
3303
3332
|
const ownMethods = [
|
|
3304
3333
|
"connect",
|
|
3305
3334
|
"disconnect",
|
|
@@ -3312,26 +3341,26 @@ var Db = class {
|
|
|
3312
3341
|
"detectClient",
|
|
3313
3342
|
"attachProfiler"
|
|
3314
3343
|
];
|
|
3315
|
-
if (prop in
|
|
3316
|
-
const value =
|
|
3344
|
+
if (prop in inst) {
|
|
3345
|
+
const value = inst[prop];
|
|
3317
3346
|
if (typeof value === "function" && ownMethods.includes(prop)) {
|
|
3318
|
-
return value.bind(
|
|
3347
|
+
return value.bind(inst);
|
|
3319
3348
|
}
|
|
3320
3349
|
if (typeof value !== "function") {
|
|
3321
3350
|
return value;
|
|
3322
3351
|
}
|
|
3323
3352
|
}
|
|
3324
|
-
if (
|
|
3325
|
-
const knexProp =
|
|
3353
|
+
if (inst.knexInstance) {
|
|
3354
|
+
const knexProp = inst.knexInstance[prop];
|
|
3326
3355
|
if (typeof knexProp === "function") {
|
|
3327
|
-
return knexProp.bind(
|
|
3356
|
+
return knexProp.bind(inst.knexInstance);
|
|
3328
3357
|
}
|
|
3329
3358
|
return knexProp;
|
|
3330
3359
|
}
|
|
3331
|
-
if (prop in
|
|
3332
|
-
const method =
|
|
3360
|
+
if (prop in inst) {
|
|
3361
|
+
const method = inst[prop];
|
|
3333
3362
|
if (typeof method === "function") {
|
|
3334
|
-
return method.bind(
|
|
3363
|
+
return method.bind(inst);
|
|
3335
3364
|
}
|
|
3336
3365
|
return method;
|
|
3337
3366
|
}
|
|
@@ -3339,9 +3368,6 @@ var Db = class {
|
|
|
3339
3368
|
}
|
|
3340
3369
|
});
|
|
3341
3370
|
}
|
|
3342
|
-
/**
|
|
3343
|
-
* Detect database client type from connection string
|
|
3344
|
-
*/
|
|
3345
3371
|
detectClient(connectionString) {
|
|
3346
3372
|
if (connectionString.match(/^postgresql/)) {
|
|
3347
3373
|
return "pg";
|
|
@@ -3351,9 +3377,6 @@ var Db = class {
|
|
|
3351
3377
|
}
|
|
3352
3378
|
return null;
|
|
3353
3379
|
}
|
|
3354
|
-
/**
|
|
3355
|
-
* Connect to the database
|
|
3356
|
-
*/
|
|
3357
3380
|
async connect() {
|
|
3358
3381
|
if (this.isConnected && this.knexInstance) {
|
|
3359
3382
|
this.logger.warn?.("[Db] Already connected");
|
|
@@ -3362,14 +3385,13 @@ var Db = class {
|
|
|
3362
3385
|
const client = this.detectClient(this.config.connectionString);
|
|
3363
3386
|
if (!client) {
|
|
3364
3387
|
throw new ParamError(
|
|
3365
|
-
|
|
3388
|
+
"Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://"
|
|
3366
3389
|
);
|
|
3367
3390
|
}
|
|
3368
3391
|
try {
|
|
3369
3392
|
const connectionConfig = {
|
|
3370
3393
|
connectionString: this.config.connectionString,
|
|
3371
3394
|
family: 4
|
|
3372
|
-
// Force IPv4 only (disable IPv6)
|
|
3373
3395
|
};
|
|
3374
3396
|
this.knexInstance = knex({
|
|
3375
3397
|
client,
|
|
@@ -3385,7 +3407,9 @@ var Db = class {
|
|
|
3385
3407
|
await this.testConnection();
|
|
3386
3408
|
}
|
|
3387
3409
|
this.isConnected = true;
|
|
3388
|
-
this.logger.debug?.(
|
|
3410
|
+
this.logger.debug?.(
|
|
3411
|
+
`[Db] Connected to database "${this.config.name || this.config.connectionString}"`
|
|
3412
|
+
);
|
|
3389
3413
|
} catch (error) {
|
|
3390
3414
|
if (error instanceof ParamError) {
|
|
3391
3415
|
throw error;
|
|
@@ -3394,9 +3418,6 @@ var Db = class {
|
|
|
3394
3418
|
throw new ParamError(`Db: Connection failed - ${errorMsg}`);
|
|
3395
3419
|
}
|
|
3396
3420
|
}
|
|
3397
|
-
/**
|
|
3398
|
-
* Disconnect from the database
|
|
3399
|
-
*/
|
|
3400
3421
|
async disconnect() {
|
|
3401
3422
|
if (!this.knexInstance) {
|
|
3402
3423
|
return;
|
|
@@ -3406,16 +3427,15 @@ var Db = class {
|
|
|
3406
3427
|
this.knexInstance = null;
|
|
3407
3428
|
this.isConnected = false;
|
|
3408
3429
|
this.queriesLog = [];
|
|
3409
|
-
this.logger.debug?.(
|
|
3430
|
+
this.logger.debug?.(
|
|
3431
|
+
`[Db] Disconnected from database "${this.config.name || this.config.connectionString}"`
|
|
3432
|
+
);
|
|
3410
3433
|
} catch (error) {
|
|
3411
3434
|
const errorMsg = this.getErrorMessage(error);
|
|
3412
3435
|
this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
|
|
3413
3436
|
throw error;
|
|
3414
3437
|
}
|
|
3415
3438
|
}
|
|
3416
|
-
/**
|
|
3417
|
-
* Extract error message from various error types
|
|
3418
|
-
*/
|
|
3419
3439
|
getErrorMessage(error) {
|
|
3420
3440
|
if (error instanceof AggregateError) {
|
|
3421
3441
|
const errors = error.errors || [];
|
|
@@ -3440,9 +3460,11 @@ var Db = class {
|
|
|
3440
3460
|
return `${code} (tried: ${addresses.join(", ")})`;
|
|
3441
3461
|
}
|
|
3442
3462
|
}
|
|
3443
|
-
const uniqueMessages = [
|
|
3444
|
-
|
|
3445
|
-
|
|
3463
|
+
const uniqueMessages = [
|
|
3464
|
+
...new Set(
|
|
3465
|
+
errors.map((e) => e instanceof Error ? e.message : String(e))
|
|
3466
|
+
)
|
|
3467
|
+
];
|
|
3446
3468
|
if (uniqueMessages.length === 1) {
|
|
3447
3469
|
return uniqueMessages[0];
|
|
3448
3470
|
}
|
|
@@ -3451,28 +3473,25 @@ var Db = class {
|
|
|
3451
3473
|
return error.message || "Multiple errors occurred";
|
|
3452
3474
|
}
|
|
3453
3475
|
if (error instanceof Error) {
|
|
3454
|
-
const
|
|
3455
|
-
if (
|
|
3456
|
-
return `${
|
|
3476
|
+
const code = error.code;
|
|
3477
|
+
if (code) {
|
|
3478
|
+
return `${code}: ${error.message || String(error)}`;
|
|
3457
3479
|
}
|
|
3458
3480
|
return error.message || String(error);
|
|
3459
3481
|
}
|
|
3460
3482
|
if (typeof error === "string") {
|
|
3461
3483
|
return error;
|
|
3462
3484
|
}
|
|
3463
|
-
if (error
|
|
3485
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
3464
3486
|
const msg = String(error.message);
|
|
3465
|
-
const
|
|
3466
|
-
if (
|
|
3467
|
-
return `${
|
|
3487
|
+
const code = error.code;
|
|
3488
|
+
if (code) {
|
|
3489
|
+
return `${code}: ${msg}`;
|
|
3468
3490
|
}
|
|
3469
3491
|
return msg;
|
|
3470
3492
|
}
|
|
3471
3493
|
return String(error) || "Unknown error";
|
|
3472
3494
|
}
|
|
3473
|
-
/**
|
|
3474
|
-
* Test database connection
|
|
3475
|
-
*/
|
|
3476
3495
|
async testConnection() {
|
|
3477
3496
|
if (!this.knexInstance) {
|
|
3478
3497
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -3488,9 +3507,6 @@ var Db = class {
|
|
|
3488
3507
|
throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
|
|
3489
3508
|
}
|
|
3490
3509
|
}
|
|
3491
|
-
/**
|
|
3492
|
-
* Attach query profiler to log all queries
|
|
3493
|
-
*/
|
|
3494
3510
|
attachProfiler() {
|
|
3495
3511
|
if (!this.knexInstance) {
|
|
3496
3512
|
return;
|
|
@@ -3500,7 +3516,7 @@ var Db = class {
|
|
|
3500
3516
|
this.knexInstance.on("query", (query) => {
|
|
3501
3517
|
query.__startTime = process.hrtime();
|
|
3502
3518
|
});
|
|
3503
|
-
this.knexInstance.on("query-response", (
|
|
3519
|
+
this.knexInstance.on("query-response", (_response, query) => {
|
|
3504
3520
|
const [seconds, nanoseconds] = process.hrtime(query.__startTime);
|
|
3505
3521
|
const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
|
|
3506
3522
|
const logEntry = {
|
|
@@ -3515,15 +3531,9 @@ var Db = class {
|
|
|
3515
3531
|
this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
|
|
3516
3532
|
});
|
|
3517
3533
|
}
|
|
3518
|
-
/**
|
|
3519
|
-
* Get query log (only available if profiling is enabled)
|
|
3520
|
-
*/
|
|
3521
3534
|
getQueryLog() {
|
|
3522
3535
|
return [...this.queriesLog];
|
|
3523
3536
|
}
|
|
3524
|
-
/**
|
|
3525
|
-
* Check if a table exists
|
|
3526
|
-
*/
|
|
3527
3537
|
async tableExists(tableName) {
|
|
3528
3538
|
if (!this.knexInstance) {
|
|
3529
3539
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -3535,65 +3545,28 @@ var Db = class {
|
|
|
3535
3545
|
throw error;
|
|
3536
3546
|
}
|
|
3537
3547
|
}
|
|
3538
|
-
/**
|
|
3539
|
-
* Get the underlying Knex instance (for advanced usage)
|
|
3540
|
-
*/
|
|
3541
3548
|
getKnex() {
|
|
3542
3549
|
if (!this.knexInstance) {
|
|
3543
3550
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
3544
3551
|
}
|
|
3545
3552
|
return this.knexInstance;
|
|
3546
3553
|
}
|
|
3547
|
-
/**
|
|
3548
|
-
* Get connection status
|
|
3549
|
-
*/
|
|
3550
3554
|
isConnectedToDb() {
|
|
3551
3555
|
return this.isConnected && this.knexInstance !== null;
|
|
3552
3556
|
}
|
|
3553
|
-
/**
|
|
3554
|
-
* Initialize Db with context (connects and registers disconnect cleanup).
|
|
3555
|
-
* Params are read via getAllForModule("db", defs). Same as dbInit(context, dbNameOrConnectionString).
|
|
3556
|
-
*/
|
|
3557
|
-
static async init(context, dbNameOrConnectionString) {
|
|
3558
|
-
return dbFindAndConnect(context, dbNameOrConnectionString);
|
|
3559
|
-
}
|
|
3560
3557
|
};
|
|
3561
3558
|
function capitalizeFirstLetter(str) {
|
|
3562
3559
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
3563
3560
|
}
|
|
3564
|
-
async function dbConnect(context,
|
|
3565
|
-
const defs = {
|
|
3566
|
-
testDbConnection: "boolean default true",
|
|
3567
|
-
name: "string",
|
|
3568
|
-
poolMin: "number default 2",
|
|
3569
|
-
poolMax: "number default 10",
|
|
3570
|
-
acquireConnectionTimeout: "number default 10000",
|
|
3571
|
-
sslRejectUnauthorized: "boolean default false"
|
|
3572
|
-
};
|
|
3573
|
-
const paramsConfig = context.params.getAllForModule(defs);
|
|
3574
|
-
const config2 = {
|
|
3575
|
-
connectionString,
|
|
3576
|
-
name: paramsConfig.name || name || "default",
|
|
3577
|
-
testConnection: paramsConfig.testDbConnection,
|
|
3578
|
-
profile: dbProfile ?? false,
|
|
3579
|
-
pool: {
|
|
3580
|
-
min: paramsConfig.poolMin,
|
|
3581
|
-
max: paramsConfig.poolMax
|
|
3582
|
-
},
|
|
3583
|
-
acquireConnectionTimeout: paramsConfig.acquireConnectionTimeout,
|
|
3584
|
-
ssl: {
|
|
3585
|
-
rejectUnauthorized: paramsConfig.sslRejectUnauthorized
|
|
3586
|
-
},
|
|
3587
|
-
logger: context.logger
|
|
3588
|
-
};
|
|
3561
|
+
async function dbConnect(context, config2) {
|
|
3589
3562
|
try {
|
|
3590
3563
|
const db = new Db(config2);
|
|
3591
3564
|
context.registerCleanup(async () => {
|
|
3592
3565
|
await db.disconnect();
|
|
3593
|
-
context.logger.debug(`[Db] instance "${name
|
|
3566
|
+
context.logger.debug?.(`[Db] instance "${config2.name}" disconnected`);
|
|
3594
3567
|
});
|
|
3595
3568
|
await db.connect();
|
|
3596
|
-
context.logger.debug(`[Db] instance "${name
|
|
3569
|
+
context.logger.debug?.(`[Db] instance "${config2.name}" initialized`);
|
|
3597
3570
|
return db;
|
|
3598
3571
|
} catch (error) {
|
|
3599
3572
|
if (error instanceof ParamError) {
|
|
@@ -3603,52 +3576,266 @@ async function dbConnect(context, connectionString, name, dbProfile) {
|
|
|
3603
3576
|
throw new ParamError(`[Db] connect error: ${errorMsg}`);
|
|
3604
3577
|
}
|
|
3605
3578
|
}
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
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")
|
|
3622
3617
|
};
|
|
3623
|
-
|
|
3624
|
-
dbName = paramsConfig.dbName;
|
|
3625
|
-
dbConnectionString = paramsConfig.dbConnectionString;
|
|
3626
|
-
dbProfile = paramsConfig.dbProfile;
|
|
3627
|
-
}
|
|
3628
|
-
if (!dbName && !dbConnectionString) {
|
|
3629
|
-
throw new ParamError("Db: either dbName or dbConnectionString must be specified");
|
|
3630
|
-
}
|
|
3631
|
-
if (dbName) {
|
|
3632
|
-
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
3633
|
-
dbConnectionString = await context.params.get(paramName, "string");
|
|
3634
|
-
if (!dbConnectionString) {
|
|
3618
|
+
if (!config2.bucketName) {
|
|
3635
3619
|
throw new ParamError(
|
|
3636
|
-
`
|
|
3620
|
+
`S3: bucket name not configured for profile "${profile}" (set s3Bucket${cap} or S3_BUCKET_${profile.toUpperCase()})`
|
|
3637
3621
|
);
|
|
3638
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);
|
|
3639
3654
|
}
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
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
|
+
};
|
|
3646
3833
|
|
|
3647
|
-
// src/logger/index.
|
|
3834
|
+
// src/logger/index.js
|
|
3648
3835
|
import chalk from "chalk";
|
|
3649
3836
|
import util from "util";
|
|
3650
3837
|
|
|
3651
|
-
// src/logger/transports.
|
|
3838
|
+
// src/logger/transports.js
|
|
3652
3839
|
var ConsoleTransport = class {
|
|
3653
3840
|
write(payload) {
|
|
3654
3841
|
console.info(payload);
|
|
@@ -3668,7 +3855,7 @@ var ParentProcessTransport = class {
|
|
|
3668
3855
|
}
|
|
3669
3856
|
};
|
|
3670
3857
|
|
|
3671
|
-
// src/logger/index.
|
|
3858
|
+
// src/logger/index.js
|
|
3672
3859
|
var ALL_LEVELS = [
|
|
3673
3860
|
"silly",
|
|
3674
3861
|
"debug",
|
|
@@ -3741,6 +3928,7 @@ var Logger = class _Logger {
|
|
|
3741
3928
|
}
|
|
3742
3929
|
/**
|
|
3743
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.
|
|
3744
3932
|
*/
|
|
3745
3933
|
static init(context, options) {
|
|
3746
3934
|
const paramDefs = {
|
|
@@ -3754,7 +3942,7 @@ var Logger = class _Logger {
|
|
|
3754
3942
|
progressWithTimes: "boolean default false",
|
|
3755
3943
|
progressThrottleMs: "number"
|
|
3756
3944
|
};
|
|
3757
|
-
const discovered = context.params.getAllForModule(paramDefs);
|
|
3945
|
+
const discovered = context.params.getAllForModule("logger", paramDefs);
|
|
3758
3946
|
const config2 = { ...discovered, ...options };
|
|
3759
3947
|
const logger = new _Logger(context, config2);
|
|
3760
3948
|
context.logger = logger;
|
|
@@ -3950,9 +4138,9 @@ var Logger = class _Logger {
|
|
|
3950
4138
|
}
|
|
3951
4139
|
};
|
|
3952
4140
|
|
|
3953
|
-
// src/init/index.
|
|
4141
|
+
// src/init/index.js
|
|
3954
4142
|
import { EventEmitter } from "events";
|
|
3955
|
-
function extractComponentOptions(opts,
|
|
4143
|
+
function extractComponentOptions(opts, _componentName) {
|
|
3956
4144
|
const reservedKeys = ["overrides", "defaults", "modules"];
|
|
3957
4145
|
const componentOptions = {};
|
|
3958
4146
|
for (const [key, value] of Object.entries(opts)) {
|
|
@@ -3997,7 +4185,10 @@ function setupContext(opts = {}) {
|
|
|
3997
4185
|
return setup(opts);
|
|
3998
4186
|
}
|
|
3999
4187
|
|
|
4000
|
-
// src/
|
|
4188
|
+
// src/tasks/index.js
|
|
4189
|
+
import os3 from "os";
|
|
4190
|
+
|
|
4191
|
+
// src/utils/core-utils.js
|
|
4001
4192
|
function sleepMs(ms) {
|
|
4002
4193
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
4003
4194
|
}
|
|
@@ -4006,14 +4197,82 @@ function toJsonColumn(value) {
|
|
|
4006
4197
|
return JSON.stringify(value);
|
|
4007
4198
|
}
|
|
4008
4199
|
|
|
4009
|
-
// src/tasks/servicesRegistry.
|
|
4010
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
4011
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
4200
|
+
// src/tasks/servicesRegistry.js
|
|
4012
4201
|
import os from "os";
|
|
4013
|
-
import path4 from "path";
|
|
4014
4202
|
|
|
4015
|
-
// src/tasks/taskUtils.
|
|
4203
|
+
// src/tasks/taskUtils.js
|
|
4016
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
|
|
4017
4276
|
function getDb(context) {
|
|
4018
4277
|
const db = context.db;
|
|
4019
4278
|
if (!db) {
|
|
@@ -4021,113 +4280,117 @@ function getDb(context) {
|
|
|
4021
4280
|
}
|
|
4022
4281
|
return db;
|
|
4023
4282
|
}
|
|
4024
|
-
function queueToTableNames(
|
|
4025
|
-
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
|
|
4026
|
-
throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
|
|
4027
|
-
}
|
|
4283
|
+
function queueToTableNames(queueName) {
|
|
4028
4284
|
return {
|
|
4029
|
-
tasksTable:
|
|
4030
|
-
historyTable: `${
|
|
4285
|
+
tasksTable: queueName,
|
|
4286
|
+
historyTable: `${queueName}_history`,
|
|
4287
|
+
registryTable: `${queueName}_services_registry`
|
|
4031
4288
|
};
|
|
4032
4289
|
}
|
|
4033
|
-
function
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
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;
|
|
4317
|
+
return {
|
|
4318
|
+
...snapshot,
|
|
4319
|
+
...overrides
|
|
4320
|
+
};
|
|
4038
4321
|
}
|
|
4039
4322
|
async function ensureTaskTables(context, options = {}) {
|
|
4040
|
-
const
|
|
4323
|
+
const queueName = options.queueName ?? "tasks";
|
|
4041
4324
|
const recreate = options.recreate ?? false;
|
|
4042
4325
|
const db = getDb(context);
|
|
4043
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
4326
|
+
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
4044
4327
|
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
4045
4328
|
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
4329
|
+
const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
|
|
4046
4330
|
if (recreate) {
|
|
4047
4331
|
await db.schema.dropTableIfExists(historyTable);
|
|
4048
4332
|
await db.schema.dropTableIfExists(tasksTable);
|
|
4333
|
+
await db.schema.dropTableIfExists(registryTable);
|
|
4049
4334
|
}
|
|
4050
4335
|
if (needsTasks) {
|
|
4051
4336
|
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
4052
4337
|
await db.schema.createTable(tasksTable, (t) => {
|
|
4053
|
-
t
|
|
4054
|
-
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
4055
|
-
t.timestamp("started_at");
|
|
4056
|
-
t.timestamp("completed_at");
|
|
4057
|
-
t.integer("priority").notNullable().defaultTo(0);
|
|
4058
|
-
t.text("schedule");
|
|
4059
|
-
t.timestamp("past_due").defaultTo(null);
|
|
4060
|
-
t.text("target").notNullable();
|
|
4061
|
-
t.text("task").notNullable();
|
|
4062
|
-
t.json("params");
|
|
4063
|
-
t.text("opid");
|
|
4064
|
-
t.timestamp("paused_at").defaultTo(null);
|
|
4065
|
-
t.text("progress");
|
|
4066
|
-
t.boolean("success");
|
|
4067
|
-
t.json("results");
|
|
4068
|
-
});
|
|
4069
|
-
await db.schema.alterTable(tasksTable, (t) => {
|
|
4070
|
-
t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
|
|
4071
|
-
t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
|
|
4072
|
-
t.index(["target", "task"], `${tasksTable}_target_task_idx`);
|
|
4338
|
+
defineTasksTable(t, db, tasksTable);
|
|
4073
4339
|
});
|
|
4074
4340
|
}
|
|
4075
4341
|
if (needsHistory) {
|
|
4076
4342
|
await db.schema.createTable(historyTable, (t) => {
|
|
4077
|
-
t
|
|
4078
|
-
t.timestamp("created_at").notNullable();
|
|
4079
|
-
t.timestamp("started_at");
|
|
4080
|
-
t.timestamp("completed_at");
|
|
4081
|
-
t.integer("priority").notNullable().defaultTo(0);
|
|
4082
|
-
t.text("schedule");
|
|
4083
|
-
t.timestamp("past_due").defaultTo(null);
|
|
4084
|
-
t.text("target").notNullable();
|
|
4085
|
-
t.text("task").notNullable();
|
|
4086
|
-
t.json("params");
|
|
4087
|
-
t.text("opid");
|
|
4088
|
-
t.text("progress");
|
|
4089
|
-
t.boolean("success");
|
|
4090
|
-
t.json("results");
|
|
4091
|
-
});
|
|
4092
|
-
await db.schema.alterTable(historyTable, (t) => {
|
|
4093
|
-
t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
|
|
4094
|
-
t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
|
|
4343
|
+
defineTasksTable(t, db, historyTable);
|
|
4095
4344
|
});
|
|
4096
4345
|
}
|
|
4097
|
-
const registryTable = servicesRegistryTable(queue);
|
|
4098
|
-
const needsRegistry = !await db.tableExists(registryTable);
|
|
4099
4346
|
if (needsRegistry) {
|
|
4100
4347
|
await db.schema.createTable(registryTable, (t) => {
|
|
4101
4348
|
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
4102
|
-
t.
|
|
4103
|
-
t.text("queue").notNullable();
|
|
4349
|
+
t.text("queue_name").notNullable();
|
|
4104
4350
|
t.text("service_group").notNullable();
|
|
4351
|
+
t.integer("instance_number").notNullable().defaultTo(1);
|
|
4105
4352
|
t.text("service_name").notNullable();
|
|
4106
|
-
t.text("
|
|
4107
|
-
t.text("hostname");
|
|
4353
|
+
t.text("server_name").notNullable();
|
|
4108
4354
|
t.integer("pid");
|
|
4109
4355
|
t.json("metadata");
|
|
4110
4356
|
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
4111
4357
|
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
4112
|
-
t.unique(["
|
|
4113
|
-
t.index(["
|
|
4114
|
-
t.index(["
|
|
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`);
|
|
4115
4361
|
});
|
|
4116
4362
|
}
|
|
4117
4363
|
}
|
|
4118
4364
|
async function enqueueTask(context, options) {
|
|
4119
4365
|
const db = getDb(context);
|
|
4120
|
-
const
|
|
4121
|
-
const { tasksTable } = queueToTableNames(
|
|
4366
|
+
const queueName = options.queueName ?? "tasks";
|
|
4367
|
+
const { tasksTable } = queueToTableNames(queueName);
|
|
4122
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
|
+
}
|
|
4123
4380
|
await db(tasksTable).insert({
|
|
4124
4381
|
id,
|
|
4125
|
-
|
|
4126
|
-
task: options.task,
|
|
4382
|
+
name,
|
|
4127
4383
|
params: toJsonColumn(options.params ?? null),
|
|
4128
4384
|
opid: options.opid ?? null,
|
|
4129
|
-
priority: options.priority ??
|
|
4130
|
-
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()
|
|
4131
4394
|
});
|
|
4132
4395
|
return id;
|
|
4133
4396
|
}
|
|
@@ -4138,7 +4401,7 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
|
4138
4401
|
});
|
|
4139
4402
|
}
|
|
4140
4403
|
|
|
4141
|
-
// src/tasks/servicesRegistry.
|
|
4404
|
+
// src/tasks/servicesRegistry.js
|
|
4142
4405
|
function getDb2(context) {
|
|
4143
4406
|
const db = context.db;
|
|
4144
4407
|
if (!db) {
|
|
@@ -4152,7 +4415,7 @@ function parseMetadataColumn(value) {
|
|
|
4152
4415
|
if (typeof value === "string") {
|
|
4153
4416
|
try {
|
|
4154
4417
|
const p = JSON.parse(value);
|
|
4155
|
-
return p && typeof p === "object" && !Array.isArray(
|
|
4418
|
+
return p && typeof p === "object" && !Array.isArray(value) ? p : {};
|
|
4156
4419
|
} catch {
|
|
4157
4420
|
return {};
|
|
4158
4421
|
}
|
|
@@ -4162,6 +4425,7 @@ function parseMetadataColumn(value) {
|
|
|
4162
4425
|
var DEFAULT_GROUP_MAX_INSTANCES = {
|
|
4163
4426
|
intake: 1,
|
|
4164
4427
|
harvest: 1,
|
|
4428
|
+
harvester: 0,
|
|
4165
4429
|
loader: 0,
|
|
4166
4430
|
photos: 0,
|
|
4167
4431
|
photosprocessor: 0,
|
|
@@ -4171,25 +4435,6 @@ function sanitizeNamePart(raw) {
|
|
|
4171
4435
|
const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
4172
4436
|
return s.slice(0, 80) || "runner";
|
|
4173
4437
|
}
|
|
4174
|
-
function identityFilePath(identityDir, queue, serviceGroup) {
|
|
4175
|
-
const safeQ = sanitizeNamePart(queue);
|
|
4176
|
-
const safeG = sanitizeNamePart(serviceGroup);
|
|
4177
|
-
return path4.join(identityDir, `${safeQ}_${safeG}.json`);
|
|
4178
|
-
}
|
|
4179
|
-
async function readIdentityFile(filePath) {
|
|
4180
|
-
try {
|
|
4181
|
-
const text = await readFile(filePath, "utf8");
|
|
4182
|
-
const parsed = JSON.parse(text);
|
|
4183
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
4184
|
-
} catch {
|
|
4185
|
-
return {};
|
|
4186
|
-
}
|
|
4187
|
-
}
|
|
4188
|
-
async function writeIdentityFile(filePath, data) {
|
|
4189
|
-
await mkdir(path4.dirname(filePath), { recursive: true });
|
|
4190
|
-
await writeFile(filePath, `${JSON.stringify(data, null, 2)}
|
|
4191
|
-
`, "utf8");
|
|
4192
|
-
}
|
|
4193
4438
|
function resolveMaxInstances(serviceGroup, override) {
|
|
4194
4439
|
if (override !== void 0 && Number.isFinite(override)) {
|
|
4195
4440
|
return Math.max(0, Math.floor(Number(override)));
|
|
@@ -4197,179 +4442,190 @@ function resolveMaxInstances(serviceGroup, override) {
|
|
|
4197
4442
|
const g = serviceGroup.trim().toLowerCase();
|
|
4198
4443
|
return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
|
|
4199
4444
|
}
|
|
4200
|
-
async function countAliveInGroup(db, registryTable,
|
|
4445
|
+
async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
|
|
4201
4446
|
const cutoff = new Date(Date.now() - staleMs);
|
|
4202
|
-
let q = db(registryTable).where({
|
|
4203
|
-
if (
|
|
4204
|
-
q = q.whereNot("
|
|
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);
|
|
4205
4450
|
}
|
|
4206
4451
|
const row = await q.count("id as count").first();
|
|
4207
4452
|
return Number(row?.count ?? 0);
|
|
4208
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
|
+
}
|
|
4209
4464
|
function isUniqueViolation(error) {
|
|
4210
4465
|
const code = error?.code ?? error?.errno;
|
|
4211
4466
|
return code === "23505" || String(error?.message || "").includes("duplicate key");
|
|
4212
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
|
+
}
|
|
4213
4495
|
async function registerInServicesRegistry(context, options) {
|
|
4214
4496
|
const db = getDb2(context);
|
|
4215
|
-
const registryTable =
|
|
4497
|
+
const registryTable = queueToTableNames(options.queueName).registryTable;
|
|
4216
4498
|
const serviceGroup = options.serviceGroup.trim();
|
|
4217
4499
|
if (!serviceGroup) {
|
|
4218
4500
|
throw new Error("registerInServicesRegistry: serviceGroup is required");
|
|
4219
4501
|
}
|
|
4220
|
-
const
|
|
4221
|
-
let identity = await readIdentityFile(identityPath);
|
|
4222
|
-
let instanceId = typeof identity.instanceId === "string" && identity.instanceId.trim() ? identity.instanceId.trim() : randomUUID2();
|
|
4223
|
-
identity.instanceId = instanceId;
|
|
4224
|
-
await writeIdentityFile(identityPath, identity);
|
|
4225
|
-
const hostname = os.hostname();
|
|
4502
|
+
const serverName = os.hostname();
|
|
4226
4503
|
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
4227
|
-
const meta =
|
|
4228
|
-
const
|
|
4229
|
-
|
|
4230
|
-
await db(registryTable).where({ instance_id: instanceId }).update({
|
|
4231
|
-
target: options.target,
|
|
4232
|
-
hostname,
|
|
4233
|
-
pid,
|
|
4234
|
-
metadata: meta,
|
|
4235
|
-
last_seen_at: db.fn.now()
|
|
4236
|
-
});
|
|
4237
|
-
const serviceName = String(existing.service_name);
|
|
4238
|
-
identity.serviceName = serviceName;
|
|
4239
|
-
await writeIdentityFile(identityPath, identity);
|
|
4240
|
-
const reg = {
|
|
4241
|
-
instanceId,
|
|
4242
|
-
serviceName,
|
|
4243
|
-
serviceGroup,
|
|
4244
|
-
queue: options.queue,
|
|
4245
|
-
target: options.target,
|
|
4246
|
-
rowId: String(existing.id)
|
|
4247
|
-
};
|
|
4248
|
-
context.servicesRegistry = reg;
|
|
4249
|
-
context.runnerHeartbeat = reg;
|
|
4250
|
-
context.logger.info?.(
|
|
4251
|
-
`[services-registry] resumed instance_id=${instanceId} name=${serviceName} group=${serviceGroup} queue=${options.queue}`
|
|
4252
|
-
);
|
|
4253
|
-
return {
|
|
4254
|
-
instanceId,
|
|
4255
|
-
serviceName,
|
|
4256
|
-
serviceGroup,
|
|
4257
|
-
queue: options.queue,
|
|
4258
|
-
target: options.target,
|
|
4259
|
-
rowId: String(existing.id),
|
|
4260
|
-
registryTable
|
|
4261
|
-
};
|
|
4262
|
-
}
|
|
4504
|
+
const meta = buildMetadata(options);
|
|
4505
|
+
const groupBase = sanitizeNamePart(serviceGroup);
|
|
4506
|
+
const hostBase = sanitizeNamePart(serverName);
|
|
4263
4507
|
const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
|
|
4264
|
-
const
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
options.queue,
|
|
4268
|
-
serviceGroup,
|
|
4269
|
-
options.staleMs,
|
|
4270
|
-
instanceId
|
|
4271
|
-
);
|
|
4272
|
-
if (maxAllowed > 0 && aliveOthers >= maxAllowed) {
|
|
4273
|
-
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveOthers} alive (max ${maxAllowed}, queue=${options.queue}).`;
|
|
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}).`;
|
|
4274
4511
|
if (options.enforceMaxInstances) {
|
|
4275
4512
|
throw new Error(msg);
|
|
4276
4513
|
}
|
|
4277
4514
|
context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
|
|
4278
4515
|
}
|
|
4279
|
-
const
|
|
4280
|
-
const
|
|
4281
|
-
const
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
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;
|
|
4299
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;
|
|
4300
4567
|
}
|
|
4301
|
-
}
|
|
4302
|
-
let inserted;
|
|
4303
|
-
for (const candidate of eachServiceNameCandidate(baseCandidates)) {
|
|
4304
4568
|
try {
|
|
4305
4569
|
const rows = await db(registryTable).insert({
|
|
4306
|
-
|
|
4307
|
-
queue: options.queue,
|
|
4570
|
+
queue_name: options.queueName,
|
|
4308
4571
|
service_group: serviceGroup,
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4572
|
+
instance_number: instanceNumber,
|
|
4573
|
+
service_name: serviceNameRaw,
|
|
4574
|
+
server_name: serverName,
|
|
4312
4575
|
pid,
|
|
4313
4576
|
metadata: meta,
|
|
4314
|
-
last_seen_at: db.fn.now()
|
|
4577
|
+
last_seen_at: db.fn.now(),
|
|
4578
|
+
created_at: db.fn.now()
|
|
4315
4579
|
}).returning(["id", "service_name"]);
|
|
4316
4580
|
const row = Array.isArray(rows) ? rows[0] : rows;
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
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) : "";
|
|
4320
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;
|
|
4321
4602
|
} catch (error) {
|
|
4322
4603
|
if (!isUniqueViolation(error)) {
|
|
4323
4604
|
throw error;
|
|
4324
4605
|
}
|
|
4606
|
+
context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
|
|
4325
4607
|
}
|
|
4326
4608
|
}
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
`[services-registry] could not allocate a unique service_name for group=${serviceGroup} queue=${options.queue} (too many collisions).`
|
|
4330
|
-
);
|
|
4331
|
-
}
|
|
4332
|
-
identity.serviceName = inserted.service_name;
|
|
4333
|
-
await writeIdentityFile(identityPath, identity);
|
|
4334
|
-
const regNew = {
|
|
4335
|
-
instanceId,
|
|
4336
|
-
serviceName: inserted.service_name,
|
|
4337
|
-
serviceGroup,
|
|
4338
|
-
queue: options.queue,
|
|
4339
|
-
target: options.target,
|
|
4340
|
-
rowId: inserted.id
|
|
4341
|
-
};
|
|
4342
|
-
context.servicesRegistry = regNew;
|
|
4343
|
-
context.runnerHeartbeat = regNew;
|
|
4344
|
-
context.logger.info?.(
|
|
4345
|
-
`[services-registry] registered instance_id=${instanceId} name=${inserted.service_name} group=${serviceGroup} queue=${options.queue} target=${options.target}`
|
|
4609
|
+
throw new Error(
|
|
4610
|
+
`[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
|
|
4346
4611
|
);
|
|
4347
|
-
return {
|
|
4348
|
-
instanceId,
|
|
4349
|
-
serviceName: inserted.service_name,
|
|
4350
|
-
serviceGroup,
|
|
4351
|
-
queue: options.queue,
|
|
4352
|
-
target: options.target,
|
|
4353
|
-
rowId: inserted.id,
|
|
4354
|
-
registryTable
|
|
4355
|
-
};
|
|
4356
4612
|
}
|
|
4357
4613
|
async function touchServicesRegistry(context, registration) {
|
|
4358
4614
|
const db = getDb2(context);
|
|
4359
|
-
const
|
|
4615
|
+
const serverName = os.hostname();
|
|
4360
4616
|
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
4361
|
-
await db(registration.registryTable).where({
|
|
4617
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
4362
4618
|
last_seen_at: db.fn.now(),
|
|
4363
|
-
|
|
4619
|
+
server_name: serverName,
|
|
4364
4620
|
pid
|
|
4365
4621
|
});
|
|
4366
4622
|
}
|
|
4367
4623
|
async function updateServicesRegistryMetadata(context, registration, patch) {
|
|
4368
4624
|
const db = getDb2(context);
|
|
4369
|
-
const row = await db(registration.registryTable).where({
|
|
4625
|
+
const row = await db(registration.registryTable).where({ id: registration.rowId }).first();
|
|
4370
4626
|
const prev = parseMetadataColumn(row?.metadata);
|
|
4371
4627
|
const merged = { ...prev, ...patch };
|
|
4372
|
-
await db(registration.registryTable).where({
|
|
4628
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
4373
4629
|
metadata: toJsonColumn(merged),
|
|
4374
4630
|
last_seen_at: db.fn.now()
|
|
4375
4631
|
});
|
|
@@ -4377,14 +4633,14 @@ async function updateServicesRegistryMetadata(context, registration, patch) {
|
|
|
4377
4633
|
}
|
|
4378
4634
|
async function unregisterServicesRegistry(context, registration) {
|
|
4379
4635
|
const db = getDb2(context);
|
|
4380
|
-
await db(registration.registryTable).where({
|
|
4381
|
-
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName}
|
|
4636
|
+
await db(registration.registryTable).where({ id: registration.rowId }).delete();
|
|
4637
|
+
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
|
|
4382
4638
|
}
|
|
4383
|
-
async function listServicesRegistry(context, options = {
|
|
4639
|
+
async function listServicesRegistry(context, options = { queueName: "tasks" }) {
|
|
4384
4640
|
const db = getDb2(context);
|
|
4385
4641
|
const staleMs = options.staleMs ?? 6e4;
|
|
4386
4642
|
const cutoff = new Date(Date.now() - staleMs);
|
|
4387
|
-
const table =
|
|
4643
|
+
const table = queueToTableNames(options.queueName).registryTable;
|
|
4388
4644
|
let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
|
|
4389
4645
|
if (options.serviceGroup?.trim()) {
|
|
4390
4646
|
q = q.where({ service_group: options.serviceGroup.trim() });
|
|
@@ -4392,7 +4648,8 @@ async function listServicesRegistry(context, options = { queue: "tasks" }) {
|
|
|
4392
4648
|
return await q;
|
|
4393
4649
|
}
|
|
4394
4650
|
|
|
4395
|
-
// src/tasks/taskLogs.
|
|
4651
|
+
// src/tasks/taskLogs.js
|
|
4652
|
+
import path4 from "path";
|
|
4396
4653
|
function getLogsState(context) {
|
|
4397
4654
|
const holder = context;
|
|
4398
4655
|
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
@@ -4445,33 +4702,135 @@ function getLogsState(context) {
|
|
|
4445
4702
|
holder.__tasksLogsState = state;
|
|
4446
4703
|
return state;
|
|
4447
4704
|
}
|
|
4448
|
-
function
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
if (level === "error" || level === "fatal") return true;
|
|
4453
|
-
if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
|
|
4454
|
-
return false;
|
|
4455
|
-
}
|
|
4456
|
-
if (typeof payload === "string") {
|
|
4457
|
-
return /\berror\b/i.test(payload);
|
|
4458
|
-
}
|
|
4459
|
-
return false;
|
|
4705
|
+
function ipcLogTargetKey(target) {
|
|
4706
|
+
const bp = target.basePath ?? "";
|
|
4707
|
+
const ns = target.namespace ?? "";
|
|
4708
|
+
return `${bp}::${ns}::${target.tableName}`;
|
|
4460
4709
|
}
|
|
4461
|
-
function
|
|
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);
|
|
4793
|
+
return state;
|
|
4794
|
+
}
|
|
4795
|
+
function isErrorPayload(payload) {
|
|
4796
|
+
if (!payload) return false;
|
|
4797
|
+
if (typeof payload === "object") {
|
|
4798
|
+
const level = typeof payload.level === "string" ? payload.level.toLowerCase() : "";
|
|
4799
|
+
if (level === "error" || level === "fatal") return true;
|
|
4800
|
+
if (typeof payload.message === "string" && /\berror\b/i.test(payload.message)) return true;
|
|
4801
|
+
return false;
|
|
4802
|
+
}
|
|
4803
|
+
if (typeof payload === "string") {
|
|
4804
|
+
return /\berror\b/i.test(payload);
|
|
4805
|
+
}
|
|
4806
|
+
return false;
|
|
4807
|
+
}
|
|
4808
|
+
function buildLogRecord(task, payload) {
|
|
4462
4809
|
const params = task.params && typeof task.params === "object" ? task.params : {};
|
|
4463
4810
|
return {
|
|
4464
4811
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4465
4812
|
opid: task.opid ?? null,
|
|
4466
4813
|
taskId: task.id,
|
|
4467
|
-
taskName: task.
|
|
4468
|
-
target: task.
|
|
4814
|
+
taskName: task.name,
|
|
4815
|
+
target: task.service_group,
|
|
4469
4816
|
source: typeof params.source === "string" ? params.source : null,
|
|
4470
4817
|
resource: typeof params.resource === "string" ? params.resource : null,
|
|
4471
4818
|
payload
|
|
4472
4819
|
};
|
|
4473
4820
|
}
|
|
4474
|
-
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
|
+
}
|
|
4475
4834
|
const state = getLogsState(context);
|
|
4476
4835
|
if (!state.db && !state.errorDb) return;
|
|
4477
4836
|
const record = buildLogRecord(task, payload);
|
|
@@ -4488,84 +4847,293 @@ function appendTaskIpcLog(context, task, payload) {
|
|
|
4488
4847
|
context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
|
|
4489
4848
|
});
|
|
4490
4849
|
}
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
let current = field;
|
|
4500
|
-
while (true) {
|
|
4501
|
-
const match = regex.exec(current);
|
|
4502
|
-
if (!match) break;
|
|
4503
|
-
const raw = match[0];
|
|
4504
|
-
let first = Number(match[1]);
|
|
4505
|
-
let last = Number(match[2]);
|
|
4506
|
-
if (last < first) {
|
|
4507
|
-
[first, last] = [last, first];
|
|
4508
|
-
}
|
|
4509
|
-
const values = [];
|
|
4510
|
-
for (let i = first; i <= last; i += 1) {
|
|
4511
|
-
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);
|
|
4512
4858
|
}
|
|
4513
|
-
current = current.replace(raw, values.join(","));
|
|
4514
|
-
}
|
|
4515
|
-
return current;
|
|
4516
|
-
}
|
|
4517
|
-
function resolveSteps(field) {
|
|
4518
|
-
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
4519
|
-
if (!match) return field;
|
|
4520
|
-
const base = match[1];
|
|
4521
|
-
const step = Number(match[2]);
|
|
4522
|
-
if (!Number.isFinite(step) || step <= 0) return field;
|
|
4523
|
-
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
4524
|
-
}
|
|
4525
|
-
function convertPattern(pattern) {
|
|
4526
|
-
const parts = pattern.trim().split(/\s+/);
|
|
4527
|
-
if (parts.length !== 6) {
|
|
4528
|
-
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
4529
4859
|
}
|
|
4530
|
-
|
|
4531
|
-
}
|
|
4532
|
-
function fieldMatches(field, value) {
|
|
4533
|
-
const allowed = field.split(",").map((v) => Number(v));
|
|
4534
|
-
return allowed.includes(value);
|
|
4535
|
-
}
|
|
4536
|
-
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
4537
|
-
const parsed = convertPattern(pattern);
|
|
4538
|
-
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);
|
|
4539
4861
|
}
|
|
4540
4862
|
|
|
4541
|
-
// src/tasks/
|
|
4542
|
-
var
|
|
4543
|
-
|
|
4544
|
-
|
|
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
|
+
*/
|
|
4545
4877
|
constructor(context, task) {
|
|
4546
4878
|
this.context = context;
|
|
4547
4879
|
this.task = task;
|
|
4548
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
|
+
*/
|
|
4549
4887
|
cantRunReason() {
|
|
4550
4888
|
return false;
|
|
4551
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
|
+
*/
|
|
4552
4896
|
requestStop(_allowanceMs) {
|
|
4553
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
|
+
}
|
|
4554
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
|
+
}
|
|
4555
5076
|
|
|
4556
|
-
// src/tasks/coreTasks/TaskPing.
|
|
4557
|
-
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
|
+
*/
|
|
4558
5088
|
async run() {
|
|
4559
5089
|
this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
|
|
4560
5090
|
return { success: true, results: "pong" };
|
|
4561
5091
|
}
|
|
4562
5092
|
};
|
|
4563
5093
|
|
|
4564
|
-
// src/tasks/coreTasks/TaskSampleProcess.
|
|
4565
|
-
var TaskSampleProcess = class extends
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
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
|
+
*/
|
|
4569
5137
|
requestStop(allowanceMs) {
|
|
4570
5138
|
this.stopRequested = true;
|
|
4571
5139
|
this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
|
|
@@ -4573,6 +5141,14 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4573
5141
|
`[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
|
|
4574
5142
|
);
|
|
4575
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
|
+
*/
|
|
4576
5152
|
async run(reportProgress) {
|
|
4577
5153
|
const totalRaw = this.task?.params?.total ?? 10;
|
|
4578
5154
|
const delayRaw = this.task?.params?.delay ?? 1e3;
|
|
@@ -4654,7 +5230,7 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4654
5230
|
}
|
|
4655
5231
|
};
|
|
4656
5232
|
|
|
4657
|
-
// src/tasks/coreTasks/TaskShellCommand.
|
|
5233
|
+
// src/tasks/coreTasks/TaskShellCommand.js
|
|
4658
5234
|
import { spawn } from "child_process";
|
|
4659
5235
|
function runShellCommand(command, cwd) {
|
|
4660
5236
|
return new Promise((resolve2, reject) => {
|
|
@@ -4684,7 +5260,27 @@ function runShellCommand(command, cwd) {
|
|
|
4684
5260
|
});
|
|
4685
5261
|
});
|
|
4686
5262
|
}
|
|
4687
|
-
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
|
+
*/
|
|
4688
5284
|
async run() {
|
|
4689
5285
|
const params = this.task?.params;
|
|
4690
5286
|
const commandRaw = typeof params === "string" ? params : params?.command;
|
|
@@ -4733,7 +5329,7 @@ var TaskShellCommand = class extends TaskMaster {
|
|
|
4733
5329
|
}
|
|
4734
5330
|
};
|
|
4735
5331
|
|
|
4736
|
-
// src/tasks/coreTasks/TaskSystemInfo.
|
|
5332
|
+
// src/tasks/coreTasks/TaskSystemInfo.js
|
|
4737
5333
|
import os2 from "os";
|
|
4738
5334
|
import fs4 from "fs/promises";
|
|
4739
5335
|
function toGb(valueBytes) {
|
|
@@ -4753,7 +5349,16 @@ async function getDiskStats() {
|
|
|
4753
5349
|
free: toGb(free)
|
|
4754
5350
|
};
|
|
4755
5351
|
}
|
|
4756
|
-
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
|
+
*/
|
|
4757
5362
|
async run() {
|
|
4758
5363
|
try {
|
|
4759
5364
|
const totalMemory = os2.totalmem();
|
|
@@ -4805,8 +5410,31 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
4805
5410
|
}
|
|
4806
5411
|
};
|
|
4807
5412
|
|
|
4808
|
-
// src/tasks/coreTasks/TaskSumAB.
|
|
4809
|
-
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
|
+
*/
|
|
4810
5438
|
async run() {
|
|
4811
5439
|
const a = this.task?.params?.a;
|
|
4812
5440
|
const b = this.task?.params?.b;
|
|
@@ -4837,8 +5465,46 @@ var TaskSumAB = class extends TaskMaster {
|
|
|
4837
5465
|
}
|
|
4838
5466
|
};
|
|
4839
5467
|
|
|
4840
|
-
// src/tasks/coreTasks/TaskStopRunner.
|
|
4841
|
-
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
|
+
*/
|
|
4842
5508
|
async run() {
|
|
4843
5509
|
const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
|
|
4844
5510
|
this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
|
|
@@ -4853,172 +5519,396 @@ var TaskStopRunner = class extends TaskMaster {
|
|
|
4853
5519
|
}
|
|
4854
5520
|
};
|
|
4855
5521
|
|
|
4856
|
-
// 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
|
|
4857
5586
|
var TasksRegistry = class _TasksRegistry {
|
|
4858
|
-
|
|
5587
|
+
/**
|
|
5588
|
+
* @param {Record<string, Function>} [initial] Optional seed entries to copy in.
|
|
5589
|
+
*/
|
|
4859
5590
|
constructor(initial) {
|
|
5591
|
+
this.map = {};
|
|
4860
5592
|
if (initial) {
|
|
4861
5593
|
this.addMany(initial);
|
|
4862
5594
|
}
|
|
4863
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
|
+
*/
|
|
4864
5602
|
static withCoreTasks() {
|
|
4865
|
-
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);
|
|
4866
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
|
+
*/
|
|
4867
5612
|
add(taskName, taskClass) {
|
|
4868
5613
|
this.map[taskName] = taskClass;
|
|
4869
5614
|
return this;
|
|
4870
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
|
+
*/
|
|
4871
5622
|
addMany(entries) {
|
|
4872
5623
|
for (const [name, klass] of Object.entries(entries)) {
|
|
4873
5624
|
this.add(name, klass);
|
|
4874
5625
|
}
|
|
4875
5626
|
return this;
|
|
4876
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
|
+
*/
|
|
4877
5635
|
get(taskName) {
|
|
4878
5636
|
return this.map[taskName];
|
|
4879
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
|
+
*/
|
|
4880
5687
|
listSupportedTasks() {
|
|
4881
5688
|
return Object.keys(this.map).sort();
|
|
4882
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
|
+
*/
|
|
4883
5696
|
toObject() {
|
|
4884
5697
|
return { ...this.map };
|
|
4885
5698
|
}
|
|
4886
5699
|
};
|
|
4887
5700
|
|
|
4888
|
-
// 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
|
|
4889
5726
|
import { spawn as spawn2 } from "child_process";
|
|
5727
|
+
var MAX_PROGRESS_TEXT_LEN = 4e3;
|
|
4890
5728
|
function toCliArgs(args = []) {
|
|
4891
5729
|
return args.filter((a) => typeof a === "string" && a.length > 0);
|
|
4892
5730
|
}
|
|
4893
5731
|
function formatChildLogPrefix(task) {
|
|
4894
|
-
return `${task.
|
|
5732
|
+
return `${task.name}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
|
|
4895
5733
|
}
|
|
4896
|
-
|
|
4897
|
-
|
|
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) {
|
|
4898
5771
|
const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
|
|
4899
5772
|
const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
}
|
|
4913
|
-
}
|
|
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)")
|
|
4914
5785
|
);
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
let
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
|
|
4928
|
-
}).catch((error) => {
|
|
4929
|
-
context.logger.warn?.(
|
|
4930
|
-
`[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
4931
|
-
);
|
|
4932
|
-
});
|
|
4933
|
-
if (options.onProgress) {
|
|
4934
|
-
progressCallbackChain = progressCallbackChain.then(async () => {
|
|
4935
|
-
await options.onProgress?.(text.slice(0, 4e3));
|
|
4936
|
-
}).catch((error) => {
|
|
4937
|
-
context.logger.warn?.(
|
|
4938
|
-
`[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
4939
|
-
);
|
|
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(() => {
|
|
4940
5798
|
});
|
|
4941
5799
|
}
|
|
4942
5800
|
};
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
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
|
|
4956
5824
|
};
|
|
4957
|
-
|
|
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
|
+
});
|
|
5849
|
+
};
|
|
5850
|
+
child.stdout?.on("data", (chunk) => {
|
|
4958
5851
|
const text = String(chunk);
|
|
4959
|
-
stdout += text;
|
|
5852
|
+
state.stdout += text;
|
|
4960
5853
|
if (text.trim()) {
|
|
4961
5854
|
context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
4962
|
-
updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
|
|
4963
5855
|
}
|
|
4964
5856
|
});
|
|
4965
|
-
child.stderr
|
|
5857
|
+
child.stderr?.on("data", (chunk) => {
|
|
4966
5858
|
const text = String(chunk);
|
|
4967
|
-
stderr += text;
|
|
5859
|
+
state.stderr += text;
|
|
4968
5860
|
if (text.trim()) {
|
|
4969
5861
|
context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
4970
5862
|
}
|
|
4971
5863
|
});
|
|
4972
5864
|
child.on("message", (message) => {
|
|
4973
5865
|
if (message && typeof message === "object" && "__taskWorkerResult" in message) {
|
|
4974
|
-
workerResult = message.__taskWorkerResult;
|
|
5866
|
+
state.workerResult = message.__taskWorkerResult;
|
|
4975
5867
|
return;
|
|
4976
5868
|
}
|
|
4977
5869
|
if (message && typeof message === "object") {
|
|
4978
5870
|
const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
|
|
4979
5871
|
if (level === "error" || level === "fatal") {
|
|
4980
|
-
hadErrorMessage = true;
|
|
5872
|
+
state.hadErrorMessage = true;
|
|
4981
5873
|
}
|
|
4982
5874
|
}
|
|
4983
|
-
appendTaskIpcLog(context, options.task, message);
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
4998
|
-
}
|
|
4999
|
-
} else {
|
|
5000
|
-
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
5001
|
-
}
|
|
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)}`);
|
|
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;
|
|
5002
5889
|
}
|
|
5890
|
+
forwardChildLogToParent(context, prefix, message);
|
|
5003
5891
|
});
|
|
5004
5892
|
return await new Promise((resolve2, reject) => {
|
|
5005
5893
|
child.on("error", (error) => reject(error));
|
|
5006
5894
|
child.on("close", (exitCode, signal) => {
|
|
5007
|
-
|
|
5895
|
+
void (async () => {
|
|
5896
|
+
await flushTaskIpcLogs(context);
|
|
5897
|
+
await progressQueue.drain();
|
|
5008
5898
|
resolve2({
|
|
5009
5899
|
exitCode,
|
|
5010
5900
|
signal,
|
|
5011
|
-
stdout: stdout.trim(),
|
|
5012
|
-
stderr: stderr.trim(),
|
|
5013
|
-
workerResult,
|
|
5014
|
-
hadErrorMessage
|
|
5901
|
+
stdout: state.stdout.trim(),
|
|
5902
|
+
stderr: state.stderr.trim(),
|
|
5903
|
+
workerResult: state.workerResult,
|
|
5904
|
+
hadErrorMessage: state.hadErrorMessage
|
|
5015
5905
|
});
|
|
5016
|
-
});
|
|
5906
|
+
})();
|
|
5017
5907
|
});
|
|
5018
5908
|
});
|
|
5019
5909
|
}
|
|
5020
5910
|
|
|
5021
|
-
// src/tasks/index.
|
|
5911
|
+
// src/tasks/index.js
|
|
5022
5912
|
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
5023
5913
|
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
5024
5914
|
function getDb3(context) {
|
|
@@ -5033,22 +5923,13 @@ function normalizeRegistry(registry) {
|
|
|
5033
5923
|
if (registry instanceof TasksRegistry) return registry;
|
|
5034
5924
|
return new TasksRegistry().addMany(registry);
|
|
5035
5925
|
}
|
|
5036
|
-
function
|
|
5037
|
-
if (!value) return void 0;
|
|
5038
|
-
if (Array.isArray(value)) {
|
|
5039
|
-
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
5040
|
-
return out2.length ? out2 : void 0;
|
|
5041
|
-
}
|
|
5042
|
-
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
5043
|
-
return out.length ? out : void 0;
|
|
5044
|
-
}
|
|
5045
|
-
async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
|
|
5926
|
+
async function enqueueStopTask(context, serviceGroup, queueName = "tasks", allowanceMs = 5e3) {
|
|
5046
5927
|
return enqueueTask(context, {
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
task: "stopRunner",
|
|
5928
|
+
queueName,
|
|
5929
|
+
name: "stopRunner",
|
|
5050
5930
|
params: { allowanceMs },
|
|
5051
|
-
priority:
|
|
5931
|
+
priority: 0,
|
|
5932
|
+
serviceGroup
|
|
5052
5933
|
});
|
|
5053
5934
|
}
|
|
5054
5935
|
async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
|
|
@@ -5066,18 +5947,20 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
|
|
|
5066
5947
|
}
|
|
5067
5948
|
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
5068
5949
|
const db = getDb3(context);
|
|
5069
|
-
const taskName = row.
|
|
5950
|
+
const taskName = row.name;
|
|
5070
5951
|
const TaskClass = registry.get(taskName);
|
|
5071
|
-
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
5072
5952
|
if (!TaskClass) {
|
|
5073
5953
|
const err = { message: `Unknown task "${taskName}"` };
|
|
5074
|
-
await db(historyTable).insert(
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
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
|
+
);
|
|
5081
5964
|
if (row.schedule) {
|
|
5082
5965
|
await db(tasksTable).where({ id: row.id }).update({
|
|
5083
5966
|
started_at: null,
|
|
@@ -5085,7 +5968,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5085
5968
|
success: false,
|
|
5086
5969
|
results: toJsonColumn(err),
|
|
5087
5970
|
past_due: null,
|
|
5088
|
-
|
|
5971
|
+
status: "paused",
|
|
5972
|
+
status_changed_at: db.fn.now(),
|
|
5089
5973
|
progress: LOCKED_BY_ERROR_MESSAGE
|
|
5090
5974
|
});
|
|
5091
5975
|
} else {
|
|
@@ -5112,13 +5996,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5112
5996
|
} finally {
|
|
5113
5997
|
runningTaskInstances.delete(row.id);
|
|
5114
5998
|
}
|
|
5115
|
-
await db(historyTable).insert(
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
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
|
+
);
|
|
5122
6009
|
if (!success) {
|
|
5123
6010
|
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
5124
6011
|
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
@@ -5133,19 +6020,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5133
6020
|
const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
|
|
5134
6021
|
appendTaskIpcLog(context, row, {
|
|
5135
6022
|
level: "error",
|
|
5136
|
-
message: `[tasks] task failed: ${row.
|
|
6023
|
+
message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
|
|
5137
6024
|
details: results
|
|
5138
6025
|
});
|
|
5139
6026
|
}
|
|
5140
6027
|
if (row.schedule) {
|
|
5141
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
|
+
}
|
|
5142
6035
|
await db(tasksTable).where({ id: row.id }).update({
|
|
5143
6036
|
started_at: null,
|
|
5144
6037
|
completed_at: /* @__PURE__ */ new Date(),
|
|
5145
6038
|
success,
|
|
5146
6039
|
results: toJsonColumn(results),
|
|
5147
6040
|
progress: null,
|
|
5148
|
-
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
|
|
5149
6049
|
});
|
|
5150
6050
|
} else {
|
|
5151
6051
|
await db(tasksTable).where({ id: row.id }).update({
|
|
@@ -5153,7 +6053,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5153
6053
|
completed_at: /* @__PURE__ */ new Date(),
|
|
5154
6054
|
success,
|
|
5155
6055
|
results: toJsonColumn(results),
|
|
5156
|
-
|
|
6056
|
+
status: "paused",
|
|
6057
|
+
status_changed_at: db.fn.now(),
|
|
5157
6058
|
progress: LOCKED_BY_ERROR_MESSAGE,
|
|
5158
6059
|
past_due: null
|
|
5159
6060
|
});
|
|
@@ -5165,58 +6066,93 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5165
6066
|
const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
|
|
5166
6067
|
return { stopRunnerRequested, stopAllowanceMs };
|
|
5167
6068
|
}
|
|
5168
|
-
|
|
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) {
|
|
5169
6078
|
const db = getDb3(context);
|
|
5170
|
-
let query = db(tasksTable).
|
|
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);
|
|
5171
6082
|
if (taskNames && taskNames.length > 0) {
|
|
5172
|
-
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");
|
|
5173
6095
|
}
|
|
5174
6096
|
const candidates = await query;
|
|
6097
|
+
shuffleTaskRowsInPlace(candidates);
|
|
5175
6098
|
for (const row of candidates) {
|
|
5176
6099
|
if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
|
|
5177
6100
|
continue;
|
|
5178
6101
|
}
|
|
5179
|
-
const TaskClass = registry.get(row.
|
|
5180
|
-
if (TaskClass) {
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
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
|
+
});
|
|
5191
6114
|
}
|
|
6115
|
+
continue;
|
|
5192
6116
|
}
|
|
5193
|
-
const
|
|
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;
|
|
6126
|
+
}
|
|
6127
|
+
const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
|
|
5194
6128
|
const claimed = Array.isArray(updated) ? updated[0] : null;
|
|
5195
6129
|
if (claimed) return claimed;
|
|
5196
6130
|
}
|
|
5197
6131
|
return null;
|
|
5198
6132
|
}
|
|
5199
6133
|
async function runTasksLoop(context, options) {
|
|
5200
|
-
const
|
|
6134
|
+
const queueName = options.queueName ?? "tasks";
|
|
5201
6135
|
const target = options.target;
|
|
5202
6136
|
const pollMs = options.pollMs ?? 1e3;
|
|
5203
|
-
const
|
|
6137
|
+
const claimJitterMs = options.claimJitterMs ?? 0;
|
|
6138
|
+
const maxParallel = options.maxParallel ?? 32;
|
|
5204
6139
|
const scanLimit = options.scanLimit ?? 100;
|
|
5205
6140
|
const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5206
6141
|
const registry = normalizeRegistry(options.registry);
|
|
5207
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
6142
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
5208
6143
|
if (!target) throw new Error("runTasksLoop: target is required");
|
|
6144
|
+
context.tasksQueueName = queueName;
|
|
5209
6145
|
const runningPromises = /* @__PURE__ */ new Set();
|
|
5210
6146
|
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
5211
6147
|
let runningStopControlPromise = null;
|
|
5212
6148
|
let stopRequested = false;
|
|
5213
6149
|
let stopAllowanceMs = 5e3;
|
|
5214
|
-
context.
|
|
6150
|
+
context.tasksRunnerStop = false;
|
|
5215
6151
|
let registryReg = null;
|
|
5216
6152
|
let registryInterval = null;
|
|
6153
|
+
let runnerIdentity = null;
|
|
5217
6154
|
const hbGroup = options.runnerServiceGroup?.trim();
|
|
5218
6155
|
if (hbGroup) {
|
|
5219
|
-
const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
|
|
5220
6156
|
const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
|
|
5221
6157
|
const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
|
|
5222
6158
|
const defaultMeta = {
|
|
@@ -5224,16 +6160,21 @@ async function runTasksLoop(context, options) {
|
|
|
5224
6160
|
allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
|
|
5225
6161
|
};
|
|
5226
6162
|
registryReg = await registerInServicesRegistry(context, {
|
|
5227
|
-
|
|
6163
|
+
queueName,
|
|
5228
6164
|
target,
|
|
5229
6165
|
serviceGroup: hbGroup,
|
|
5230
6166
|
serviceName: options.runnerServiceName,
|
|
5231
|
-
|
|
6167
|
+
instanceNumber: options.runnerInstanceNumber,
|
|
5232
6168
|
staleMs,
|
|
5233
6169
|
groupMaxInstances: options.runnerGroupMaxInstances,
|
|
5234
6170
|
enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
|
|
5235
6171
|
metadata: options.runnerMetadata ?? defaultMeta
|
|
5236
6172
|
});
|
|
6173
|
+
runnerIdentity = {
|
|
6174
|
+
service_name: registryReg.serviceName,
|
|
6175
|
+
server_name: os3.hostname(),
|
|
6176
|
+
instance_number: registryReg.instanceNumber
|
|
6177
|
+
};
|
|
5237
6178
|
registryInterval = setInterval(() => {
|
|
5238
6179
|
void touchServicesRegistry(context, registryReg).catch((err) => {
|
|
5239
6180
|
context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
|
|
@@ -5241,7 +6182,7 @@ async function runTasksLoop(context, options) {
|
|
|
5241
6182
|
}, hbIntervalMs);
|
|
5242
6183
|
}
|
|
5243
6184
|
try {
|
|
5244
|
-
while (!context.isStop() && !stopRequested &&
|
|
6185
|
+
while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
|
|
5245
6186
|
if (!runningStopControlPromise) {
|
|
5246
6187
|
const claimedStopTask = await claimNextRunnableTask(
|
|
5247
6188
|
context,
|
|
@@ -5249,7 +6190,8 @@ async function runTasksLoop(context, options) {
|
|
|
5249
6190
|
target,
|
|
5250
6191
|
registry,
|
|
5251
6192
|
10,
|
|
5252
|
-
["stopRunner", "stop"]
|
|
6193
|
+
["stopRunner", "stop"],
|
|
6194
|
+
runnerIdentity
|
|
5253
6195
|
);
|
|
5254
6196
|
if (claimedStopTask) {
|
|
5255
6197
|
runningStopControlPromise = executeClaimedTask(
|
|
@@ -5263,7 +6205,7 @@ async function runTasksLoop(context, options) {
|
|
|
5263
6205
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5264
6206
|
stopRequested = true;
|
|
5265
6207
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5266
|
-
context.
|
|
6208
|
+
context.tasksRunnerStop = true;
|
|
5267
6209
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5268
6210
|
}
|
|
5269
6211
|
}).finally(() => {
|
|
@@ -5271,6 +6213,9 @@ async function runTasksLoop(context, options) {
|
|
|
5271
6213
|
});
|
|
5272
6214
|
}
|
|
5273
6215
|
}
|
|
6216
|
+
if (claimJitterMs > 0) {
|
|
6217
|
+
await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
|
|
6218
|
+
}
|
|
5274
6219
|
while (runningPromises.size < maxParallel) {
|
|
5275
6220
|
const claimed = await claimNextRunnableTask(
|
|
5276
6221
|
context,
|
|
@@ -5278,14 +6223,15 @@ async function runTasksLoop(context, options) {
|
|
|
5278
6223
|
target,
|
|
5279
6224
|
registry,
|
|
5280
6225
|
scanLimit,
|
|
5281
|
-
allowedTasks
|
|
6226
|
+
allowedTasks,
|
|
6227
|
+
runnerIdentity
|
|
5282
6228
|
);
|
|
5283
6229
|
if (!claimed) break;
|
|
5284
6230
|
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
5285
6231
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5286
6232
|
stopRequested = true;
|
|
5287
6233
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5288
|
-
context.
|
|
6234
|
+
context.tasksRunnerStop = true;
|
|
5289
6235
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5290
6236
|
}
|
|
5291
6237
|
}).finally(() => {
|
|
@@ -5293,7 +6239,16 @@ async function runTasksLoop(context, options) {
|
|
|
5293
6239
|
});
|
|
5294
6240
|
runningPromises.add(p);
|
|
5295
6241
|
}
|
|
5296
|
-
|
|
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)]);
|
|
6251
|
+
}
|
|
5297
6252
|
}
|
|
5298
6253
|
if (context.isStop() && !stopRequested) {
|
|
5299
6254
|
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
@@ -5329,89 +6284,130 @@ async function runTasksLoop(context, options) {
|
|
|
5329
6284
|
}
|
|
5330
6285
|
async function waitForTaskResult(context, taskId, options = {}) {
|
|
5331
6286
|
const db = getDb3(context);
|
|
5332
|
-
const
|
|
6287
|
+
const queueName = options.queueName ?? "tasks";
|
|
5333
6288
|
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
5334
6289
|
const pollMs = options.pollMs ?? 500;
|
|
5335
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
6290
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
5336
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
|
+
}
|
|
5337
6303
|
while (Date.now() <= deadline) {
|
|
5338
|
-
const
|
|
5339
|
-
if (
|
|
6304
|
+
const legacy = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
|
|
6305
|
+
if (legacy) {
|
|
6306
|
+
return legacy;
|
|
6307
|
+
}
|
|
5340
6308
|
const pending = await db(tasksTable).where({ id: taskId }).first();
|
|
5341
|
-
if (
|
|
5342
|
-
|
|
5343
|
-
|
|
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;
|
|
5344
6323
|
}
|
|
5345
6324
|
await sleepMs(pollMs);
|
|
5346
6325
|
}
|
|
5347
6326
|
return null;
|
|
5348
6327
|
}
|
|
5349
6328
|
var TasksManager = class _TasksManager {
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
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
|
+
*/
|
|
5367
6351
|
constructor(context, options = {}) {
|
|
5368
6352
|
this.context = context;
|
|
5369
|
-
this.
|
|
6353
|
+
this.queueName = options.queueName ?? "tasks";
|
|
5370
6354
|
this.target = options.target ?? "localRunner";
|
|
5371
6355
|
this.recreateTaskTables = options.recreateTaskTables ?? false;
|
|
5372
6356
|
this.pollMs = options.pollMs ?? 1e3;
|
|
6357
|
+
this.claimJitterMs = options.claimJitterMs ?? 0;
|
|
5373
6358
|
this.maxParallel = options.maxParallel ?? 1;
|
|
5374
6359
|
this.scanLimit = options.scanLimit ?? 100;
|
|
5375
6360
|
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5376
6361
|
this.registry = normalizeRegistry(options.registry);
|
|
5377
6362
|
this.runnerServiceGroup = options.runnerServiceGroup;
|
|
5378
6363
|
this.runnerServiceName = options.runnerServiceName;
|
|
5379
|
-
this.
|
|
6364
|
+
this.runnerInstanceNumber = options.runnerInstanceNumber;
|
|
5380
6365
|
this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
|
|
5381
6366
|
this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
|
|
5382
6367
|
this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
|
|
5383
6368
|
this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
|
|
5384
6369
|
this.runnerMetadata = options.runnerMetadata;
|
|
5385
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
|
+
*/
|
|
5386
6380
|
static init(context, options = {}) {
|
|
5387
6381
|
const defs = {
|
|
5388
6382
|
table: "string default tasks",
|
|
5389
6383
|
target: "string default localRunner",
|
|
5390
6384
|
recreateTaskTables: "boolean default false",
|
|
5391
6385
|
pollMs: "number default 1000",
|
|
6386
|
+
claimJitterMs: "number default 0",
|
|
5392
6387
|
maxParallel: "number default 1",
|
|
5393
6388
|
scanLimit: "number default 100",
|
|
5394
6389
|
allowedTasks: "string",
|
|
5395
6390
|
runnerServiceGroup: "string",
|
|
5396
6391
|
runnerServiceName: "string",
|
|
5397
|
-
|
|
6392
|
+
runnerInstanceNumber: "number",
|
|
5398
6393
|
runnerHeartbeatIntervalMs: "number default 10000",
|
|
5399
6394
|
runnerHeartbeatStaleMs: "number default 45000",
|
|
5400
6395
|
runnerGroupMaxInstances: "number",
|
|
5401
6396
|
runnerEnforceMaxInstances: "boolean default true"
|
|
5402
6397
|
};
|
|
5403
|
-
const discovered = context.params.getAllForModule(defs);
|
|
6398
|
+
const discovered = context.params.getAllForModule("tasks", defs);
|
|
5404
6399
|
const resolved = {
|
|
5405
|
-
|
|
6400
|
+
queueName: discovered.table,
|
|
5406
6401
|
target: discovered.target,
|
|
5407
6402
|
recreateTaskTables: discovered.recreateTaskTables,
|
|
5408
6403
|
pollMs: discovered.pollMs,
|
|
6404
|
+
claimJitterMs: discovered.claimJitterMs,
|
|
5409
6405
|
maxParallel: discovered.maxParallel,
|
|
5410
6406
|
scanLimit: discovered.scanLimit,
|
|
5411
6407
|
allowedTasks: discovered.allowedTasks,
|
|
5412
6408
|
runnerServiceGroup: discovered.runnerServiceGroup,
|
|
5413
6409
|
runnerServiceName: discovered.runnerServiceName,
|
|
5414
|
-
|
|
6410
|
+
runnerInstanceNumber: discovered.runnerInstanceNumber,
|
|
5415
6411
|
runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
|
|
5416
6412
|
runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
|
|
5417
6413
|
runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
|
|
@@ -5420,24 +6416,39 @@ var TasksManager = class _TasksManager {
|
|
|
5420
6416
|
};
|
|
5421
6417
|
return new _TasksManager(context, resolved);
|
|
5422
6418
|
}
|
|
6419
|
+
/**
|
|
6420
|
+
* Idempotently ensure the three backing tables exist for this queue.
|
|
6421
|
+
*
|
|
6422
|
+
* @param {{ recreate?: boolean }} [options]
|
|
6423
|
+
* @returns {Promise<void>}
|
|
6424
|
+
*/
|
|
5423
6425
|
async ensureTaskTables(options = {}) {
|
|
5424
6426
|
await ensureTaskTables(this.context, {
|
|
5425
|
-
|
|
6427
|
+
queueName: this.queueName,
|
|
5426
6428
|
recreate: options.recreate ?? this.recreateTaskTables
|
|
5427
6429
|
});
|
|
5428
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
|
+
*/
|
|
5429
6439
|
async runTasksLoop(options = {}) {
|
|
5430
6440
|
await runTasksLoop(this.context, {
|
|
5431
|
-
|
|
6441
|
+
queueName: options.queueName ?? this.queueName,
|
|
5432
6442
|
target: options.target ?? this.target,
|
|
5433
6443
|
pollMs: options.pollMs ?? this.pollMs,
|
|
6444
|
+
claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
|
|
5434
6445
|
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
5435
6446
|
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
5436
6447
|
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
5437
6448
|
registry: options.registry ?? this.registry,
|
|
5438
6449
|
runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
|
|
5439
6450
|
runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
|
|
5440
|
-
|
|
6451
|
+
runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
|
|
5441
6452
|
runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
|
|
5442
6453
|
runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
|
|
5443
6454
|
runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
|
|
@@ -5447,8 +6458,9 @@ var TasksManager = class _TasksManager {
|
|
|
5447
6458
|
}
|
|
5448
6459
|
};
|
|
5449
6460
|
export {
|
|
6461
|
+
AbstractTask,
|
|
5450
6462
|
Args,
|
|
5451
|
-
|
|
6463
|
+
Box4 as Box,
|
|
5452
6464
|
Db,
|
|
5453
6465
|
Divider,
|
|
5454
6466
|
FileDatabase,
|
|
@@ -5461,14 +6473,16 @@ export {
|
|
|
5461
6473
|
MultiColumnListComponent,
|
|
5462
6474
|
MultiColumnListWithPreviewComponent,
|
|
5463
6475
|
Params,
|
|
5464
|
-
|
|
6476
|
+
React2 as React,
|
|
6477
|
+
S3,
|
|
6478
|
+
SERVICE_TASK_NAMES,
|
|
5465
6479
|
ScreenBody,
|
|
5466
6480
|
ScreenContainer,
|
|
5467
6481
|
ScreenDivider,
|
|
5468
6482
|
ScreenFooter,
|
|
5469
6483
|
ScreenRow,
|
|
5470
6484
|
ScreenTitle,
|
|
5471
|
-
|
|
6485
|
+
TaskGetLogs,
|
|
5472
6486
|
TaskPing,
|
|
5473
6487
|
TaskSampleProcess,
|
|
5474
6488
|
TaskShellCommand,
|
|
@@ -5483,17 +6497,17 @@ export {
|
|
|
5483
6497
|
buildBreadcrumb,
|
|
5484
6498
|
buildDetailBreadcrumb,
|
|
5485
6499
|
buildFooter,
|
|
5486
|
-
|
|
5487
|
-
dbFindAndConnect,
|
|
5488
|
-
dbInit,
|
|
6500
|
+
convertPattern,
|
|
5489
6501
|
defaultFileSynopsisFunction,
|
|
5490
6502
|
defaultTasksRegistry,
|
|
5491
6503
|
defaultVersionSynopsisFunction,
|
|
5492
6504
|
enqueueStopTask,
|
|
5493
6505
|
enqueueTask,
|
|
5494
6506
|
ensureTaskTables,
|
|
6507
|
+
flushTaskIpcLogs,
|
|
5495
6508
|
getArgsInstance,
|
|
5496
6509
|
createElement2 as h,
|
|
6510
|
+
ipcFileLogsTableNameForSourceResource,
|
|
5497
6511
|
joiEdateType,
|
|
5498
6512
|
joiStringArrayType,
|
|
5499
6513
|
listServicesRegistry as listAliveRunnerHeartbeats,
|
|
@@ -5501,14 +6515,22 @@ export {
|
|
|
5501
6515
|
listSources,
|
|
5502
6516
|
listTables,
|
|
5503
6517
|
load,
|
|
6518
|
+
matchesParsedPattern,
|
|
6519
|
+
memo,
|
|
6520
|
+
mergeAllowedTasksWithServiceTasks,
|
|
6521
|
+
nextTimeMatch,
|
|
6522
|
+
normalizeAllowedTasks,
|
|
5504
6523
|
organizeFooterMessages,
|
|
5505
6524
|
queueToTableNames,
|
|
6525
|
+
readTaskIpcLogsSnapshot,
|
|
5506
6526
|
registerInServicesRegistry,
|
|
5507
6527
|
registerInServicesRegistry as registerRunnerHeartbeat,
|
|
6528
|
+
resolveAsterisks,
|
|
6529
|
+
resolveIpcFileLogsDir,
|
|
6530
|
+
resolveRanges,
|
|
6531
|
+
resolveSteps,
|
|
5508
6532
|
runNodeTaskScript,
|
|
5509
6533
|
runTasksLoop,
|
|
5510
|
-
servicesRegistryTable as runnerHeartbeatsTable,
|
|
5511
|
-
servicesRegistryTable,
|
|
5512
6534
|
setupContext,
|
|
5513
6535
|
showListScreen,
|
|
5514
6536
|
showMenuScreen,
|
|
@@ -5516,6 +6538,8 @@ export {
|
|
|
5516
6538
|
showMultiColumnListWithPreviewScreen,
|
|
5517
6539
|
showScreen,
|
|
5518
6540
|
showWordGridScreen,
|
|
6541
|
+
taskHistoryInsertFromQueueRow,
|
|
6542
|
+
timeMatcher,
|
|
5519
6543
|
touchServicesRegistry as touchRunnerHeartbeat,
|
|
5520
6544
|
touchServicesRegistry,
|
|
5521
6545
|
unregisterServicesRegistry as unregisterRunnerHeartbeat,
|
|
@@ -5523,10 +6547,11 @@ export {
|
|
|
5523
6547
|
updateServicesRegistryMetadata,
|
|
5524
6548
|
updateTaskProgress,
|
|
5525
6549
|
useCallback,
|
|
5526
|
-
|
|
6550
|
+
useEffect2 as useEffect,
|
|
5527
6551
|
useInput2 as useInput,
|
|
6552
|
+
useLayoutEffect,
|
|
5528
6553
|
useMemo,
|
|
5529
|
-
|
|
6554
|
+
useRef2 as useRef,
|
|
5530
6555
|
useState3 as useState,
|
|
5531
6556
|
waitForTaskResult
|
|
5532
6557
|
};
|