target-supabase-sdk 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -6
- package/dist/auth/auth.api.d.ts.map +1 -1
- package/dist/auth/auth.api.js +2 -1
- package/dist/auth/auth.api.js.map +1 -1
- package/dist/browser.d.ts +52 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +45 -0
- package/dist/browser.js.map +1 -0
- package/dist/command/command.api.d.ts +43 -0
- package/dist/command/command.api.d.ts.map +1 -0
- package/dist/command/command.api.js +58 -0
- package/dist/command/command.api.js.map +1 -0
- package/dist/command/command.interface.d.ts +14 -0
- package/dist/command/command.interface.d.ts.map +1 -0
- package/dist/command/command.interface.js +9 -0
- package/dist/command/command.interface.js.map +1 -0
- package/dist/core.api.d.ts +70 -6
- package/dist/core.api.d.ts.map +1 -1
- package/dist/core.api.js +191 -13
- package/dist/core.api.js.map +1 -1
- package/dist/file/file-manager.d.ts +1 -1
- package/dist/file/file-manager.d.ts.map +1 -1
- package/dist/file/file-manager.js +1 -1
- package/dist/file/file-manager.js.map +1 -1
- package/dist/index.d.ts +2 -40
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -34
- package/dist/index.js.map +1 -1
- package/dist/link/link.interface.d.ts +1 -1
- package/dist/link/link.interface.d.ts.map +1 -1
- package/dist/node/node-log-scope.d.ts +11 -0
- package/dist/node/node-log-scope.d.ts.map +1 -0
- package/dist/node/node-log-scope.js +7 -0
- package/dist/node/node-log-scope.js.map +1 -0
- package/dist/node/node-runtime.base.d.ts +42 -0
- package/dist/node/node-runtime.base.d.ts.map +1 -0
- package/dist/node/node-runtime.base.js +397 -0
- package/dist/node/node-runtime.base.js.map +1 -0
- package/dist/node/node-task.preset.d.ts +1 -0
- package/dist/node/node-task.preset.d.ts.map +1 -0
- package/dist/node/node-task.preset.js +14 -0
- package/dist/node/node-task.preset.js.map +1 -0
- package/dist/node/node.api.d.ts +67 -31
- package/dist/node/node.api.d.ts.map +1 -1
- package/dist/node/node.api.js +65 -102
- package/dist/node/node.api.js.map +1 -1
- package/dist/node/node.interface.d.ts +7 -0
- package/dist/node/node.interface.d.ts.map +1 -1
- package/dist/node/node.interface.js +2 -0
- package/dist/node/node.interface.js.map +1 -1
- package/dist/node/node.utils.d.ts +5 -0
- package/dist/node/node.utils.d.ts.map +1 -0
- package/dist/node/node.utils.js +36 -0
- package/dist/node/node.utils.js.map +1 -0
- package/dist/node/task-node.d.ts +20 -0
- package/dist/node/task-node.d.ts.map +1 -0
- package/dist/node/task-node.js +245 -0
- package/dist/node/task-node.js.map +1 -0
- package/dist/node/trigger-node.d.ts +18 -0
- package/dist/node/trigger-node.d.ts.map +1 -0
- package/dist/node/trigger-node.js +150 -0
- package/dist/node/trigger-node.js.map +1 -0
- package/dist/node.d.ts +22 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +19 -0
- package/dist/node.js.map +1 -0
- package/dist/parcel/parcel-manager.d.ts +1 -1
- package/dist/parcel/parcel-manager.d.ts.map +1 -1
- package/dist/parcel/parcel-manager.js +1 -1
- package/dist/parcel/parcel-manager.js.map +1 -1
- package/dist/repo/repo-context.utils.d.ts +24 -0
- package/dist/repo/repo-context.utils.d.ts.map +1 -0
- package/dist/repo/repo-context.utils.js +81 -0
- package/dist/repo/repo-context.utils.js.map +1 -0
- package/dist/repo/repo-manager.d.ts +42 -0
- package/dist/repo/repo-manager.d.ts.map +1 -0
- package/dist/repo/repo-manager.js +166 -0
- package/dist/repo/repo-manager.js.map +1 -0
- package/dist/repo/repo.api.d.ts +16 -0
- package/dist/repo/repo.api.d.ts.map +1 -0
- package/dist/repo/repo.api.js +40 -0
- package/dist/repo/repo.api.js.map +1 -0
- package/dist/repo/repo.interface.d.ts +12 -5
- package/dist/repo/repo.interface.d.ts.map +1 -1
- package/dist/repo/repo.interface.js +2 -2
- package/dist/repo/repo.interface.js.map +1 -1
- package/dist/repo/repo.script-loader.d.ts +19 -0
- package/dist/repo/repo.script-loader.d.ts.map +1 -0
- package/dist/repo/repo.script-loader.js +108 -0
- package/dist/repo/repo.script-loader.js.map +1 -0
- package/dist/shared/log/create-api-logger.d.ts +21 -0
- package/dist/shared/log/create-api-logger.d.ts.map +1 -0
- package/dist/shared/log/create-api-logger.js +19 -0
- package/dist/shared/log/create-api-logger.js.map +1 -0
- package/dist/shared/log/index.d.ts +10 -0
- package/dist/shared/log/index.d.ts.map +1 -0
- package/dist/shared/log/index.js +10 -0
- package/dist/shared/log/index.js.map +1 -0
- package/dist/shared/log/log-manager.d.ts +93 -0
- package/dist/shared/log/log-manager.d.ts.map +1 -0
- package/dist/shared/log/log-manager.js +237 -0
- package/dist/shared/log/log-manager.js.map +1 -0
- package/dist/shared/log/log-scope.d.ts +49 -0
- package/dist/shared/log/log-scope.d.ts.map +1 -0
- package/dist/shared/log/log-scope.js +70 -0
- package/dist/shared/log/log-scope.js.map +1 -0
- package/dist/shared/utils/config-path.utils.d.ts +38 -0
- package/dist/shared/utils/config-path.utils.d.ts.map +1 -0
- package/dist/shared/utils/config-path.utils.js +85 -0
- package/dist/shared/utils/config-path.utils.js.map +1 -0
- package/dist/shared/utils/error.utils.d.ts +11 -0
- package/dist/shared/utils/error.utils.d.ts.map +1 -0
- package/dist/shared/utils/error.utils.js +15 -0
- package/dist/shared/utils/error.utils.js.map +1 -0
- package/dist/shared/utils/id.utils.d.ts +3 -0
- package/dist/shared/utils/id.utils.d.ts.map +1 -0
- package/dist/shared/utils/id.utils.js +5 -0
- package/dist/shared/utils/id.utils.js.map +1 -0
- package/dist/supabase.js +1 -1
- package/dist/supabase.js.map +1 -1
- package/dist/task/index.d.ts +15 -0
- package/dist/task/index.d.ts.map +1 -0
- package/dist/task/index.js +14 -0
- package/dist/task/index.js.map +1 -0
- package/dist/task/local-task-registry.d.ts +37 -0
- package/dist/task/local-task-registry.d.ts.map +1 -0
- package/dist/task/local-task-registry.js +214 -0
- package/dist/task/local-task-registry.js.map +1 -0
- package/dist/task/task-manager.d.ts +64 -0
- package/dist/task/task-manager.d.ts.map +1 -0
- package/dist/task/task-manager.js +196 -0
- package/dist/task/task-manager.js.map +1 -0
- package/dist/task/task-post.api.d.ts +49 -0
- package/dist/task/task-post.api.d.ts.map +1 -0
- package/dist/task/task-post.api.js +91 -0
- package/dist/task/task-post.api.js.map +1 -0
- package/dist/task/task-repo-context.d.ts +97 -0
- package/dist/task/task-repo-context.d.ts.map +1 -0
- package/dist/task/task-repo-context.js +12 -0
- package/dist/task/task-repo-context.js.map +1 -0
- package/dist/task/task-repo-validation.d.ts +46 -0
- package/dist/task/task-repo-validation.d.ts.map +1 -0
- package/dist/task/task-repo-validation.js +96 -0
- package/dist/task/task-repo-validation.js.map +1 -0
- package/dist/task/task.api.d.ts +198 -0
- package/dist/task/task.api.d.ts.map +1 -1
- package/dist/task/task.api.js +302 -1
- package/dist/task/task.api.js.map +1 -1
- package/dist/task/task.interface.d.ts +11 -2
- package/dist/task/task.interface.d.ts.map +1 -1
- package/dist/task/task.interface.js +10 -0
- package/dist/task/task.interface.js.map +1 -1
- package/dist/trigger/trigger.api.d.ts +140 -0
- package/dist/trigger/trigger.api.d.ts.map +1 -0
- package/dist/trigger/trigger.api.js +125 -0
- package/dist/trigger/trigger.api.js.map +1 -0
- package/dist/trigger/trigger.interface.d.ts +43 -0
- package/dist/trigger/trigger.interface.d.ts.map +1 -0
- package/dist/trigger/trigger.interface.js +11 -0
- package/dist/trigger/trigger.interface.js.map +1 -0
- package/dist/trigger/trigger.utils.d.ts +6 -0
- package/dist/trigger/trigger.utils.d.ts.map +1 -0
- package/dist/trigger/trigger.utils.js +39 -0
- package/dist/trigger/trigger.utils.js.map +1 -0
- package/package.json +30 -8
- package/src/auth/auth.api.ts +3 -1
- package/src/browser.ts +100 -0
- package/src/command/command.api.ts +88 -0
- package/src/command/command.interface.ts +17 -0
- package/src/core.api.ts +305 -26
- package/src/file/file-manager.ts +1 -1
- package/src/index.ts +2 -64
- package/src/link/link.interface.ts +1 -1
- package/src/node/node-log-scope.ts +15 -0
- package/src/node/node-runtime.base.ts +450 -0
- package/src/node/node-task.preset.ts +13 -0
- package/src/node/node.api.ts +95 -117
- package/src/node/node.interface.ts +8 -0
- package/src/node/node.utils.ts +36 -0
- package/src/node/task-node.ts +284 -0
- package/src/node/trigger-node.ts +180 -0
- package/src/node.ts +62 -0
- package/src/parcel/parcel-manager.ts +1 -1
- package/src/repo/repo-context.utils.ts +111 -0
- package/src/repo/repo-manager.ts +245 -0
- package/src/repo/repo.api.ts +50 -0
- package/src/repo/repo.interface.ts +13 -5
- package/src/repo/repo.script-loader.ts +140 -0
- package/src/shared/log/create-api-logger.ts +36 -0
- package/src/shared/log/index.ts +23 -0
- package/src/shared/log/log-manager.ts +335 -0
- package/src/shared/log/log-scope.ts +123 -0
- package/src/shared/utils/config-path.utils.ts +104 -0
- package/src/shared/utils/error.utils.ts +15 -0
- package/src/shared/utils/id.utils.ts +4 -0
- package/src/supabase.ts +1 -1
- package/src/task/index.ts +48 -0
- package/src/task/local-task-registry.ts +282 -0
- package/src/task/task-manager.ts +301 -0
- package/src/task/task-post.api.ts +116 -0
- package/src/task/task-repo-context.ts +105 -0
- package/src/task/task-repo-validation.ts +171 -0
- package/src/task/task.api.ts +369 -0
- package/src/task/task.interface.ts +12 -2
- package/src/trigger/trigger.api.ts +166 -0
- package/src/trigger/trigger.interface.ts +50 -0
- package/src/trigger/trigger.utils.ts +56 -0
- package/dist/task/task-node-manager.d.ts +0 -1
- package/dist/task/task-node-manager.d.ts.map +0 -1
- package/dist/task/task-node-manager.js +0 -2
- package/dist/task/task-node-manager.js.map +0 -1
- package/src/task/task-node-manager.ts +0 -0
package/src/node/node.api.ts
CHANGED
|
@@ -1,161 +1,139 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createTarget, getPossibleTarget, updateTargetDetails } from "../core.api";
|
|
1
|
+
import { createTarget, QueryFilter, updateTargetDetails, validateWithSchema } from "../core.api";
|
|
3
2
|
import { generateResponse } from "../core.interface";
|
|
4
|
-
import {
|
|
3
|
+
import { createApiLogger } from "../shared/log";
|
|
4
|
+
import { z } from "zod";
|
|
5
5
|
import { CategoryNode, Node, NodeDetails, NodeStatus } from "./node.interface";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
}
|
|
7
|
+
const NODE_STATUS_FIELD = "details->>status" as const;
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
const nodeIdSchema = z.string().trim().min(1);
|
|
10
|
+
const traceIdSchema = z.string().trim().min(1).optional();
|
|
11
|
+
|
|
12
|
+
// ─── Zod schemas ─────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export const postRegisterNodeSchema = z.object({
|
|
15
|
+
traceId: traceIdSchema,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export type PostRegisterNodePayload = z.infer<typeof postRegisterNodeSchema>;
|
|
19
|
+
|
|
20
|
+
export const patchNodeHeartBeatSchema = z.object({
|
|
21
|
+
nodeId: nodeIdSchema,
|
|
22
|
+
traceId: traceIdSchema,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export type PatchNodeHeartBeatPayload = z.infer<typeof patchNodeHeartBeatSchema>;
|
|
26
|
+
|
|
27
|
+
export const patchStopNodeSchema = z.object({
|
|
28
|
+
nodeId: nodeIdSchema,
|
|
29
|
+
traceId: traceIdSchema,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export type PatchStopNodePayload = z.infer<typeof patchStopNodeSchema>;
|
|
22
33
|
|
|
34
|
+
export const patchChangeNodeStatusSchema = z.object({
|
|
35
|
+
nodeId: nodeIdSchema,
|
|
36
|
+
status: z.nativeEnum(NodeStatus),
|
|
37
|
+
/** When set, UPDATE only if current status matches (optimistic lock). */
|
|
38
|
+
fromStatus: z.nativeEnum(NodeStatus).optional(),
|
|
39
|
+
traceId: traceIdSchema,
|
|
40
|
+
});
|
|
23
41
|
|
|
42
|
+
export type PatchChangeNodeStatusPayload = z.infer<typeof patchChangeNodeStatusSchema>;
|
|
24
43
|
|
|
25
|
-
|
|
44
|
+
// ─── Optimistic lock helpers ─────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
function lockOnNodeStatus(status: NodeStatus): QueryFilter[] {
|
|
47
|
+
return [{ field: NODE_STATUS_FIELD, operator: "eq", value: status }];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── postRegisterNode ────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export const postRegisterNode = validateWithSchema(
|
|
53
|
+
postRegisterNodeSchema,
|
|
54
|
+
"postRegisterNodeSchema"
|
|
55
|
+
)(async (payload) => {
|
|
26
56
|
return createTarget<Node, PostRegisterNodePayload>({
|
|
27
|
-
payload
|
|
28
|
-
|
|
29
|
-
createFn: (validPayload) => {
|
|
57
|
+
payload,
|
|
58
|
+
createFn: () => {
|
|
30
59
|
const details: NodeDetails = {
|
|
31
60
|
manifestVersion: 0,
|
|
32
61
|
status: NodeStatus.READY,
|
|
33
62
|
lastHeartBeat: Date.now(),
|
|
34
63
|
};
|
|
35
64
|
return {
|
|
36
|
-
name:
|
|
65
|
+
name: "",
|
|
37
66
|
category: CategoryNode.NODE,
|
|
38
|
-
value:
|
|
67
|
+
value: "",
|
|
39
68
|
tagList: [],
|
|
40
69
|
details,
|
|
41
70
|
};
|
|
42
71
|
},
|
|
43
72
|
});
|
|
44
|
-
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ─── patchNodeHeartBeat ──────────────────────────────────────────────────────
|
|
45
76
|
|
|
46
|
-
export const patchNodeHeartBeat =
|
|
77
|
+
export const patchNodeHeartBeat = validateWithSchema(
|
|
78
|
+
patchNodeHeartBeatSchema,
|
|
79
|
+
"patchNodeHeartBeatSchema"
|
|
80
|
+
)(async ({ nodeId }) => {
|
|
47
81
|
const lastHeartBeat = Date.now();
|
|
48
82
|
await updateTargetDetails<Node, NodeDetails>({
|
|
49
83
|
id: nodeId,
|
|
50
|
-
updateFn: (details) => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
};
|
|
55
|
-
},
|
|
84
|
+
updateFn: (details) => ({
|
|
85
|
+
...details,
|
|
86
|
+
lastHeartBeat,
|
|
87
|
+
}),
|
|
56
88
|
});
|
|
57
89
|
return generateResponse.success(lastHeartBeat);
|
|
58
|
-
};
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ─── patchStopNode ───────────────────────────────────────────────────────────
|
|
59
93
|
|
|
60
|
-
export const patchStopNode =
|
|
94
|
+
export const patchStopNode = validateWithSchema(
|
|
95
|
+
patchStopNodeSchema,
|
|
96
|
+
"patchStopNodeSchema"
|
|
97
|
+
)(async ({ nodeId }) => {
|
|
61
98
|
const lastHeartBeat = Date.now();
|
|
62
99
|
// TODO 等待任務全部完成才下綫 應該先使用NodeStatus.DRAINING
|
|
63
100
|
await updateTargetDetails<Node, NodeDetails>({
|
|
64
101
|
id: nodeId,
|
|
65
|
-
updateFn: (details) => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
};
|
|
71
|
-
},
|
|
102
|
+
updateFn: (details) => ({
|
|
103
|
+
...details,
|
|
104
|
+
lastHeartBeat,
|
|
105
|
+
status: NodeStatus.LOST,
|
|
106
|
+
}),
|
|
72
107
|
});
|
|
73
108
|
return generateResponse.success(lastHeartBeat);
|
|
74
|
-
};
|
|
109
|
+
});
|
|
75
110
|
|
|
76
|
-
|
|
77
|
-
nodeId: string;
|
|
78
|
-
availableTaskList: string[];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Atomically claim one TODO task for this node.
|
|
83
|
-
*
|
|
84
|
-
* Flow: find a candidate (TODO + value in availableTaskList) → UPDATE with optimistic lock
|
|
85
|
-
* (status must still be TODO) → return the updated Task row for the executor to run.
|
|
86
|
-
*
|
|
87
|
-
* Returns `data: null` when no TODO task matches. On optimistic lock conflict,
|
|
88
|
-
* `updateTargetDetails` throws — callers may catch `isOptimisticLockError` to retry.
|
|
89
|
-
*/
|
|
90
|
-
export const patchClaimTask = async ({
|
|
91
|
-
nodeId,
|
|
92
|
-
availableTaskList,
|
|
93
|
-
}: PatchClaimTaskPayload) => {
|
|
94
|
-
if (availableTaskList.length === 0) {
|
|
95
|
-
console.log("[patchClaimTask] skipped: availableTaskList is empty", { nodeId });
|
|
96
|
-
return generateResponse.success(null);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Step 1: Pick one matching TODO task (not locked; another node may claim the same row next).
|
|
100
|
-
const { data: possibleTask } = await getPossibleTarget({
|
|
101
|
-
filterList: [
|
|
102
|
-
{
|
|
103
|
-
field: "category",
|
|
104
|
-
operator: "eq",
|
|
105
|
-
value: CategoryTask.TASK,
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
field: "details->>status",
|
|
109
|
-
operator: "eq",
|
|
110
|
-
value: TaskStatus.TODO,
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
field: "value",
|
|
114
|
-
operator: "in",
|
|
115
|
-
value: availableTaskList,
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
});
|
|
111
|
+
// ─── patchChangeNodeStatus ───────────────────────────────────────────────────
|
|
119
112
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return generateResponse.success(null);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const todoTask = possibleTask as Task;
|
|
129
|
-
console.log("[patchClaimTask] attempting claim", {
|
|
130
|
-
nodeId,
|
|
131
|
-
taskId: todoTask.id,
|
|
132
|
-
taskValue: todoTask.value,
|
|
133
|
-
});
|
|
113
|
+
export const patchChangeNodeStatus = validateWithSchema(
|
|
114
|
+
patchChangeNodeStatusSchema,
|
|
115
|
+
"patchChangeNodeStatusSchema"
|
|
116
|
+
)(async ({ nodeId, status, fromStatus, traceId }) => {
|
|
117
|
+
const logger = createApiLogger("patchChangeNodeStatus", { traceId, labels: { nodeId } });
|
|
134
118
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
optimisticLockFilterList: [
|
|
139
|
-
{
|
|
140
|
-
field: "details->>status",
|
|
141
|
-
operator: "eq",
|
|
142
|
-
value: TaskStatus.TODO,
|
|
143
|
-
},
|
|
144
|
-
],
|
|
119
|
+
const data = await updateTargetDetails<Node, NodeDetails>({
|
|
120
|
+
id: nodeId,
|
|
121
|
+
optimisticLockFilterList: fromStatus != null ? lockOnNodeStatus(fromStatus) : [],
|
|
145
122
|
updateFn: (details) => ({
|
|
146
123
|
...details,
|
|
147
|
-
status
|
|
148
|
-
|
|
124
|
+
status,
|
|
125
|
+
lastHeartBeat: Date.now(),
|
|
149
126
|
}),
|
|
150
127
|
});
|
|
151
128
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
129
|
+
logger.info("节点状态更新成功", {
|
|
130
|
+
topic: "node",
|
|
131
|
+
data: {
|
|
132
|
+
nodeId,
|
|
133
|
+
status,
|
|
134
|
+
fromStatus,
|
|
135
|
+
},
|
|
157
136
|
});
|
|
158
137
|
|
|
159
|
-
// Return full updated row so executor can read repo/params without another fetch.
|
|
160
138
|
return generateResponse.success(data);
|
|
161
|
-
};
|
|
139
|
+
});
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Target } from "../core.interface";
|
|
2
2
|
|
|
3
3
|
export enum NodeStatus {
|
|
4
|
+
/** Registered, not yet in worker loop. */
|
|
4
5
|
READY = "READY",
|
|
6
|
+
/** Worker main loop is running (accepts tasks until shutdown). */
|
|
5
7
|
BUSY = "BUSY",
|
|
6
8
|
/** Heartbeat lost or voluntarily offline; considered unavailable by the scheduler. */
|
|
7
9
|
LOST = "LOST",
|
|
@@ -30,3 +32,9 @@ export interface NodeDetails {
|
|
|
30
32
|
lastHeartBeat: number;
|
|
31
33
|
};
|
|
32
34
|
|
|
35
|
+
/** 主循环单轮运行时上下文 — 由 start() 创建并向下传递 */
|
|
36
|
+
export interface NodeLoopContext {
|
|
37
|
+
loopTraceId: string;
|
|
38
|
+
nodeId: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const formatTimestamp = (timestamp: number): string => {
|
|
2
|
+
return new Intl.DateTimeFormat("zh-CN", {
|
|
3
|
+
year: "numeric",
|
|
4
|
+
month: "2-digit",
|
|
5
|
+
day: "2-digit",
|
|
6
|
+
hour: "2-digit",
|
|
7
|
+
minute: "2-digit",
|
|
8
|
+
second: "2-digit",
|
|
9
|
+
hour12: false,
|
|
10
|
+
fractionalSecondDigits: 3,
|
|
11
|
+
}).format(timestamp);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const formatRelativeTime = (timestamp: number): string => {
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
const diff = now - timestamp;
|
|
17
|
+
|
|
18
|
+
if (diff < 1000) return "刚刚";
|
|
19
|
+
if (diff < 60000) return `${Math.floor(diff / 1000)}秒前`;
|
|
20
|
+
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`;
|
|
21
|
+
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`;
|
|
22
|
+
|
|
23
|
+
return formatTimestamp(timestamp);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const getRandomInterval = (): number => {
|
|
27
|
+
const min = 15 * 1000;
|
|
28
|
+
const max = 1 * 60 * 1000;
|
|
29
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const formatHeartbeat = (timestamp: number): string => {
|
|
33
|
+
const absolute = formatTimestamp(timestamp);
|
|
34
|
+
const relative = formatRelativeTime(timestamp);
|
|
35
|
+
return `${absolute} (${relative})`;
|
|
36
|
+
};
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import {
|
|
2
|
+
logManager,
|
|
3
|
+
withModule,
|
|
4
|
+
type LoggerWithScope,
|
|
5
|
+
} from "../shared/log";
|
|
6
|
+
import { scopeForNodeLoop } from "./node-log-scope";
|
|
7
|
+
import { getErrorMessage, toError } from "../shared/utils/error.utils";
|
|
8
|
+
import { TaskManager, type TaskRunResult } from "../task/task-manager";
|
|
9
|
+
import { patchChangeTaskStatus, patchClaimTask } from "../task/task.api";
|
|
10
|
+
import { TaskStatusAction } from "../task/task.interface";
|
|
11
|
+
import { NodeLoopContext } from "./node.interface";
|
|
12
|
+
import { BaseNodeRuntime } from "./node-runtime.base";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Task worker node: commands → heartbeat → claim & execute tasks.
|
|
16
|
+
* Built on {@link BaseNodeRuntime}.
|
|
17
|
+
*/
|
|
18
|
+
class TaskNode extends BaseNodeRuntime {
|
|
19
|
+
private _availableTaskList: string[] = [];
|
|
20
|
+
private _includeRemote = true;
|
|
21
|
+
|
|
22
|
+
get availableTaskList(): readonly string[] {
|
|
23
|
+
return [...this._availableTaskList];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
super("taskNode");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected async onBeforeRegisterNode(logger: LoggerWithScope): Promise<void> {
|
|
31
|
+
logger.info("開始註冊任務", { topic: "node" });
|
|
32
|
+
const { availableTaskList, includeRemote } = await TaskManager.registerTasks({ logger });
|
|
33
|
+
this._availableTaskList = availableTaskList;
|
|
34
|
+
this._includeRemote = includeRemote;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
protected async runLoopSteps(ctx: NodeLoopContext, heartbeatOk: boolean): Promise<void> {
|
|
38
|
+
const loopScope = scopeForNodeLoop({
|
|
39
|
+
module: `${this.runtimeModule}-loop-iteration`,
|
|
40
|
+
traceId: ctx.loopTraceId,
|
|
41
|
+
nodeId: ctx.nodeId,
|
|
42
|
+
});
|
|
43
|
+
if (!heartbeatOk) {
|
|
44
|
+
const { logger } = logManager.withScope(loopScope);
|
|
45
|
+
logger.warn("心跳失敗,本輪跳過任務認領", { topic: "node" });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
await this.runAsWorker(ctx);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async runAsWorker(ctx: NodeLoopContext) {
|
|
52
|
+
const { loopTraceId, nodeId } = ctx;
|
|
53
|
+
const loopScope = scopeForNodeLoop({ module: "runAsWorker", traceId: loopTraceId, nodeId });
|
|
54
|
+
const { logger } = logManager.withScope(loopScope);
|
|
55
|
+
|
|
56
|
+
const availableTaskList = this.availableTaskList;
|
|
57
|
+
if (availableTaskList.length === 0) {
|
|
58
|
+
logger.warn("未配置可認領任務類型,跳過本輪", { topic: "task" });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
logger.info("向 Supabase 認領任務", {
|
|
63
|
+
topic: "task",
|
|
64
|
+
data: { availableTaskList },
|
|
65
|
+
});
|
|
66
|
+
const { data: task, error: claimError } = await patchClaimTask({
|
|
67
|
+
nodeId,
|
|
68
|
+
availableTaskList: [...availableTaskList],
|
|
69
|
+
traceId: loopTraceId,
|
|
70
|
+
});
|
|
71
|
+
if (claimError) {
|
|
72
|
+
logger.error("認領任務 API 失敗", {
|
|
73
|
+
topic: "task",
|
|
74
|
+
data: {
|
|
75
|
+
availableTaskList,
|
|
76
|
+
error: claimError.message,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (task == null) {
|
|
82
|
+
logger.info("本輪無匹配的 TODO 任務", {
|
|
83
|
+
topic: "task",
|
|
84
|
+
data: { availableTaskList },
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
logger.info("認領任務成功,準備執行", {
|
|
90
|
+
topic: "task",
|
|
91
|
+
data: {
|
|
92
|
+
taskId: task.id,
|
|
93
|
+
taskName: task.name,
|
|
94
|
+
taskTypeKey: task.value,
|
|
95
|
+
taskStatus: task.details.status,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const taskTraceId = task.details.traceId ?? loopTraceId;
|
|
100
|
+
const taskScope = scopeForNodeLoop({
|
|
101
|
+
module: "prepareTask",
|
|
102
|
+
traceId: taskTraceId,
|
|
103
|
+
nodeId,
|
|
104
|
+
traceParentId: loopTraceId,
|
|
105
|
+
});
|
|
106
|
+
const { logger: prepareLogger } = logManager.withScope(taskScope);
|
|
107
|
+
const prepareResult = await TaskManager.prepareTask({
|
|
108
|
+
logger: prepareLogger,
|
|
109
|
+
task,
|
|
110
|
+
includeRemote: this._includeRemote,
|
|
111
|
+
});
|
|
112
|
+
const { isSuccess: isPrepareSuccess, taskFn, code, message, reason, step } = prepareResult;
|
|
113
|
+
if (!isPrepareSuccess || taskFn == null) {
|
|
114
|
+
prepareLogger.critical("任務準備失敗,無法繼續執行,中止任務並 RESET 為 OPEN", {
|
|
115
|
+
topic: "task",
|
|
116
|
+
data: {
|
|
117
|
+
taskId: task.id,
|
|
118
|
+
taskTypeKey: task.value,
|
|
119
|
+
code,
|
|
120
|
+
message,
|
|
121
|
+
reason,
|
|
122
|
+
step,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
await this.abortTaskRun({
|
|
126
|
+
logger: prepareLogger,
|
|
127
|
+
taskId: task.id,
|
|
128
|
+
nodeId,
|
|
129
|
+
traceId: taskTraceId,
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const { logger: executeLogger } = logManager.withScope(withModule(taskScope, "executeTask"));
|
|
135
|
+
const onExecuteAbort = async (error?: Error) => {
|
|
136
|
+
logger.critical("任務執行遇到未知錯誤,中止任務並 RESET 為 OPEN", {
|
|
137
|
+
topic: "task",
|
|
138
|
+
data: {
|
|
139
|
+
taskId: task.id,
|
|
140
|
+
taskName: task.name,
|
|
141
|
+
taskTypeKey: task.value,
|
|
142
|
+
error: getErrorMessage(error),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
await this.abortTaskRun({
|
|
146
|
+
logger: executeLogger,
|
|
147
|
+
taskId: task.id,
|
|
148
|
+
nodeId,
|
|
149
|
+
traceId: taskTraceId,
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
executeLogger.info("開始執行業務邏輯", {
|
|
154
|
+
topic: "task",
|
|
155
|
+
data: { taskId: task.id, displayName: taskFn.displayName },
|
|
156
|
+
});
|
|
157
|
+
const startTime = Date.now();
|
|
158
|
+
let result: TaskRunResult | null = null;
|
|
159
|
+
try {
|
|
160
|
+
result = await taskFn();
|
|
161
|
+
} catch (error) {
|
|
162
|
+
await onExecuteAbort(toError(error));
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (result == null) {
|
|
166
|
+
await onExecuteAbort(new Error("任務執行結果為空"));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
executeLogger.info("任務執行完成", {
|
|
171
|
+
topic: "task",
|
|
172
|
+
data: { taskId: task.id, displayName: taskFn.displayName },
|
|
173
|
+
});
|
|
174
|
+
const { isSuccess: isTaskSuccess, cost, extra } = result ?? { isSuccess: false, cost: 0, extra: null };
|
|
175
|
+
if (isTaskSuccess) {
|
|
176
|
+
await this.finalizeTaskRun("success", {
|
|
177
|
+
logger: executeLogger,
|
|
178
|
+
taskId: task.id,
|
|
179
|
+
taskTypeKey: task.value,
|
|
180
|
+
nodeId,
|
|
181
|
+
cost,
|
|
182
|
+
extra,
|
|
183
|
+
traceId: taskTraceId,
|
|
184
|
+
});
|
|
185
|
+
} else {
|
|
186
|
+
await this.finalizeTaskRun("failure", {
|
|
187
|
+
logger: executeLogger,
|
|
188
|
+
taskId: task.id,
|
|
189
|
+
taskTypeKey: task.value,
|
|
190
|
+
nodeId,
|
|
191
|
+
cost,
|
|
192
|
+
extra,
|
|
193
|
+
traceId: taskTraceId,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const duration = Date.now() - startTime;
|
|
197
|
+
executeLogger.info("業務邏輯執行完成", {
|
|
198
|
+
topic: "task",
|
|
199
|
+
data: { taskId: task.id, durationMs: duration },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private async abortTaskRun(params: {
|
|
204
|
+
logger: LoggerWithScope;
|
|
205
|
+
taskId: string;
|
|
206
|
+
nodeId: string;
|
|
207
|
+
traceId: string;
|
|
208
|
+
extra?: TaskRunResult["extra"];
|
|
209
|
+
}): Promise<boolean> {
|
|
210
|
+
const { logger, taskId, nodeId, traceId, extra } = params;
|
|
211
|
+
const logData = { taskId, nodeId };
|
|
212
|
+
const extraValue = typeof extra === "string" ? extra : undefined;
|
|
213
|
+
|
|
214
|
+
const { error } = await patchChangeTaskStatus({
|
|
215
|
+
id: taskId,
|
|
216
|
+
action: TaskStatusAction.RESET,
|
|
217
|
+
nodeId,
|
|
218
|
+
extra: extraValue,
|
|
219
|
+
traceId,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
if (error) {
|
|
223
|
+
logger.error(error.message, { topic: "task", data: logData });
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
logger.info("任務已中止並 RESET 為 OPEN", { topic: "task", data: logData });
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private async finalizeTaskRun(
|
|
231
|
+
outcome: "success" | "failure",
|
|
232
|
+
params: {
|
|
233
|
+
logger: LoggerWithScope;
|
|
234
|
+
taskId: string;
|
|
235
|
+
taskTypeKey: string;
|
|
236
|
+
nodeId: string;
|
|
237
|
+
cost: TaskRunResult["cost"];
|
|
238
|
+
extra: TaskRunResult["extr\a"];
|
|
239
|
+
traceId: string;
|
|
240
|
+
}
|
|
241
|
+
): Promise<void> {
|
|
242
|
+
const { logger, taskId, nodeId, taskTypeKey, cost, extra, traceId } = params;
|
|
243
|
+
const outcomePrefix = outcome === "success" ? "任務成功" : "任務失敗";
|
|
244
|
+
const logData = {
|
|
245
|
+
taskTypeKey,
|
|
246
|
+
taskId,
|
|
247
|
+
nodeId,
|
|
248
|
+
...(extra != null && extra !== "" ? { extra } : {}),
|
|
249
|
+
};
|
|
250
|
+
const extraValue = typeof extra === "string" ? extra : undefined;
|
|
251
|
+
|
|
252
|
+
const { error: changeTaskStatusError } =
|
|
253
|
+
outcome === "success"
|
|
254
|
+
? await patchChangeTaskStatus({
|
|
255
|
+
id: taskId,
|
|
256
|
+
action: TaskStatusAction.FINISH,
|
|
257
|
+
nodeId,
|
|
258
|
+
cost,
|
|
259
|
+
extra: extraValue,
|
|
260
|
+
traceId,
|
|
261
|
+
})
|
|
262
|
+
: await patchChangeTaskStatus({
|
|
263
|
+
id: taskId,
|
|
264
|
+
action: TaskStatusAction.RESET,
|
|
265
|
+
nodeId,
|
|
266
|
+
extra: extraValue,
|
|
267
|
+
traceId,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (changeTaskStatusError) {
|
|
271
|
+
const upperMessage = `${outcomePrefix}但是更新 TaskStatus 失敗,產生孤兒 Task,請儘快排查問題`;
|
|
272
|
+
logger.critical(upperMessage, { topic: "task", data: logData });
|
|
273
|
+
throw new Error(upperMessage);
|
|
274
|
+
}
|
|
275
|
+
logger.info("更新 TaskStatus 成功", { topic: "task", data: logData });
|
|
276
|
+
if (outcome === "success") {
|
|
277
|
+
logger.success("任務執行結束,業務邏輯成功", { topic: "task", data: logData });
|
|
278
|
+
} else {
|
|
279
|
+
logger.warn("任務執行結束,業務邏輯失敗", { topic: "task", data: logData });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export { TaskNode };
|