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,171 @@
|
|
|
1
|
+
import { RepoManager, type RepoContextFailureReason } from "../repo/repo-manager";
|
|
2
|
+
import { LoggerWithScope } from "../shared/log";
|
|
3
|
+
import { bootstrapLocalTasks, type BootstrapLocalTasksOptions } from "./local-task-registry";
|
|
4
|
+
import { ResultCode } from "./task.interface";
|
|
5
|
+
import type { TaskRepoContext } from "./task-repo-context";
|
|
6
|
+
|
|
7
|
+
export type TaskRepoValidationFailureReason =
|
|
8
|
+
| "MISSING_PARAMS"
|
|
9
|
+
| RepoContextFailureReason
|
|
10
|
+
| "PARAMS_VALIDATION_FAILED"
|
|
11
|
+
| "TASK_TYPE_KEY_MISMATCH";
|
|
12
|
+
|
|
13
|
+
export type ValidateTaskRepoAndParamsInput = {
|
|
14
|
+
logger: LoggerWithScope;
|
|
15
|
+
taskTypeKey: string;
|
|
16
|
+
params: unknown;
|
|
17
|
+
/**
|
|
18
|
+
* When true, run {@link bootstrapLocalTasks} before resolve (CLI / Trigger / postTask).
|
|
19
|
+
* Worker paths should omit — {@link TaskManager.registerTasks} already bootstrapped.
|
|
20
|
+
*/
|
|
21
|
+
bootstrapLocal?: boolean;
|
|
22
|
+
bootstrapLocalOptions?: BootstrapLocalTasksOptions;
|
|
23
|
+
/**
|
|
24
|
+
* When false, do not fetch Repo from Supabase if the task type is not locally registered.
|
|
25
|
+
* Default `true`. Align with {@link RegisterTasksOptions.includeRemote}.
|
|
26
|
+
*/
|
|
27
|
+
includeRemote?: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type ValidateTaskRepoAndParamsFailure = {
|
|
31
|
+
isValid: false;
|
|
32
|
+
code: ResultCode;
|
|
33
|
+
message: string;
|
|
34
|
+
reason: TaskRepoValidationFailureReason;
|
|
35
|
+
step: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type ValidateTaskRepoAndParamsSuccess = {
|
|
39
|
+
isValid: true;
|
|
40
|
+
repoContext: TaskRepoContext;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type ValidateTaskRepoAndParamsResult =
|
|
44
|
+
| ValidateTaskRepoAndParamsFailure
|
|
45
|
+
| ValidateTaskRepoAndParamsSuccess;
|
|
46
|
+
|
|
47
|
+
function validateTaskParams(
|
|
48
|
+
validator: TaskRepoContext["taskParamsValidator"],
|
|
49
|
+
taskParams: unknown,
|
|
50
|
+
logger: LoggerWithScope
|
|
51
|
+
): boolean {
|
|
52
|
+
try {
|
|
53
|
+
return validator(taskParams) === true;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
logger.warn("taskParamsValidator 拋出異常", {
|
|
56
|
+
topic: "task",
|
|
57
|
+
data: { error: error instanceof Error ? error.message : error },
|
|
58
|
+
});
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function invalid(params: {
|
|
64
|
+
reason: TaskRepoValidationFailureReason;
|
|
65
|
+
code: ResultCode;
|
|
66
|
+
message: string;
|
|
67
|
+
step: string;
|
|
68
|
+
}): ValidateTaskRepoAndParamsFailure {
|
|
69
|
+
return { isValid: false, ...params };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolve task Repo context and validate `params` via `taskParamsValidator`.
|
|
74
|
+
*
|
|
75
|
+
* Repo resolution uses {@link RepoManager.getRepoContext}:
|
|
76
|
+
* local registry first; when no local entry and `includeRemote`, loads from Supabase.
|
|
77
|
+
*/
|
|
78
|
+
async function validateTaskRepoAndParams({
|
|
79
|
+
logger,
|
|
80
|
+
taskTypeKey,
|
|
81
|
+
params,
|
|
82
|
+
bootstrapLocal = false,
|
|
83
|
+
bootstrapLocalOptions,
|
|
84
|
+
includeRemote = true,
|
|
85
|
+
}: ValidateTaskRepoAndParamsInput): Promise<ValidateTaskRepoAndParamsResult> {
|
|
86
|
+
if (params == null) {
|
|
87
|
+
return invalid({
|
|
88
|
+
reason: "MISSING_PARAMS",
|
|
89
|
+
code: ResultCode.PARAMS_NOT_VALID,
|
|
90
|
+
message: "任務缺少 params",
|
|
91
|
+
step: "params",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (bootstrapLocal) {
|
|
96
|
+
const bootstrap = await bootstrapLocalTasks({
|
|
97
|
+
...bootstrapLocalOptions,
|
|
98
|
+
forTaskTypeKey: taskTypeKey,
|
|
99
|
+
});
|
|
100
|
+
logger.debug("本地任務 bootstrap 完成", {
|
|
101
|
+
topic: "task",
|
|
102
|
+
data: {
|
|
103
|
+
taskTypeKey,
|
|
104
|
+
step: "local_bootstrap",
|
|
105
|
+
status: bootstrap.status,
|
|
106
|
+
registered: bootstrap.registered,
|
|
107
|
+
cached: bootstrap.cached ?? false,
|
|
108
|
+
message: bootstrap.message,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
logger.debug("開始解析 Repo 上下文", {
|
|
114
|
+
topic: "task",
|
|
115
|
+
data: { taskTypeKey, step: "repo_load", includeRemote },
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const {
|
|
119
|
+
context: repoContext,
|
|
120
|
+
error: repoError,
|
|
121
|
+
failureReason,
|
|
122
|
+
resolvedVia,
|
|
123
|
+
} = await RepoManager.getRepoContext<TaskRepoContext>({
|
|
124
|
+
logger,
|
|
125
|
+
taskTypeKey,
|
|
126
|
+
includeRemote,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
if (repoContext == null) {
|
|
130
|
+
return invalid({
|
|
131
|
+
reason: failureReason ?? "REMOTE_REPO_LOAD_FAILED",
|
|
132
|
+
code: ResultCode.REPO_NOT_VALID,
|
|
133
|
+
message: repoError ?? "無法加載 Repo 上下文",
|
|
134
|
+
step: "repo_load",
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
logger.debug("Repo 上下文解析完成", {
|
|
139
|
+
topic: "task",
|
|
140
|
+
data: { taskTypeKey, step: "repo_load", resolvedVia },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (repoContext.taskFn.taskTypeKey !== taskTypeKey) {
|
|
144
|
+
return invalid({
|
|
145
|
+
reason: "TASK_TYPE_KEY_MISMATCH",
|
|
146
|
+
code: ResultCode.REPO_NOT_VALID,
|
|
147
|
+
message: `taskFn.taskTypeKey (${repoContext.taskFn.taskTypeKey}) 與 task.value (${taskTypeKey}) 不一致`,
|
|
148
|
+
step: "task_type_key",
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
logger.debug("開始校驗任務參數", { topic: "task", data: { taskTypeKey, step: "params_validation" } });
|
|
153
|
+
if (!validateTaskParams(repoContext.taskParamsValidator, params, logger)) {
|
|
154
|
+
return invalid({
|
|
155
|
+
reason: "PARAMS_VALIDATION_FAILED",
|
|
156
|
+
code: ResultCode.PARAMS_NOT_VALID,
|
|
157
|
+
message: "任務參數校驗失敗",
|
|
158
|
+
step: "params_validation",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
logger.debug("任務參數校驗通過", { topic: "task", data: { taskTypeKey, step: "params_validation" } });
|
|
163
|
+
|
|
164
|
+
return { isValid: true, repoContext };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const TaskRepoValidation = {
|
|
168
|
+
validate: validateTaskRepoAndParams,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export { TaskRepoValidation, validateTaskRepoAndParams };
|
package/src/task/task.api.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createTarget, getPossibleTarget, QueryFilter, updateTargetDetails, validateWithSchema, isOptimisticLockError, OPTIMISTIC_LOCK_FAILED_MESSAGE } from "../core.api";
|
|
2
2
|
import { generateResponse, type SupabaseResponse } from "../core.interface";
|
|
3
|
-
import { createApiLogger, type
|
|
3
|
+
import { createApiLogger, logManager, type LoggerWithScope } from "../shared/log";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { CategoryTask, ResultCode, Task, TaskDetails, TaskStatus, TaskStatusAction } from "./task.interface";
|
|
6
6
|
|
|
@@ -44,8 +44,9 @@ const TASK_NODE_ID_FIELD = "details->>nodeId" as const;
|
|
|
44
44
|
|
|
45
45
|
const taskIdSchema = z.string().trim().min(1);
|
|
46
46
|
const nodeIdSchema = z.string().trim().min(1);
|
|
47
|
-
/** Optional — orchestrator (e.g.
|
|
47
|
+
/** Optional — orchestrator (e.g. TaskNode loop) passes its trace to correlate logs. */
|
|
48
48
|
const traceIdSchema = z.string().trim().min(1).optional();
|
|
49
|
+
const traceParentIdSchema = z.string().trim().min(1).nullable().optional();
|
|
49
50
|
|
|
50
51
|
const patchChangeTaskStatusBaseSchema = {
|
|
51
52
|
id: taskIdSchema,
|
|
@@ -107,25 +108,6 @@ export const patchClaimTaskSchema = z.object({
|
|
|
107
108
|
|
|
108
109
|
export type PatchClaimTaskPayload = z.infer<typeof patchClaimTaskSchema>;
|
|
109
110
|
|
|
110
|
-
/** Initial status for a new task — OPEN (draft) or TODO (claimable by workers). */
|
|
111
|
-
const postTaskInitialStatusSchema = z
|
|
112
|
-
.union([z.literal(TaskStatus.OPEN), z.literal(TaskStatus.TODO)])
|
|
113
|
-
.optional()
|
|
114
|
-
.default(TaskStatus.TODO);
|
|
115
|
-
|
|
116
|
-
export const postTaskSchema = z.object({
|
|
117
|
-
name: z.string().trim().min(1),
|
|
118
|
-
/** Task type key — matches `task.value`, {@link patchClaimTask}, {@link Repo.value}, and local Repo registry. */
|
|
119
|
-
value: z.string().trim().min(1),
|
|
120
|
-
params: z.unknown(),
|
|
121
|
-
taskStatus: postTaskInitialStatusSchema,
|
|
122
|
-
tagList: z.array(z.string()).optional().default([]),
|
|
123
|
-
extra: z.string().optional(),
|
|
124
|
-
traceId: traceIdSchema,
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
export type PostTaskPayload = z.infer<typeof postTaskSchema>;
|
|
128
|
-
|
|
129
111
|
// ─── Optimistic lock helpers ─────────────────────────────────────────────────
|
|
130
112
|
|
|
131
113
|
function lockOnStatus(...allowed: TaskStatus[]): QueryFilter[] {
|
|
@@ -178,20 +160,20 @@ async function transitionTask({
|
|
|
178
160
|
}
|
|
179
161
|
|
|
180
162
|
function logTaskTransition(
|
|
181
|
-
logger:
|
|
163
|
+
logger: LoggerWithScope,
|
|
182
164
|
action: TaskStatusAction | "patchTaskProgress" | "patchClaimTask",
|
|
183
165
|
task: Task,
|
|
184
|
-
|
|
166
|
+
data?: Record<string, unknown>
|
|
185
167
|
): void {
|
|
186
168
|
logger.info("任务状态迁移成功", {
|
|
187
169
|
topic: "task",
|
|
188
|
-
|
|
170
|
+
data: {
|
|
189
171
|
action,
|
|
190
172
|
taskId: task.id,
|
|
191
173
|
taskValue: task.value,
|
|
192
174
|
status: task.details.status,
|
|
193
175
|
nodeId: task.details.nodeId,
|
|
194
|
-
...
|
|
176
|
+
...data,
|
|
195
177
|
},
|
|
196
178
|
});
|
|
197
179
|
}
|
|
@@ -314,7 +296,9 @@ export const patchChangeTaskStatus = validateWithSchema(
|
|
|
314
296
|
}
|
|
315
297
|
default: {
|
|
316
298
|
const _exhaustive: never = action;
|
|
317
|
-
|
|
299
|
+
const message = `[patchChangeTaskStatus] Unknown action: ${String(_exhaustive)}`;
|
|
300
|
+
logger.error(message, { topic: "task", data: { action: String(_exhaustive) } });
|
|
301
|
+
return generateResponse.error(message);
|
|
318
302
|
}
|
|
319
303
|
}
|
|
320
304
|
});
|
|
@@ -326,7 +310,7 @@ export const patchTaskProgress = validateWithSchema(
|
|
|
326
310
|
patchTaskProgressSchema,
|
|
327
311
|
"patchTaskProgressSchema"
|
|
328
312
|
)(async ({ id, progress, nodeId, traceId }) => {
|
|
329
|
-
const logger = createApiLogger("patchTaskProgress", { traceId, nodeId });
|
|
313
|
+
const logger = createApiLogger("patchTaskProgress", { traceId, labels: { nodeId } });
|
|
330
314
|
const data = await transitionTask({
|
|
331
315
|
id,
|
|
332
316
|
optimisticLockFilterList: lockOnDoingOwner(nodeId),
|
|
@@ -354,7 +338,7 @@ export const patchClaimTask = validateWithSchema(
|
|
|
354
338
|
patchClaimTaskSchema,
|
|
355
339
|
"patchClaimTaskSchema"
|
|
356
340
|
)(async ({ nodeId, availableTaskList, traceId }): Promise<SupabaseResponse<Task | null>> => {
|
|
357
|
-
const logger = createApiLogger("patchClaimTask", { traceId, nodeId });
|
|
341
|
+
const logger = createApiLogger("patchClaimTask", { traceId, labels: { nodeId } });
|
|
358
342
|
|
|
359
343
|
try {
|
|
360
344
|
const { data: possibleTask } = await getPossibleTarget({
|
|
@@ -379,51 +363,7 @@ export const patchClaimTask = validateWithSchema(
|
|
|
379
363
|
return generateResponse.error(OPTIMISTIC_LOCK_FAILED_MESSAGE, undefined, "OPTIMISTIC_LOCK") as SupabaseResponse<Task | null>;
|
|
380
364
|
}
|
|
381
365
|
const message = error instanceof Error ? error.message : String(error);
|
|
382
|
-
logger.error("認領任務失敗", { topic: "task",
|
|
366
|
+
logger.error("認領任務失敗", { topic: "task", data: { error: message } });
|
|
383
367
|
return generateResponse.error(message) as SupabaseResponse<Task | null>;
|
|
384
368
|
}
|
|
385
369
|
});
|
|
386
|
-
|
|
387
|
-
// ─── postTask ────────────────────────────────────────────────────────────────
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Create a task row (scheduler / admin).
|
|
391
|
-
*
|
|
392
|
-
* `taskStatus` defaults to **TODO** (worker-claimable). Pass **OPEN** for draft tasks
|
|
393
|
-
* that require {@link patchChangeTaskStatus} `publish` before workers can claim.
|
|
394
|
-
*/
|
|
395
|
-
export const postTask = validateWithSchema(
|
|
396
|
-
postTaskSchema,
|
|
397
|
-
"postTaskSchema"
|
|
398
|
-
)(async ({ name, value, params, taskStatus, tagList, extra, traceId }) => {
|
|
399
|
-
const logger = createApiLogger("postTask", { traceId });
|
|
400
|
-
|
|
401
|
-
const result = await createTarget<Task, PostTaskPayload>({
|
|
402
|
-
payload: { name, value, params, taskStatus, tagList, extra, traceId },
|
|
403
|
-
createFn: () => ({
|
|
404
|
-
name,
|
|
405
|
-
value,
|
|
406
|
-
category: CategoryTask.TASK,
|
|
407
|
-
tagList,
|
|
408
|
-
extra,
|
|
409
|
-
details: {
|
|
410
|
-
manifestVersion: 0,
|
|
411
|
-
status: taskStatus,
|
|
412
|
-
params,
|
|
413
|
-
progress: 0,
|
|
414
|
-
nodeId: null,
|
|
415
|
-
},
|
|
416
|
-
}),
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
logger.info("任務已創建", {
|
|
420
|
-
topic: "task",
|
|
421
|
-
context: {
|
|
422
|
-
taskId: result.data?.id,
|
|
423
|
-
taskTypeKey: value,
|
|
424
|
-
status: taskStatus,
|
|
425
|
-
},
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
return result;
|
|
429
|
-
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { createTarget, scanTargetList, updateTargetDetails, validateWithSchema } from "../core.api";
|
|
2
|
+
import { generateResponse, type SupabaseResponse } from "../core.interface";
|
|
3
|
+
import { TaskStatus } from "../task/task.interface";
|
|
4
|
+
import { createApiLogger } from "../shared/log";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import {
|
|
7
|
+
CategoryTrigger,
|
|
8
|
+
Trigger,
|
|
9
|
+
TriggerStatus,
|
|
10
|
+
type TriggerAction,
|
|
11
|
+
type TriggerSchedule,
|
|
12
|
+
} from "./trigger.interface";
|
|
13
|
+
|
|
14
|
+
const TRIGGER_STATUS_FIELD = "details->>status" as const;
|
|
15
|
+
|
|
16
|
+
const traceIdSchema = z.string().trim().min(1).optional();
|
|
17
|
+
|
|
18
|
+
const dailyScheduleSchema = z.object({
|
|
19
|
+
kind: z.literal("daily"),
|
|
20
|
+
hour: z.number().int().min(0).max(23),
|
|
21
|
+
minute: z.number().int().min(0).max(59),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const postTaskActionSchema = z.object({
|
|
25
|
+
kind: z.literal("post_task"),
|
|
26
|
+
taskTypeKey: z.string().trim().min(1),
|
|
27
|
+
taskParams: z.unknown(),
|
|
28
|
+
taskName: z.string().trim().min(1).optional(),
|
|
29
|
+
taskStatus: z.enum([TaskStatus.OPEN, TaskStatus.TODO]).optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const postTriggerSchema = z.object({
|
|
33
|
+
name: z.string().trim().min(1),
|
|
34
|
+
value: z.string().trim().min(1),
|
|
35
|
+
status: z.nativeEnum(TriggerStatus).optional().default(TriggerStatus.ENABLED),
|
|
36
|
+
schedule: dailyScheduleSchema,
|
|
37
|
+
action: postTaskActionSchema,
|
|
38
|
+
tagList: z.array(z.string()).optional().default([]),
|
|
39
|
+
traceId: traceIdSchema,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export type PostTriggerPayload = z.infer<typeof postTriggerSchema>;
|
|
43
|
+
|
|
44
|
+
export const scanEnabledTriggersSchema = z.object({
|
|
45
|
+
traceId: traceIdSchema,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export type ScanEnabledTriggersPayload = z.infer<typeof scanEnabledTriggersSchema>;
|
|
49
|
+
|
|
50
|
+
export const patchTriggerFiredSchema = z.object({
|
|
51
|
+
triggerId: z.string().trim().min(1),
|
|
52
|
+
fireKey: z.string().trim().min(1),
|
|
53
|
+
expectedLastFireKey: z.string().nullable().optional(),
|
|
54
|
+
traceId: traceIdSchema,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export type PatchTriggerFiredPayload = z.infer<typeof patchTriggerFiredSchema>;
|
|
58
|
+
|
|
59
|
+
function enabledTriggerFilter() {
|
|
60
|
+
return [{ field: TRIGGER_STATUS_FIELD, operator: "eq" as const, value: TriggerStatus.ENABLED }];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** List all ENABLED triggers (Phase 1: in-memory due check). */
|
|
64
|
+
export const scanEnabledTriggers = validateWithSchema(
|
|
65
|
+
scanEnabledTriggersSchema,
|
|
66
|
+
"scanEnabledTriggersSchema"
|
|
67
|
+
)(async ({ traceId }) => {
|
|
68
|
+
const logger = createApiLogger("scanEnabledTriggers", { traceId });
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const { data: triggerList } = await scanTargetList<Trigger>({
|
|
72
|
+
category: CategoryTrigger.TRIGGER,
|
|
73
|
+
filterList: enabledTriggerFilter(),
|
|
74
|
+
orderBy: { field: "created_at", ascending: true },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const rows = triggerList ?? [];
|
|
78
|
+
logger.info("已掃描 ENABLED Trigger", {
|
|
79
|
+
topic: "trigger",
|
|
80
|
+
data: { count: rows.length },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return generateResponse.success(rows);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
86
|
+
return generateResponse.error(message) as SupabaseResponse<Trigger[]>;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/** Create a trigger row (scheduler / admin). */
|
|
91
|
+
export const postTrigger = validateWithSchema(
|
|
92
|
+
postTriggerSchema,
|
|
93
|
+
"postTriggerSchema"
|
|
94
|
+
)(async ({ name, value, status, schedule, action, tagList, traceId }) => {
|
|
95
|
+
const logger = createApiLogger("postTrigger", { traceId });
|
|
96
|
+
|
|
97
|
+
const details = {
|
|
98
|
+
manifestVersion: 0,
|
|
99
|
+
status,
|
|
100
|
+
schedule: schedule as TriggerSchedule,
|
|
101
|
+
action: action as TriggerAction,
|
|
102
|
+
lastFiredAt: null,
|
|
103
|
+
lastFireKey: null,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const result = await createTarget<Trigger, PostTriggerPayload>({
|
|
107
|
+
payload: { name, value, status, schedule, action, tagList, traceId },
|
|
108
|
+
createFn: () => ({
|
|
109
|
+
name,
|
|
110
|
+
value,
|
|
111
|
+
category: CategoryTrigger.TRIGGER,
|
|
112
|
+
tagList,
|
|
113
|
+
details,
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
logger.info("Trigger 已創建", {
|
|
118
|
+
topic: "trigger",
|
|
119
|
+
data: { triggerId: result.data?.id, triggerKey: value, status },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Mark trigger as fired for `fireKey`.
|
|
127
|
+
* When `expectedLastFireKey` is set, uses optimistic lock on that field.
|
|
128
|
+
*/
|
|
129
|
+
export const patchTriggerFired = validateWithSchema(
|
|
130
|
+
patchTriggerFiredSchema,
|
|
131
|
+
"patchTriggerFiredSchema"
|
|
132
|
+
)(async ({ triggerId, fireKey, expectedLastFireKey, traceId }) => {
|
|
133
|
+
const logger = createApiLogger("patchTriggerFired", { traceId });
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const optimisticLockFilterList =
|
|
138
|
+
expectedLastFireKey != null && expectedLastFireKey !== ""
|
|
139
|
+
? [{ field: "details->>lastFireKey" as const, operator: "eq" as const, value: expectedLastFireKey }]
|
|
140
|
+
: [];
|
|
141
|
+
|
|
142
|
+
const data = await updateTargetDetails<Trigger, Trigger["details"]>({
|
|
143
|
+
id: triggerId,
|
|
144
|
+
optimisticLockFilterList,
|
|
145
|
+
updateFn: (current) => ({
|
|
146
|
+
...current,
|
|
147
|
+
lastFireKey: fireKey,
|
|
148
|
+
lastFiredAt: now,
|
|
149
|
+
}),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
logger.success("Trigger 觸發記錄已更新", {
|
|
153
|
+
topic: "trigger",
|
|
154
|
+
data: { triggerId, fireKey },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return generateResponse.success(data);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
160
|
+
logger.warn("Trigger 觸發記錄更新失敗", {
|
|
161
|
+
topic: "trigger",
|
|
162
|
+
data: { triggerId, fireKey, error: message },
|
|
163
|
+
});
|
|
164
|
+
return generateResponse.error(message);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Target } from "../core.interface";
|
|
2
|
+
import { TaskStatus } from "../task/task.interface";
|
|
3
|
+
|
|
4
|
+
export enum CategoryTrigger {
|
|
5
|
+
TRIGGER = "trigger",
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export enum TriggerStatus {
|
|
9
|
+
ENABLED = "ENABLED",
|
|
10
|
+
DISABLED = "DISABLED",
|
|
11
|
+
PAUSED = "PAUSED",
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Phase 1: daily fire at `hour`:`minute` UTC. */
|
|
15
|
+
export interface TriggerDailySchedule {
|
|
16
|
+
kind: "daily";
|
|
17
|
+
hour: number;
|
|
18
|
+
minute: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type TriggerSchedule = TriggerDailySchedule;
|
|
22
|
+
|
|
23
|
+
export interface TriggerPostTaskAction {
|
|
24
|
+
kind: "post_task";
|
|
25
|
+
taskTypeKey: string;
|
|
26
|
+
taskParams: unknown;
|
|
27
|
+
taskName?: string;
|
|
28
|
+
taskStatus?: TaskStatus.OPEN | TaskStatus.TODO;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type TriggerAction = TriggerPostTaskAction;
|
|
32
|
+
|
|
33
|
+
export interface Trigger extends Target {
|
|
34
|
+
/** Human readable */
|
|
35
|
+
name: string;
|
|
36
|
+
/** Unique trigger key */
|
|
37
|
+
value: string;
|
|
38
|
+
details: TriggerDetails;
|
|
39
|
+
category: CategoryTrigger;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TriggerDetails {
|
|
43
|
+
manifestVersion: number;
|
|
44
|
+
status: TriggerStatus;
|
|
45
|
+
schedule: TriggerSchedule;
|
|
46
|
+
action: TriggerAction;
|
|
47
|
+
lastFiredAt?: number | null;
|
|
48
|
+
/** Idempotency key for the last successful fire, e.g. `daily:2026-06-18` (UTC date). */
|
|
49
|
+
lastFireKey?: string | null;
|
|
50
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Trigger, TriggerDailySchedule } from "./trigger.interface";
|
|
2
|
+
|
|
3
|
+
interface UtcDateParts {
|
|
4
|
+
year: number;
|
|
5
|
+
month: number;
|
|
6
|
+
day: number;
|
|
7
|
+
hour: number;
|
|
8
|
+
minute: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseUtcDateParts(now: Date): UtcDateParts {
|
|
12
|
+
return {
|
|
13
|
+
year: now.getUTCFullYear(),
|
|
14
|
+
month: now.getUTCMonth() + 1,
|
|
15
|
+
day: now.getUTCDate(),
|
|
16
|
+
hour: now.getUTCHours(),
|
|
17
|
+
minute: now.getUTCMinutes(),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildDailyFireKey(_schedule: TriggerDailySchedule, now: Date): string {
|
|
22
|
+
const { year, month, day } = parseUtcDateParts(now);
|
|
23
|
+
const yyyy = String(year).padStart(4, "0");
|
|
24
|
+
const mm = String(month).padStart(2, "0");
|
|
25
|
+
const dd = String(day).padStart(2, "0");
|
|
26
|
+
return `daily:${yyyy}-${mm}-${dd}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isDailyScheduleDue(
|
|
30
|
+
schedule: TriggerDailySchedule,
|
|
31
|
+
now: Date,
|
|
32
|
+
lastFireKey: string | null | undefined
|
|
33
|
+
): boolean {
|
|
34
|
+
const parts = parseUtcDateParts(now);
|
|
35
|
+
if (parts.hour !== schedule.hour || parts.minute !== schedule.minute) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const fireKey = buildDailyFireKey(schedule, now);
|
|
39
|
+
return lastFireKey !== fireKey;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isTriggerDue(trigger: Trigger, now: Date = new Date()): boolean {
|
|
43
|
+
const { schedule, lastFireKey } = trigger.details;
|
|
44
|
+
if (schedule.kind !== "daily") {
|
|
45
|
+
throw new Error(`Unsupported trigger schedule kind: ${String((schedule as { kind?: string }).kind)}`);
|
|
46
|
+
}
|
|
47
|
+
return isDailyScheduleDue(schedule, now, lastFireKey);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function buildFireKey(trigger: Trigger, now: Date = new Date()): string {
|
|
51
|
+
const { schedule } = trigger.details;
|
|
52
|
+
if (schedule.kind !== "daily") {
|
|
53
|
+
throw new Error(`Unsupported trigger schedule kind: ${String((schedule as { kind?: string }).kind)}`);
|
|
54
|
+
}
|
|
55
|
+
return buildDailyFireKey(schedule, now);
|
|
56
|
+
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { CommandType } from "../command/command.interface";
|
|
2
|
-
import { NodeLoopContext } from "./node.interface";
|
|
3
|
-
declare class NodeManager {
|
|
4
|
-
private static readonly HEARTBEAT_FAILURE_THRESHOLD;
|
|
5
|
-
private _localNodeId;
|
|
6
|
-
private _isNodeIdLocked;
|
|
7
|
-
private _availableTaskList;
|
|
8
|
-
private isRunning;
|
|
9
|
-
private loopCount;
|
|
10
|
-
private consecutiveHeartbeatFailures;
|
|
11
|
-
/** 進程生命週期 traceId — bootstrap / register / shutdown 共用 */
|
|
12
|
-
private readonly startupTraceId;
|
|
13
|
-
/** 進行中的 shutdown;並發/重複調用復用同一 Promise。 */
|
|
14
|
-
private shutdownPromise;
|
|
15
|
-
/** start() 冪等:重複調用復用同一 Promise。 */
|
|
16
|
-
private startPromise;
|
|
17
|
-
get availableTaskList(): readonly string[];
|
|
18
|
-
get localNodeId(): string | null;
|
|
19
|
-
set localNodeId(value: string);
|
|
20
|
-
constructor();
|
|
21
|
-
private getLogNodeId;
|
|
22
|
-
private isShuttingDown;
|
|
23
|
-
/** Fire-and-forget:供信號/致命路徑調用;shutdown Promise 內部吞掉 rejection,避免 unhandledRejection 遞迴。 */
|
|
24
|
-
private requestShutdown;
|
|
25
|
-
/** 啟動日誌 + SIGTERM/SIGINT/uncaughtException/unhandledRejection → shutdown */
|
|
26
|
-
private registerProcessLifecycle;
|
|
27
|
-
start(): Promise<void>;
|
|
28
|
-
private runStart;
|
|
29
|
-
runAsWorker(ctx: NodeLoopContext): Promise<void>;
|
|
30
|
-
/** 執行過程遇到未知錯誤以至於無法繼續執行時:DOING → OPEN(RESET),釋放已認領的任務。 */
|
|
31
|
-
private abortTaskRun;
|
|
32
|
-
/** Task 執行結束後更新 TaskStatus */
|
|
33
|
-
private finalizeTaskRun;
|
|
34
|
-
/**
|
|
35
|
-
* 關閉節點並退出進程。冪等:並發或重複調用復用同一進行中的 Promise,
|
|
36
|
-
* 避免重複 patchStopNode / process.exit。
|
|
37
|
-
*/
|
|
38
|
-
shutdown(options?: {
|
|
39
|
-
trigger?: string;
|
|
40
|
-
}): Promise<void>;
|
|
41
|
-
private runShutdown;
|
|
42
|
-
/** @returns 心跳是否成功 */
|
|
43
|
-
heartbeat(ctx: NodeLoopContext): Promise<boolean>;
|
|
44
|
-
batchCommand(ctx: NodeLoopContext): Promise<void>;
|
|
45
|
-
executeCommand(cmd: CommandType): Promise<void>;
|
|
46
|
-
}
|
|
47
|
-
export { NodeManager };
|
|
48
|
-
//# sourceMappingURL=node-manager.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"node-manager.d.ts","sourceRoot":"","sources":["../../src/node/node-manager.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAO3D,OAAO,EAAE,eAAe,EAAc,MAAM,kBAAkB,CAAC;AAG/D,cAAM,WAAW;IACb,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,2BAA2B,CAAK;IAExD,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,kBAAkB,CAAgB;IAE1C,OAAO,CAAC,SAAS,CAAU;IAC3B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,4BAA4B,CAAK;IACzC,0DAA0D;IAC1D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,yCAAyC;IACzC,OAAO,CAAC,eAAe,CAA8B;IACrD,mCAAmC;IACnC,OAAO,CAAC,YAAY,CAA8B;IAElD,IAAI,iBAAiB,IAAI,SAAS,MAAM,EAAE,CAEzC;IAED,IAAI,WAAW,IAAI,MAAM,GAAG,IAAI,CAE/B;IACD,IAAI,WAAW,CAAC,KAAK,EAAE,MAAM,EAU5B;;IAQD,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,cAAc;IAItB,2FAA2F;IAC3F,OAAO,CAAC,eAAe;IAIvB,4EAA4E;IAC5E,OAAO,CAAC,wBAAwB;IAsD1B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAQd,QAAQ;IAyHhB,WAAW,CAAC,GAAG,EAAE,eAAe;IAwItC,yDAAyD;YAC3C,YAAY;IA8B1B,8BAA8B;YAChB,eAAe;IAoD7B;;;OAGG;IACG,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YAiC/C,WAAW;IAuCzB,sBAAsB;IAChB,SAAS,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC;IA+CjD,YAAY,CAAC,GAAG,EAAE,eAAe;IAgEjC,cAAc,CAAC,GAAG,EAAE,WAAW;CAWxC;AAED,OAAO,EAAE,WAAW,EAAE,CAAC"}
|