target-supabase-sdk 0.1.9 → 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 +66 -0
- 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.js +5 -5
- package/dist/command/command.api.js.map +1 -1
- package/dist/index.d.ts +2 -48
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -41
- 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.api.d.ts +3 -3
- package/dist/node/node.api.js +3 -3
- package/dist/node/node.api.js.map +1 -1
- 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/repo/repo-manager.d.ts +14 -3
- package/dist/repo/repo-manager.d.ts.map +1 -1
- package/dist/repo/repo-manager.js +27 -17
- package/dist/repo/repo-manager.js.map +1 -1
- package/dist/repo/repo.script-loader.d.ts +3 -3
- package/dist/repo/repo.script-loader.d.ts.map +1 -1
- package/dist/repo/repo.script-loader.js +10 -10
- package/dist/repo/repo.script-loader.js.map +1 -1
- package/dist/shared/log/create-api-logger.d.ts +10 -6
- package/dist/shared/log/create-api-logger.d.ts.map +1 -1
- package/dist/shared/log/create-api-logger.js +10 -6
- package/dist/shared/log/create-api-logger.js.map +1 -1
- 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 +25 -46
- package/dist/shared/log/log-manager.d.ts.map +1 -1
- package/dist/shared/log/log-manager.js +59 -40
- package/dist/shared/log/log-manager.js.map +1 -1
- 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/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 +10 -0
- package/dist/task/local-task-registry.d.ts.map +1 -1
- package/dist/task/local-task-registry.js +83 -22
- package/dist/task/local-task-registry.js.map +1 -1
- package/dist/task/task-manager.d.ts +18 -6
- package/dist/task/task-manager.d.ts.map +1 -1
- package/dist/task/task-manager.js +46 -67
- package/dist/task/task-manager.js.map +1 -1
- 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 +2 -0
- package/dist/task/task-repo-context.d.ts.map +1 -1
- package/dist/task/task-repo-context.js.map +1 -1
- 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 +11 -53
- package/dist/task/task.api.d.ts.map +1 -1
- package/dist/task/task.api.js +13 -61
- package/dist/task/task.api.js.map +1 -1
- package/dist/task/task.interface.d.ts +2 -0
- package/dist/task/task.interface.d.ts.map +1 -1
- 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 +21 -6
- package/src/browser.ts +100 -0
- package/src/command/command.api.ts +5 -5
- package/src/index.ts +2 -79
- 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.api.ts +3 -3
- package/src/node/task-node.ts +284 -0
- package/src/node/trigger-node.ts +180 -0
- package/src/node.ts +62 -0
- package/src/repo/repo-manager.ts +49 -21
- package/src/repo/repo.script-loader.ts +13 -13
- package/src/shared/log/create-api-logger.ts +23 -12
- package/src/shared/log/index.ts +23 -0
- package/src/shared/log/log-manager.ts +85 -90
- package/src/shared/log/log-scope.ts +123 -0
- package/src/task/index.ts +48 -0
- package/src/task/local-task-registry.ts +110 -23
- package/src/task/task-manager.ts +73 -84
- package/src/task/task-post.api.ts +116 -0
- package/src/task/task-repo-context.ts +2 -0
- package/src/task/task-repo-validation.ts +171 -0
- package/src/task/task.api.ts +13 -73
- package/src/task/task.interface.ts +2 -0
- 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/node/node-manager.d.ts +0 -48
- package/dist/node/node-manager.d.ts.map +0 -1
- package/dist/node/node-manager.js +0 -619
- package/dist/node/node-manager.js.map +0 -1
- package/src/node/node-manager.ts +0 -673
|
@@ -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 };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { logManager, type LoggerWithScope } from "../shared/log";
|
|
2
|
+
import { scopeForNodeLoop } from "./node-log-scope";
|
|
3
|
+
import { postTask } from "../task/task-post.api";
|
|
4
|
+
import { TaskStatus } from "../task/task.interface";
|
|
5
|
+
import { patchTriggerFired, scanEnabledTriggers } from "../trigger/trigger.api";
|
|
6
|
+
import type { Trigger, TriggerPostTaskAction } from "../trigger/trigger.interface";
|
|
7
|
+
import { buildFireKey, isTriggerDue } from "../trigger/trigger.utils";
|
|
8
|
+
import { NodeLoopContext } from "./node.interface";
|
|
9
|
+
import { BaseNodeRuntime } from "./node-runtime.base";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Dedicated trigger scheduler node.
|
|
13
|
+
* Main loop: commands → heartbeat → evaluate due triggers → postTask.
|
|
14
|
+
*
|
|
15
|
+
* Phase 1: run a **single** TriggerNode process per deployment to avoid duplicate fires.
|
|
16
|
+
*/
|
|
17
|
+
class TriggerNode extends BaseNodeRuntime {
|
|
18
|
+
constructor() {
|
|
19
|
+
super("triggerNode");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected async onBeforeRegisterNode(logger: LoggerWithScope): Promise<void> {
|
|
23
|
+
logger.info("Trigger 節點啟動,跳過任務註冊", { topic: "trigger" });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
protected async runLoopSteps(ctx: NodeLoopContext, heartbeatOk: boolean): Promise<void> {
|
|
27
|
+
const loopScope = scopeForNodeLoop({
|
|
28
|
+
module: `${this.runtimeModule}-loop-iteration`,
|
|
29
|
+
traceId: ctx.loopTraceId,
|
|
30
|
+
nodeId: ctx.nodeId,
|
|
31
|
+
});
|
|
32
|
+
if (!heartbeatOk) {
|
|
33
|
+
const { logger } = logManager.withScope(loopScope);
|
|
34
|
+
logger.warn("心跳失敗,本輪跳過 Trigger 評估", { topic: "trigger" });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
await this.evaluateTriggers(ctx);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async evaluateTriggers(ctx: NodeLoopContext): Promise<void> {
|
|
41
|
+
const { loopTraceId, nodeId } = ctx;
|
|
42
|
+
const loopScope = scopeForNodeLoop({ module: "evaluateTriggers", traceId: loopTraceId, nodeId });
|
|
43
|
+
const { logger } = logManager.withScope(loopScope);
|
|
44
|
+
|
|
45
|
+
logger.debug("開始評估 Trigger", { topic: "trigger" });
|
|
46
|
+
|
|
47
|
+
const { data: triggerList = [], error } = await scanEnabledTriggers({ traceId: loopTraceId });
|
|
48
|
+
if (error) {
|
|
49
|
+
logger.warn("掃描 Trigger 失敗", {
|
|
50
|
+
topic: "trigger",
|
|
51
|
+
data: { error: error.message },
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (triggerList.length === 0) {
|
|
56
|
+
logger.info("無 ENABLED Trigger", { topic: "trigger" });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const now = new Date();
|
|
61
|
+
let dueCount = 0;
|
|
62
|
+
let firedCount = 0;
|
|
63
|
+
|
|
64
|
+
for (const trigger of triggerList) {
|
|
65
|
+
if (!isTriggerDue(trigger, now)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
dueCount++;
|
|
69
|
+
const fired = await this.fireTrigger(ctx, trigger, now);
|
|
70
|
+
if (fired) {
|
|
71
|
+
firedCount++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
logger.info("Trigger 評估完成", {
|
|
76
|
+
topic: "trigger",
|
|
77
|
+
data: {
|
|
78
|
+
scanned: triggerList.length,
|
|
79
|
+
due: dueCount,
|
|
80
|
+
fired: firedCount,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private async fireTrigger(ctx: NodeLoopContext, trigger: Trigger, now: Date): Promise<boolean> {
|
|
86
|
+
const { loopTraceId, nodeId } = ctx;
|
|
87
|
+
const { logger } = logManager.withScope(
|
|
88
|
+
scopeForNodeLoop({ module: "fireTrigger", traceId: loopTraceId, nodeId })
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const fireKey = buildFireKey(trigger, now);
|
|
92
|
+
const action = trigger.details.action;
|
|
93
|
+
|
|
94
|
+
if (action.kind !== "post_task") {
|
|
95
|
+
logger.warn("不支援的 Trigger action", {
|
|
96
|
+
topic: "trigger",
|
|
97
|
+
data: { triggerId: trigger.id, kind: action.kind },
|
|
98
|
+
});
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
logger.info("Trigger 到期,準備 postTask", {
|
|
103
|
+
topic: "trigger",
|
|
104
|
+
data: {
|
|
105
|
+
triggerId: trigger.id,
|
|
106
|
+
triggerKey: trigger.value,
|
|
107
|
+
fireKey,
|
|
108
|
+
taskTypeKey: action.taskTypeKey,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const taskExtra = `trigger:${trigger.id}:${fireKey}`;
|
|
113
|
+
const { data: task, error: postError } = await postTask(
|
|
114
|
+
buildPostTaskPayload(trigger, action, taskExtra, loopTraceId)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if (postError) {
|
|
118
|
+
logger.error("Trigger postTask 失敗", {
|
|
119
|
+
topic: "trigger",
|
|
120
|
+
data: {
|
|
121
|
+
triggerId: trigger.id,
|
|
122
|
+
fireKey,
|
|
123
|
+
error: postError.message,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const { error: patchError } = await patchTriggerFired({
|
|
130
|
+
triggerId: trigger.id,
|
|
131
|
+
fireKey,
|
|
132
|
+
expectedLastFireKey: trigger.details.lastFireKey ?? null,
|
|
133
|
+
traceId: loopTraceId,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (patchError) {
|
|
137
|
+
logger.warn("Trigger 已 postTask 但更新 lastFireKey 失敗", {
|
|
138
|
+
topic: "trigger",
|
|
139
|
+
data: {
|
|
140
|
+
triggerId: trigger.id,
|
|
141
|
+
taskId: task?.id,
|
|
142
|
+
fireKey,
|
|
143
|
+
error: patchError.message,
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
logger.success("Trigger 觸發成功", {
|
|
150
|
+
topic: "trigger",
|
|
151
|
+
data: {
|
|
152
|
+
triggerId: trigger.id,
|
|
153
|
+
triggerKey: trigger.value,
|
|
154
|
+
fireKey,
|
|
155
|
+
taskId: task?.id,
|
|
156
|
+
taskTraceId: task?.details.traceId,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function buildPostTaskPayload(
|
|
164
|
+
trigger: Trigger,
|
|
165
|
+
action: TriggerPostTaskAction,
|
|
166
|
+
extra: string,
|
|
167
|
+
loopTraceId: string
|
|
168
|
+
) {
|
|
169
|
+
return {
|
|
170
|
+
name: action.taskName ?? trigger.name,
|
|
171
|
+
value: action.taskTypeKey,
|
|
172
|
+
params: action.taskParams,
|
|
173
|
+
taskStatus: action.taskStatus ?? TaskStatus.TODO,
|
|
174
|
+
tagList: trigger.tagList ?? [],
|
|
175
|
+
extra,
|
|
176
|
+
traceParentId: loopTraceId,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export { TriggerNode };
|
package/src/node.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js runtime entry — browser API plus local FS / worker orchestration.
|
|
3
|
+
*
|
|
4
|
+
* import { TaskManager, RepoManager, TaskNode } from "target-supabase-sdk/node";
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export * from "./browser";
|
|
8
|
+
|
|
9
|
+
export { TaskManager } from "./task/task-manager";
|
|
10
|
+
export type {
|
|
11
|
+
PrepareTaskFailureReason,
|
|
12
|
+
PrepareTaskParams,
|
|
13
|
+
PrepareTaskResponse,
|
|
14
|
+
RegisterTasksOptions,
|
|
15
|
+
RegisterTasksResult,
|
|
16
|
+
TaskFn,
|
|
17
|
+
TaskRepoContext,
|
|
18
|
+
TaskRunResult,
|
|
19
|
+
ExecutableTaskFn,
|
|
20
|
+
TaskRepoScriptDetails,
|
|
21
|
+
TaskRepoScriptRecord,
|
|
22
|
+
TaskRunnerRootConfig,
|
|
23
|
+
TaskLocalPackageConfig,
|
|
24
|
+
BootstrapLocalTasksResult,
|
|
25
|
+
BootstrapLocalTasksStatus,
|
|
26
|
+
} from "./task/task-manager";
|
|
27
|
+
export {
|
|
28
|
+
TASK_REPO_SCRIPT_CATEGORY,
|
|
29
|
+
TASK_RUNNER_CONFIG_DIR,
|
|
30
|
+
TASK_RUNNER_ROOT_CONFIG_FILENAME,
|
|
31
|
+
TASK_RUNNER_ROOT_CONFIG_RELATIVE_PATH,
|
|
32
|
+
TASK_RUNNER_ROOT_CONFIG_LEGACY_RELATIVE_PATH,
|
|
33
|
+
TASK_LOCAL_PACKAGE_CONFIG_FILENAME,
|
|
34
|
+
} from "./task/task-manager";
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
postTask,
|
|
38
|
+
postTaskSchema,
|
|
39
|
+
} from "./task/task-post.api";
|
|
40
|
+
export type { PostTaskPayload } from "./task/task-post.api";
|
|
41
|
+
|
|
42
|
+
export {
|
|
43
|
+
TaskRepoValidation,
|
|
44
|
+
validateTaskRepoAndParams,
|
|
45
|
+
} from "./task/task-repo-validation";
|
|
46
|
+
export type {
|
|
47
|
+
TaskRepoValidationFailureReason,
|
|
48
|
+
ValidateTaskRepoAndParamsInput,
|
|
49
|
+
ValidateTaskRepoAndParamsResult,
|
|
50
|
+
} from "./task/task-repo-validation";
|
|
51
|
+
|
|
52
|
+
export { RepoManager } from "./repo/repo-manager";
|
|
53
|
+
|
|
54
|
+
export { TaskNode } from "./node/task-node";
|
|
55
|
+
export { TriggerNode } from "./node/trigger-node";
|
|
56
|
+
export { BaseNodeRuntime } from "./node/node-runtime.base";
|
|
57
|
+
|
|
58
|
+
export * from "./command/command.interface";
|
|
59
|
+
export * from "./command/command.api";
|
|
60
|
+
|
|
61
|
+
export * from "./trigger/trigger.interface";
|
|
62
|
+
export * from "./trigger/trigger.api";
|
package/src/repo/repo-manager.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { getPossibleTarget } from "../core.api";
|
|
1
|
+
import { getPossibleTarget } from "../core.api";
|
|
2
2
|
import { SupabaseInitializer } from "../supabase";
|
|
3
|
-
import {
|
|
3
|
+
import { LoggerWithScope } from "../shared/log";
|
|
4
4
|
import type { TaskRepoContext, TaskRepoScriptRecord } from "../task/task-repo-context";
|
|
5
5
|
import { TASK_REPO_SCRIPT_CATEGORY } from "../task/task-repo-context";
|
|
6
6
|
import {
|
|
@@ -24,16 +24,31 @@ type LocalRepoEntry =
|
|
|
24
24
|
const localRepoRegistry = new Map<string, LocalRepoEntry>();
|
|
25
25
|
const remoteContextCache = new Map<string, TaskRepoContext>();
|
|
26
26
|
|
|
27
|
+
export type RepoContextFailureReason =
|
|
28
|
+
| "LOCAL_REPO_LOAD_FAILED"
|
|
29
|
+
| "LOCAL_REPO_NOT_FOUND"
|
|
30
|
+
| "REMOTE_REPO_NOT_FOUND"
|
|
31
|
+
| "REMOTE_REPO_LOAD_FAILED";
|
|
32
|
+
|
|
33
|
+
export type RepoContextResolvedVia = "local" | "remote";
|
|
34
|
+
|
|
27
35
|
export interface GetRepoContextParams {
|
|
28
|
-
logger:
|
|
36
|
+
logger: LoggerWithScope;
|
|
29
37
|
/** Task type key (`task.value`) — matches {@link Repo.value} for remote lookup; local registry key */
|
|
30
38
|
taskTypeKey: string;
|
|
39
|
+
/**
|
|
40
|
+
* When false, skip Supabase fetch if the task type is not in the local registry.
|
|
41
|
+
* Default `true`.
|
|
42
|
+
*/
|
|
43
|
+
includeRemote?: boolean;
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
export interface GetRepoContextResult<T extends TaskRepoContext = TaskRepoContext> {
|
|
34
47
|
context: T | null;
|
|
35
48
|
/** Present when `context` is null — import / DB / format failure at repo layer */
|
|
36
49
|
error?: string;
|
|
50
|
+
failureReason?: RepoContextFailureReason;
|
|
51
|
+
resolvedVia?: RepoContextResolvedVia;
|
|
37
52
|
}
|
|
38
53
|
|
|
39
54
|
function buildRemoteCacheKey(taskTypeKey: string, contentHash: string): string {
|
|
@@ -65,7 +80,7 @@ async function fetchRemoteScripts(taskTypeKey: string): Promise<TaskRepoScriptRe
|
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
async function loadLocalEntry(
|
|
68
|
-
logger:
|
|
83
|
+
logger: LoggerWithScope,
|
|
69
84
|
entry: LocalRepoEntry,
|
|
70
85
|
taskTypeKey: string
|
|
71
86
|
): Promise<TaskRepoContext | null> {
|
|
@@ -91,16 +106,16 @@ async function loadLocalEntry(
|
|
|
91
106
|
}
|
|
92
107
|
|
|
93
108
|
async function loadRemoteRepoContext(
|
|
94
|
-
logger:
|
|
109
|
+
logger: LoggerWithScope,
|
|
95
110
|
taskTypeKey: string
|
|
96
111
|
): Promise<GetRepoContextResult> {
|
|
97
|
-
logger.info("從 Supabase 加載 Repo", { topic: "repo",
|
|
112
|
+
logger.info("從 Supabase 加載 Repo", { topic: "repo", data: { taskTypeKey } });
|
|
98
113
|
|
|
99
114
|
const remoteRepo = await fetchRemoteRepo(taskTypeKey);
|
|
100
115
|
if (remoteRepo == null) {
|
|
101
116
|
const error = `Supabase 未找到 Repo (value=${taskTypeKey})`;
|
|
102
|
-
logger.warn(error, { topic: "repo",
|
|
103
|
-
return { context: null, error };
|
|
117
|
+
logger.warn(error, { topic: "repo", data: { taskTypeKey } });
|
|
118
|
+
return { context: null, error, failureReason: "REMOTE_REPO_NOT_FOUND" };
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
const repoUrl = remoteRepo.details?.url?.trim() ?? "";
|
|
@@ -108,8 +123,8 @@ async function loadRemoteRepoContext(
|
|
|
108
123
|
const cacheKey = buildRemoteCacheKey(taskTypeKey, repoHash);
|
|
109
124
|
const cached = remoteContextCache.get(cacheKey);
|
|
110
125
|
if (cached != null) {
|
|
111
|
-
logger.info("命中遠程 Repo 上下文緩存", { topic: "repo",
|
|
112
|
-
return { context: cached };
|
|
126
|
+
logger.info("命中遠程 Repo 上下文緩存", { topic: "repo", data: { cacheKey } });
|
|
127
|
+
return { context: cached, resolvedVia: "remote" };
|
|
113
128
|
}
|
|
114
129
|
|
|
115
130
|
const scripts = await fetchRemoteScripts(taskTypeKey);
|
|
@@ -123,7 +138,7 @@ async function loadRemoteRepoContext(
|
|
|
123
138
|
if (loaded == null && repoUrl !== "") {
|
|
124
139
|
logger.info("未找到 script 行,嘗試從 Repo.details.url 加載", {
|
|
125
140
|
topic: "repo",
|
|
126
|
-
|
|
141
|
+
data: { taskTypeKey, url: repoUrl },
|
|
127
142
|
});
|
|
128
143
|
loaded = await loadRepoContextFromUrl({ logger, url: repoUrl, taskTypeKey });
|
|
129
144
|
}
|
|
@@ -132,47 +147,55 @@ async function loadRemoteRepoContext(
|
|
|
132
147
|
const error = `無法從 Repo 加載 TaskRepoContext (hasUrl=${repoUrl !== ""}, scriptCount=${scripts.length})`;
|
|
133
148
|
logger.warn(error, {
|
|
134
149
|
topic: "repo",
|
|
135
|
-
|
|
150
|
+
data: { taskTypeKey, hasUrl: repoUrl !== "", scriptCount: scripts.length },
|
|
136
151
|
});
|
|
137
|
-
return { context: null, error };
|
|
152
|
+
return { context: null, error, failureReason: "REMOTE_REPO_LOAD_FAILED" };
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
remoteContextCache.set(cacheKey, loaded.context);
|
|
141
|
-
return { context: loaded.context };
|
|
156
|
+
return { context: loaded.context, resolvedVia: "remote" };
|
|
142
157
|
}
|
|
143
158
|
|
|
144
159
|
const getRepoContext = async <RepoContext extends TaskRepoContext>({
|
|
145
160
|
logger,
|
|
146
161
|
taskTypeKey,
|
|
162
|
+
includeRemote = true,
|
|
147
163
|
}: GetRepoContextParams): Promise<GetRepoContextResult<RepoContext>> => {
|
|
148
164
|
const localEntry = localRepoRegistry.get(taskTypeKey);
|
|
149
165
|
if (localEntry != null) {
|
|
150
|
-
logger.info("使用本地註冊的 Repo", { topic: "repo",
|
|
166
|
+
logger.info("使用本地註冊的 Repo", { topic: "repo", data: { taskTypeKey } });
|
|
151
167
|
const context = await loadLocalEntry(logger, localEntry, taskTypeKey);
|
|
152
168
|
if (context != null && isTaskRepoContext(context)) {
|
|
153
|
-
return { context: context as RepoContext };
|
|
169
|
+
return { context: context as RepoContext, resolvedVia: "local" };
|
|
154
170
|
}
|
|
155
171
|
const error = "本地 Repo 模組加載失敗或未導出有效的 TaskRepoContext";
|
|
156
|
-
logger.warn(error, { topic: "repo",
|
|
157
|
-
return { context: null, error };
|
|
172
|
+
logger.warn(error, { topic: "repo", data: { taskTypeKey } });
|
|
173
|
+
return { context: null, error, failureReason: "LOCAL_REPO_LOAD_FAILED" };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!includeRemote) {
|
|
177
|
+
const error = `本地未註冊 Repo (value=${taskTypeKey})`;
|
|
178
|
+
logger.warn(error, { topic: "repo", data: { taskTypeKey, includeRemote: false } });
|
|
179
|
+
return { context: null, error, failureReason: "LOCAL_REPO_NOT_FOUND" };
|
|
158
180
|
}
|
|
159
181
|
|
|
160
182
|
try {
|
|
161
183
|
const result = await loadRemoteRepoContext(logger, taskTypeKey);
|
|
162
184
|
if (result.context != null && isTaskRepoContext(result.context)) {
|
|
163
|
-
return { context: result.context as RepoContext };
|
|
185
|
+
return { context: result.context as RepoContext, resolvedVia: "remote" };
|
|
164
186
|
}
|
|
165
187
|
return {
|
|
166
188
|
context: null,
|
|
167
189
|
error: result.error ?? "遠程 Repo 上下文無效",
|
|
190
|
+
failureReason: result.failureReason ?? "REMOTE_REPO_LOAD_FAILED",
|
|
168
191
|
};
|
|
169
192
|
} catch (error) {
|
|
170
193
|
const message = error instanceof Error ? error.message : String(error);
|
|
171
194
|
logger.error("從 Supabase 加載 Repo 失敗", {
|
|
172
195
|
topic: "repo",
|
|
173
|
-
|
|
196
|
+
data: { taskTypeKey, error: message },
|
|
174
197
|
});
|
|
175
|
-
return { context: null, error: message };
|
|
198
|
+
return { context: null, error: message, failureReason: "REMOTE_REPO_LOAD_FAILED" };
|
|
176
199
|
}
|
|
177
200
|
};
|
|
178
201
|
|
|
@@ -196,6 +219,10 @@ function unregisterLocalRepo(taskTypeKey: string): void {
|
|
|
196
219
|
localRepoRegistry.delete(taskTypeKey);
|
|
197
220
|
}
|
|
198
221
|
|
|
222
|
+
function hasLocalRepo(taskTypeKey: string): boolean {
|
|
223
|
+
return localRepoRegistry.has(taskTypeKey);
|
|
224
|
+
}
|
|
225
|
+
|
|
199
226
|
function getRegisteredLocalTaskTypeKeys(): string[] {
|
|
200
227
|
return [...localRepoRegistry.keys()];
|
|
201
228
|
}
|
|
@@ -209,6 +236,7 @@ const RepoManager = {
|
|
|
209
236
|
registerLocalRepoContext,
|
|
210
237
|
registerLocalModule,
|
|
211
238
|
unregisterLocalRepo,
|
|
239
|
+
hasLocalRepo,
|
|
212
240
|
getRegisteredLocalTaskTypeKeys,
|
|
213
241
|
clearRemoteContextCache,
|
|
214
242
|
clearRepoScriptModuleCache,
|