@nmakarov/cli-toolkit 0.21.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/args.cjs +1 -4
- package/dist/args.cjs.map +1 -1
- package/dist/args.js +1 -1
- package/dist/args.js.map +1 -1
- package/dist/cli-runner.cjs +1370 -623
- package/dist/cli-runner.cjs.map +1 -1
- package/dist/cli-runner.js +1415 -667
- package/dist/cli-runner.js.map +1 -1
- package/dist/db.cjs +173 -158
- package/dist/db.cjs.map +1 -1
- package/dist/db.js +172 -151
- package/dist/db.js.map +1 -1
- package/dist/errors.cjs +2 -2
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.js +2 -1
- package/dist/errors.js.map +1 -1
- package/dist/filedatabase.cjs +19 -19
- package/dist/filedatabase.cjs.map +1 -1
- package/dist/filedatabase.js +19 -16
- package/dist/filedatabase.js.map +1 -1
- package/dist/http-client.cjs +9 -11
- package/dist/http-client.cjs.map +1 -1
- package/dist/http-client.js +10 -9
- package/dist/http-client.js.map +1 -1
- package/dist/http-client2.cjs +34 -37
- package/dist/http-client2.cjs.map +1 -1
- package/dist/http-client2.js +34 -34
- package/dist/http-client2.js.map +1 -1
- package/dist/index.cjs +1831 -713
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1837 -713
- package/dist/index.js.map +1 -1
- package/dist/init.cjs +93 -68
- package/dist/init.cjs.map +1 -1
- package/dist/init.js +108 -82
- package/dist/init.js.map +1 -1
- package/dist/logger.cjs +5 -5
- package/dist/logger.cjs.map +1 -1
- package/dist/logger.js +5 -4
- package/dist/logger.js.map +1 -1
- package/dist/mock-server.cjs +21 -33
- package/dist/mock-server.cjs.map +1 -1
- package/dist/mock-server.js +21 -28
- package/dist/mock-server.js.map +1 -1
- package/dist/params.cjs +18 -9
- package/dist/params.cjs.map +1 -1
- package/dist/params.js +18 -6
- package/dist/params.js.map +1 -1
- package/dist/s3.cjs +286 -0
- package/dist/s3.cjs.map +1 -0
- package/dist/s3.js +273 -0
- package/dist/s3.js.map +1 -0
- package/dist/screen.cjs +34 -39
- package/dist/screen.cjs.map +1 -1
- package/dist/screen.js +48 -46
- package/dist/screen.js.map +1 -1
- package/dist/tasks.cjs +1354 -501
- package/dist/tasks.cjs.map +1 -1
- package/dist/tasks.js +1369 -527
- package/dist/tasks.js.map +1 -1
- package/dist/utils.cjs +7 -8
- package/dist/utils.cjs.map +1 -1
- package/dist/utils.js +6 -6
- package/dist/utils.js.map +1 -1
- package/package.json +32 -47
- package/scripts/ssm/{parse-cli.ts → parse-cli.js} +4 -4
- package/scripts/ssm/{ssm-admin.ts → ssm-admin.js} +12 -12
- package/scripts/ssm/{ssm-pull.ts → ssm-pull.js} +10 -13
package/dist/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
|
}
|
|
@@ -2003,6 +1996,18 @@ var Params = class _Params {
|
|
|
2003
1996
|
this._currentModule = prev;
|
|
2004
1997
|
}
|
|
2005
1998
|
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Async variant of {@link runWithModule} for modules that await params.get().
|
|
2001
|
+
*/
|
|
2002
|
+
async runWithModuleAsync(moduleName, fn) {
|
|
2003
|
+
const prev = this._currentModule;
|
|
2004
|
+
this._currentModule = moduleName;
|
|
2005
|
+
try {
|
|
2006
|
+
return await fn();
|
|
2007
|
+
} finally {
|
|
2008
|
+
this._currentModule = prev;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2006
2011
|
/**
|
|
2007
2012
|
* Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
|
|
2008
2013
|
*/
|
|
@@ -2063,14 +2068,14 @@ var Params = class _Params {
|
|
|
2063
2068
|
}
|
|
2064
2069
|
};
|
|
2065
2070
|
|
|
2066
|
-
// src/
|
|
2071
|
+
// src/index.js
|
|
2067
2072
|
init_screen();
|
|
2068
2073
|
|
|
2069
|
-
// src/filedatabase/index.
|
|
2074
|
+
// src/filedatabase/index.js
|
|
2070
2075
|
import fs3 from "fs";
|
|
2071
2076
|
import path3 from "path";
|
|
2072
2077
|
|
|
2073
|
-
// src/utils/os-utils.
|
|
2078
|
+
// src/utils/os-utils.js
|
|
2074
2079
|
import fs from "fs";
|
|
2075
2080
|
import path from "path";
|
|
2076
2081
|
import { execSync } from "child_process";
|
|
@@ -2099,7 +2104,7 @@ function getFreeDiskSpace(targetPath) {
|
|
|
2099
2104
|
}
|
|
2100
2105
|
}
|
|
2101
2106
|
|
|
2102
|
-
// src/utils/fs-utils.
|
|
2107
|
+
// src/utils/fs-utils.js
|
|
2103
2108
|
import fs2 from "fs";
|
|
2104
2109
|
import path2 from "path";
|
|
2105
2110
|
async function ensurePath(...pathParts) {
|
|
@@ -2123,7 +2128,7 @@ function getFileExtension(dataType) {
|
|
|
2123
2128
|
}
|
|
2124
2129
|
}
|
|
2125
2130
|
|
|
2126
|
-
// src/utils/format-utils.
|
|
2131
|
+
// src/utils/format-utils.js
|
|
2127
2132
|
function bytesToHumanReadable(bytes) {
|
|
2128
2133
|
if (bytes === 0) return "0 B";
|
|
2129
2134
|
const k = 1024;
|
|
@@ -2132,7 +2137,7 @@ function bytesToHumanReadable(bytes) {
|
|
|
2132
2137
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
2133
2138
|
}
|
|
2134
2139
|
|
|
2135
|
-
// src/utils/date-utils.
|
|
2140
|
+
// src/utils/date-utils.js
|
|
2136
2141
|
function isTimestampFolder(folderName) {
|
|
2137
2142
|
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
|
|
2138
2143
|
if (!isoRegex.test(folderName)) {
|
|
@@ -2142,7 +2147,7 @@ function isTimestampFolder(folderName) {
|
|
|
2142
2147
|
return !isNaN(date.getTime()) && date.getTime() > 0;
|
|
2143
2148
|
}
|
|
2144
2149
|
|
|
2145
|
-
// src/filedatabase/serializers.
|
|
2150
|
+
// src/filedatabase/serializers.js
|
|
2146
2151
|
function detectDataType(data) {
|
|
2147
2152
|
if (Array.isArray(data)) {
|
|
2148
2153
|
return "json-array";
|
|
@@ -2174,7 +2179,7 @@ function deserializeData(rawData, dataType) {
|
|
|
2174
2179
|
}
|
|
2175
2180
|
}
|
|
2176
2181
|
|
|
2177
|
-
// src/filedatabase/synopsis-functions.
|
|
2182
|
+
// src/filedatabase/synopsis-functions.js
|
|
2178
2183
|
function defaultFileSynopsisFunction(fileEntry, data) {
|
|
2179
2184
|
if (!Array.isArray(data) || data.length === 0) {
|
|
2180
2185
|
return { ...fileEntry };
|
|
@@ -2242,7 +2247,7 @@ function defaultVersionSynopsisFunction(metadata) {
|
|
|
2242
2247
|
return result;
|
|
2243
2248
|
}
|
|
2244
2249
|
|
|
2245
|
-
// src/filedatabase/index.
|
|
2250
|
+
// src/filedatabase/index.js
|
|
2246
2251
|
var FileDatabase = class _FileDatabase {
|
|
2247
2252
|
basePath;
|
|
2248
2253
|
namespace;
|
|
@@ -2328,7 +2333,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2328
2333
|
if (errors.length) {
|
|
2329
2334
|
throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
|
|
2330
2335
|
}
|
|
2331
|
-
|
|
2336
|
+
const parts = [this.basePath, this.namespace];
|
|
2332
2337
|
if (this.tableName) {
|
|
2333
2338
|
parts.push(...this.tableName.split("/"));
|
|
2334
2339
|
}
|
|
@@ -2821,14 +2826,17 @@ var FileDatabase = class _FileDatabase {
|
|
|
2821
2826
|
* Prepare the instance for read or write operations
|
|
2822
2827
|
* This discovers state and sets up internal members based on mode and current data
|
|
2823
2828
|
*/
|
|
2824
|
-
async prepare(
|
|
2829
|
+
async prepare(options) {
|
|
2830
|
+
const { write, read, version, deferInitialVersion } = options;
|
|
2825
2831
|
if (write) {
|
|
2826
2832
|
if (this.versioned) {
|
|
2827
2833
|
if (this.currentVersion === null) {
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2834
|
+
if (!deferInitialVersion) {
|
|
2835
|
+
await this.makeNewVersion();
|
|
2836
|
+
this.metadata = this.getDefaultMetadata();
|
|
2837
|
+
this.metadata.version = this.currentVersion;
|
|
2838
|
+
this.makeNewFile();
|
|
2839
|
+
}
|
|
2832
2840
|
} else {
|
|
2833
2841
|
if (!this.metadata.files.length) {
|
|
2834
2842
|
this.metadata = await this.figureMetadata(this.currentVersion);
|
|
@@ -2938,7 +2946,7 @@ var FileDatabase = class _FileDatabase {
|
|
|
2938
2946
|
if (options.forceNewVersion && !this.versioned) {
|
|
2939
2947
|
throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
|
|
2940
2948
|
}
|
|
2941
|
-
await this.prepare({ write: true });
|
|
2949
|
+
await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
|
|
2942
2950
|
const incomingDataType = detectDataType(data);
|
|
2943
2951
|
this.metadata.dataType = incomingDataType;
|
|
2944
2952
|
if (options.forceNewVersion) {
|
|
@@ -3253,22 +3261,90 @@ function listSources(basePath) {
|
|
|
3253
3261
|
}
|
|
3254
3262
|
}
|
|
3255
3263
|
|
|
3256
|
-
// src/db/index.
|
|
3264
|
+
// src/db/index.js
|
|
3257
3265
|
import knex from "knex";
|
|
3266
|
+
var KNEX_DEFAULTS = {
|
|
3267
|
+
testConnection: true,
|
|
3268
|
+
pool: { min: 2, max: 10 },
|
|
3269
|
+
acquireConnectionTimeout: 1e4,
|
|
3270
|
+
ssl: { rejectUnauthorized: false }
|
|
3271
|
+
};
|
|
3258
3272
|
var Db = class {
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3273
|
+
static async init(context, options = {}) {
|
|
3274
|
+
const buildConfig = async () => {
|
|
3275
|
+
const defs = {
|
|
3276
|
+
dbName: "string",
|
|
3277
|
+
dbProfile: "boolean default false"
|
|
3278
|
+
};
|
|
3279
|
+
const discovered = context?.params?.getAllForModule?.("db", defs) ?? {};
|
|
3280
|
+
const merged = { ...discovered, ...options };
|
|
3281
|
+
let { dbName, dbProfile } = merged;
|
|
3282
|
+
let dbConnectionString = options.dbConnectionString ?? options.connectionString;
|
|
3283
|
+
let connectionParam = dbConnectionString ? "options" : null;
|
|
3284
|
+
if (!dbConnectionString) {
|
|
3285
|
+
const src = context?.args?.getSource?.("dbConnectionString");
|
|
3286
|
+
if (src === "cli" || src === "overrides" || src === "config") {
|
|
3287
|
+
dbConnectionString = await context.params.get("dbConnectionString", "string");
|
|
3288
|
+
connectionParam = "dbConnectionString";
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
if (!dbConnectionString && dbName && /^(postgresql|mysql):\/\//.test(dbName)) {
|
|
3292
|
+
dbConnectionString = dbName;
|
|
3293
|
+
dbName = void 0;
|
|
3294
|
+
}
|
|
3295
|
+
if (!dbConnectionString && dbName) {
|
|
3296
|
+
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
3297
|
+
dbConnectionString = await context.params.get(paramName, "string");
|
|
3298
|
+
connectionParam = paramName;
|
|
3299
|
+
if (!dbConnectionString) {
|
|
3300
|
+
throw new ParamError(
|
|
3301
|
+
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
3302
|
+
);
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
if (!dbConnectionString) {
|
|
3306
|
+
dbConnectionString = await context.params.get("dbConnectionString", "string");
|
|
3307
|
+
if (dbConnectionString) {
|
|
3308
|
+
connectionParam = "dbConnectionString";
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
if (!dbConnectionString) {
|
|
3312
|
+
if (!dbName) {
|
|
3313
|
+
dbName = "local";
|
|
3314
|
+
}
|
|
3315
|
+
const paramName = `dbConnectionString${capitalizeFirstLetter(dbName)}`;
|
|
3316
|
+
dbConnectionString = await context.params.get(paramName, "string");
|
|
3317
|
+
connectionParam = paramName;
|
|
3318
|
+
if (!dbConnectionString) {
|
|
3319
|
+
throw new ParamError(
|
|
3320
|
+
`Db: cannot find dbConnectionString for dbName="${dbName}" (looked for param "${paramName}")`
|
|
3321
|
+
);
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
const displayName = resolveDbDisplayName(
|
|
3325
|
+
dbName,
|
|
3326
|
+
connectionParam,
|
|
3327
|
+
context?.args?.env,
|
|
3328
|
+
merged.name
|
|
3329
|
+
);
|
|
3330
|
+
return {
|
|
3331
|
+
...KNEX_DEFAULTS,
|
|
3332
|
+
connectionString: dbConnectionString,
|
|
3333
|
+
name: displayName,
|
|
3334
|
+
profile: !!dbProfile,
|
|
3335
|
+
logger: context.logger
|
|
3336
|
+
};
|
|
3337
|
+
};
|
|
3338
|
+
const config2 = context?.params?.runWithModuleAsync ? await context.params.runWithModuleAsync("db", buildConfig) : await buildConfig();
|
|
3339
|
+
return dbConnect(context, config2);
|
|
3340
|
+
}
|
|
3268
3341
|
constructor(config2) {
|
|
3269
|
-
if (!config2.connectionString) {
|
|
3342
|
+
if (!config2 || !config2.connectionString) {
|
|
3270
3343
|
throw new ParamError("Db: connectionString is required");
|
|
3271
3344
|
}
|
|
3345
|
+
this.knexInstance = null;
|
|
3346
|
+
this.isConnected = false;
|
|
3347
|
+
this.queriesLog = [];
|
|
3272
3348
|
this.config = {
|
|
3273
3349
|
testConnection: true,
|
|
3274
3350
|
profile: false,
|
|
@@ -3276,30 +3352,27 @@ var Db = class {
|
|
|
3276
3352
|
acquireConnectionTimeout: 1e4,
|
|
3277
3353
|
ssl: { rejectUnauthorized: false },
|
|
3278
3354
|
logger: console,
|
|
3279
|
-
name: "default",
|
|
3280
3355
|
...config2
|
|
3281
3356
|
};
|
|
3282
3357
|
this.logger = this.config.logger;
|
|
3283
3358
|
const instance2 = this;
|
|
3284
|
-
const callableWrapper = function(
|
|
3359
|
+
const callableWrapper = function() {
|
|
3285
3360
|
throw new Error("This should never be called directly");
|
|
3286
3361
|
};
|
|
3287
3362
|
callableWrapper._instance = instance2;
|
|
3288
3363
|
return new Proxy(callableWrapper, {
|
|
3289
|
-
|
|
3290
|
-
apply: (target, thisArg, argumentsList) => {
|
|
3364
|
+
apply: (target, _thisArg, argumentsList) => {
|
|
3291
3365
|
const inst = target._instance;
|
|
3292
3366
|
if (!inst.knexInstance) {
|
|
3293
3367
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
3294
3368
|
}
|
|
3295
3369
|
return inst.knexInstance(...argumentsList);
|
|
3296
3370
|
},
|
|
3297
|
-
// Intercept property access: db.schema, db.raw, etc.
|
|
3298
3371
|
get: (target, prop) => {
|
|
3299
3372
|
if (prop === "_instance") {
|
|
3300
3373
|
return target._instance;
|
|
3301
3374
|
}
|
|
3302
|
-
const
|
|
3375
|
+
const inst = target._instance;
|
|
3303
3376
|
const ownMethods = [
|
|
3304
3377
|
"connect",
|
|
3305
3378
|
"disconnect",
|
|
@@ -3312,26 +3385,26 @@ var Db = class {
|
|
|
3312
3385
|
"detectClient",
|
|
3313
3386
|
"attachProfiler"
|
|
3314
3387
|
];
|
|
3315
|
-
if (prop in
|
|
3316
|
-
const value =
|
|
3388
|
+
if (prop in inst) {
|
|
3389
|
+
const value = inst[prop];
|
|
3317
3390
|
if (typeof value === "function" && ownMethods.includes(prop)) {
|
|
3318
|
-
return value.bind(
|
|
3391
|
+
return value.bind(inst);
|
|
3319
3392
|
}
|
|
3320
3393
|
if (typeof value !== "function") {
|
|
3321
3394
|
return value;
|
|
3322
3395
|
}
|
|
3323
3396
|
}
|
|
3324
|
-
if (
|
|
3325
|
-
const knexProp =
|
|
3397
|
+
if (inst.knexInstance) {
|
|
3398
|
+
const knexProp = inst.knexInstance[prop];
|
|
3326
3399
|
if (typeof knexProp === "function") {
|
|
3327
|
-
return knexProp.bind(
|
|
3400
|
+
return knexProp.bind(inst.knexInstance);
|
|
3328
3401
|
}
|
|
3329
3402
|
return knexProp;
|
|
3330
3403
|
}
|
|
3331
|
-
if (prop in
|
|
3332
|
-
const method =
|
|
3404
|
+
if (prop in inst) {
|
|
3405
|
+
const method = inst[prop];
|
|
3333
3406
|
if (typeof method === "function") {
|
|
3334
|
-
return method.bind(
|
|
3407
|
+
return method.bind(inst);
|
|
3335
3408
|
}
|
|
3336
3409
|
return method;
|
|
3337
3410
|
}
|
|
@@ -3339,9 +3412,6 @@ var Db = class {
|
|
|
3339
3412
|
}
|
|
3340
3413
|
});
|
|
3341
3414
|
}
|
|
3342
|
-
/**
|
|
3343
|
-
* Detect database client type from connection string
|
|
3344
|
-
*/
|
|
3345
3415
|
detectClient(connectionString) {
|
|
3346
3416
|
if (connectionString.match(/^postgresql/)) {
|
|
3347
3417
|
return "pg";
|
|
@@ -3351,9 +3421,6 @@ var Db = class {
|
|
|
3351
3421
|
}
|
|
3352
3422
|
return null;
|
|
3353
3423
|
}
|
|
3354
|
-
/**
|
|
3355
|
-
* Connect to the database
|
|
3356
|
-
*/
|
|
3357
3424
|
async connect() {
|
|
3358
3425
|
if (this.isConnected && this.knexInstance) {
|
|
3359
3426
|
this.logger.warn?.("[Db] Already connected");
|
|
@@ -3362,14 +3429,13 @@ var Db = class {
|
|
|
3362
3429
|
const client = this.detectClient(this.config.connectionString);
|
|
3363
3430
|
if (!client) {
|
|
3364
3431
|
throw new ParamError(
|
|
3365
|
-
|
|
3432
|
+
"Db: Cannot determine client type from connection string. Expected postgresql:// or mysql://"
|
|
3366
3433
|
);
|
|
3367
3434
|
}
|
|
3368
3435
|
try {
|
|
3369
3436
|
const connectionConfig = {
|
|
3370
3437
|
connectionString: this.config.connectionString,
|
|
3371
3438
|
family: 4
|
|
3372
|
-
// Force IPv4 only (disable IPv6)
|
|
3373
3439
|
};
|
|
3374
3440
|
this.knexInstance = knex({
|
|
3375
3441
|
client,
|
|
@@ -3385,7 +3451,7 @@ var Db = class {
|
|
|
3385
3451
|
await this.testConnection();
|
|
3386
3452
|
}
|
|
3387
3453
|
this.isConnected = true;
|
|
3388
|
-
this.logger.debug?.(
|
|
3454
|
+
this.logger.debug?.(formatDbConnectMessage(this.config.name, this.config.connectionString));
|
|
3389
3455
|
} catch (error) {
|
|
3390
3456
|
if (error instanceof ParamError) {
|
|
3391
3457
|
throw error;
|
|
@@ -3394,9 +3460,6 @@ var Db = class {
|
|
|
3394
3460
|
throw new ParamError(`Db: Connection failed - ${errorMsg}`);
|
|
3395
3461
|
}
|
|
3396
3462
|
}
|
|
3397
|
-
/**
|
|
3398
|
-
* Disconnect from the database
|
|
3399
|
-
*/
|
|
3400
3463
|
async disconnect() {
|
|
3401
3464
|
if (!this.knexInstance) {
|
|
3402
3465
|
return;
|
|
@@ -3406,16 +3469,13 @@ var Db = class {
|
|
|
3406
3469
|
this.knexInstance = null;
|
|
3407
3470
|
this.isConnected = false;
|
|
3408
3471
|
this.queriesLog = [];
|
|
3409
|
-
this.logger.debug?.(
|
|
3472
|
+
this.logger.debug?.(formatDbDisconnectMessage(this.config.name, this.config.connectionString));
|
|
3410
3473
|
} catch (error) {
|
|
3411
3474
|
const errorMsg = this.getErrorMessage(error);
|
|
3412
3475
|
this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
|
|
3413
3476
|
throw error;
|
|
3414
3477
|
}
|
|
3415
3478
|
}
|
|
3416
|
-
/**
|
|
3417
|
-
* Extract error message from various error types
|
|
3418
|
-
*/
|
|
3419
3479
|
getErrorMessage(error) {
|
|
3420
3480
|
if (error instanceof AggregateError) {
|
|
3421
3481
|
const errors = error.errors || [];
|
|
@@ -3440,9 +3500,11 @@ var Db = class {
|
|
|
3440
3500
|
return `${code} (tried: ${addresses.join(", ")})`;
|
|
3441
3501
|
}
|
|
3442
3502
|
}
|
|
3443
|
-
const uniqueMessages = [
|
|
3444
|
-
|
|
3445
|
-
|
|
3503
|
+
const uniqueMessages = [
|
|
3504
|
+
...new Set(
|
|
3505
|
+
errors.map((e) => e instanceof Error ? e.message : String(e))
|
|
3506
|
+
)
|
|
3507
|
+
];
|
|
3446
3508
|
if (uniqueMessages.length === 1) {
|
|
3447
3509
|
return uniqueMessages[0];
|
|
3448
3510
|
}
|
|
@@ -3451,28 +3513,25 @@ var Db = class {
|
|
|
3451
3513
|
return error.message || "Multiple errors occurred";
|
|
3452
3514
|
}
|
|
3453
3515
|
if (error instanceof Error) {
|
|
3454
|
-
const
|
|
3455
|
-
if (
|
|
3456
|
-
return `${
|
|
3516
|
+
const code = error.code;
|
|
3517
|
+
if (code) {
|
|
3518
|
+
return `${code}: ${error.message || String(error)}`;
|
|
3457
3519
|
}
|
|
3458
3520
|
return error.message || String(error);
|
|
3459
3521
|
}
|
|
3460
3522
|
if (typeof error === "string") {
|
|
3461
3523
|
return error;
|
|
3462
3524
|
}
|
|
3463
|
-
if (error
|
|
3525
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
3464
3526
|
const msg = String(error.message);
|
|
3465
|
-
const
|
|
3466
|
-
if (
|
|
3467
|
-
return `${
|
|
3527
|
+
const code = error.code;
|
|
3528
|
+
if (code) {
|
|
3529
|
+
return `${code}: ${msg}`;
|
|
3468
3530
|
}
|
|
3469
3531
|
return msg;
|
|
3470
3532
|
}
|
|
3471
3533
|
return String(error) || "Unknown error";
|
|
3472
3534
|
}
|
|
3473
|
-
/**
|
|
3474
|
-
* Test database connection
|
|
3475
|
-
*/
|
|
3476
3535
|
async testConnection() {
|
|
3477
3536
|
if (!this.knexInstance) {
|
|
3478
3537
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -3488,9 +3547,6 @@ var Db = class {
|
|
|
3488
3547
|
throw new ParamError(`Db: Connection test failed - ${errorMsg}`);
|
|
3489
3548
|
}
|
|
3490
3549
|
}
|
|
3491
|
-
/**
|
|
3492
|
-
* Attach query profiler to log all queries
|
|
3493
|
-
*/
|
|
3494
3550
|
attachProfiler() {
|
|
3495
3551
|
if (!this.knexInstance) {
|
|
3496
3552
|
return;
|
|
@@ -3500,7 +3556,7 @@ var Db = class {
|
|
|
3500
3556
|
this.knexInstance.on("query", (query) => {
|
|
3501
3557
|
query.__startTime = process.hrtime();
|
|
3502
3558
|
});
|
|
3503
|
-
this.knexInstance.on("query-response", (
|
|
3559
|
+
this.knexInstance.on("query-response", (_response, query) => {
|
|
3504
3560
|
const [seconds, nanoseconds] = process.hrtime(query.__startTime);
|
|
3505
3561
|
const executionTimeMs = (seconds * 1e3 + nanoseconds / 1e6).toFixed(2);
|
|
3506
3562
|
const logEntry = {
|
|
@@ -3515,15 +3571,9 @@ var Db = class {
|
|
|
3515
3571
|
this.logger.error?.(`[Db] Query failed: ${query.sql}`, error);
|
|
3516
3572
|
});
|
|
3517
3573
|
}
|
|
3518
|
-
/**
|
|
3519
|
-
* Get query log (only available if profiling is enabled)
|
|
3520
|
-
*/
|
|
3521
3574
|
getQueryLog() {
|
|
3522
3575
|
return [...this.queriesLog];
|
|
3523
3576
|
}
|
|
3524
|
-
/**
|
|
3525
|
-
* Check if a table exists
|
|
3526
|
-
*/
|
|
3527
3577
|
async tableExists(tableName) {
|
|
3528
3578
|
if (!this.knexInstance) {
|
|
3529
3579
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
@@ -3535,65 +3585,87 @@ var Db = class {
|
|
|
3535
3585
|
throw error;
|
|
3536
3586
|
}
|
|
3537
3587
|
}
|
|
3538
|
-
/**
|
|
3539
|
-
* Get the underlying Knex instance (for advanced usage)
|
|
3540
|
-
*/
|
|
3541
3588
|
getKnex() {
|
|
3542
3589
|
if (!this.knexInstance) {
|
|
3543
3590
|
throw new Error("Db: Not connected. Call connect() first.");
|
|
3544
3591
|
}
|
|
3545
3592
|
return this.knexInstance;
|
|
3546
3593
|
}
|
|
3547
|
-
/**
|
|
3548
|
-
* Get connection status
|
|
3549
|
-
*/
|
|
3550
3594
|
isConnectedToDb() {
|
|
3551
3595
|
return this.isConnected && this.knexInstance !== null;
|
|
3552
3596
|
}
|
|
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
3597
|
};
|
|
3561
3598
|
function capitalizeFirstLetter(str) {
|
|
3562
3599
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
3563
3600
|
}
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
}
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3601
|
+
function resolveDbDisplayName(dbName, connectionParam, argsEnv, mergedName) {
|
|
3602
|
+
if (dbName) {
|
|
3603
|
+
return dbName;
|
|
3604
|
+
}
|
|
3605
|
+
if (mergedName) {
|
|
3606
|
+
return mergedName;
|
|
3607
|
+
}
|
|
3608
|
+
if (connectionParam?.startsWith("dbConnectionString") && connectionParam.length > "dbConnectionString".length) {
|
|
3609
|
+
return connectionParam.slice("dbConnectionString".length).toLowerCase();
|
|
3610
|
+
}
|
|
3611
|
+
if (connectionParam === "dbConnectionString" && argsEnv) {
|
|
3612
|
+
return argsEnv;
|
|
3613
|
+
}
|
|
3614
|
+
return void 0;
|
|
3615
|
+
}
|
|
3616
|
+
function formatDbConnectMessage(name, connectionString) {
|
|
3617
|
+
const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
|
|
3618
|
+
if (name) {
|
|
3619
|
+
return `[Db] Connected to database "${name}"${endpointSuffix}`;
|
|
3620
|
+
}
|
|
3621
|
+
return `[Db] Connected${endpointSuffix}`;
|
|
3622
|
+
}
|
|
3623
|
+
function formatDbDisconnectMessage(name, connectionString) {
|
|
3624
|
+
const endpointSuffix = formatConnectionEndpointSuffix(connectionString);
|
|
3625
|
+
if (name) {
|
|
3626
|
+
return `[Db] Disconnected from database "${name}"${endpointSuffix}`;
|
|
3627
|
+
}
|
|
3628
|
+
return `[Db] Disconnected${endpointSuffix}`;
|
|
3629
|
+
}
|
|
3630
|
+
function formatDbInstanceMessage(action, name) {
|
|
3631
|
+
if (name) {
|
|
3632
|
+
return `[Db] instance "${name}" ${action}`;
|
|
3633
|
+
}
|
|
3634
|
+
return `[Db] instance ${action}`;
|
|
3635
|
+
}
|
|
3636
|
+
function formatConnectionEndpointSuffix(connectionString) {
|
|
3637
|
+
const endpoint = formatConnectionEndpoint(connectionString);
|
|
3638
|
+
return endpoint ? ` (${endpoint})` : "";
|
|
3639
|
+
}
|
|
3640
|
+
function formatConnectionEndpoint(connectionString) {
|
|
3641
|
+
try {
|
|
3642
|
+
const url = new URL(connectionString);
|
|
3643
|
+
const host = url.hostname;
|
|
3644
|
+
if (!host) {
|
|
3645
|
+
return null;
|
|
3646
|
+
}
|
|
3647
|
+
let port = url.port;
|
|
3648
|
+
if (!port) {
|
|
3649
|
+
if (url.protocol === "postgresql:") {
|
|
3650
|
+
port = "5432";
|
|
3651
|
+
} else if (url.protocol === "mysql:") {
|
|
3652
|
+
port = "3306";
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
return port ? `${host}:${port}` : host;
|
|
3656
|
+
} catch {
|
|
3657
|
+
return null;
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
async function dbConnect(context, config2) {
|
|
3589
3661
|
try {
|
|
3590
3662
|
const db = new Db(config2);
|
|
3591
3663
|
context.registerCleanup(async () => {
|
|
3592
3664
|
await db.disconnect();
|
|
3593
|
-
context.logger.debug(
|
|
3665
|
+
context.logger.debug?.(formatDbInstanceMessage("disconnected", config2.name));
|
|
3594
3666
|
});
|
|
3595
3667
|
await db.connect();
|
|
3596
|
-
context.logger.debug(
|
|
3668
|
+
context.logger.debug?.(formatDbInstanceMessage("initialized", config2.name));
|
|
3597
3669
|
return db;
|
|
3598
3670
|
} catch (error) {
|
|
3599
3671
|
if (error instanceof ParamError) {
|
|
@@ -3603,52 +3675,266 @@ async function dbConnect(context, connectionString, name, dbProfile) {
|
|
|
3603
3675
|
throw new ParamError(`[Db] connect error: ${errorMsg}`);
|
|
3604
3676
|
}
|
|
3605
3677
|
}
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3678
|
+
|
|
3679
|
+
// src/s3/index.js
|
|
3680
|
+
import {
|
|
3681
|
+
S3Client,
|
|
3682
|
+
HeadBucketCommand,
|
|
3683
|
+
HeadObjectCommand,
|
|
3684
|
+
GetObjectCommand,
|
|
3685
|
+
PutObjectCommand,
|
|
3686
|
+
DeleteObjectCommand,
|
|
3687
|
+
CopyObjectCommand,
|
|
3688
|
+
ListObjectsV2Command,
|
|
3689
|
+
PutObjectTaggingCommand,
|
|
3690
|
+
GetObjectTaggingCommand
|
|
3691
|
+
} from "@aws-sdk/client-s3";
|
|
3692
|
+
var DEFAULT_PROFILE = "local";
|
|
3693
|
+
function capitalize(str) {
|
|
3694
|
+
if (!str) return str;
|
|
3695
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
3696
|
+
}
|
|
3697
|
+
var S3 = class _S3 {
|
|
3698
|
+
/**
|
|
3699
|
+
* Build an S3 instance. Reads bucket profile from --bucket (default "local"),
|
|
3700
|
+
* then resolves per-profile params, builds the SDK client, and returns the
|
|
3701
|
+
* instance. Optionally pings the bucket once to verify reachability.
|
|
3702
|
+
*/
|
|
3703
|
+
static async init(context, options = {}) {
|
|
3704
|
+
const profileDef = { bucket: "string" };
|
|
3705
|
+
const discovered = context?.params?.getAllForModule?.("s3", profileDef) ?? {};
|
|
3706
|
+
const profile = options.bucket ?? discovered.bucket ?? DEFAULT_PROFILE;
|
|
3707
|
+
const cap = capitalize(profile);
|
|
3708
|
+
const config2 = {
|
|
3709
|
+
profile,
|
|
3710
|
+
bucketName: options.bucketName ?? await context.params.get(`s3Bucket${cap}`, "string"),
|
|
3711
|
+
region: options.region ?? await context.params.get(`s3Region${cap}`, "string default us-east-1"),
|
|
3712
|
+
endpoint: options.endpoint ?? await context.params.get(`s3Endpoint${cap}`, "string"),
|
|
3713
|
+
forcePathStyle: options.forcePathStyle ?? await context.params.get(`s3ForcePathStyle${cap}`, "boolean default false"),
|
|
3714
|
+
accessKeyId: options.accessKeyId ?? await context.params.get(`s3AccessKeyId${cap}`, "string"),
|
|
3715
|
+
secretAccessKey: options.secretAccessKey ?? await context.params.get(`s3SecretAccessKey${cap}`, "string")
|
|
3622
3716
|
};
|
|
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) {
|
|
3717
|
+
if (!config2.bucketName) {
|
|
3635
3718
|
throw new ParamError(
|
|
3636
|
-
`
|
|
3719
|
+
`S3: bucket name not configured for profile "${profile}" (set s3Bucket${cap} or S3_BUCKET_${profile.toUpperCase()})`
|
|
3637
3720
|
);
|
|
3638
3721
|
}
|
|
3722
|
+
const s3 = new _S3(context, config2);
|
|
3723
|
+
if (options.testBucket !== false) {
|
|
3724
|
+
try {
|
|
3725
|
+
await s3.bucketExists();
|
|
3726
|
+
context.logger?.debug?.(
|
|
3727
|
+
`[S3] profile="${profile}" bucket="${config2.bucketName}" reachable`
|
|
3728
|
+
);
|
|
3729
|
+
} catch (err) {
|
|
3730
|
+
context.logger?.warn?.(
|
|
3731
|
+
`[S3] profile="${profile}" bucket="${config2.bucketName}" reachability test failed: ${err?.message ?? err}`
|
|
3732
|
+
);
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
return s3;
|
|
3736
|
+
}
|
|
3737
|
+
constructor(context, config2) {
|
|
3738
|
+
this.logger = context?.logger ?? console;
|
|
3739
|
+
this.profile = config2.profile;
|
|
3740
|
+
this.bucketName = config2.bucketName;
|
|
3741
|
+
this.region = config2.region;
|
|
3742
|
+
this.endpoint = config2.endpoint || null;
|
|
3743
|
+
const clientConfig = { region: config2.region };
|
|
3744
|
+
if (config2.endpoint) clientConfig.endpoint = config2.endpoint;
|
|
3745
|
+
if (config2.forcePathStyle) clientConfig.forcePathStyle = true;
|
|
3746
|
+
if (config2.accessKeyId && config2.secretAccessKey) {
|
|
3747
|
+
clientConfig.credentials = {
|
|
3748
|
+
accessKeyId: config2.accessKeyId,
|
|
3749
|
+
secretAccessKey: config2.secretAccessKey
|
|
3750
|
+
};
|
|
3751
|
+
}
|
|
3752
|
+
this.client = new S3Client(clientConfig);
|
|
3639
3753
|
}
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3754
|
+
// ── info ────────────────────────────────────────────────────────────────
|
|
3755
|
+
getBucketName() {
|
|
3756
|
+
return this.bucketName;
|
|
3757
|
+
}
|
|
3758
|
+
getProfile() {
|
|
3759
|
+
return this.profile;
|
|
3760
|
+
}
|
|
3761
|
+
getRegion() {
|
|
3762
|
+
return this.region;
|
|
3763
|
+
}
|
|
3764
|
+
getEndpoint() {
|
|
3765
|
+
return this.endpoint;
|
|
3766
|
+
}
|
|
3767
|
+
// ── reachability ────────────────────────────────────────────────────────
|
|
3768
|
+
async bucketExists() {
|
|
3769
|
+
await this.client.send(new HeadBucketCommand({ Bucket: this.bucketName }));
|
|
3770
|
+
return true;
|
|
3771
|
+
}
|
|
3772
|
+
// ── HEAD / GET ──────────────────────────────────────────────────────────
|
|
3773
|
+
/** Returns null on 404; never throws for "missing". Other errors throw. */
|
|
3774
|
+
async headObject(key) {
|
|
3775
|
+
try {
|
|
3776
|
+
const out = await this.client.send(new HeadObjectCommand({
|
|
3777
|
+
Bucket: this.bucketName,
|
|
3778
|
+
Key: key
|
|
3779
|
+
}));
|
|
3780
|
+
return {
|
|
3781
|
+
etag: out.ETag,
|
|
3782
|
+
size: out.ContentLength,
|
|
3783
|
+
contentType: out.ContentType,
|
|
3784
|
+
lastModified: out.LastModified,
|
|
3785
|
+
metadata: out.Metadata,
|
|
3786
|
+
storageClass: out.StorageClass
|
|
3787
|
+
};
|
|
3788
|
+
} catch (err) {
|
|
3789
|
+
if (this._isNotFound(err)) return null;
|
|
3790
|
+
throw err;
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
/** Returns { body: Readable, contentType, contentLength, etag, ... } or null on 404. */
|
|
3794
|
+
async getObject(key) {
|
|
3795
|
+
try {
|
|
3796
|
+
const out = await this.client.send(new GetObjectCommand({
|
|
3797
|
+
Bucket: this.bucketName,
|
|
3798
|
+
Key: key
|
|
3799
|
+
}));
|
|
3800
|
+
return {
|
|
3801
|
+
body: out.Body,
|
|
3802
|
+
contentType: out.ContentType,
|
|
3803
|
+
contentLength: out.ContentLength,
|
|
3804
|
+
etag: out.ETag,
|
|
3805
|
+
lastModified: out.LastModified,
|
|
3806
|
+
metadata: out.Metadata
|
|
3807
|
+
};
|
|
3808
|
+
} catch (err) {
|
|
3809
|
+
if (this._isNotFound(err)) return null;
|
|
3810
|
+
throw err;
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
/** Buffers the whole object. Use only for small objects (manifests, JSON). */
|
|
3814
|
+
async getObjectBytes(key) {
|
|
3815
|
+
const obj = await this.getObject(key);
|
|
3816
|
+
if (!obj) return null;
|
|
3817
|
+
const chunks = [];
|
|
3818
|
+
for await (const chunk of obj.body) chunks.push(chunk);
|
|
3819
|
+
return { ...obj, body: Buffer.concat(chunks) };
|
|
3820
|
+
}
|
|
3821
|
+
/** Convenience for JSON manifests. Returns parsed object or null on 404. */
|
|
3822
|
+
async getJson(key) {
|
|
3823
|
+
const obj = await this.getObjectBytes(key);
|
|
3824
|
+
if (!obj) return null;
|
|
3825
|
+
return JSON.parse(obj.body.toString("utf8"));
|
|
3826
|
+
}
|
|
3827
|
+
// ── PUT ─────────────────────────────────────────────────────────────────
|
|
3828
|
+
async putObject({ key, body, contentType, contentLength, tags, metadata }) {
|
|
3829
|
+
const cmd = new PutObjectCommand({
|
|
3830
|
+
Bucket: this.bucketName,
|
|
3831
|
+
Key: key,
|
|
3832
|
+
Body: body,
|
|
3833
|
+
...contentType && { ContentType: contentType },
|
|
3834
|
+
...contentLength != null && { ContentLength: contentLength },
|
|
3835
|
+
...metadata && { Metadata: metadata },
|
|
3836
|
+
...tags && { Tagging: this._tagsToQuery(tags) }
|
|
3837
|
+
});
|
|
3838
|
+
return this.client.send(cmd);
|
|
3839
|
+
}
|
|
3840
|
+
/** Convenience for JSON manifests. */
|
|
3841
|
+
async putJson(key, value, opts = {}) {
|
|
3842
|
+
const json = JSON.stringify(value, null, opts.pretty ? 2 : 0);
|
|
3843
|
+
const body = Buffer.from(json, "utf8");
|
|
3844
|
+
return this.putObject({
|
|
3845
|
+
key,
|
|
3846
|
+
body,
|
|
3847
|
+
contentType: "application/json",
|
|
3848
|
+
contentLength: body.length,
|
|
3849
|
+
tags: opts.tags,
|
|
3850
|
+
metadata: opts.metadata
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
3853
|
+
// ── DELETE ──────────────────────────────────────────────────────────────
|
|
3854
|
+
async deleteObject(key) {
|
|
3855
|
+
return this.client.send(new DeleteObjectCommand({
|
|
3856
|
+
Bucket: this.bucketName,
|
|
3857
|
+
Key: key
|
|
3858
|
+
}));
|
|
3859
|
+
}
|
|
3860
|
+
// ── COPY (for migration: legacy → new bucket, or intra-bucket "rename") ─
|
|
3861
|
+
async copyObject({ sourceBucket, sourceKey, key, contentType, metadata, tags }) {
|
|
3862
|
+
const src = sourceBucket || this.bucketName;
|
|
3863
|
+
const cmd = new CopyObjectCommand({
|
|
3864
|
+
Bucket: this.bucketName,
|
|
3865
|
+
Key: key,
|
|
3866
|
+
CopySource: encodeURIComponent(`${src}/${sourceKey}`),
|
|
3867
|
+
...contentType && {
|
|
3868
|
+
ContentType: contentType,
|
|
3869
|
+
MetadataDirective: "REPLACE"
|
|
3870
|
+
},
|
|
3871
|
+
...metadata && {
|
|
3872
|
+
Metadata: metadata,
|
|
3873
|
+
MetadataDirective: "REPLACE"
|
|
3874
|
+
},
|
|
3875
|
+
...tags && {
|
|
3876
|
+
Tagging: this._tagsToQuery(tags),
|
|
3877
|
+
TaggingDirective: "REPLACE"
|
|
3878
|
+
}
|
|
3879
|
+
});
|
|
3880
|
+
return this.client.send(cmd);
|
|
3881
|
+
}
|
|
3882
|
+
// ── LIST ────────────────────────────────────────────────────────────────
|
|
3883
|
+
async listObjects(prefix, { keysOnly = false, maxKeys = 1e3, continuationToken } = {}) {
|
|
3884
|
+
const out = await this.client.send(new ListObjectsV2Command({
|
|
3885
|
+
Bucket: this.bucketName,
|
|
3886
|
+
Prefix: prefix,
|
|
3887
|
+
MaxKeys: maxKeys,
|
|
3888
|
+
ContinuationToken: continuationToken
|
|
3889
|
+
}));
|
|
3890
|
+
const items = (out.Contents ?? []).map((o) => ({
|
|
3891
|
+
key: o.Key,
|
|
3892
|
+
size: o.Size,
|
|
3893
|
+
etag: o.ETag,
|
|
3894
|
+
lastModified: o.LastModified,
|
|
3895
|
+
storageClass: o.StorageClass
|
|
3896
|
+
}));
|
|
3897
|
+
return {
|
|
3898
|
+
items: keysOnly ? items.map((i) => i.key) : items,
|
|
3899
|
+
isTruncated: !!out.IsTruncated,
|
|
3900
|
+
nextContinuationToken: out.NextContinuationToken
|
|
3901
|
+
};
|
|
3902
|
+
}
|
|
3903
|
+
// ── TAGS (used for lifecycle rules, e.g. status=closed → Glacier IR) ────
|
|
3904
|
+
async putObjectTagging(key, tags) {
|
|
3905
|
+
return this.client.send(new PutObjectTaggingCommand({
|
|
3906
|
+
Bucket: this.bucketName,
|
|
3907
|
+
Key: key,
|
|
3908
|
+
Tagging: { TagSet: this._tagsToTagSet(tags) }
|
|
3909
|
+
}));
|
|
3910
|
+
}
|
|
3911
|
+
async getObjectTagging(key) {
|
|
3912
|
+
const out = await this.client.send(new GetObjectTaggingCommand({
|
|
3913
|
+
Bucket: this.bucketName,
|
|
3914
|
+
Key: key
|
|
3915
|
+
}));
|
|
3916
|
+
const tags = {};
|
|
3917
|
+
for (const t of out.TagSet ?? []) tags[t.Key] = t.Value;
|
|
3918
|
+
return tags;
|
|
3919
|
+
}
|
|
3920
|
+
// ── internals ───────────────────────────────────────────────────────────
|
|
3921
|
+
_isNotFound(err) {
|
|
3922
|
+
const status = err?.$metadata?.httpStatusCode;
|
|
3923
|
+
return status === 404 || err?.name === "NotFound" || err?.name === "NoSuchKey" || err?.Code === "NoSuchKey";
|
|
3924
|
+
}
|
|
3925
|
+
_tagsToQuery(tags) {
|
|
3926
|
+
return Object.entries(tags).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
|
|
3927
|
+
}
|
|
3928
|
+
_tagsToTagSet(tags) {
|
|
3929
|
+
return Object.entries(tags).map(([Key, Value]) => ({ Key, Value: String(Value) }));
|
|
3930
|
+
}
|
|
3931
|
+
};
|
|
3646
3932
|
|
|
3647
|
-
// src/logger/index.
|
|
3933
|
+
// src/logger/index.js
|
|
3648
3934
|
import chalk from "chalk";
|
|
3649
3935
|
import util from "util";
|
|
3650
3936
|
|
|
3651
|
-
// src/logger/transports.
|
|
3937
|
+
// src/logger/transports.js
|
|
3652
3938
|
var ConsoleTransport = class {
|
|
3653
3939
|
write(payload) {
|
|
3654
3940
|
console.info(payload);
|
|
@@ -3668,7 +3954,7 @@ var ParentProcessTransport = class {
|
|
|
3668
3954
|
}
|
|
3669
3955
|
};
|
|
3670
3956
|
|
|
3671
|
-
// src/logger/index.
|
|
3957
|
+
// src/logger/index.js
|
|
3672
3958
|
var ALL_LEVELS = [
|
|
3673
3959
|
"silly",
|
|
3674
3960
|
"debug",
|
|
@@ -3741,6 +4027,7 @@ var Logger = class _Logger {
|
|
|
3741
4027
|
}
|
|
3742
4028
|
/**
|
|
3743
4029
|
* Initialize logger from context and CLI parameters. Whatever is in options goes (after discovered params).
|
|
4030
|
+
* Params are tracked under the `logger` module for --showUsedParams.
|
|
3744
4031
|
*/
|
|
3745
4032
|
static init(context, options) {
|
|
3746
4033
|
const paramDefs = {
|
|
@@ -3754,7 +4041,7 @@ var Logger = class _Logger {
|
|
|
3754
4041
|
progressWithTimes: "boolean default false",
|
|
3755
4042
|
progressThrottleMs: "number"
|
|
3756
4043
|
};
|
|
3757
|
-
const discovered = context.params.getAllForModule(paramDefs);
|
|
4044
|
+
const discovered = context.params.getAllForModule("logger", paramDefs);
|
|
3758
4045
|
const config2 = { ...discovered, ...options };
|
|
3759
4046
|
const logger = new _Logger(context, config2);
|
|
3760
4047
|
context.logger = logger;
|
|
@@ -3950,9 +4237,9 @@ var Logger = class _Logger {
|
|
|
3950
4237
|
}
|
|
3951
4238
|
};
|
|
3952
4239
|
|
|
3953
|
-
// src/init/index.
|
|
4240
|
+
// src/init/index.js
|
|
3954
4241
|
import { EventEmitter } from "events";
|
|
3955
|
-
function extractComponentOptions(opts,
|
|
4242
|
+
function extractComponentOptions(opts, _componentName) {
|
|
3956
4243
|
const reservedKeys = ["overrides", "defaults", "modules"];
|
|
3957
4244
|
const componentOptions = {};
|
|
3958
4245
|
for (const [key, value] of Object.entries(opts)) {
|
|
@@ -3997,7 +4284,10 @@ function setupContext(opts = {}) {
|
|
|
3997
4284
|
return setup(opts);
|
|
3998
4285
|
}
|
|
3999
4286
|
|
|
4000
|
-
// src/
|
|
4287
|
+
// src/tasks/index.js
|
|
4288
|
+
import os3 from "os";
|
|
4289
|
+
|
|
4290
|
+
// src/utils/core-utils.js
|
|
4001
4291
|
function sleepMs(ms) {
|
|
4002
4292
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
4003
4293
|
}
|
|
@@ -4006,14 +4296,82 @@ function toJsonColumn(value) {
|
|
|
4006
4296
|
return JSON.stringify(value);
|
|
4007
4297
|
}
|
|
4008
4298
|
|
|
4009
|
-
// src/tasks/servicesRegistry.
|
|
4010
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
4011
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
4299
|
+
// src/tasks/servicesRegistry.js
|
|
4012
4300
|
import os from "os";
|
|
4013
|
-
import path4 from "path";
|
|
4014
4301
|
|
|
4015
|
-
// src/tasks/taskUtils.
|
|
4302
|
+
// src/tasks/taskUtils.js
|
|
4016
4303
|
import { randomUUID } from "crypto";
|
|
4304
|
+
|
|
4305
|
+
// src/tasks/time-matcher.js
|
|
4306
|
+
var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
|
|
4307
|
+
function resolveAsterisks(field, range) {
|
|
4308
|
+
return field.includes("*") ? field.replace("*", range) : field;
|
|
4309
|
+
}
|
|
4310
|
+
function resolveRanges(field) {
|
|
4311
|
+
const regex = /(\d+)-(\d+)/;
|
|
4312
|
+
let current = field;
|
|
4313
|
+
while (true) {
|
|
4314
|
+
const match = regex.exec(current);
|
|
4315
|
+
if (!match) break;
|
|
4316
|
+
const raw = match[0];
|
|
4317
|
+
let first = Number(match[1]);
|
|
4318
|
+
let last = Number(match[2]);
|
|
4319
|
+
if (last < first) {
|
|
4320
|
+
[first, last] = [last, first];
|
|
4321
|
+
}
|
|
4322
|
+
const values = [];
|
|
4323
|
+
for (let i = first; i <= last; i += 1) {
|
|
4324
|
+
values.push(i);
|
|
4325
|
+
}
|
|
4326
|
+
current = current.replace(raw, values.join(","));
|
|
4327
|
+
}
|
|
4328
|
+
return current;
|
|
4329
|
+
}
|
|
4330
|
+
function resolveSteps(field) {
|
|
4331
|
+
const match = /^(.+)\/(\d+)$/.exec(field);
|
|
4332
|
+
if (!match) return field;
|
|
4333
|
+
const base = match[1];
|
|
4334
|
+
const step = Number(match[2]);
|
|
4335
|
+
if (!Number.isFinite(step) || step <= 0) return field;
|
|
4336
|
+
return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
|
|
4337
|
+
}
|
|
4338
|
+
function convertPattern(pattern) {
|
|
4339
|
+
const parts = pattern.trim().split(/\s+/);
|
|
4340
|
+
if (parts.length !== 6) {
|
|
4341
|
+
throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
|
|
4342
|
+
}
|
|
4343
|
+
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
4344
|
+
}
|
|
4345
|
+
function fieldMatches(field, value) {
|
|
4346
|
+
const allowed = field.split(",").map((v) => Number(v));
|
|
4347
|
+
return allowed.includes(value);
|
|
4348
|
+
}
|
|
4349
|
+
function matchesParsedPattern(parsed, date) {
|
|
4350
|
+
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());
|
|
4351
|
+
}
|
|
4352
|
+
function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
|
|
4353
|
+
const parsed = convertPattern(pattern);
|
|
4354
|
+
return matchesParsedPattern(parsed, date);
|
|
4355
|
+
}
|
|
4356
|
+
var MS_PER_SECOND = 1e3;
|
|
4357
|
+
var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
|
|
4358
|
+
function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
|
|
4359
|
+
const parsed = convertPattern(pattern);
|
|
4360
|
+
let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
|
|
4361
|
+
const end = t + maxSearchMs;
|
|
4362
|
+
while (t <= end) {
|
|
4363
|
+
const date = new Date(t);
|
|
4364
|
+
if (matchesParsedPattern(parsed, date)) {
|
|
4365
|
+
return date;
|
|
4366
|
+
}
|
|
4367
|
+
t += MS_PER_SECOND;
|
|
4368
|
+
}
|
|
4369
|
+
throw new Error(
|
|
4370
|
+
`nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
|
|
4371
|
+
);
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4374
|
+
// src/tasks/taskUtils.js
|
|
4017
4375
|
function getDb(context) {
|
|
4018
4376
|
const db = context.db;
|
|
4019
4377
|
if (!db) {
|
|
@@ -4021,113 +4379,117 @@ function getDb(context) {
|
|
|
4021
4379
|
}
|
|
4022
4380
|
return db;
|
|
4023
4381
|
}
|
|
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
|
-
}
|
|
4382
|
+
function queueToTableNames(queueName) {
|
|
4028
4383
|
return {
|
|
4029
|
-
tasksTable:
|
|
4030
|
-
historyTable: `${
|
|
4384
|
+
tasksTable: queueName,
|
|
4385
|
+
historyTable: `${queueName}_history`,
|
|
4386
|
+
registryTable: `${queueName}_services_registry`
|
|
4031
4387
|
};
|
|
4032
4388
|
}
|
|
4033
|
-
function
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4389
|
+
function defineTasksTable(t, db, tableNameForIndex) {
|
|
4390
|
+
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
4391
|
+
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
4392
|
+
t.timestamp("started_at");
|
|
4393
|
+
t.timestamp("completed_at");
|
|
4394
|
+
t.integer("priority").notNullable().defaultTo(50);
|
|
4395
|
+
t.text("schedule");
|
|
4396
|
+
t.timestamp("next_run_at").defaultTo(null);
|
|
4397
|
+
t.timestamp("past_due").defaultTo(null);
|
|
4398
|
+
t.text("name").notNullable();
|
|
4399
|
+
t.text("opid");
|
|
4400
|
+
t.json("params");
|
|
4401
|
+
t.text("service_group");
|
|
4402
|
+
t.integer("instance_number");
|
|
4403
|
+
t.text("service_name");
|
|
4404
|
+
t.text("server_name");
|
|
4405
|
+
t.text("status").notNullable().defaultTo("idle");
|
|
4406
|
+
t.timestamp("status_changed_at").defaultTo(null);
|
|
4407
|
+
t.text("progress");
|
|
4408
|
+
t.boolean("success");
|
|
4409
|
+
t.json("results");
|
|
4410
|
+
t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
|
|
4411
|
+
t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
|
|
4412
|
+
}
|
|
4413
|
+
function taskHistoryInsertFromQueueRow(row, overrides) {
|
|
4414
|
+
const { id, ...snapshot } = row;
|
|
4415
|
+
void id;
|
|
4416
|
+
return {
|
|
4417
|
+
...snapshot,
|
|
4418
|
+
...overrides
|
|
4419
|
+
};
|
|
4038
4420
|
}
|
|
4039
4421
|
async function ensureTaskTables(context, options = {}) {
|
|
4040
|
-
const
|
|
4422
|
+
const queueName = options.queueName ?? "tasks";
|
|
4041
4423
|
const recreate = options.recreate ?? false;
|
|
4042
4424
|
const db = getDb(context);
|
|
4043
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
4425
|
+
const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
|
|
4044
4426
|
const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
|
|
4045
4427
|
const needsHistory = recreate ? true : !await db.tableExists(historyTable);
|
|
4428
|
+
const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
|
|
4046
4429
|
if (recreate) {
|
|
4047
4430
|
await db.schema.dropTableIfExists(historyTable);
|
|
4048
4431
|
await db.schema.dropTableIfExists(tasksTable);
|
|
4432
|
+
await db.schema.dropTableIfExists(registryTable);
|
|
4049
4433
|
}
|
|
4050
4434
|
if (needsTasks) {
|
|
4051
4435
|
await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
4052
4436
|
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`);
|
|
4437
|
+
defineTasksTable(t, db, tasksTable);
|
|
4073
4438
|
});
|
|
4074
4439
|
}
|
|
4075
4440
|
if (needsHistory) {
|
|
4076
4441
|
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`);
|
|
4442
|
+
defineTasksTable(t, db, historyTable);
|
|
4095
4443
|
});
|
|
4096
4444
|
}
|
|
4097
|
-
const registryTable = servicesRegistryTable(queue);
|
|
4098
|
-
const needsRegistry = !await db.tableExists(registryTable);
|
|
4099
4445
|
if (needsRegistry) {
|
|
4100
4446
|
await db.schema.createTable(registryTable, (t) => {
|
|
4101
4447
|
t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
|
|
4102
|
-
t.
|
|
4103
|
-
t.text("queue").notNullable();
|
|
4448
|
+
t.text("queue_name").notNullable();
|
|
4104
4449
|
t.text("service_group").notNullable();
|
|
4450
|
+
t.integer("instance_number").notNullable().defaultTo(1);
|
|
4105
4451
|
t.text("service_name").notNullable();
|
|
4106
|
-
t.text("
|
|
4107
|
-
t.text("hostname");
|
|
4452
|
+
t.text("server_name").notNullable();
|
|
4108
4453
|
t.integer("pid");
|
|
4109
4454
|
t.json("metadata");
|
|
4110
4455
|
t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
|
|
4111
4456
|
t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
|
|
4112
|
-
t.unique(["
|
|
4113
|
-
t.index(["
|
|
4114
|
-
t.index(["
|
|
4457
|
+
t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
|
|
4458
|
+
t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
|
|
4459
|
+
t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
|
|
4115
4460
|
});
|
|
4116
4461
|
}
|
|
4117
4462
|
}
|
|
4118
4463
|
async function enqueueTask(context, options) {
|
|
4119
4464
|
const db = getDb(context);
|
|
4120
|
-
const
|
|
4121
|
-
const { tasksTable } = queueToTableNames(
|
|
4465
|
+
const queueName = options.queueName ?? "tasks";
|
|
4466
|
+
const { tasksTable } = queueToTableNames(queueName);
|
|
4122
4467
|
const id = randomUUID();
|
|
4468
|
+
const name = options.name ?? options.task;
|
|
4469
|
+
if (!name) {
|
|
4470
|
+
throw new Error("enqueueTask: name (or task) is required");
|
|
4471
|
+
}
|
|
4472
|
+
const schedule = options.schedule?.trim() ? options.schedule : null;
|
|
4473
|
+
let nextRunAt = null;
|
|
4474
|
+
if (options.nextRunAt !== void 0) {
|
|
4475
|
+
nextRunAt = options.nextRunAt == null ? null : new Date(options.nextRunAt);
|
|
4476
|
+
} else if (schedule) {
|
|
4477
|
+
nextRunAt = nextTimeMatch(schedule, /* @__PURE__ */ new Date());
|
|
4478
|
+
}
|
|
4123
4479
|
await db(tasksTable).insert({
|
|
4124
4480
|
id,
|
|
4125
|
-
|
|
4126
|
-
task: options.task,
|
|
4481
|
+
name,
|
|
4127
4482
|
params: toJsonColumn(options.params ?? null),
|
|
4128
4483
|
opid: options.opid ?? null,
|
|
4129
|
-
priority: options.priority ??
|
|
4130
|
-
schedule
|
|
4484
|
+
priority: options.priority ?? 50,
|
|
4485
|
+
schedule,
|
|
4486
|
+
next_run_at: nextRunAt,
|
|
4487
|
+
service_group: options.serviceGroup ?? null,
|
|
4488
|
+
instance_number: options.instanceNumber ?? null,
|
|
4489
|
+
service_name: options.serviceName ?? null,
|
|
4490
|
+
server_name: options.serverName ?? null,
|
|
4491
|
+
status: "idle",
|
|
4492
|
+
status_changed_at: db.fn.now()
|
|
4131
4493
|
});
|
|
4132
4494
|
return id;
|
|
4133
4495
|
}
|
|
@@ -4138,7 +4500,7 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
|
|
|
4138
4500
|
});
|
|
4139
4501
|
}
|
|
4140
4502
|
|
|
4141
|
-
// src/tasks/servicesRegistry.
|
|
4503
|
+
// src/tasks/servicesRegistry.js
|
|
4142
4504
|
function getDb2(context) {
|
|
4143
4505
|
const db = context.db;
|
|
4144
4506
|
if (!db) {
|
|
@@ -4152,7 +4514,7 @@ function parseMetadataColumn(value) {
|
|
|
4152
4514
|
if (typeof value === "string") {
|
|
4153
4515
|
try {
|
|
4154
4516
|
const p = JSON.parse(value);
|
|
4155
|
-
return p && typeof p === "object" && !Array.isArray(
|
|
4517
|
+
return p && typeof p === "object" && !Array.isArray(value) ? p : {};
|
|
4156
4518
|
} catch {
|
|
4157
4519
|
return {};
|
|
4158
4520
|
}
|
|
@@ -4162,6 +4524,7 @@ function parseMetadataColumn(value) {
|
|
|
4162
4524
|
var DEFAULT_GROUP_MAX_INSTANCES = {
|
|
4163
4525
|
intake: 1,
|
|
4164
4526
|
harvest: 1,
|
|
4527
|
+
harvester: 0,
|
|
4165
4528
|
loader: 0,
|
|
4166
4529
|
photos: 0,
|
|
4167
4530
|
photosprocessor: 0,
|
|
@@ -4171,25 +4534,6 @@ function sanitizeNamePart(raw) {
|
|
|
4171
4534
|
const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
4172
4535
|
return s.slice(0, 80) || "runner";
|
|
4173
4536
|
}
|
|
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
4537
|
function resolveMaxInstances(serviceGroup, override) {
|
|
4194
4538
|
if (override !== void 0 && Number.isFinite(override)) {
|
|
4195
4539
|
return Math.max(0, Math.floor(Number(override)));
|
|
@@ -4197,179 +4541,190 @@ function resolveMaxInstances(serviceGroup, override) {
|
|
|
4197
4541
|
const g = serviceGroup.trim().toLowerCase();
|
|
4198
4542
|
return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
|
|
4199
4543
|
}
|
|
4200
|
-
async function countAliveInGroup(db, registryTable,
|
|
4544
|
+
async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
|
|
4201
4545
|
const cutoff = new Date(Date.now() - staleMs);
|
|
4202
|
-
let q = db(registryTable).where({
|
|
4203
|
-
if (
|
|
4204
|
-
q = q.whereNot("
|
|
4546
|
+
let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
|
|
4547
|
+
if (excludeRowId) {
|
|
4548
|
+
q = q.whereNot("id", excludeRowId);
|
|
4205
4549
|
}
|
|
4206
4550
|
const row = await q.count("id as count").first();
|
|
4207
4551
|
return Number(row?.count ?? 0);
|
|
4208
4552
|
}
|
|
4553
|
+
async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
|
|
4554
|
+
const cutoff = new Date(Date.now() - staleMs);
|
|
4555
|
+
const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
|
|
4556
|
+
const set = /* @__PURE__ */ new Set();
|
|
4557
|
+
for (const r of rows) {
|
|
4558
|
+
const n = Number(r.instance_number);
|
|
4559
|
+
if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
|
|
4560
|
+
}
|
|
4561
|
+
return set;
|
|
4562
|
+
}
|
|
4209
4563
|
function isUniqueViolation(error) {
|
|
4210
4564
|
const code = error?.code ?? error?.errno;
|
|
4211
4565
|
return code === "23505" || String(error?.message || "").includes("duplicate key");
|
|
4212
4566
|
}
|
|
4567
|
+
function buildMetadata(options) {
|
|
4568
|
+
const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
|
|
4569
|
+
if (options.target) {
|
|
4570
|
+
base.runnerTarget = options.target;
|
|
4571
|
+
}
|
|
4572
|
+
return toJsonColumn(Object.keys(base).length ? base : null);
|
|
4573
|
+
}
|
|
4574
|
+
function allocateInstanceNumber(occupied, explicit, maxSlots) {
|
|
4575
|
+
if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
|
|
4576
|
+
const e = Math.max(1, Math.floor(Number(explicit)));
|
|
4577
|
+
if (occupied.has(e)) {
|
|
4578
|
+
throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
|
|
4579
|
+
}
|
|
4580
|
+
if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
|
|
4581
|
+
throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
|
|
4582
|
+
}
|
|
4583
|
+
return e;
|
|
4584
|
+
}
|
|
4585
|
+
const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
|
|
4586
|
+
for (let n = 1; n <= cap; n++) {
|
|
4587
|
+
if (!occupied.has(n)) return n;
|
|
4588
|
+
}
|
|
4589
|
+
throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
|
|
4590
|
+
}
|
|
4591
|
+
function defaultServiceName(groupBase, hostBase, instanceNumber) {
|
|
4592
|
+
return `${groupBase}-${hostBase}-${instanceNumber}`;
|
|
4593
|
+
}
|
|
4213
4594
|
async function registerInServicesRegistry(context, options) {
|
|
4214
4595
|
const db = getDb2(context);
|
|
4215
|
-
const registryTable =
|
|
4596
|
+
const registryTable = queueToTableNames(options.queueName).registryTable;
|
|
4216
4597
|
const serviceGroup = options.serviceGroup.trim();
|
|
4217
4598
|
if (!serviceGroup) {
|
|
4218
4599
|
throw new Error("registerInServicesRegistry: serviceGroup is required");
|
|
4219
4600
|
}
|
|
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();
|
|
4601
|
+
const serverName = os.hostname();
|
|
4226
4602
|
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
|
-
}
|
|
4603
|
+
const meta = buildMetadata(options);
|
|
4604
|
+
const groupBase = sanitizeNamePart(serviceGroup);
|
|
4605
|
+
const hostBase = sanitizeNamePart(serverName);
|
|
4263
4606
|
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}).`;
|
|
4607
|
+
const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
|
|
4608
|
+
if (maxAllowed > 0 && aliveCount >= maxAllowed) {
|
|
4609
|
+
const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
|
|
4274
4610
|
if (options.enforceMaxInstances) {
|
|
4275
4611
|
throw new Error(msg);
|
|
4276
4612
|
}
|
|
4277
4613
|
context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
|
|
4278
4614
|
}
|
|
4279
|
-
const
|
|
4280
|
-
const
|
|
4281
|
-
const
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
service_name: candidate,
|
|
4310
|
-
target: options.target,
|
|
4311
|
-
hostname,
|
|
4615
|
+
const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
|
|
4616
|
+
const cutoff = new Date(Date.now() - options.staleMs);
|
|
4617
|
+
const MAX_ATTEMPTS = 8;
|
|
4618
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
4619
|
+
const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
|
|
4620
|
+
const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
|
|
4621
|
+
const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
|
|
4622
|
+
const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
4623
|
+
if (existing) {
|
|
4624
|
+
const lastSeen = new Date(existing.last_seen_at);
|
|
4625
|
+
const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
|
|
4626
|
+
if (isAlive) {
|
|
4627
|
+
if (options.serviceName?.trim()) {
|
|
4628
|
+
throw new Error(
|
|
4629
|
+
`[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
|
|
4630
|
+
);
|
|
4631
|
+
}
|
|
4632
|
+
context.logger.warn?.(
|
|
4633
|
+
`[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
|
|
4634
|
+
);
|
|
4635
|
+
if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
|
|
4636
|
+
throw new Error(
|
|
4637
|
+
`[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
|
|
4638
|
+
);
|
|
4639
|
+
}
|
|
4640
|
+
await new Promise((r) => setTimeout(r, 50 + attempt * 30));
|
|
4641
|
+
continue;
|
|
4642
|
+
}
|
|
4643
|
+
await db(registryTable).where({ id: existing.id }).update({
|
|
4644
|
+
server_name: serverName,
|
|
4312
4645
|
pid,
|
|
4313
4646
|
metadata: meta,
|
|
4647
|
+
service_group: serviceGroup,
|
|
4648
|
+
instance_number: instanceNumber,
|
|
4314
4649
|
last_seen_at: db.fn.now()
|
|
4650
|
+
});
|
|
4651
|
+
const reg = {
|
|
4652
|
+
serviceName: serviceNameRaw,
|
|
4653
|
+
serviceGroup,
|
|
4654
|
+
queueName: options.queueName,
|
|
4655
|
+
target: options.target,
|
|
4656
|
+
rowId: String(existing.id),
|
|
4657
|
+
registryTable,
|
|
4658
|
+
instanceNumber
|
|
4659
|
+
};
|
|
4660
|
+
context.servicesRegistry = reg;
|
|
4661
|
+
context.runnerHeartbeat = reg;
|
|
4662
|
+
context.logger.info?.(
|
|
4663
|
+
`[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
4664
|
+
);
|
|
4665
|
+
return reg;
|
|
4666
|
+
}
|
|
4667
|
+
try {
|
|
4668
|
+
const rows = await db(registryTable).insert({
|
|
4669
|
+
queue_name: options.queueName,
|
|
4670
|
+
service_group: serviceGroup,
|
|
4671
|
+
instance_number: instanceNumber,
|
|
4672
|
+
service_name: serviceNameRaw,
|
|
4673
|
+
server_name: serverName,
|
|
4674
|
+
pid,
|
|
4675
|
+
metadata: meta,
|
|
4676
|
+
last_seen_at: db.fn.now(),
|
|
4677
|
+
created_at: db.fn.now()
|
|
4315
4678
|
}).returning(["id", "service_name"]);
|
|
4316
4679
|
const row = Array.isArray(rows) ? rows[0] : rows;
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4680
|
+
let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
|
|
4681
|
+
if (!rowId) {
|
|
4682
|
+
const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
|
|
4683
|
+
rowId = again?.id != null ? String(again.id) : "";
|
|
4320
4684
|
}
|
|
4685
|
+
if (!rowId) continue;
|
|
4686
|
+
const regNew = {
|
|
4687
|
+
serviceName: String(row?.service_name ?? serviceNameRaw),
|
|
4688
|
+
serviceGroup,
|
|
4689
|
+
queueName: options.queueName,
|
|
4690
|
+
target: options.target,
|
|
4691
|
+
rowId,
|
|
4692
|
+
registryTable,
|
|
4693
|
+
instanceNumber
|
|
4694
|
+
};
|
|
4695
|
+
context.servicesRegistry = regNew;
|
|
4696
|
+
context.runnerHeartbeat = regNew;
|
|
4697
|
+
context.logger.info?.(
|
|
4698
|
+
`[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
|
|
4699
|
+
);
|
|
4700
|
+
return regNew;
|
|
4321
4701
|
} catch (error) {
|
|
4322
4702
|
if (!isUniqueViolation(error)) {
|
|
4323
4703
|
throw error;
|
|
4324
4704
|
}
|
|
4705
|
+
context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
|
|
4325
4706
|
}
|
|
4326
4707
|
}
|
|
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}`
|
|
4708
|
+
throw new Error(
|
|
4709
|
+
`[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
|
|
4346
4710
|
);
|
|
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
4711
|
}
|
|
4357
4712
|
async function touchServicesRegistry(context, registration) {
|
|
4358
4713
|
const db = getDb2(context);
|
|
4359
|
-
const
|
|
4714
|
+
const serverName = os.hostname();
|
|
4360
4715
|
const pid = typeof process.pid === "number" ? process.pid : null;
|
|
4361
|
-
await db(registration.registryTable).where({
|
|
4716
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
4362
4717
|
last_seen_at: db.fn.now(),
|
|
4363
|
-
|
|
4718
|
+
server_name: serverName,
|
|
4364
4719
|
pid
|
|
4365
4720
|
});
|
|
4366
4721
|
}
|
|
4367
4722
|
async function updateServicesRegistryMetadata(context, registration, patch) {
|
|
4368
4723
|
const db = getDb2(context);
|
|
4369
|
-
const row = await db(registration.registryTable).where({
|
|
4724
|
+
const row = await db(registration.registryTable).where({ id: registration.rowId }).first();
|
|
4370
4725
|
const prev = parseMetadataColumn(row?.metadata);
|
|
4371
4726
|
const merged = { ...prev, ...patch };
|
|
4372
|
-
await db(registration.registryTable).where({
|
|
4727
|
+
await db(registration.registryTable).where({ id: registration.rowId }).update({
|
|
4373
4728
|
metadata: toJsonColumn(merged),
|
|
4374
4729
|
last_seen_at: db.fn.now()
|
|
4375
4730
|
});
|
|
@@ -4377,14 +4732,14 @@ async function updateServicesRegistryMetadata(context, registration, patch) {
|
|
|
4377
4732
|
}
|
|
4378
4733
|
async function unregisterServicesRegistry(context, registration) {
|
|
4379
4734
|
const db = getDb2(context);
|
|
4380
|
-
await db(registration.registryTable).where({
|
|
4381
|
-
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName}
|
|
4735
|
+
await db(registration.registryTable).where({ id: registration.rowId }).delete();
|
|
4736
|
+
context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
|
|
4382
4737
|
}
|
|
4383
|
-
async function listServicesRegistry(context, options = {
|
|
4738
|
+
async function listServicesRegistry(context, options = { queueName: "tasks" }) {
|
|
4384
4739
|
const db = getDb2(context);
|
|
4385
4740
|
const staleMs = options.staleMs ?? 6e4;
|
|
4386
4741
|
const cutoff = new Date(Date.now() - staleMs);
|
|
4387
|
-
const table =
|
|
4742
|
+
const table = queueToTableNames(options.queueName).registryTable;
|
|
4388
4743
|
let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
|
|
4389
4744
|
if (options.serviceGroup?.trim()) {
|
|
4390
4745
|
q = q.where({ service_group: options.serviceGroup.trim() });
|
|
@@ -4392,7 +4747,8 @@ async function listServicesRegistry(context, options = { queue: "tasks" }) {
|
|
|
4392
4747
|
return await q;
|
|
4393
4748
|
}
|
|
4394
4749
|
|
|
4395
|
-
// src/tasks/taskLogs.
|
|
4750
|
+
// src/tasks/taskLogs.js
|
|
4751
|
+
import path4 from "path";
|
|
4396
4752
|
function getLogsState(context) {
|
|
4397
4753
|
const holder = context;
|
|
4398
4754
|
if (holder.__tasksLogsState) return holder.__tasksLogsState;
|
|
@@ -4445,6 +4801,96 @@ function getLogsState(context) {
|
|
|
4445
4801
|
holder.__tasksLogsState = state;
|
|
4446
4802
|
return state;
|
|
4447
4803
|
}
|
|
4804
|
+
function ipcLogTargetKey(target) {
|
|
4805
|
+
const bp = target.basePath ?? "";
|
|
4806
|
+
const ns = target.namespace ?? "";
|
|
4807
|
+
return `${bp}::${ns}::${target.tableName}`;
|
|
4808
|
+
}
|
|
4809
|
+
function ipcFileLogsTableNameForSourceResource(source, resource) {
|
|
4810
|
+
const seg = (s) => {
|
|
4811
|
+
const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
4812
|
+
return t.length ? t : "x";
|
|
4813
|
+
};
|
|
4814
|
+
return `${seg(source)}/${seg(resource)}`;
|
|
4815
|
+
}
|
|
4816
|
+
async function readTaskIpcLogsSnapshot(context, options) {
|
|
4817
|
+
const holder = context;
|
|
4818
|
+
const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
|
|
4819
|
+
const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
|
|
4820
|
+
const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
|
|
4821
|
+
const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
|
|
4822
|
+
const fd = new FileDatabase({
|
|
4823
|
+
basePath,
|
|
4824
|
+
namespace,
|
|
4825
|
+
tableName,
|
|
4826
|
+
versioned: true,
|
|
4827
|
+
useMetadata: true,
|
|
4828
|
+
maxVersions: 30,
|
|
4829
|
+
pageSize: 2e3,
|
|
4830
|
+
logger: holder.logger
|
|
4831
|
+
});
|
|
4832
|
+
const versions = await fd.getVersions();
|
|
4833
|
+
if (versions.length === 0) {
|
|
4834
|
+
return { records: [], latestTs: null };
|
|
4835
|
+
}
|
|
4836
|
+
const latest = versions[versions.length - 1];
|
|
4837
|
+
const raw = await fd.read({ version: latest });
|
|
4838
|
+
const arr = Array.isArray(raw) ? raw : [];
|
|
4839
|
+
let filtered = arr;
|
|
4840
|
+
if (options.afterTs && String(options.afterTs).trim()) {
|
|
4841
|
+
const cut = String(options.afterTs).trim();
|
|
4842
|
+
filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
|
|
4843
|
+
}
|
|
4844
|
+
let latestTs = null;
|
|
4845
|
+
for (const r of filtered) {
|
|
4846
|
+
const ts = typeof r?.ts === "string" ? String(r.ts) : null;
|
|
4847
|
+
if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
|
|
4848
|
+
}
|
|
4849
|
+
const incremental = !!(options.afterTs && String(options.afterTs).trim());
|
|
4850
|
+
const maxReturn = incremental ? 1e4 : tail;
|
|
4851
|
+
const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
|
|
4852
|
+
return { records: sliced, latestTs };
|
|
4853
|
+
}
|
|
4854
|
+
function resolveIpcFileLogsDir(context, target) {
|
|
4855
|
+
const holder = context;
|
|
4856
|
+
const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
|
|
4857
|
+
const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
|
|
4858
|
+
const segments = target.tableName.split("/").filter(Boolean);
|
|
4859
|
+
return path4.resolve(basePath, namespace, ...segments);
|
|
4860
|
+
}
|
|
4861
|
+
function getLogsStateForTarget(context, target) {
|
|
4862
|
+
const holder = context;
|
|
4863
|
+
const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
|
|
4864
|
+
const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
4865
|
+
if (!enabled) return null;
|
|
4866
|
+
if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
|
|
4867
|
+
const map = holder.__tasksLogsTargetStates;
|
|
4868
|
+
const key = ipcLogTargetKey(target);
|
|
4869
|
+
if (map.has(key)) return map.get(key);
|
|
4870
|
+
const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
|
|
4871
|
+
const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
|
|
4872
|
+
const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
|
|
4873
|
+
const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
|
|
4874
|
+
const db = new FileDatabase({
|
|
4875
|
+
basePath,
|
|
4876
|
+
namespace,
|
|
4877
|
+
tableName: target.tableName,
|
|
4878
|
+
versioned: true,
|
|
4879
|
+
useMetadata: true,
|
|
4880
|
+
maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
|
|
4881
|
+
pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
|
|
4882
|
+
logger: holder.logger
|
|
4883
|
+
});
|
|
4884
|
+
const state = {
|
|
4885
|
+
db,
|
|
4886
|
+
errorDb: null,
|
|
4887
|
+
queue: Promise.resolve(),
|
|
4888
|
+
initialized: false,
|
|
4889
|
+
errorInitialized: false
|
|
4890
|
+
};
|
|
4891
|
+
map.set(key, state);
|
|
4892
|
+
return state;
|
|
4893
|
+
}
|
|
4448
4894
|
function isErrorPayload(payload) {
|
|
4449
4895
|
if (!payload) return false;
|
|
4450
4896
|
if (typeof payload === "object") {
|
|
@@ -4464,14 +4910,26 @@ function buildLogRecord(task, payload) {
|
|
|
4464
4910
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4465
4911
|
opid: task.opid ?? null,
|
|
4466
4912
|
taskId: task.id,
|
|
4467
|
-
taskName: task.
|
|
4468
|
-
target: task.
|
|
4913
|
+
taskName: task.name,
|
|
4914
|
+
target: task.service_group,
|
|
4469
4915
|
source: typeof params.source === "string" ? params.source : null,
|
|
4470
4916
|
resource: typeof params.resource === "string" ? params.resource : null,
|
|
4471
4917
|
payload
|
|
4472
4918
|
};
|
|
4473
4919
|
}
|
|
4474
|
-
function appendTaskIpcLog(context, task, payload) {
|
|
4920
|
+
function appendTaskIpcLog(context, task, payload, target) {
|
|
4921
|
+
if (target) {
|
|
4922
|
+
const state2 = getLogsStateForTarget(context, target);
|
|
4923
|
+
if (!state2?.db) return;
|
|
4924
|
+
const record2 = buildLogRecord(task, payload);
|
|
4925
|
+
state2.queue = state2.queue.then(async () => {
|
|
4926
|
+
await state2.db.write([record2], { forceNewVersion: !state2.initialized });
|
|
4927
|
+
state2.initialized = true;
|
|
4928
|
+
}).catch((error) => {
|
|
4929
|
+
context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
|
|
4930
|
+
});
|
|
4931
|
+
return;
|
|
4932
|
+
}
|
|
4475
4933
|
const state = getLogsState(context);
|
|
4476
4934
|
if (!state.db && !state.errorDb) return;
|
|
4477
4935
|
const record = buildLogRecord(task, payload);
|
|
@@ -4488,84 +4946,293 @@ function appendTaskIpcLog(context, task, payload) {
|
|
|
4488
4946
|
context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
|
|
4489
4947
|
});
|
|
4490
4948
|
}
|
|
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);
|
|
4949
|
+
async function flushTaskIpcLogs(context) {
|
|
4950
|
+
const holder = context;
|
|
4951
|
+
const promises = [];
|
|
4952
|
+
if (holder.__tasksLogsState?.queue) promises.push(holder.__tasksLogsState.queue);
|
|
4953
|
+
const map = holder.__tasksLogsTargetStates;
|
|
4954
|
+
if (map) {
|
|
4955
|
+
for (const s of map.values()) {
|
|
4956
|
+
if (s.queue) promises.push(s.queue);
|
|
4512
4957
|
}
|
|
4513
|
-
current = current.replace(raw, values.join(","));
|
|
4514
4958
|
}
|
|
4515
|
-
|
|
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
|
-
}
|
|
4530
|
-
return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
|
|
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());
|
|
4959
|
+
await Promise.all(promises);
|
|
4539
4960
|
}
|
|
4540
4961
|
|
|
4541
|
-
// src/tasks/
|
|
4542
|
-
var
|
|
4543
|
-
|
|
4544
|
-
|
|
4962
|
+
// src/tasks/AbstractTask.js
|
|
4963
|
+
var AbstractTask = class _AbstractTask {
|
|
4964
|
+
/**
|
|
4965
|
+
* Whether `send-task` should wait for completion (and print a result
|
|
4966
|
+
* report) when no explicit `--wait` / `--noWait` flag is given. Defaults
|
|
4967
|
+
* to false; short-lived probe tasks (e.g. `ping`) override to true.
|
|
4968
|
+
*
|
|
4969
|
+
* @type {boolean}
|
|
4970
|
+
*/
|
|
4971
|
+
static defaultWaitForResult = false;
|
|
4972
|
+
/**
|
|
4973
|
+
* @param {object} context Runner context (db, logger, params, emitter...).
|
|
4974
|
+
* @param {object} task Task row as claimed from the queue.
|
|
4975
|
+
*/
|
|
4545
4976
|
constructor(context, task) {
|
|
4546
4977
|
this.context = context;
|
|
4547
4978
|
this.task = task;
|
|
4548
4979
|
}
|
|
4980
|
+
/**
|
|
4981
|
+
* Return a short reason string when the task should be deferred (e.g. "locked
|
|
4982
|
+
* by source"), or `false`/falsy when it is free to run. Default: always `false`.
|
|
4983
|
+
*
|
|
4984
|
+
* @returns {string | false | Promise<string | false>}
|
|
4985
|
+
*/
|
|
4549
4986
|
cantRunReason() {
|
|
4550
4987
|
return false;
|
|
4551
4988
|
}
|
|
4989
|
+
/**
|
|
4990
|
+
* Called by the runner when a stop has been requested. Subclasses running
|
|
4991
|
+
* long loops should flip a flag here and check it between iterations.
|
|
4992
|
+
*
|
|
4993
|
+
* @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
|
|
4994
|
+
*/
|
|
4552
4995
|
requestStop(_allowanceMs) {
|
|
4553
4996
|
}
|
|
4997
|
+
/**
|
|
4998
|
+
* Perform the task. Must be implemented by subclasses.
|
|
4999
|
+
*
|
|
5000
|
+
* @param {(progress: unknown) => Promise<void>} _reportProgress
|
|
5001
|
+
* Updates the DB `progress` column. Accepts any serializable value;
|
|
5002
|
+
* strings are stored verbatim, objects are JSON-stringified.
|
|
5003
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5004
|
+
*/
|
|
5005
|
+
async run(_reportProgress) {
|
|
5006
|
+
throw new Error("AbstractTask.run must be implemented by subclass");
|
|
5007
|
+
}
|
|
5008
|
+
/**
|
|
5009
|
+
* Resolve a complete row payload for this task — envelope fields (queue,
|
|
5010
|
+
* priority, targeting, schedule…) plus the inner `params` blob produced by
|
|
5011
|
+
* {@link AbstractTask.resolveCustomParams}. Output shape matches
|
|
5012
|
+
* {@link enqueueTask}'s `options` argument, so the typical call is:
|
|
5013
|
+
*
|
|
5014
|
+
* const payload = await TaskClass.resolveParams(context, { name });
|
|
5015
|
+
* await enqueueTask(context, payload);
|
|
5016
|
+
*
|
|
5017
|
+
* Validation failures throw {@link ParamError} so the script aborts before
|
|
5018
|
+
* a malformed row hits the DB.
|
|
5019
|
+
*
|
|
5020
|
+
* @param {object} context
|
|
5021
|
+
* @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
|
|
5022
|
+
* Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
|
|
5023
|
+
* `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
|
|
5024
|
+
* `nextRunAt`, plus `params` (object — overlay onto inner blob).
|
|
5025
|
+
* @returns {Promise<object>}
|
|
5026
|
+
*/
|
|
5027
|
+
static async resolveParams(context, overrides = {}) {
|
|
5028
|
+
const main = _AbstractTask._resolveMainFields(context, overrides);
|
|
5029
|
+
const params = await this.resolveCustomParams(context, overrides);
|
|
5030
|
+
return { ...main, params };
|
|
5031
|
+
}
|
|
5032
|
+
/**
|
|
5033
|
+
* Resolve the inner JSON blob stored in the `params` column. Default
|
|
5034
|
+
* implementation passes through `--paramsJson` (parsed as a JSON object)
|
|
5035
|
+
* overlaid with `overrides.params` when supplied; returns `null` when
|
|
5036
|
+
* neither is provided.
|
|
5037
|
+
*
|
|
5038
|
+
* Subclasses with typed fields should override and call
|
|
5039
|
+
* {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
|
|
5040
|
+
* resolution, then validate and throw {@link ParamError} on bad input.
|
|
5041
|
+
*
|
|
5042
|
+
* @param {object} context
|
|
5043
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5044
|
+
* @returns {Promise<object|null>}
|
|
5045
|
+
*/
|
|
5046
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5047
|
+
return _AbstractTask._defaultParamsBlob(context, overrides);
|
|
5048
|
+
}
|
|
5049
|
+
/**
|
|
5050
|
+
* Read main task envelope fields from `context.params` (CLI/env), with
|
|
5051
|
+
* any matching key on `overrides` taking precedence. Internal; called by
|
|
5052
|
+
* {@link AbstractTask.resolveParams}.
|
|
5053
|
+
*
|
|
5054
|
+
* @param {object} context
|
|
5055
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5056
|
+
* @returns {object}
|
|
5057
|
+
*/
|
|
5058
|
+
static _resolveMainFields(context, overrides = {}) {
|
|
5059
|
+
const defs = {
|
|
5060
|
+
queueName: "string default tasks",
|
|
5061
|
+
priority: "number default 50",
|
|
5062
|
+
serviceGroup: "string",
|
|
5063
|
+
serviceName: "string",
|
|
5064
|
+
instanceNumber: "number",
|
|
5065
|
+
serverName: "string",
|
|
5066
|
+
opid: "string",
|
|
5067
|
+
schedule: "string"
|
|
5068
|
+
};
|
|
5069
|
+
const cli = context.params.getAllForModule("task-envelope", defs);
|
|
5070
|
+
const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
|
|
5071
|
+
if (!name) {
|
|
5072
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
5073
|
+
}
|
|
5074
|
+
let instanceNumber;
|
|
5075
|
+
const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
|
|
5076
|
+
if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
|
|
5077
|
+
const n = Number(rawInstance);
|
|
5078
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
5079
|
+
throw new ParamError("--instanceNumber must be a positive integer when set");
|
|
5080
|
+
}
|
|
5081
|
+
instanceNumber = n;
|
|
5082
|
+
} else {
|
|
5083
|
+
instanceNumber = null;
|
|
5084
|
+
}
|
|
5085
|
+
const priorityRaw = overrides.priority ?? cli.priority ?? 50;
|
|
5086
|
+
const priority = Number(priorityRaw);
|
|
5087
|
+
if (!Number.isFinite(priority)) {
|
|
5088
|
+
throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
|
|
5089
|
+
}
|
|
5090
|
+
return {
|
|
5091
|
+
name,
|
|
5092
|
+
queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
|
|
5093
|
+
priority,
|
|
5094
|
+
serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
|
|
5095
|
+
serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
|
|
5096
|
+
instanceNumber,
|
|
5097
|
+
serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
|
|
5098
|
+
opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
|
|
5099
|
+
schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
|
|
5100
|
+
nextRunAt: overrides.nextRunAt ?? null
|
|
5101
|
+
};
|
|
5102
|
+
}
|
|
5103
|
+
/**
|
|
5104
|
+
* Default inner-params resolver: parses `--paramsJson` (must be a JSON
|
|
5105
|
+
* object), then overlays `overrides.params` on top. Returns `null` when
|
|
5106
|
+
* neither is provided.
|
|
5107
|
+
*
|
|
5108
|
+
* @param {object} context
|
|
5109
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5110
|
+
* @returns {object|null}
|
|
5111
|
+
*/
|
|
5112
|
+
static _defaultParamsBlob(context, overrides = {}) {
|
|
5113
|
+
const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
|
|
5114
|
+
const fromJson = parseParamsJson(cli.paramsJson);
|
|
5115
|
+
const fromOverride = pickParamsObject(overrides);
|
|
5116
|
+
if (!fromJson && !fromOverride) return null;
|
|
5117
|
+
return { ...fromJson ?? {}, ...fromOverride ?? {} };
|
|
5118
|
+
}
|
|
5119
|
+
/**
|
|
5120
|
+
* Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
|
|
5121
|
+
* params (per `defs`) plus `--paramsJson` under a module namespace, then
|
|
5122
|
+
* merges them with explicit `overrides.params` in increasing priority:
|
|
5123
|
+
*
|
|
5124
|
+
* typed CLI flags → --paramsJson → overrides.params
|
|
5125
|
+
*
|
|
5126
|
+
* Undefined values are dropped so defaults declared in `defs` aren't
|
|
5127
|
+
* overwritten by missing-flag noise. Returns the merged object; the
|
|
5128
|
+
* caller is responsible for validation and throwing `ParamError`.
|
|
5129
|
+
*
|
|
5130
|
+
* @param {object} context
|
|
5131
|
+
* @param {string} moduleName Namespace for `--showUsedParams` grouping.
|
|
5132
|
+
* @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
|
|
5133
|
+
* @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
|
|
5134
|
+
* @returns {Record<string, unknown>}
|
|
5135
|
+
*/
|
|
5136
|
+
static _mergeTypedParams(context, moduleName, defs, overrides = {}) {
|
|
5137
|
+
const fullDefs = { ...defs, paramsJson: "string" };
|
|
5138
|
+
const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
|
|
5139
|
+
const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
|
|
5140
|
+
const fromCli = {};
|
|
5141
|
+
for (const [k, v] of Object.entries(cliRaw)) {
|
|
5142
|
+
if (k === "paramsJson") continue;
|
|
5143
|
+
if (v !== void 0 && v !== null) fromCli[k] = v;
|
|
5144
|
+
}
|
|
5145
|
+
const fromOverride = pickParamsObject(overrides) ?? {};
|
|
5146
|
+
return { ...fromCli, ...fromJson, ...fromOverride };
|
|
5147
|
+
}
|
|
4554
5148
|
};
|
|
5149
|
+
function emptyToUndef(s) {
|
|
5150
|
+
if (s === void 0 || s === null) return void 0;
|
|
5151
|
+
if (typeof s !== "string") return s;
|
|
5152
|
+
const t = s.trim();
|
|
5153
|
+
return t.length ? t : void 0;
|
|
5154
|
+
}
|
|
5155
|
+
function parseParamsJson(raw) {
|
|
5156
|
+
if (raw == null) return null;
|
|
5157
|
+
const t = String(raw).trim();
|
|
5158
|
+
if (!t) return null;
|
|
5159
|
+
let parsed;
|
|
5160
|
+
try {
|
|
5161
|
+
parsed = JSON.parse(t);
|
|
5162
|
+
} catch (e) {
|
|
5163
|
+
throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
|
|
5164
|
+
}
|
|
5165
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5166
|
+
throw new ParamError("--paramsJson must be a JSON object");
|
|
5167
|
+
}
|
|
5168
|
+
return parsed;
|
|
5169
|
+
}
|
|
5170
|
+
function pickParamsObject(overrides) {
|
|
5171
|
+
const p = overrides?.params;
|
|
5172
|
+
if (p && typeof p === "object" && !Array.isArray(p)) return p;
|
|
5173
|
+
return void 0;
|
|
5174
|
+
}
|
|
4555
5175
|
|
|
4556
|
-
// src/tasks/coreTasks/TaskPing.
|
|
4557
|
-
var TaskPing = class extends
|
|
5176
|
+
// src/tasks/coreTasks/TaskPing.js
|
|
5177
|
+
var TaskPing = class extends AbstractTask {
|
|
5178
|
+
/** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
|
|
5179
|
+
static defaultWaitForResult = true;
|
|
5180
|
+
/** Ping takes no params. */
|
|
5181
|
+
static async resolveCustomParams() {
|
|
5182
|
+
return null;
|
|
5183
|
+
}
|
|
5184
|
+
/**
|
|
5185
|
+
* @returns {Promise<{ success: true, results: "pong" }>}
|
|
5186
|
+
*/
|
|
4558
5187
|
async run() {
|
|
4559
5188
|
this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
|
|
4560
5189
|
return { success: true, results: "pong" };
|
|
4561
5190
|
}
|
|
4562
5191
|
};
|
|
4563
5192
|
|
|
4564
|
-
// src/tasks/coreTasks/TaskSampleProcess.
|
|
4565
|
-
var TaskSampleProcess = class extends
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
5193
|
+
// src/tasks/coreTasks/TaskSampleProcess.js
|
|
5194
|
+
var TaskSampleProcess = class extends AbstractTask {
|
|
5195
|
+
/**
|
|
5196
|
+
* @param {object} context
|
|
5197
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5198
|
+
* @returns {Promise<{ total: number, delay: number, name?: string }>}
|
|
5199
|
+
*/
|
|
5200
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5201
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
|
|
5202
|
+
total: "number default 10",
|
|
5203
|
+
delay: "number default 1000",
|
|
5204
|
+
name: "string"
|
|
5205
|
+
}, overrides);
|
|
5206
|
+
const total = Number(merged.total);
|
|
5207
|
+
const delay = Number(merged.delay);
|
|
5208
|
+
if (!Number.isInteger(total) || total <= 0) {
|
|
5209
|
+
throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
|
|
5210
|
+
}
|
|
5211
|
+
if (!Number.isInteger(delay) || delay < 0) {
|
|
5212
|
+
throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
|
|
5213
|
+
}
|
|
5214
|
+
const out = { total, delay };
|
|
5215
|
+
if (typeof merged.name === "string" && merged.name.trim()) {
|
|
5216
|
+
out.name = merged.name.trim();
|
|
5217
|
+
}
|
|
5218
|
+
return out;
|
|
5219
|
+
}
|
|
5220
|
+
/**
|
|
5221
|
+
* @param {object} context
|
|
5222
|
+
* @param {object} task
|
|
5223
|
+
*/
|
|
5224
|
+
constructor(context, task) {
|
|
5225
|
+
super(context, task);
|
|
5226
|
+
this.stopRequested = false;
|
|
5227
|
+
this.stopAllowanceMs = 0;
|
|
5228
|
+
this.stopDecisionLogged = false;
|
|
5229
|
+
}
|
|
5230
|
+
/**
|
|
5231
|
+
* Runner-facing stop signal. Records the allowance window so the main loop
|
|
5232
|
+
* can decide per-iteration whether to finish or abort early.
|
|
5233
|
+
*
|
|
5234
|
+
* @param {number} allowanceMs
|
|
5235
|
+
*/
|
|
4569
5236
|
requestStop(allowanceMs) {
|
|
4570
5237
|
this.stopRequested = true;
|
|
4571
5238
|
this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
|
|
@@ -4573,6 +5240,14 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4573
5240
|
`[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
|
|
4574
5241
|
);
|
|
4575
5242
|
}
|
|
5243
|
+
/**
|
|
5244
|
+
* Iterate `total` times, sleeping `delay` ms between ticks and reporting
|
|
5245
|
+
* progress every iteration. Validates params up front; invalid values short-
|
|
5246
|
+
* circuit to a structured failure without starting the loop.
|
|
5247
|
+
*
|
|
5248
|
+
* @param {(progress: object) => Promise<void>} reportProgress
|
|
5249
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5250
|
+
*/
|
|
4576
5251
|
async run(reportProgress) {
|
|
4577
5252
|
const totalRaw = this.task?.params?.total ?? 10;
|
|
4578
5253
|
const delayRaw = this.task?.params?.delay ?? 1e3;
|
|
@@ -4654,7 +5329,7 @@ var TaskSampleProcess = class extends TaskMaster {
|
|
|
4654
5329
|
}
|
|
4655
5330
|
};
|
|
4656
5331
|
|
|
4657
|
-
// src/tasks/coreTasks/TaskShellCommand.
|
|
5332
|
+
// src/tasks/coreTasks/TaskShellCommand.js
|
|
4658
5333
|
import { spawn } from "child_process";
|
|
4659
5334
|
function runShellCommand(command, cwd) {
|
|
4660
5335
|
return new Promise((resolve2, reject) => {
|
|
@@ -4684,7 +5359,27 @@ function runShellCommand(command, cwd) {
|
|
|
4684
5359
|
});
|
|
4685
5360
|
});
|
|
4686
5361
|
}
|
|
4687
|
-
var TaskShellCommand = class extends
|
|
5362
|
+
var TaskShellCommand = class extends AbstractTask {
|
|
5363
|
+
/**
|
|
5364
|
+
* @param {object} context
|
|
5365
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5366
|
+
* @returns {Promise<{ command: string, cwd?: string }>}
|
|
5367
|
+
*/
|
|
5368
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5369
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
|
|
5370
|
+
command: "string",
|
|
5371
|
+
cwd: "string"
|
|
5372
|
+
}, overrides);
|
|
5373
|
+
const command = typeof merged.command === "string" ? merged.command.trim() : "";
|
|
5374
|
+
if (!command) {
|
|
5375
|
+
throw new ParamError('shellCommand: param "command" must be a non-empty string');
|
|
5376
|
+
}
|
|
5377
|
+
const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
|
|
5378
|
+
return cwd ? { command, cwd } : { command };
|
|
5379
|
+
}
|
|
5380
|
+
/**
|
|
5381
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5382
|
+
*/
|
|
4688
5383
|
async run() {
|
|
4689
5384
|
const params = this.task?.params;
|
|
4690
5385
|
const commandRaw = typeof params === "string" ? params : params?.command;
|
|
@@ -4733,7 +5428,7 @@ var TaskShellCommand = class extends TaskMaster {
|
|
|
4733
5428
|
}
|
|
4734
5429
|
};
|
|
4735
5430
|
|
|
4736
|
-
// src/tasks/coreTasks/TaskSystemInfo.
|
|
5431
|
+
// src/tasks/coreTasks/TaskSystemInfo.js
|
|
4737
5432
|
import os2 from "os";
|
|
4738
5433
|
import fs4 from "fs/promises";
|
|
4739
5434
|
function toGb(valueBytes) {
|
|
@@ -4753,7 +5448,16 @@ async function getDiskStats() {
|
|
|
4753
5448
|
free: toGb(free)
|
|
4754
5449
|
};
|
|
4755
5450
|
}
|
|
4756
|
-
var TaskSystemInfo = class extends
|
|
5451
|
+
var TaskSystemInfo = class extends AbstractTask {
|
|
5452
|
+
/** Same UX expectation as `ping` — short probe, print the result. */
|
|
5453
|
+
static defaultWaitForResult = true;
|
|
5454
|
+
/** systemInfo takes no params. */
|
|
5455
|
+
static async resolveCustomParams() {
|
|
5456
|
+
return null;
|
|
5457
|
+
}
|
|
5458
|
+
/**
|
|
5459
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5460
|
+
*/
|
|
4757
5461
|
async run() {
|
|
4758
5462
|
try {
|
|
4759
5463
|
const totalMemory = os2.totalmem();
|
|
@@ -4805,8 +5509,31 @@ var TaskSystemInfo = class extends TaskMaster {
|
|
|
4805
5509
|
}
|
|
4806
5510
|
};
|
|
4807
5511
|
|
|
4808
|
-
// src/tasks/coreTasks/TaskSumAB.
|
|
4809
|
-
var TaskSumAB = class extends
|
|
5512
|
+
// src/tasks/coreTasks/TaskSumAB.js
|
|
5513
|
+
var TaskSumAB = class extends AbstractTask {
|
|
5514
|
+
/** Short, deterministic — wait by default so callers see the sum. */
|
|
5515
|
+
static defaultWaitForResult = true;
|
|
5516
|
+
/**
|
|
5517
|
+
* @param {object} context
|
|
5518
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5519
|
+
* @returns {Promise<{ a: number, b: number }>}
|
|
5520
|
+
*/
|
|
5521
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5522
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
|
|
5523
|
+
a: "number",
|
|
5524
|
+
b: "number"
|
|
5525
|
+
}, overrides);
|
|
5526
|
+
if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
|
|
5527
|
+
throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
|
|
5528
|
+
}
|
|
5529
|
+
if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
|
|
5530
|
+
throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
|
|
5531
|
+
}
|
|
5532
|
+
return { a: merged.a, b: merged.b };
|
|
5533
|
+
}
|
|
5534
|
+
/**
|
|
5535
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5536
|
+
*/
|
|
4810
5537
|
async run() {
|
|
4811
5538
|
const a = this.task?.params?.a;
|
|
4812
5539
|
const b = this.task?.params?.b;
|
|
@@ -4837,8 +5564,46 @@ var TaskSumAB = class extends TaskMaster {
|
|
|
4837
5564
|
}
|
|
4838
5565
|
};
|
|
4839
5566
|
|
|
4840
|
-
// src/tasks/coreTasks/TaskStopRunner.
|
|
4841
|
-
var TaskStopRunner = class extends
|
|
5567
|
+
// src/tasks/coreTasks/TaskStopRunner.js
|
|
5568
|
+
var TaskStopRunner = class extends AbstractTask {
|
|
5569
|
+
/**
|
|
5570
|
+
* Stop tasks must target a concrete instance — without `serviceName` the
|
|
5571
|
+
* row would race against any worker on the queue. Layered on top of the
|
|
5572
|
+
* envelope built by {@link AbstractTask.resolveParams}.
|
|
5573
|
+
*
|
|
5574
|
+
* @param {object} context
|
|
5575
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5576
|
+
* @returns {Promise<object>}
|
|
5577
|
+
*/
|
|
5578
|
+
static async resolveParams(context, overrides = {}) {
|
|
5579
|
+
const main = await super.resolveParams(context, overrides);
|
|
5580
|
+
if (!main.serviceName) {
|
|
5581
|
+
throw new ParamError(
|
|
5582
|
+
"stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
|
|
5583
|
+
);
|
|
5584
|
+
}
|
|
5585
|
+
return main;
|
|
5586
|
+
}
|
|
5587
|
+
/**
|
|
5588
|
+
* @param {object} context
|
|
5589
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5590
|
+
* @returns {Promise<{ allowanceMs: number }>}
|
|
5591
|
+
*/
|
|
5592
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5593
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
|
|
5594
|
+
allowanceMs: "number default 5000"
|
|
5595
|
+
}, overrides);
|
|
5596
|
+
const allowanceMs = Number(merged.allowanceMs);
|
|
5597
|
+
if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
|
|
5598
|
+
throw new ParamError(
|
|
5599
|
+
`stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
|
|
5600
|
+
);
|
|
5601
|
+
}
|
|
5602
|
+
return { allowanceMs };
|
|
5603
|
+
}
|
|
5604
|
+
/**
|
|
5605
|
+
* @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
|
|
5606
|
+
*/
|
|
4842
5607
|
async run() {
|
|
4843
5608
|
const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
|
|
4844
5609
|
this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
|
|
@@ -4853,172 +5618,396 @@ var TaskStopRunner = class extends TaskMaster {
|
|
|
4853
5618
|
}
|
|
4854
5619
|
};
|
|
4855
5620
|
|
|
4856
|
-
// src/tasks/
|
|
5621
|
+
// src/tasks/coreTasks/TaskGetLogs.js
|
|
5622
|
+
var TaskGetLogs = class extends AbstractTask {
|
|
5623
|
+
/**
|
|
5624
|
+
* @param {object} context
|
|
5625
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5626
|
+
* @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
|
|
5627
|
+
*/
|
|
5628
|
+
static async resolveCustomParams(context, overrides = {}) {
|
|
5629
|
+
const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
|
|
5630
|
+
source: "string",
|
|
5631
|
+
resource: "string",
|
|
5632
|
+
tail: "number default 100",
|
|
5633
|
+
afterTs: "string"
|
|
5634
|
+
}, overrides);
|
|
5635
|
+
const source = typeof merged.source === "string" ? merged.source.trim() : "";
|
|
5636
|
+
const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
|
|
5637
|
+
if (!source) throw new ParamError('getLogs: param "source" is required');
|
|
5638
|
+
if (!resource) throw new ParamError('getLogs: param "resource" is required');
|
|
5639
|
+
let tail = Number(merged.tail);
|
|
5640
|
+
if (!Number.isFinite(tail) || tail < 1) tail = 100;
|
|
5641
|
+
tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
|
|
5642
|
+
const out = { source, resource, tail };
|
|
5643
|
+
if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
|
|
5644
|
+
out.afterTs = merged.afterTs.trim();
|
|
5645
|
+
}
|
|
5646
|
+
return out;
|
|
5647
|
+
}
|
|
5648
|
+
/**
|
|
5649
|
+
* @param {unknown} _reportProgress Unused (single-shot read, no progress events).
|
|
5650
|
+
* @returns {Promise<{ success: boolean, results: unknown }>}
|
|
5651
|
+
*/
|
|
5652
|
+
async run(_reportProgress) {
|
|
5653
|
+
const p = this.task.params ?? {};
|
|
5654
|
+
const source = String(p.source ?? "").trim();
|
|
5655
|
+
const resource = String(p.resource ?? "").trim();
|
|
5656
|
+
const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
|
|
5657
|
+
const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
|
|
5658
|
+
if (!source || !resource) {
|
|
5659
|
+
return {
|
|
5660
|
+
success: false,
|
|
5661
|
+
results: { error: 'getLogs requires params "source" and "resource"' }
|
|
5662
|
+
};
|
|
5663
|
+
}
|
|
5664
|
+
try {
|
|
5665
|
+
const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
|
|
5666
|
+
source,
|
|
5667
|
+
resource,
|
|
5668
|
+
tail,
|
|
5669
|
+
afterTs
|
|
5670
|
+
});
|
|
5671
|
+
return {
|
|
5672
|
+
success: true,
|
|
5673
|
+
results: { records, latestTs, source, resource }
|
|
5674
|
+
};
|
|
5675
|
+
} catch (e) {
|
|
5676
|
+
return {
|
|
5677
|
+
success: false,
|
|
5678
|
+
results: { error: e?.message ?? String(e) }
|
|
5679
|
+
};
|
|
5680
|
+
}
|
|
5681
|
+
}
|
|
5682
|
+
};
|
|
5683
|
+
|
|
5684
|
+
// src/tasks/TasksRegistry.js
|
|
4857
5685
|
var TasksRegistry = class _TasksRegistry {
|
|
4858
|
-
|
|
5686
|
+
/**
|
|
5687
|
+
* @param {Record<string, Function>} [initial] Optional seed entries to copy in.
|
|
5688
|
+
*/
|
|
4859
5689
|
constructor(initial) {
|
|
5690
|
+
this.map = {};
|
|
4860
5691
|
if (initial) {
|
|
4861
5692
|
this.addMany(initial);
|
|
4862
5693
|
}
|
|
4863
5694
|
}
|
|
5695
|
+
/**
|
|
5696
|
+
* Build a registry pre-populated with every core task plus legacy aliases.
|
|
5697
|
+
* Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
|
|
5698
|
+
*
|
|
5699
|
+
* @returns {TasksRegistry}
|
|
5700
|
+
*/
|
|
4864
5701
|
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);
|
|
5702
|
+
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
5703
|
}
|
|
5704
|
+
/**
|
|
5705
|
+
* Register a single task class under a name. Overwrites any previous entry.
|
|
5706
|
+
*
|
|
5707
|
+
* @param {string} taskName
|
|
5708
|
+
* @param {Function} taskClass Subclass of `AbstractTask`.
|
|
5709
|
+
* @returns {this}
|
|
5710
|
+
*/
|
|
4867
5711
|
add(taskName, taskClass) {
|
|
4868
5712
|
this.map[taskName] = taskClass;
|
|
4869
5713
|
return this;
|
|
4870
5714
|
}
|
|
5715
|
+
/**
|
|
5716
|
+
* Bulk-register a name → class map. Later calls override earlier ones.
|
|
5717
|
+
*
|
|
5718
|
+
* @param {Record<string, Function>} entries
|
|
5719
|
+
* @returns {this}
|
|
5720
|
+
*/
|
|
4871
5721
|
addMany(entries) {
|
|
4872
5722
|
for (const [name, klass] of Object.entries(entries)) {
|
|
4873
5723
|
this.add(name, klass);
|
|
4874
5724
|
}
|
|
4875
5725
|
return this;
|
|
4876
5726
|
}
|
|
5727
|
+
/**
|
|
5728
|
+
* Look up a task class by name. Returns `undefined` when the name is unknown;
|
|
5729
|
+
* the runner treats that as "some other worker may handle this" and skips.
|
|
5730
|
+
*
|
|
5731
|
+
* @param {string} taskName
|
|
5732
|
+
* @returns {Function | undefined}
|
|
5733
|
+
*/
|
|
4877
5734
|
get(taskName) {
|
|
4878
5735
|
return this.map[taskName];
|
|
4879
5736
|
}
|
|
5737
|
+
/**
|
|
5738
|
+
* Strict variant of {@link get}: throws {@link ParamError} (with the list
|
|
5739
|
+
* of supported names) when `taskName` is unknown. Use from enqueuer code
|
|
5740
|
+
* paths where an unknown name is a hard CLI/programmer error.
|
|
5741
|
+
*
|
|
5742
|
+
* @param {string} taskName
|
|
5743
|
+
* @returns {Function}
|
|
5744
|
+
*/
|
|
5745
|
+
requireClass(taskName) {
|
|
5746
|
+
const TaskClass = taskName ? this.map[taskName] : void 0;
|
|
5747
|
+
if (!TaskClass) {
|
|
5748
|
+
const supported = this.listSupportedTasks().join(", ") || "(none)";
|
|
5749
|
+
throw new ParamError(
|
|
5750
|
+
`Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
|
|
5751
|
+
);
|
|
5752
|
+
}
|
|
5753
|
+
return TaskClass;
|
|
5754
|
+
}
|
|
5755
|
+
/**
|
|
5756
|
+
* Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
|
|
5757
|
+
* from `overrides` or `context.params`, look up the class, and delegate
|
|
5758
|
+
* to its static {@link AbstractTask.resolveParams} with `name` seeded into
|
|
5759
|
+
* the overrides. The returned object is shaped for {@link enqueueTask}.
|
|
5760
|
+
*
|
|
5761
|
+
* Validation failures (unknown task, missing required custom params, etc.)
|
|
5762
|
+
* surface as {@link ParamError} so the caller aborts cleanly before any
|
|
5763
|
+
* row is inserted.
|
|
5764
|
+
*
|
|
5765
|
+
* @param {object} context
|
|
5766
|
+
* @param {Record<string, unknown>} [overrides]
|
|
5767
|
+
* @returns {Promise<object>}
|
|
5768
|
+
*/
|
|
5769
|
+
async resolveTaskParams(context, overrides = {}) {
|
|
5770
|
+
const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
|
|
5771
|
+
const fromCli = context.params.get("name", "string");
|
|
5772
|
+
const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
|
|
5773
|
+
const name = overrideName || cliName;
|
|
5774
|
+
if (!name) {
|
|
5775
|
+
throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
|
|
5776
|
+
}
|
|
5777
|
+
const TaskClass = this.requireClass(name);
|
|
5778
|
+
return TaskClass.resolveParams(context, { ...overrides, name });
|
|
5779
|
+
}
|
|
5780
|
+
/**
|
|
5781
|
+
* Names of every registered task, sorted alphabetically (useful for CLI output
|
|
5782
|
+
* and allowlist sanity checks).
|
|
5783
|
+
*
|
|
5784
|
+
* @returns {string[]}
|
|
5785
|
+
*/
|
|
4880
5786
|
listSupportedTasks() {
|
|
4881
5787
|
return Object.keys(this.map).sort();
|
|
4882
5788
|
}
|
|
5789
|
+
/**
|
|
5790
|
+
* Shallow copy of the internal map, for handing to `addMany` on another registry
|
|
5791
|
+
* or for serialization.
|
|
5792
|
+
*
|
|
5793
|
+
* @returns {Record<string, Function>}
|
|
5794
|
+
*/
|
|
4883
5795
|
toObject() {
|
|
4884
5796
|
return { ...this.map };
|
|
4885
5797
|
}
|
|
4886
5798
|
};
|
|
4887
5799
|
|
|
4888
|
-
// src/tasks/
|
|
5800
|
+
// src/tasks/serviceTaskAllowlist.js
|
|
5801
|
+
var SERVICE_TASK_NAMES = [
|
|
5802
|
+
"ping",
|
|
5803
|
+
"stop",
|
|
5804
|
+
"stopRunner",
|
|
5805
|
+
"shellCommand",
|
|
5806
|
+
"systemInfo",
|
|
5807
|
+
"info",
|
|
5808
|
+
"getLogs"
|
|
5809
|
+
];
|
|
5810
|
+
function normalizeAllowedTasks(value) {
|
|
5811
|
+
if (!value) return void 0;
|
|
5812
|
+
if (Array.isArray(value)) {
|
|
5813
|
+
const out2 = value.map((v) => String(v).trim()).filter(Boolean);
|
|
5814
|
+
return out2.length ? out2 : void 0;
|
|
5815
|
+
}
|
|
5816
|
+
const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
|
|
5817
|
+
return out.length ? out : void 0;
|
|
5818
|
+
}
|
|
5819
|
+
function mergeAllowedTasksWithServiceTasks(names) {
|
|
5820
|
+
const set = /* @__PURE__ */ new Set([...SERVICE_TASK_NAMES, ...names ?? []]);
|
|
5821
|
+
return Array.from(set).sort();
|
|
5822
|
+
}
|
|
5823
|
+
|
|
5824
|
+
// src/tasks/taskScriptRunner.js
|
|
4889
5825
|
import { spawn as spawn2 } from "child_process";
|
|
5826
|
+
var MAX_PROGRESS_TEXT_LEN = 4e3;
|
|
4890
5827
|
function toCliArgs(args = []) {
|
|
4891
5828
|
return args.filter((a) => typeof a === "string" && a.length > 0);
|
|
4892
5829
|
}
|
|
4893
5830
|
function formatChildLogPrefix(task) {
|
|
4894
|
-
return `${task.
|
|
5831
|
+
return `${task.name}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
|
|
4895
5832
|
}
|
|
4896
|
-
|
|
4897
|
-
|
|
5833
|
+
function isProgressPayload(payload) {
|
|
5834
|
+
if (!payload || typeof payload !== "object") return false;
|
|
5835
|
+
if (payload.level !== "progress") return false;
|
|
5836
|
+
const count = Number(payload.count);
|
|
5837
|
+
const total = Number(payload.total);
|
|
5838
|
+
return Number.isFinite(count) && Number.isFinite(total) && total > 0;
|
|
5839
|
+
}
|
|
5840
|
+
function formatProgressText(payload, fallbackPrefix) {
|
|
5841
|
+
const pfx = payload.prefix ? `${payload.prefix} ` : fallbackPrefix ? `${fallbackPrefix} ` : "";
|
|
5842
|
+
const label = typeof payload.message === "string" && payload.message ? `${payload.message} ` : "";
|
|
5843
|
+
return `${pfx}${label}${payload.count}/${payload.total}`;
|
|
5844
|
+
}
|
|
5845
|
+
function forwardChildLogToParent(context, prefix, message) {
|
|
5846
|
+
if (!message || typeof message !== "object") return;
|
|
5847
|
+
const text = typeof message.message === "string" ? message.message : null;
|
|
5848
|
+
if (!text) return;
|
|
5849
|
+
const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
|
|
5850
|
+
const line = `[child:${prefix}] ${text}`;
|
|
5851
|
+
const logger = context.logger;
|
|
5852
|
+
switch (level) {
|
|
5853
|
+
case "error":
|
|
5854
|
+
case "fatal":
|
|
5855
|
+
logger.error?.(line);
|
|
5856
|
+
return;
|
|
5857
|
+
case "warn":
|
|
5858
|
+
case "warning":
|
|
5859
|
+
logger.warn?.(line);
|
|
5860
|
+
return;
|
|
5861
|
+
case "debug":
|
|
5862
|
+
logger.debug?.(line);
|
|
5863
|
+
return;
|
|
5864
|
+
case "info":
|
|
5865
|
+
default:
|
|
5866
|
+
logger.info?.(line);
|
|
5867
|
+
}
|
|
5868
|
+
}
|
|
5869
|
+
function buildNodeArgs(scriptPath, cliArgs) {
|
|
4898
5870
|
const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
|
|
4899
5871
|
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
|
-
}
|
|
5872
|
+
return hasTsRuntimeInParent ? [...inheritedExecArgs, scriptPath, ...cliArgs] : ["--import", "tsx", scriptPath, ...cliArgs];
|
|
5873
|
+
}
|
|
5874
|
+
function resolveTasksTableName(context) {
|
|
5875
|
+
return context.tasksQueueName || context.params?.get?.("table") || "tasks";
|
|
5876
|
+
}
|
|
5877
|
+
function announceIpcFileLogsTarget(context, options) {
|
|
5878
|
+
if (!options.ipcFileLogs) return;
|
|
5879
|
+
const logsDir = resolveIpcFileLogsDir(context, options.ipcFileLogs);
|
|
5880
|
+
const enabledRaw = context.params?.get?.("tasksLogsEnabled");
|
|
5881
|
+
const logsEnabled = enabledRaw === void 0 ? true : !!enabledRaw;
|
|
5882
|
+
context.logger.info?.(
|
|
5883
|
+
`[tasks] IPC file logs: ${logsDir}` + (logsEnabled ? "" : " (tasksLogsEnabled=false; not persisted)")
|
|
4914
5884
|
);
|
|
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
|
-
);
|
|
5885
|
+
}
|
|
5886
|
+
function createSerializedQueue() {
|
|
5887
|
+
let chain = Promise.resolve();
|
|
5888
|
+
return {
|
|
5889
|
+
push(fn) {
|
|
5890
|
+
chain = chain.then(fn, () => {
|
|
5891
|
+
}).catch(() => {
|
|
5892
|
+
});
|
|
5893
|
+
return chain;
|
|
5894
|
+
},
|
|
5895
|
+
drain() {
|
|
5896
|
+
return chain.catch(() => {
|
|
4940
5897
|
});
|
|
4941
5898
|
}
|
|
4942
5899
|
};
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
5900
|
+
}
|
|
5901
|
+
async function runNodeTaskScript(context, options) {
|
|
5902
|
+
const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
|
|
5903
|
+
const nodeArgs = buildNodeArgs(options.scriptPath, cliArgs);
|
|
5904
|
+
const child = spawn2(process.execPath, nodeArgs, {
|
|
5905
|
+
cwd: options.cwd || process.cwd(),
|
|
5906
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"],
|
|
5907
|
+
env: {
|
|
5908
|
+
...process.env,
|
|
5909
|
+
TASK_ID: options.task.id,
|
|
5910
|
+
TASK_NAME: options.task.name,
|
|
5911
|
+
TASK_OPID: options.task.opid || ""
|
|
5912
|
+
}
|
|
5913
|
+
});
|
|
5914
|
+
announceIpcFileLogsTarget(context, options);
|
|
5915
|
+
const prefix = formatChildLogPrefix(options.task);
|
|
5916
|
+
const tasksTable = resolveTasksTableName(context);
|
|
5917
|
+
const progressQueue = createSerializedQueue();
|
|
5918
|
+
const state = {
|
|
5919
|
+
stdout: "",
|
|
5920
|
+
stderr: "",
|
|
5921
|
+
workerResult: null,
|
|
5922
|
+
hadErrorMessage: false
|
|
5923
|
+
};
|
|
5924
|
+
const writeProgress = (text) => {
|
|
5925
|
+
const trimmed = typeof text === "string" ? text.slice(0, MAX_PROGRESS_TEXT_LEN) : "";
|
|
5926
|
+
if (!trimmed) return;
|
|
5927
|
+
progressQueue.push(async () => {
|
|
5928
|
+
const db = context.db;
|
|
5929
|
+
if (db) {
|
|
5930
|
+
try {
|
|
5931
|
+
await db(tasksTable).where({ id: options.task.id }).update({ progress: trimmed });
|
|
5932
|
+
} catch (error) {
|
|
5933
|
+
context.logger.warn?.(
|
|
5934
|
+
`[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
5935
|
+
);
|
|
5936
|
+
}
|
|
5937
|
+
}
|
|
5938
|
+
if (options.onProgress) {
|
|
5939
|
+
try {
|
|
5940
|
+
await options.onProgress(trimmed);
|
|
5941
|
+
} catch (error) {
|
|
5942
|
+
context.logger.warn?.(
|
|
5943
|
+
`[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
|
|
5944
|
+
);
|
|
5945
|
+
}
|
|
5946
|
+
}
|
|
5947
|
+
});
|
|
4956
5948
|
};
|
|
4957
|
-
child.stdout
|
|
5949
|
+
child.stdout?.on("data", (chunk) => {
|
|
4958
5950
|
const text = String(chunk);
|
|
4959
|
-
stdout += text;
|
|
5951
|
+
state.stdout += text;
|
|
4960
5952
|
if (text.trim()) {
|
|
4961
5953
|
context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
4962
|
-
updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
|
|
4963
5954
|
}
|
|
4964
5955
|
});
|
|
4965
|
-
child.stderr
|
|
5956
|
+
child.stderr?.on("data", (chunk) => {
|
|
4966
5957
|
const text = String(chunk);
|
|
4967
|
-
stderr += text;
|
|
5958
|
+
state.stderr += text;
|
|
4968
5959
|
if (text.trim()) {
|
|
4969
5960
|
context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
|
|
4970
5961
|
}
|
|
4971
5962
|
});
|
|
4972
5963
|
child.on("message", (message) => {
|
|
4973
5964
|
if (message && typeof message === "object" && "__taskWorkerResult" in message) {
|
|
4974
|
-
workerResult = message.__taskWorkerResult;
|
|
5965
|
+
state.workerResult = message.__taskWorkerResult;
|
|
4975
5966
|
return;
|
|
4976
5967
|
}
|
|
4977
5968
|
if (message && typeof message === "object") {
|
|
4978
5969
|
const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
|
|
4979
5970
|
if (level === "error" || level === "fatal") {
|
|
4980
|
-
hadErrorMessage = true;
|
|
5971
|
+
state.hadErrorMessage = true;
|
|
4981
5972
|
}
|
|
4982
5973
|
}
|
|
4983
|
-
appendTaskIpcLog(context, options.task, message);
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
const countNum = Number(String(message.count).trim());
|
|
4989
|
-
const totalNum = Number(message.total);
|
|
4990
|
-
if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
|
|
4991
|
-
context.logger.progress(message.message || "progress", {
|
|
4992
|
-
prefix: message.prefix || prefix,
|
|
4993
|
-
count: countNum,
|
|
4994
|
-
total: totalNum
|
|
4995
|
-
});
|
|
4996
|
-
} else {
|
|
4997
|
-
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
4998
|
-
}
|
|
4999
|
-
} else {
|
|
5000
|
-
context.logger.info?.(`[child:${prefix}] ${progressText}`);
|
|
5001
|
-
}
|
|
5974
|
+
appendTaskIpcLog(context, options.task, message, options.ipcFileLogs);
|
|
5975
|
+
try {
|
|
5976
|
+
options.onChildIpcMessage?.(message);
|
|
5977
|
+
} catch (e) {
|
|
5978
|
+
context.logger.warn?.(`[tasks] onChildIpcMessage failed: ${e?.message ?? String(e)}`);
|
|
5002
5979
|
}
|
|
5980
|
+
if (isProgressPayload(message)) {
|
|
5981
|
+
context.logger.progress(message.message || "progress", {
|
|
5982
|
+
prefix: message.prefix || prefix,
|
|
5983
|
+
count: Number(message.count),
|
|
5984
|
+
total: Number(message.total)
|
|
5985
|
+
});
|
|
5986
|
+
writeProgress(formatProgressText(message, prefix));
|
|
5987
|
+
return;
|
|
5988
|
+
}
|
|
5989
|
+
forwardChildLogToParent(context, prefix, message);
|
|
5003
5990
|
});
|
|
5004
5991
|
return await new Promise((resolve2, reject) => {
|
|
5005
5992
|
child.on("error", (error) => reject(error));
|
|
5006
5993
|
child.on("close", (exitCode, signal) => {
|
|
5007
|
-
|
|
5994
|
+
void (async () => {
|
|
5995
|
+
await flushTaskIpcLogs(context);
|
|
5996
|
+
await progressQueue.drain();
|
|
5008
5997
|
resolve2({
|
|
5009
5998
|
exitCode,
|
|
5010
5999
|
signal,
|
|
5011
|
-
stdout: stdout.trim(),
|
|
5012
|
-
stderr: stderr.trim(),
|
|
5013
|
-
workerResult,
|
|
5014
|
-
hadErrorMessage
|
|
6000
|
+
stdout: state.stdout.trim(),
|
|
6001
|
+
stderr: state.stderr.trim(),
|
|
6002
|
+
workerResult: state.workerResult,
|
|
6003
|
+
hadErrorMessage: state.hadErrorMessage
|
|
5015
6004
|
});
|
|
5016
|
-
});
|
|
6005
|
+
})();
|
|
5017
6006
|
});
|
|
5018
6007
|
});
|
|
5019
6008
|
}
|
|
5020
6009
|
|
|
5021
|
-
// src/tasks/index.
|
|
6010
|
+
// src/tasks/index.js
|
|
5022
6011
|
var LOCKED_BY_ERROR_MESSAGE = "locked by error";
|
|
5023
6012
|
var defaultTasksRegistry = TasksRegistry.withCoreTasks();
|
|
5024
6013
|
function getDb3(context) {
|
|
@@ -5033,22 +6022,13 @@ function normalizeRegistry(registry) {
|
|
|
5033
6022
|
if (registry instanceof TasksRegistry) return registry;
|
|
5034
6023
|
return new TasksRegistry().addMany(registry);
|
|
5035
6024
|
}
|
|
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) {
|
|
6025
|
+
async function enqueueStopTask(context, serviceGroup, queueName = "tasks", allowanceMs = 5e3) {
|
|
5046
6026
|
return enqueueTask(context, {
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
task: "stopRunner",
|
|
6027
|
+
queueName,
|
|
6028
|
+
name: "stopRunner",
|
|
5050
6029
|
params: { allowanceMs },
|
|
5051
|
-
priority:
|
|
6030
|
+
priority: 0,
|
|
6031
|
+
serviceGroup
|
|
5052
6032
|
});
|
|
5053
6033
|
}
|
|
5054
6034
|
async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
|
|
@@ -5066,18 +6046,20 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
|
|
|
5066
6046
|
}
|
|
5067
6047
|
async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
|
|
5068
6048
|
const db = getDb3(context);
|
|
5069
|
-
const taskName = row.
|
|
6049
|
+
const taskName = row.name;
|
|
5070
6050
|
const TaskClass = registry.get(taskName);
|
|
5071
|
-
const { paused_at: _pausedAt, ...rowForHistory } = row;
|
|
5072
6051
|
if (!TaskClass) {
|
|
5073
6052
|
const err = { message: `Unknown task "${taskName}"` };
|
|
5074
|
-
await db(historyTable).insert(
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
6053
|
+
await db(historyTable).insert(
|
|
6054
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
6055
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
6056
|
+
success: false,
|
|
6057
|
+
status: "failed",
|
|
6058
|
+
status_changed_at: db.fn.now(),
|
|
6059
|
+
params: toJsonColumn(row.params),
|
|
6060
|
+
results: toJsonColumn(err)
|
|
6061
|
+
})
|
|
6062
|
+
);
|
|
5081
6063
|
if (row.schedule) {
|
|
5082
6064
|
await db(tasksTable).where({ id: row.id }).update({
|
|
5083
6065
|
started_at: null,
|
|
@@ -5085,7 +6067,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5085
6067
|
success: false,
|
|
5086
6068
|
results: toJsonColumn(err),
|
|
5087
6069
|
past_due: null,
|
|
5088
|
-
|
|
6070
|
+
status: "paused",
|
|
6071
|
+
status_changed_at: db.fn.now(),
|
|
5089
6072
|
progress: LOCKED_BY_ERROR_MESSAGE
|
|
5090
6073
|
});
|
|
5091
6074
|
} else {
|
|
@@ -5112,13 +6095,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5112
6095
|
} finally {
|
|
5113
6096
|
runningTaskInstances.delete(row.id);
|
|
5114
6097
|
}
|
|
5115
|
-
await db(historyTable).insert(
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
6098
|
+
await db(historyTable).insert(
|
|
6099
|
+
taskHistoryInsertFromQueueRow(row, {
|
|
6100
|
+
completed_at: /* @__PURE__ */ new Date(),
|
|
6101
|
+
success,
|
|
6102
|
+
status: success ? "completed" : "failed",
|
|
6103
|
+
status_changed_at: db.fn.now(),
|
|
6104
|
+
params: toJsonColumn(row.params),
|
|
6105
|
+
results: toJsonColumn(results)
|
|
6106
|
+
})
|
|
6107
|
+
);
|
|
5122
6108
|
if (!success) {
|
|
5123
6109
|
const dbName = String(context?.params?.get?.("dbName") || "local");
|
|
5124
6110
|
const tableName = String(context?.params?.get?.("table") || "tasks");
|
|
@@ -5133,19 +6119,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5133
6119
|
const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
|
|
5134
6120
|
appendTaskIpcLog(context, row, {
|
|
5135
6121
|
level: "error",
|
|
5136
|
-
message: `[tasks] task failed: ${row.
|
|
6122
|
+
message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
|
|
5137
6123
|
details: results
|
|
5138
6124
|
});
|
|
5139
6125
|
}
|
|
5140
6126
|
if (row.schedule) {
|
|
5141
6127
|
if (success) {
|
|
6128
|
+
let nextRunAt = null;
|
|
6129
|
+
try {
|
|
6130
|
+
nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
|
|
6131
|
+
} catch (e) {
|
|
6132
|
+
context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
|
|
6133
|
+
}
|
|
5142
6134
|
await db(tasksTable).where({ id: row.id }).update({
|
|
5143
6135
|
started_at: null,
|
|
5144
6136
|
completed_at: /* @__PURE__ */ new Date(),
|
|
5145
6137
|
success,
|
|
5146
6138
|
results: toJsonColumn(results),
|
|
5147
6139
|
progress: null,
|
|
5148
|
-
past_due: null
|
|
6140
|
+
past_due: null,
|
|
6141
|
+
status: "idle",
|
|
6142
|
+
status_changed_at: db.fn.now(),
|
|
6143
|
+
next_run_at: nextRunAt,
|
|
6144
|
+
// Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
|
|
6145
|
+
service_name: null,
|
|
6146
|
+
server_name: null,
|
|
6147
|
+
instance_number: null
|
|
5149
6148
|
});
|
|
5150
6149
|
} else {
|
|
5151
6150
|
await db(tasksTable).where({ id: row.id }).update({
|
|
@@ -5153,7 +6152,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5153
6152
|
completed_at: /* @__PURE__ */ new Date(),
|
|
5154
6153
|
success,
|
|
5155
6154
|
results: toJsonColumn(results),
|
|
5156
|
-
|
|
6155
|
+
status: "paused",
|
|
6156
|
+
status_changed_at: db.fn.now(),
|
|
5157
6157
|
progress: LOCKED_BY_ERROR_MESSAGE,
|
|
5158
6158
|
past_due: null
|
|
5159
6159
|
});
|
|
@@ -5165,58 +6165,93 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
|
|
|
5165
6165
|
const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
|
|
5166
6166
|
return { stopRunnerRequested, stopAllowanceMs };
|
|
5167
6167
|
}
|
|
5168
|
-
|
|
6168
|
+
function shuffleTaskRowsInPlace(rows) {
|
|
6169
|
+
for (let i = rows.length - 1; i > 0; i--) {
|
|
6170
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
6171
|
+
const t = rows[i];
|
|
6172
|
+
rows[i] = rows[j];
|
|
6173
|
+
rows[j] = t;
|
|
6174
|
+
}
|
|
6175
|
+
}
|
|
6176
|
+
async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
|
|
5169
6177
|
const db = getDb3(context);
|
|
5170
|
-
let query = db(tasksTable).
|
|
6178
|
+
let query = db(tasksTable).where({ status: "idle" }).where(function() {
|
|
6179
|
+
this.whereNull("service_group").orWhere({ service_group: serviceGroup });
|
|
6180
|
+
}).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
6181
|
if (taskNames && taskNames.length > 0) {
|
|
5172
|
-
query = query.whereIn("
|
|
6182
|
+
query = query.whereIn("name", taskNames);
|
|
6183
|
+
}
|
|
6184
|
+
if (runnerIdentity) {
|
|
6185
|
+
query = query.where(function() {
|
|
6186
|
+
this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
|
|
6187
|
+
}).where(function() {
|
|
6188
|
+
this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
|
|
6189
|
+
}).where(function() {
|
|
6190
|
+
this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
|
|
6191
|
+
});
|
|
6192
|
+
} else {
|
|
6193
|
+
query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
|
|
5173
6194
|
}
|
|
5174
6195
|
const candidates = await query;
|
|
6196
|
+
shuffleTaskRowsInPlace(candidates);
|
|
5175
6197
|
for (const row of candidates) {
|
|
5176
6198
|
if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
|
|
5177
6199
|
continue;
|
|
5178
6200
|
}
|
|
5179
|
-
const TaskClass = registry.get(row.
|
|
5180
|
-
if (TaskClass) {
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
6201
|
+
const TaskClass = registry.get(row.name);
|
|
6202
|
+
if (!TaskClass) {
|
|
6203
|
+
continue;
|
|
6204
|
+
}
|
|
6205
|
+
const taskInstance = new TaskClass(context, row);
|
|
6206
|
+
const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
|
|
6207
|
+
if (reason) {
|
|
6208
|
+
if (!row.past_due) {
|
|
6209
|
+
await db(tasksTable).where({ id: row.id }).update({
|
|
6210
|
+
past_due: db.fn.now(),
|
|
6211
|
+
progress: String(reason)
|
|
6212
|
+
});
|
|
5191
6213
|
}
|
|
6214
|
+
continue;
|
|
6215
|
+
}
|
|
6216
|
+
const claimPatch = {
|
|
6217
|
+
started_at: db.fn.now(),
|
|
6218
|
+
status: "running",
|
|
6219
|
+
status_changed_at: db.fn.now()
|
|
6220
|
+
};
|
|
6221
|
+
if (runnerIdentity) {
|
|
6222
|
+
claimPatch.service_name = runnerIdentity.service_name;
|
|
6223
|
+
claimPatch.server_name = runnerIdentity.server_name;
|
|
6224
|
+
claimPatch.instance_number = runnerIdentity.instance_number;
|
|
5192
6225
|
}
|
|
5193
|
-
const updated = await db(tasksTable).where({ id: row.id
|
|
6226
|
+
const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
|
|
5194
6227
|
const claimed = Array.isArray(updated) ? updated[0] : null;
|
|
5195
6228
|
if (claimed) return claimed;
|
|
5196
6229
|
}
|
|
5197
6230
|
return null;
|
|
5198
6231
|
}
|
|
5199
6232
|
async function runTasksLoop(context, options) {
|
|
5200
|
-
const
|
|
6233
|
+
const queueName = options.queueName ?? "tasks";
|
|
5201
6234
|
const target = options.target;
|
|
5202
6235
|
const pollMs = options.pollMs ?? 1e3;
|
|
5203
|
-
const
|
|
6236
|
+
const claimJitterMs = options.claimJitterMs ?? 0;
|
|
6237
|
+
const maxParallel = options.maxParallel ?? 32;
|
|
5204
6238
|
const scanLimit = options.scanLimit ?? 100;
|
|
5205
6239
|
const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5206
6240
|
const registry = normalizeRegistry(options.registry);
|
|
5207
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
6241
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
5208
6242
|
if (!target) throw new Error("runTasksLoop: target is required");
|
|
6243
|
+
context.tasksQueueName = queueName;
|
|
5209
6244
|
const runningPromises = /* @__PURE__ */ new Set();
|
|
5210
6245
|
const runningTaskInstances = /* @__PURE__ */ new Map();
|
|
5211
6246
|
let runningStopControlPromise = null;
|
|
5212
6247
|
let stopRequested = false;
|
|
5213
6248
|
let stopAllowanceMs = 5e3;
|
|
5214
|
-
context.
|
|
6249
|
+
context.tasksRunnerStop = false;
|
|
5215
6250
|
let registryReg = null;
|
|
5216
6251
|
let registryInterval = null;
|
|
6252
|
+
let runnerIdentity = null;
|
|
5217
6253
|
const hbGroup = options.runnerServiceGroup?.trim();
|
|
5218
6254
|
if (hbGroup) {
|
|
5219
|
-
const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
|
|
5220
6255
|
const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
|
|
5221
6256
|
const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
|
|
5222
6257
|
const defaultMeta = {
|
|
@@ -5224,16 +6259,21 @@ async function runTasksLoop(context, options) {
|
|
|
5224
6259
|
allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
|
|
5225
6260
|
};
|
|
5226
6261
|
registryReg = await registerInServicesRegistry(context, {
|
|
5227
|
-
|
|
6262
|
+
queueName,
|
|
5228
6263
|
target,
|
|
5229
6264
|
serviceGroup: hbGroup,
|
|
5230
6265
|
serviceName: options.runnerServiceName,
|
|
5231
|
-
|
|
6266
|
+
instanceNumber: options.runnerInstanceNumber,
|
|
5232
6267
|
staleMs,
|
|
5233
6268
|
groupMaxInstances: options.runnerGroupMaxInstances,
|
|
5234
6269
|
enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
|
|
5235
6270
|
metadata: options.runnerMetadata ?? defaultMeta
|
|
5236
6271
|
});
|
|
6272
|
+
runnerIdentity = {
|
|
6273
|
+
service_name: registryReg.serviceName,
|
|
6274
|
+
server_name: os3.hostname(),
|
|
6275
|
+
instance_number: registryReg.instanceNumber
|
|
6276
|
+
};
|
|
5237
6277
|
registryInterval = setInterval(() => {
|
|
5238
6278
|
void touchServicesRegistry(context, registryReg).catch((err) => {
|
|
5239
6279
|
context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
|
|
@@ -5241,7 +6281,7 @@ async function runTasksLoop(context, options) {
|
|
|
5241
6281
|
}, hbIntervalMs);
|
|
5242
6282
|
}
|
|
5243
6283
|
try {
|
|
5244
|
-
while (!context.isStop() && !stopRequested &&
|
|
6284
|
+
while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
|
|
5245
6285
|
if (!runningStopControlPromise) {
|
|
5246
6286
|
const claimedStopTask = await claimNextRunnableTask(
|
|
5247
6287
|
context,
|
|
@@ -5249,7 +6289,8 @@ async function runTasksLoop(context, options) {
|
|
|
5249
6289
|
target,
|
|
5250
6290
|
registry,
|
|
5251
6291
|
10,
|
|
5252
|
-
["stopRunner", "stop"]
|
|
6292
|
+
["stopRunner", "stop"],
|
|
6293
|
+
runnerIdentity
|
|
5253
6294
|
);
|
|
5254
6295
|
if (claimedStopTask) {
|
|
5255
6296
|
runningStopControlPromise = executeClaimedTask(
|
|
@@ -5263,7 +6304,7 @@ async function runTasksLoop(context, options) {
|
|
|
5263
6304
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5264
6305
|
stopRequested = true;
|
|
5265
6306
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5266
|
-
context.
|
|
6307
|
+
context.tasksRunnerStop = true;
|
|
5267
6308
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5268
6309
|
}
|
|
5269
6310
|
}).finally(() => {
|
|
@@ -5271,6 +6312,9 @@ async function runTasksLoop(context, options) {
|
|
|
5271
6312
|
});
|
|
5272
6313
|
}
|
|
5273
6314
|
}
|
|
6315
|
+
if (claimJitterMs > 0) {
|
|
6316
|
+
await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
|
|
6317
|
+
}
|
|
5274
6318
|
while (runningPromises.size < maxParallel) {
|
|
5275
6319
|
const claimed = await claimNextRunnableTask(
|
|
5276
6320
|
context,
|
|
@@ -5278,14 +6322,15 @@ async function runTasksLoop(context, options) {
|
|
|
5278
6322
|
target,
|
|
5279
6323
|
registry,
|
|
5280
6324
|
scanLimit,
|
|
5281
|
-
allowedTasks
|
|
6325
|
+
allowedTasks,
|
|
6326
|
+
runnerIdentity
|
|
5282
6327
|
);
|
|
5283
6328
|
if (!claimed) break;
|
|
5284
6329
|
const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
|
|
5285
6330
|
if (outcome.stopRunnerRequested && !stopRequested) {
|
|
5286
6331
|
stopRequested = true;
|
|
5287
6332
|
stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
|
|
5288
|
-
context.
|
|
6333
|
+
context.tasksRunnerStop = true;
|
|
5289
6334
|
await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
|
|
5290
6335
|
}
|
|
5291
6336
|
}).finally(() => {
|
|
@@ -5293,7 +6338,16 @@ async function runTasksLoop(context, options) {
|
|
|
5293
6338
|
});
|
|
5294
6339
|
runningPromises.add(p);
|
|
5295
6340
|
}
|
|
5296
|
-
|
|
6341
|
+
const wakePromises = [...runningPromises];
|
|
6342
|
+
if (runningStopControlPromise) {
|
|
6343
|
+
wakePromises.push(runningStopControlPromise);
|
|
6344
|
+
}
|
|
6345
|
+
if (wakePromises.length === 0) {
|
|
6346
|
+
await sleepMs(pollMs);
|
|
6347
|
+
} else {
|
|
6348
|
+
const safe = wakePromises.map((p) => p.catch(() => void 0));
|
|
6349
|
+
await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
|
|
6350
|
+
}
|
|
5297
6351
|
}
|
|
5298
6352
|
if (context.isStop() && !stopRequested) {
|
|
5299
6353
|
await signalRunningTasksStop(context, runningTaskInstances, 5e3);
|
|
@@ -5329,89 +6383,130 @@ async function runTasksLoop(context, options) {
|
|
|
5329
6383
|
}
|
|
5330
6384
|
async function waitForTaskResult(context, taskId, options = {}) {
|
|
5331
6385
|
const db = getDb3(context);
|
|
5332
|
-
const
|
|
6386
|
+
const queueName = options.queueName ?? "tasks";
|
|
5333
6387
|
const timeoutMs = options.timeoutMs ?? 6e4;
|
|
5334
6388
|
const pollMs = options.pollMs ?? 500;
|
|
5335
|
-
const { tasksTable, historyTable } = queueToTableNames(
|
|
6389
|
+
const { tasksTable, historyTable } = queueToTableNames(queueName);
|
|
5336
6390
|
const deadline = Date.now() + timeoutMs;
|
|
6391
|
+
const waitStartedAt = /* @__PURE__ */ new Date();
|
|
6392
|
+
let cachedNameOpid = null;
|
|
6393
|
+
async function historySinceWait(name, opid) {
|
|
6394
|
+
let q = db(historyTable).where({ name }).where("completed_at", ">=", waitStartedAt);
|
|
6395
|
+
if (opid == null || opid === "") {
|
|
6396
|
+
q = q.whereNull("opid");
|
|
6397
|
+
} else {
|
|
6398
|
+
q = q.where({ opid });
|
|
6399
|
+
}
|
|
6400
|
+
return await q.orderBy("completed_at", "desc").first();
|
|
6401
|
+
}
|
|
5337
6402
|
while (Date.now() <= deadline) {
|
|
5338
|
-
const
|
|
5339
|
-
if (
|
|
6403
|
+
const legacy = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
|
|
6404
|
+
if (legacy) {
|
|
6405
|
+
return legacy;
|
|
6406
|
+
}
|
|
5340
6407
|
const pending = await db(tasksTable).where({ id: taskId }).first();
|
|
5341
|
-
if (
|
|
5342
|
-
|
|
5343
|
-
|
|
6408
|
+
if (pending) {
|
|
6409
|
+
cachedNameOpid = { name: pending.name, opid: pending.opid };
|
|
6410
|
+
const done = await historySinceWait(pending.name, pending.opid);
|
|
6411
|
+
if (done) {
|
|
6412
|
+
return done;
|
|
6413
|
+
}
|
|
6414
|
+
} else if (cachedNameOpid) {
|
|
6415
|
+
const done = await historySinceWait(cachedNameOpid.name, cachedNameOpid.opid);
|
|
6416
|
+
if (done) {
|
|
6417
|
+
return done;
|
|
6418
|
+
}
|
|
6419
|
+
return null;
|
|
6420
|
+
} else {
|
|
6421
|
+
return null;
|
|
5344
6422
|
}
|
|
5345
6423
|
await sleepMs(pollMs);
|
|
5346
6424
|
}
|
|
5347
6425
|
return null;
|
|
5348
6426
|
}
|
|
5349
6427
|
var TasksManager = class _TasksManager {
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
6428
|
+
/**
|
|
6429
|
+
* @param {object} context
|
|
6430
|
+
* @param {{
|
|
6431
|
+
* queueName?: string,
|
|
6432
|
+
* target?: string,
|
|
6433
|
+
* recreateTaskTables?: boolean,
|
|
6434
|
+
* pollMs?: number,
|
|
6435
|
+
* claimJitterMs?: number,
|
|
6436
|
+
* maxParallel?: number,
|
|
6437
|
+
* scanLimit?: number,
|
|
6438
|
+
* allowedTasks?: string | string[],
|
|
6439
|
+
* registry?: TasksRegistry | Record<string, Function>,
|
|
6440
|
+
* runnerServiceGroup?: string,
|
|
6441
|
+
* runnerServiceName?: string,
|
|
6442
|
+
* runnerInstanceNumber?: number,
|
|
6443
|
+
* runnerHeartbeatIntervalMs?: number,
|
|
6444
|
+
* runnerHeartbeatStaleMs?: number,
|
|
6445
|
+
* runnerGroupMaxInstances?: number,
|
|
6446
|
+
* runnerEnforceMaxInstances?: boolean,
|
|
6447
|
+
* runnerMetadata?: Record<string, unknown>,
|
|
6448
|
+
* }} [options]
|
|
6449
|
+
*/
|
|
5367
6450
|
constructor(context, options = {}) {
|
|
5368
6451
|
this.context = context;
|
|
5369
|
-
this.
|
|
6452
|
+
this.queueName = options.queueName ?? "tasks";
|
|
5370
6453
|
this.target = options.target ?? "localRunner";
|
|
5371
6454
|
this.recreateTaskTables = options.recreateTaskTables ?? false;
|
|
5372
6455
|
this.pollMs = options.pollMs ?? 1e3;
|
|
6456
|
+
this.claimJitterMs = options.claimJitterMs ?? 0;
|
|
5373
6457
|
this.maxParallel = options.maxParallel ?? 1;
|
|
5374
6458
|
this.scanLimit = options.scanLimit ?? 100;
|
|
5375
6459
|
this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
|
|
5376
6460
|
this.registry = normalizeRegistry(options.registry);
|
|
5377
6461
|
this.runnerServiceGroup = options.runnerServiceGroup;
|
|
5378
6462
|
this.runnerServiceName = options.runnerServiceName;
|
|
5379
|
-
this.
|
|
6463
|
+
this.runnerInstanceNumber = options.runnerInstanceNumber;
|
|
5380
6464
|
this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
|
|
5381
6465
|
this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
|
|
5382
6466
|
this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
|
|
5383
6467
|
this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
|
|
5384
6468
|
this.runnerMetadata = options.runnerMetadata;
|
|
5385
6469
|
}
|
|
6470
|
+
/**
|
|
6471
|
+
* Preferred factory: reads defaults from `context.params` (module namespace
|
|
6472
|
+
* `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
|
|
6473
|
+
* and inline options in one consistent resolver.
|
|
6474
|
+
*
|
|
6475
|
+
* @param {object} context
|
|
6476
|
+
* @param {ConstructorParameters<typeof TasksManager>[1]} [options]
|
|
6477
|
+
* @returns {TasksManager}
|
|
6478
|
+
*/
|
|
5386
6479
|
static init(context, options = {}) {
|
|
5387
6480
|
const defs = {
|
|
5388
6481
|
table: "string default tasks",
|
|
5389
6482
|
target: "string default localRunner",
|
|
5390
6483
|
recreateTaskTables: "boolean default false",
|
|
5391
6484
|
pollMs: "number default 1000",
|
|
6485
|
+
claimJitterMs: "number default 0",
|
|
5392
6486
|
maxParallel: "number default 1",
|
|
5393
6487
|
scanLimit: "number default 100",
|
|
5394
6488
|
allowedTasks: "string",
|
|
5395
6489
|
runnerServiceGroup: "string",
|
|
5396
6490
|
runnerServiceName: "string",
|
|
5397
|
-
|
|
6491
|
+
runnerInstanceNumber: "number",
|
|
5398
6492
|
runnerHeartbeatIntervalMs: "number default 10000",
|
|
5399
6493
|
runnerHeartbeatStaleMs: "number default 45000",
|
|
5400
6494
|
runnerGroupMaxInstances: "number",
|
|
5401
6495
|
runnerEnforceMaxInstances: "boolean default true"
|
|
5402
6496
|
};
|
|
5403
|
-
const discovered = context.params.getAllForModule(defs);
|
|
6497
|
+
const discovered = context.params.getAllForModule("tasks", defs);
|
|
5404
6498
|
const resolved = {
|
|
5405
|
-
|
|
6499
|
+
queueName: discovered.table,
|
|
5406
6500
|
target: discovered.target,
|
|
5407
6501
|
recreateTaskTables: discovered.recreateTaskTables,
|
|
5408
6502
|
pollMs: discovered.pollMs,
|
|
6503
|
+
claimJitterMs: discovered.claimJitterMs,
|
|
5409
6504
|
maxParallel: discovered.maxParallel,
|
|
5410
6505
|
scanLimit: discovered.scanLimit,
|
|
5411
6506
|
allowedTasks: discovered.allowedTasks,
|
|
5412
6507
|
runnerServiceGroup: discovered.runnerServiceGroup,
|
|
5413
6508
|
runnerServiceName: discovered.runnerServiceName,
|
|
5414
|
-
|
|
6509
|
+
runnerInstanceNumber: discovered.runnerInstanceNumber,
|
|
5415
6510
|
runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
|
|
5416
6511
|
runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
|
|
5417
6512
|
runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
|
|
@@ -5420,24 +6515,39 @@ var TasksManager = class _TasksManager {
|
|
|
5420
6515
|
};
|
|
5421
6516
|
return new _TasksManager(context, resolved);
|
|
5422
6517
|
}
|
|
6518
|
+
/**
|
|
6519
|
+
* Idempotently ensure the three backing tables exist for this queue.
|
|
6520
|
+
*
|
|
6521
|
+
* @param {{ recreate?: boolean }} [options]
|
|
6522
|
+
* @returns {Promise<void>}
|
|
6523
|
+
*/
|
|
5423
6524
|
async ensureTaskTables(options = {}) {
|
|
5424
6525
|
await ensureTaskTables(this.context, {
|
|
5425
|
-
|
|
6526
|
+
queueName: this.queueName,
|
|
5426
6527
|
recreate: options.recreate ?? this.recreateTaskTables
|
|
5427
6528
|
});
|
|
5428
6529
|
}
|
|
6530
|
+
/**
|
|
6531
|
+
* Start the runner loop using this manager's resolved config. Per-call
|
|
6532
|
+
* options override the stored defaults, but `runnerMetadata` still falls
|
|
6533
|
+
* through when omitted.
|
|
6534
|
+
*
|
|
6535
|
+
* @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
|
|
6536
|
+
* @returns {Promise<void>}
|
|
6537
|
+
*/
|
|
5429
6538
|
async runTasksLoop(options = {}) {
|
|
5430
6539
|
await runTasksLoop(this.context, {
|
|
5431
|
-
|
|
6540
|
+
queueName: options.queueName ?? this.queueName,
|
|
5432
6541
|
target: options.target ?? this.target,
|
|
5433
6542
|
pollMs: options.pollMs ?? this.pollMs,
|
|
6543
|
+
claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
|
|
5434
6544
|
maxParallel: options.maxParallel ?? this.maxParallel,
|
|
5435
6545
|
scanLimit: options.scanLimit ?? this.scanLimit,
|
|
5436
6546
|
allowedTasks: options.allowedTasks ?? this.allowedTasks,
|
|
5437
6547
|
registry: options.registry ?? this.registry,
|
|
5438
6548
|
runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
|
|
5439
6549
|
runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
|
|
5440
|
-
|
|
6550
|
+
runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
|
|
5441
6551
|
runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
|
|
5442
6552
|
runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
|
|
5443
6553
|
runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
|
|
@@ -5447,8 +6557,9 @@ var TasksManager = class _TasksManager {
|
|
|
5447
6557
|
}
|
|
5448
6558
|
};
|
|
5449
6559
|
export {
|
|
6560
|
+
AbstractTask,
|
|
5450
6561
|
Args,
|
|
5451
|
-
|
|
6562
|
+
Box4 as Box,
|
|
5452
6563
|
Db,
|
|
5453
6564
|
Divider,
|
|
5454
6565
|
FileDatabase,
|
|
@@ -5461,14 +6572,16 @@ export {
|
|
|
5461
6572
|
MultiColumnListComponent,
|
|
5462
6573
|
MultiColumnListWithPreviewComponent,
|
|
5463
6574
|
Params,
|
|
5464
|
-
|
|
6575
|
+
React2 as React,
|
|
6576
|
+
S3,
|
|
6577
|
+
SERVICE_TASK_NAMES,
|
|
5465
6578
|
ScreenBody,
|
|
5466
6579
|
ScreenContainer,
|
|
5467
6580
|
ScreenDivider,
|
|
5468
6581
|
ScreenFooter,
|
|
5469
6582
|
ScreenRow,
|
|
5470
6583
|
ScreenTitle,
|
|
5471
|
-
|
|
6584
|
+
TaskGetLogs,
|
|
5472
6585
|
TaskPing,
|
|
5473
6586
|
TaskSampleProcess,
|
|
5474
6587
|
TaskShellCommand,
|
|
@@ -5483,17 +6596,17 @@ export {
|
|
|
5483
6596
|
buildBreadcrumb,
|
|
5484
6597
|
buildDetailBreadcrumb,
|
|
5485
6598
|
buildFooter,
|
|
5486
|
-
|
|
5487
|
-
dbFindAndConnect,
|
|
5488
|
-
dbInit,
|
|
6599
|
+
convertPattern,
|
|
5489
6600
|
defaultFileSynopsisFunction,
|
|
5490
6601
|
defaultTasksRegistry,
|
|
5491
6602
|
defaultVersionSynopsisFunction,
|
|
5492
6603
|
enqueueStopTask,
|
|
5493
6604
|
enqueueTask,
|
|
5494
6605
|
ensureTaskTables,
|
|
6606
|
+
flushTaskIpcLogs,
|
|
5495
6607
|
getArgsInstance,
|
|
5496
6608
|
createElement2 as h,
|
|
6609
|
+
ipcFileLogsTableNameForSourceResource,
|
|
5497
6610
|
joiEdateType,
|
|
5498
6611
|
joiStringArrayType,
|
|
5499
6612
|
listServicesRegistry as listAliveRunnerHeartbeats,
|
|
@@ -5501,14 +6614,22 @@ export {
|
|
|
5501
6614
|
listSources,
|
|
5502
6615
|
listTables,
|
|
5503
6616
|
load,
|
|
6617
|
+
matchesParsedPattern,
|
|
6618
|
+
memo,
|
|
6619
|
+
mergeAllowedTasksWithServiceTasks,
|
|
6620
|
+
nextTimeMatch,
|
|
6621
|
+
normalizeAllowedTasks,
|
|
5504
6622
|
organizeFooterMessages,
|
|
5505
6623
|
queueToTableNames,
|
|
6624
|
+
readTaskIpcLogsSnapshot,
|
|
5506
6625
|
registerInServicesRegistry,
|
|
5507
6626
|
registerInServicesRegistry as registerRunnerHeartbeat,
|
|
6627
|
+
resolveAsterisks,
|
|
6628
|
+
resolveIpcFileLogsDir,
|
|
6629
|
+
resolveRanges,
|
|
6630
|
+
resolveSteps,
|
|
5508
6631
|
runNodeTaskScript,
|
|
5509
6632
|
runTasksLoop,
|
|
5510
|
-
servicesRegistryTable as runnerHeartbeatsTable,
|
|
5511
|
-
servicesRegistryTable,
|
|
5512
6633
|
setupContext,
|
|
5513
6634
|
showListScreen,
|
|
5514
6635
|
showMenuScreen,
|
|
@@ -5516,6 +6637,8 @@ export {
|
|
|
5516
6637
|
showMultiColumnListWithPreviewScreen,
|
|
5517
6638
|
showScreen,
|
|
5518
6639
|
showWordGridScreen,
|
|
6640
|
+
taskHistoryInsertFromQueueRow,
|
|
6641
|
+
timeMatcher,
|
|
5519
6642
|
touchServicesRegistry as touchRunnerHeartbeat,
|
|
5520
6643
|
touchServicesRegistry,
|
|
5521
6644
|
unregisterServicesRegistry as unregisterRunnerHeartbeat,
|
|
@@ -5523,10 +6646,11 @@ export {
|
|
|
5523
6646
|
updateServicesRegistryMetadata,
|
|
5524
6647
|
updateTaskProgress,
|
|
5525
6648
|
useCallback,
|
|
5526
|
-
|
|
6649
|
+
useEffect2 as useEffect,
|
|
5527
6650
|
useInput2 as useInput,
|
|
6651
|
+
useLayoutEffect,
|
|
5528
6652
|
useMemo,
|
|
5529
|
-
|
|
6653
|
+
useRef2 as useRef,
|
|
5530
6654
|
useState3 as useState,
|
|
5531
6655
|
waitForTaskResult
|
|
5532
6656
|
};
|