target-supabase-sdk 0.1.8 → 0.1.9
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 +8 -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/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 +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/node/node-manager.d.ts +48 -0
- package/dist/node/node-manager.d.ts.map +1 -0
- package/dist/node/node-manager.js +619 -0
- package/dist/node/node-manager.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/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 +31 -0
- package/dist/repo/repo-manager.d.ts.map +1 -0
- package/dist/repo/repo-manager.js +156 -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 +17 -0
- package/dist/shared/log/create-api-logger.d.ts.map +1 -0
- package/dist/shared/log/create-api-logger.js +15 -0
- package/dist/shared/log/create-api-logger.js.map +1 -0
- package/dist/shared/log/log-manager.d.ts +114 -0
- package/dist/shared/log/log-manager.d.ts.map +1 -0
- package/dist/shared/log/log-manager.js +218 -0
- package/dist/shared/log/log-manager.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/local-task-registry.d.ts +27 -0
- package/dist/task/local-task-registry.d.ts.map +1 -0
- package/dist/task/local-task-registry.js +153 -0
- package/dist/task/local-task-registry.js.map +1 -0
- package/dist/task/task-manager.d.ts +52 -0
- package/dist/task/task-manager.d.ts.map +1 -0
- package/dist/task/task-manager.js +217 -0
- package/dist/task/task-manager.js.map +1 -0
- package/dist/task/task-repo-context.d.ts +95 -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.api.d.ts +240 -0
- package/dist/task/task.api.d.ts.map +1 -1
- package/dist/task/task.api.js +350 -1
- package/dist/task/task.api.js.map +1 -1
- package/dist/task/task.interface.d.ts +9 -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/package.json +10 -3
- package/src/auth/auth.api.ts +3 -1
- 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 +18 -3
- package/src/node/node-manager.ts +673 -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/parcel/parcel-manager.ts +1 -1
- package/src/repo/repo-context.utils.ts +111 -0
- package/src/repo/repo-manager.ts +217 -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 +25 -0
- package/src/shared/log/log-manager.ts +340 -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/local-task-registry.ts +195 -0
- package/src/task/task-manager.ts +312 -0
- package/src/task/task-repo-context.ts +103 -0
- package/src/task/task.api.ts +429 -0
- package/src/task/task.interface.ts +10 -2
- 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/log-manager";
|
|
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, 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
|
+
context: {
|
|
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,111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TASK_REPO_SCRIPT_CATEGORY,
|
|
3
|
+
type TaskRepoContext,
|
|
4
|
+
type TaskRepoScriptDetails,
|
|
5
|
+
type TaskRepoScriptRecord,
|
|
6
|
+
} from "../task/task-repo-context";
|
|
7
|
+
|
|
8
|
+
export { TASK_REPO_SCRIPT_CATEGORY };
|
|
9
|
+
|
|
10
|
+
export interface LoadedRepoContext {
|
|
11
|
+
context: TaskRepoContext;
|
|
12
|
+
/** Cache key segment — typically repo hash or script hash */
|
|
13
|
+
contentHash: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isTaskRepoContext(value: unknown): value is TaskRepoContext {
|
|
17
|
+
if (value == null || typeof value !== "object") {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
const record = value as TaskRepoContext;
|
|
21
|
+
return typeof record.taskParamsValidator === "function" && typeof record.taskFn === "function";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Normalize a dynamically imported module into {@link TaskRepoContext}.
|
|
26
|
+
* Supported shapes:
|
|
27
|
+
* - `export default { taskParamsValidator, taskFn }`
|
|
28
|
+
* - `export { taskParamsValidator, taskFn }`
|
|
29
|
+
* - `export default taskFn` (validator becomes `() => true`)
|
|
30
|
+
*/
|
|
31
|
+
export function normalizeRepoContextModule(
|
|
32
|
+
moduleExports: unknown,
|
|
33
|
+
taskTypeKey: string,
|
|
34
|
+
exportName?: string
|
|
35
|
+
): TaskRepoContext | null {
|
|
36
|
+
if (moduleExports == null) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const record = moduleExports as Record<string, unknown>;
|
|
41
|
+
const namedExport =
|
|
42
|
+
exportName != null && exportName !== "" ? record[exportName] : undefined;
|
|
43
|
+
const defaultExport = record.default;
|
|
44
|
+
const candidate = namedExport ?? defaultExport ?? moduleExports;
|
|
45
|
+
|
|
46
|
+
let taskParamsValidator: unknown;
|
|
47
|
+
let taskFn: unknown;
|
|
48
|
+
|
|
49
|
+
if (typeof candidate === "function") {
|
|
50
|
+
taskFn = candidate;
|
|
51
|
+
taskParamsValidator = record.taskParamsValidator;
|
|
52
|
+
} else if (typeof candidate === "object" && candidate != null) {
|
|
53
|
+
const ctx = candidate as Record<string, unknown>;
|
|
54
|
+
taskParamsValidator = ctx.taskParamsValidator;
|
|
55
|
+
taskFn = ctx.taskFn;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
taskParamsValidator ??= record.taskParamsValidator;
|
|
59
|
+
taskFn ??= record.taskFn;
|
|
60
|
+
|
|
61
|
+
if (typeof taskFn !== "function") {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof taskParamsValidator !== "function") {
|
|
66
|
+
taskParamsValidator = () => true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const fn = taskFn as TaskRepoContext["taskFn"];
|
|
70
|
+
if (fn.displayName == null || fn.displayName === "") {
|
|
71
|
+
fn.displayName = taskTypeKey;
|
|
72
|
+
}
|
|
73
|
+
if (fn.taskTypeKey == null || fn.taskTypeKey === "") {
|
|
74
|
+
fn.taskTypeKey = taskTypeKey;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
taskParamsValidator: taskParamsValidator as TaskRepoContext["taskParamsValidator"],
|
|
79
|
+
taskFn: fn,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function pickEntryScript(scripts: TaskRepoScriptRecord[]): TaskRepoScriptRecord | null {
|
|
84
|
+
if (scripts.length === 0) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const entry = scripts.find((script) => script.details.isEntry === true);
|
|
88
|
+
return entry ?? scripts[0];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function getScriptLoadKey(script: TaskRepoScriptRecord): string {
|
|
92
|
+
const { source, modulePath, hash } = script.details;
|
|
93
|
+
if (hash != null && hash !== "") {
|
|
94
|
+
return hash;
|
|
95
|
+
}
|
|
96
|
+
if (source != null && source !== "") {
|
|
97
|
+
return `source:${source.length}:${source.slice(0, 64)}`;
|
|
98
|
+
}
|
|
99
|
+
if (modulePath != null && modulePath !== "") {
|
|
100
|
+
return `path:${modulePath}`;
|
|
101
|
+
}
|
|
102
|
+
return script.id;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function assertScriptLoadable(
|
|
106
|
+
details: TaskRepoScriptDetails
|
|
107
|
+
): details is TaskRepoScriptDetails & ({ source: string } | { modulePath: string }) {
|
|
108
|
+
const hasSource = details.source != null && details.source.trim() !== "";
|
|
109
|
+
const hasModulePath = details.modulePath != null && details.modulePath.trim() !== "";
|
|
110
|
+
return hasSource || hasModulePath;
|
|
111
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { getPossibleTarget } from "../core.api";
|
|
2
|
+
import { SupabaseInitializer } from "../supabase";
|
|
3
|
+
import { LoggerWithContext } from "../shared/log/log-manager";
|
|
4
|
+
import type { TaskRepoContext, TaskRepoScriptRecord } from "../task/task-repo-context";
|
|
5
|
+
import { TASK_REPO_SCRIPT_CATEGORY } from "../task/task-repo-context";
|
|
6
|
+
import {
|
|
7
|
+
assertScriptLoadable,
|
|
8
|
+
isTaskRepoContext,
|
|
9
|
+
pickEntryScript,
|
|
10
|
+
} from "./repo-context.utils";
|
|
11
|
+
import { CategoryRepo, Repo } from "./repo.interface";
|
|
12
|
+
import {
|
|
13
|
+
clearRepoScriptModuleCache,
|
|
14
|
+
loadRepoContextFromScript,
|
|
15
|
+
loadRepoContextFromUrl,
|
|
16
|
+
} from "./repo.script-loader";
|
|
17
|
+
|
|
18
|
+
const supabase = SupabaseInitializer.getInstance();
|
|
19
|
+
|
|
20
|
+
type LocalRepoEntry =
|
|
21
|
+
| { kind: "context"; context: TaskRepoContext }
|
|
22
|
+
| { kind: "modulePath"; modulePath: string; exportName?: string };
|
|
23
|
+
|
|
24
|
+
const localRepoRegistry = new Map<string, LocalRepoEntry>();
|
|
25
|
+
const remoteContextCache = new Map<string, TaskRepoContext>();
|
|
26
|
+
|
|
27
|
+
export interface GetRepoContextParams {
|
|
28
|
+
logger: LoggerWithContext;
|
|
29
|
+
/** Task type key (`task.value`) — matches {@link Repo.value} for remote lookup; local registry key */
|
|
30
|
+
taskTypeKey: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface GetRepoContextResult<T extends TaskRepoContext = TaskRepoContext> {
|
|
34
|
+
context: T | null;
|
|
35
|
+
/** Present when `context` is null — import / DB / format failure at repo layer */
|
|
36
|
+
error?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildRemoteCacheKey(taskTypeKey: string, contentHash: string): string {
|
|
40
|
+
return `${taskTypeKey}@${contentHash}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function fetchRemoteRepo(taskTypeKey: string): Promise<Repo | null> {
|
|
44
|
+
const { data } = await getPossibleTarget({
|
|
45
|
+
filterList: [
|
|
46
|
+
{ field: "category", operator: "eq", value: CategoryRepo.REPO },
|
|
47
|
+
{ field: "value", operator: "eq", value: taskTypeKey },
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
return (data as Repo | null) ?? null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function fetchRemoteScripts(taskTypeKey: string): Promise<TaskRepoScriptRecord[]> {
|
|
54
|
+
const { data, error } = await supabase.client
|
|
55
|
+
.from("target")
|
|
56
|
+
.select("id, value, details")
|
|
57
|
+
.eq("category", TASK_REPO_SCRIPT_CATEGORY)
|
|
58
|
+
.eq("value", taskTypeKey);
|
|
59
|
+
|
|
60
|
+
if (error) {
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (data ?? []) as TaskRepoScriptRecord[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function loadLocalEntry(
|
|
68
|
+
logger: LoggerWithContext,
|
|
69
|
+
entry: LocalRepoEntry,
|
|
70
|
+
taskTypeKey: string
|
|
71
|
+
): Promise<TaskRepoContext | null> {
|
|
72
|
+
if (entry.kind === "context") {
|
|
73
|
+
return entry.context;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const loaded = await loadRepoContextFromScript({
|
|
77
|
+
logger,
|
|
78
|
+
script: {
|
|
79
|
+
id: `local:${taskTypeKey}`,
|
|
80
|
+
value: taskTypeKey,
|
|
81
|
+
details: {
|
|
82
|
+
manifestVersion: 0,
|
|
83
|
+
modulePath: entry.modulePath,
|
|
84
|
+
exportName: entry.exportName,
|
|
85
|
+
isEntry: true,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
taskTypeKey,
|
|
89
|
+
});
|
|
90
|
+
return loaded?.context ?? null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function loadRemoteRepoContext(
|
|
94
|
+
logger: LoggerWithContext,
|
|
95
|
+
taskTypeKey: string
|
|
96
|
+
): Promise<GetRepoContextResult> {
|
|
97
|
+
logger.info("從 Supabase 加載 Repo", { topic: "repo", context: { taskTypeKey } });
|
|
98
|
+
|
|
99
|
+
const remoteRepo = await fetchRemoteRepo(taskTypeKey);
|
|
100
|
+
if (remoteRepo == null) {
|
|
101
|
+
const error = `Supabase 未找到 Repo (value=${taskTypeKey})`;
|
|
102
|
+
logger.warn(error, { topic: "repo", context: { taskTypeKey } });
|
|
103
|
+
return { context: null, error };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const repoUrl = remoteRepo.details?.url?.trim() ?? "";
|
|
107
|
+
const repoHash = remoteRepo.details?.hash?.trim() ?? repoUrl;
|
|
108
|
+
const cacheKey = buildRemoteCacheKey(taskTypeKey, repoHash);
|
|
109
|
+
const cached = remoteContextCache.get(cacheKey);
|
|
110
|
+
if (cached != null) {
|
|
111
|
+
logger.info("命中遠程 Repo 上下文緩存", { topic: "repo", context: { cacheKey } });
|
|
112
|
+
return { context: cached };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const scripts = await fetchRemoteScripts(taskTypeKey);
|
|
116
|
+
const entryScript = pickEntryScript(scripts);
|
|
117
|
+
|
|
118
|
+
let loaded =
|
|
119
|
+
entryScript != null && assertScriptLoadable(entryScript.details)
|
|
120
|
+
? await loadRepoContextFromScript({ logger, script: entryScript, taskTypeKey })
|
|
121
|
+
: null;
|
|
122
|
+
|
|
123
|
+
if (loaded == null && repoUrl !== "") {
|
|
124
|
+
logger.info("未找到 script 行,嘗試從 Repo.details.url 加載", {
|
|
125
|
+
topic: "repo",
|
|
126
|
+
context: { taskTypeKey, url: repoUrl },
|
|
127
|
+
});
|
|
128
|
+
loaded = await loadRepoContextFromUrl({ logger, url: repoUrl, taskTypeKey });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (loaded == null) {
|
|
132
|
+
const error = `無法從 Repo 加載 TaskRepoContext (hasUrl=${repoUrl !== ""}, scriptCount=${scripts.length})`;
|
|
133
|
+
logger.warn(error, {
|
|
134
|
+
topic: "repo",
|
|
135
|
+
context: { taskTypeKey, hasUrl: repoUrl !== "", scriptCount: scripts.length },
|
|
136
|
+
});
|
|
137
|
+
return { context: null, error };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
remoteContextCache.set(cacheKey, loaded.context);
|
|
141
|
+
return { context: loaded.context };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const getRepoContext = async <RepoContext extends TaskRepoContext>({
|
|
145
|
+
logger,
|
|
146
|
+
taskTypeKey,
|
|
147
|
+
}: GetRepoContextParams): Promise<GetRepoContextResult<RepoContext>> => {
|
|
148
|
+
const localEntry = localRepoRegistry.get(taskTypeKey);
|
|
149
|
+
if (localEntry != null) {
|
|
150
|
+
logger.info("使用本地註冊的 Repo", { topic: "repo", context: { taskTypeKey } });
|
|
151
|
+
const context = await loadLocalEntry(logger, localEntry, taskTypeKey);
|
|
152
|
+
if (context != null && isTaskRepoContext(context)) {
|
|
153
|
+
return { context: context as RepoContext };
|
|
154
|
+
}
|
|
155
|
+
const error = "本地 Repo 模組加載失敗或未導出有效的 TaskRepoContext";
|
|
156
|
+
logger.warn(error, { topic: "repo", context: { taskTypeKey } });
|
|
157
|
+
return { context: null, error };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const result = await loadRemoteRepoContext(logger, taskTypeKey);
|
|
162
|
+
if (result.context != null && isTaskRepoContext(result.context)) {
|
|
163
|
+
return { context: result.context as RepoContext };
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
context: null,
|
|
167
|
+
error: result.error ?? "遠程 Repo 上下文無效",
|
|
168
|
+
};
|
|
169
|
+
} catch (error) {
|
|
170
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
171
|
+
logger.error("從 Supabase 加載 Repo 失敗", {
|
|
172
|
+
topic: "repo",
|
|
173
|
+
context: { taskTypeKey, error: message },
|
|
174
|
+
});
|
|
175
|
+
return { context: null, error: message };
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** Register an in-process TaskRepoContext (highest priority — no Supabase fetch). */
|
|
180
|
+
function registerLocalRepoContext(taskTypeKey: string, context: TaskRepoContext): void {
|
|
181
|
+
if (!isTaskRepoContext(context)) {
|
|
182
|
+
throw new Error("[RepoManager] Invalid TaskRepoContext");
|
|
183
|
+
}
|
|
184
|
+
localRepoRegistry.set(taskTypeKey, { kind: "context", context });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Register a local ESM module path; loaded via dynamic import when the task runs. */
|
|
188
|
+
function registerLocalModule(taskTypeKey: string, modulePath: string, exportName?: string): void {
|
|
189
|
+
if (modulePath.trim() === "") {
|
|
190
|
+
throw new Error("[RepoManager] modulePath must not be empty");
|
|
191
|
+
}
|
|
192
|
+
localRepoRegistry.set(taskTypeKey, { kind: "modulePath", modulePath, exportName });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function unregisterLocalRepo(taskTypeKey: string): void {
|
|
196
|
+
localRepoRegistry.delete(taskTypeKey);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getRegisteredLocalTaskTypeKeys(): string[] {
|
|
200
|
+
return [...localRepoRegistry.keys()];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function clearRemoteContextCache(): void {
|
|
204
|
+
remoteContextCache.clear();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const RepoManager = {
|
|
208
|
+
getRepoContext,
|
|
209
|
+
registerLocalRepoContext,
|
|
210
|
+
registerLocalModule,
|
|
211
|
+
unregisterLocalRepo,
|
|
212
|
+
getRegisteredLocalTaskTypeKeys,
|
|
213
|
+
clearRemoteContextCache,
|
|
214
|
+
clearRepoScriptModuleCache,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export { RepoManager };
|