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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readdir } from "node:fs/promises";
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { RepoManager } from "../repo/repo-manager";
|
|
@@ -23,6 +23,11 @@ export interface BootstrapLocalTasksOptions {
|
|
|
23
23
|
rootConfigPath?: string;
|
|
24
24
|
/** Base directory for resolving root config (default `process.cwd()`) */
|
|
25
25
|
cwd?: string;
|
|
26
|
+
/**
|
|
27
|
+
* When set with an unchanged config fingerprint, skip full scan if this key is already
|
|
28
|
+
* registered in {@link RepoManager}.
|
|
29
|
+
*/
|
|
30
|
+
forTaskTypeKey?: string;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
const EMPTY_RESULT: BootstrapLocalTasksResult = {
|
|
@@ -32,6 +37,14 @@ const EMPTY_RESULT: BootstrapLocalTasksResult = {
|
|
|
32
37
|
errors: [],
|
|
33
38
|
};
|
|
34
39
|
|
|
40
|
+
type BootstrapCacheState = {
|
|
41
|
+
fingerprint: string | null;
|
|
42
|
+
lastResult: BootstrapLocalTasksResult | null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let bootstrapCache: BootstrapCacheState = { fingerprint: null, lastResult: null };
|
|
46
|
+
let bootstrapInFlight: Promise<BootstrapLocalTasksResult> | null = null;
|
|
47
|
+
|
|
35
48
|
function parseRootConfig(raw: unknown, configPath: string): TaskRunnerRootConfig {
|
|
36
49
|
if (raw == null || typeof raw !== "object") {
|
|
37
50
|
throw new Error(`Root config must export an object: ${configPath}`);
|
|
@@ -78,24 +91,46 @@ function finalizeStatus(result: Omit<BootstrapLocalTasksResult, "status">): Boot
|
|
|
78
91
|
return "loaded";
|
|
79
92
|
}
|
|
80
93
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
async function computeConfigFingerprint(rootConfigPath: string, tasksRoot: string): Promise<string> {
|
|
95
|
+
const rootStat = await stat(rootConfigPath);
|
|
96
|
+
const tasksRootStat = await stat(tasksRoot);
|
|
97
|
+
const entries = await readdir(tasksRoot, { withFileTypes: true });
|
|
98
|
+
const configMtims: number[] = [];
|
|
99
|
+
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
if (!entry.isDirectory()) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const configPath = join(tasksRoot, entry.name, TASK_LOCAL_PACKAGE_CONFIG_FILENAME);
|
|
105
|
+
if (await pathExists(configPath)) {
|
|
106
|
+
const configStat = await stat(configPath);
|
|
107
|
+
configMtims.push(configStat.mtimeMs);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
configMtims.sort((a, b) => a - b);
|
|
112
|
+
return `${rootConfigPath}:${rootStat.mtimeMs}:${tasksRoot}:${tasksRootStat.mtimeMs}:${configMtims.join(",")}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function canUseCachedBootstrap(
|
|
116
|
+
fingerprint: string,
|
|
117
|
+
forTaskTypeKey: string | undefined
|
|
118
|
+
): BootstrapLocalTasksResult | null {
|
|
119
|
+
if (bootstrapCache.fingerprint !== fingerprint || bootstrapCache.lastResult == null) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (forTaskTypeKey != null && forTaskTypeKey !== "") {
|
|
124
|
+
if (!RepoManager.hasLocalRepo(forTaskTypeKey)) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { ...bootstrapCache.lastResult, cached: true };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function runBootstrapScan(
|
|
133
|
+
options: BootstrapLocalTasksOptions
|
|
99
134
|
): Promise<BootstrapLocalTasksResult> {
|
|
100
135
|
const cwd = options.cwd ?? process.cwd();
|
|
101
136
|
|
|
@@ -107,10 +142,12 @@ export async function bootstrapLocalTasks(
|
|
|
107
142
|
],
|
|
108
143
|
});
|
|
109
144
|
if (rootConfigPath == null) {
|
|
110
|
-
|
|
145
|
+
const result: BootstrapLocalTasksResult = {
|
|
111
146
|
...EMPTY_RESULT,
|
|
112
147
|
message: `No root config (tried ${TASK_RUNNER_ROOT_CONFIG_RELATIVE_PATH}, ${TASK_RUNNER_ROOT_CONFIG_LEGACY_RELATIVE_PATH})`,
|
|
113
148
|
};
|
|
149
|
+
bootstrapCache = { fingerprint: null, lastResult: result };
|
|
150
|
+
return result;
|
|
114
151
|
}
|
|
115
152
|
|
|
116
153
|
const result: Omit<BootstrapLocalTasksResult, "status"> = {
|
|
@@ -125,11 +162,19 @@ export async function bootstrapLocalTasks(
|
|
|
125
162
|
|
|
126
163
|
const tasksRoot = resolvePathFromConfigFile(rootConfigPath, taskDir);
|
|
127
164
|
if (!(await pathExists(tasksRoot))) {
|
|
128
|
-
|
|
165
|
+
const failed: BootstrapLocalTasksResult = {
|
|
129
166
|
status: "failed",
|
|
130
167
|
message: `taskDir does not exist: ${tasksRoot}`,
|
|
131
168
|
...result,
|
|
132
169
|
};
|
|
170
|
+
bootstrapCache = { fingerprint: null, lastResult: failed };
|
|
171
|
+
return failed;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const fingerprint = await computeConfigFingerprint(rootConfigPath, tasksRoot);
|
|
175
|
+
const cached = canUseCachedBootstrap(fingerprint, options.forTaskTypeKey);
|
|
176
|
+
if (cached != null) {
|
|
177
|
+
return cached;
|
|
133
178
|
}
|
|
134
179
|
|
|
135
180
|
const entries = await readdir(tasksRoot, { withFileTypes: true });
|
|
@@ -171,7 +216,7 @@ export async function bootstrapLocalTasks(
|
|
|
171
216
|
}
|
|
172
217
|
|
|
173
218
|
const status = finalizeStatus(result);
|
|
174
|
-
|
|
219
|
+
const finalResult: BootstrapLocalTasksResult = {
|
|
175
220
|
status,
|
|
176
221
|
message:
|
|
177
222
|
status === "empty"
|
|
@@ -179,13 +224,55 @@ export async function bootstrapLocalTasks(
|
|
|
179
224
|
: undefined,
|
|
180
225
|
...result,
|
|
181
226
|
};
|
|
227
|
+
bootstrapCache = { fingerprint, lastResult: finalResult };
|
|
228
|
+
return finalResult;
|
|
182
229
|
} catch (error) {
|
|
183
|
-
|
|
230
|
+
const failed: BootstrapLocalTasksResult = {
|
|
184
231
|
status: "failed",
|
|
185
232
|
message: error instanceof Error ? error.message : String(error),
|
|
186
233
|
...result,
|
|
187
234
|
};
|
|
235
|
+
bootstrapCache = { fingerprint: null, lastResult: failed };
|
|
236
|
+
return failed;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Discover local task packages from config files and register them with {@link RepoManager}.
|
|
242
|
+
*
|
|
243
|
+
* **Never throws** for missing root config — returns `{ status: "not_configured" }`.
|
|
244
|
+
*
|
|
245
|
+
* Re-scans are skipped when the config fingerprint is unchanged (root config + taskDir mtimes).
|
|
246
|
+
* Concurrent calls share a single in-flight scan (single-flight).
|
|
247
|
+
*
|
|
248
|
+
* Host app layout (recommended):
|
|
249
|
+
* ```
|
|
250
|
+
* ./config/task.config.js → { taskDir: "../tasks" }
|
|
251
|
+
* ./tasks/my-task/task.config.js
|
|
252
|
+
* ./tasks/my-task/index.mjs
|
|
253
|
+
* ```
|
|
254
|
+
*
|
|
255
|
+
* `taskDir` / `entry` paths are relative to **their config file's directory** (see config-file-relative-paths skill).
|
|
256
|
+
*
|
|
257
|
+
* Legacy fallback: `./task.config.js` at project root (`taskDir: "./tasks"`).
|
|
258
|
+
*/
|
|
259
|
+
export async function bootstrapLocalTasks(
|
|
260
|
+
options: BootstrapLocalTasksOptions = {}
|
|
261
|
+
): Promise<BootstrapLocalTasksResult> {
|
|
262
|
+
if (bootstrapInFlight != null) {
|
|
263
|
+
return bootstrapInFlight;
|
|
188
264
|
}
|
|
265
|
+
|
|
266
|
+
bootstrapInFlight = runBootstrapScan(options).finally(() => {
|
|
267
|
+
bootstrapInFlight = null;
|
|
268
|
+
});
|
|
269
|
+
return bootstrapInFlight;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Clear fingerprint cache (tests / hot-reload tooling). */
|
|
273
|
+
export function clearBootstrapLocalTasksCache(): void {
|
|
274
|
+
bootstrapCache = { fingerprint: null, lastResult: null };
|
|
275
|
+
bootstrapInFlight = null;
|
|
189
276
|
}
|
|
190
277
|
|
|
191
278
|
export function getRegisteredLocalTaskTypeKeys(): string[] {
|
package/src/task/task-manager.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { RepoManager } from "../repo/repo-manager";
|
|
2
1
|
import { getScanRemoteRepoValues } from "../repo/repo.api";
|
|
3
2
|
import { TASK_REPO_USAGE } from "../repo/repo.interface";
|
|
4
|
-
import {
|
|
3
|
+
import { LoggerWithScope } from "../shared/log";
|
|
5
4
|
import { ResultCode, Task } from "./task.interface";
|
|
6
|
-
import { TaskFn,
|
|
5
|
+
import { TaskFn, TaskRunResult, ExecutableTaskFn } from "./task-repo-context";
|
|
7
6
|
|
|
8
7
|
export type {
|
|
9
8
|
TaskFn,
|
|
@@ -28,15 +27,21 @@ export {
|
|
|
28
27
|
import {
|
|
29
28
|
bootstrapLocalTasks,
|
|
30
29
|
BootstrapLocalTasksOptions,
|
|
30
|
+
clearBootstrapLocalTasksCache,
|
|
31
31
|
getRegisteredLocalTaskTypeKeys,
|
|
32
32
|
} from "./local-task-registry";
|
|
33
33
|
import type { BootstrapLocalTasksResult } from "./task-repo-context";
|
|
34
|
+
import { TaskRepoValidation, type TaskRepoValidationFailureReason } from "./task-repo-validation";
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
/** @deprecated use {@link TaskRepoValidationFailureReason} */
|
|
37
|
+
export type PrepareTaskFailureReason = TaskRepoValidationFailureReason;
|
|
38
|
+
|
|
39
|
+
export type {
|
|
40
|
+
TaskRepoValidationFailureReason,
|
|
41
|
+
ValidateTaskRepoAndParamsInput,
|
|
42
|
+
ValidateTaskRepoAndParamsResult,
|
|
43
|
+
} from "./task-repo-validation";
|
|
44
|
+
export { TaskRepoValidation } from "./task-repo-validation";
|
|
40
45
|
|
|
41
46
|
export interface PrepareTaskResponse {
|
|
42
47
|
isSuccess: boolean;
|
|
@@ -49,12 +54,18 @@ export interface PrepareTaskResponse {
|
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
export interface PrepareTaskParams {
|
|
52
|
-
logger:
|
|
57
|
+
logger: LoggerWithScope;
|
|
53
58
|
task: Task;
|
|
59
|
+
/**
|
|
60
|
+
* When false, do not fetch Repo from Supabase during validation.
|
|
61
|
+
* Should match {@link RegisterTasksOptions.includeRemote} on the worker.
|
|
62
|
+
*/
|
|
63
|
+
includeRemote?: boolean;
|
|
64
|
+
bootstrapLocalOptions?: BootstrapLocalTasksOptions;
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
export interface RegisterTasksOptions {
|
|
57
|
-
logger:
|
|
68
|
+
logger: LoggerWithScope;
|
|
58
69
|
local?: BootstrapLocalTasksOptions;
|
|
59
70
|
/** When `true` (default), union local keys with remote `Repo.value` from Supabase. */
|
|
60
71
|
includeRemote?: boolean;
|
|
@@ -64,32 +75,33 @@ export interface RegisterTasksResult {
|
|
|
64
75
|
availableTaskList: string[];
|
|
65
76
|
local: BootstrapLocalTasksResult;
|
|
66
77
|
remote: { values: string[] };
|
|
78
|
+
includeRemote: boolean;
|
|
67
79
|
}
|
|
68
80
|
|
|
69
|
-
function logLocalBootstrap(logger:
|
|
81
|
+
function logLocalBootstrap(logger: LoggerWithScope, bootstrap: BootstrapLocalTasksResult): void {
|
|
70
82
|
switch (bootstrap.status) {
|
|
71
83
|
case "not_configured":
|
|
72
84
|
logger.info("未找到本地任務配置,跳過本地註冊", {
|
|
73
85
|
topic: "task",
|
|
74
|
-
|
|
86
|
+
data: { message: bootstrap.message },
|
|
75
87
|
});
|
|
76
88
|
break;
|
|
77
89
|
case "failed":
|
|
78
90
|
logger.warn("本地任務註冊失敗", {
|
|
79
91
|
topic: "task",
|
|
80
|
-
|
|
92
|
+
data: { message: bootstrap.message, errors: bootstrap.errors },
|
|
81
93
|
});
|
|
82
94
|
break;
|
|
83
95
|
case "empty":
|
|
84
96
|
logger.info("本地任務掃描完成,無已註冊任務", {
|
|
85
97
|
topic: "task",
|
|
86
|
-
|
|
98
|
+
data: { message: bootstrap.message, skipped: bootstrap.skipped },
|
|
87
99
|
});
|
|
88
100
|
break;
|
|
89
101
|
case "loaded":
|
|
90
102
|
logger.info("本地任務註冊完成", {
|
|
91
103
|
topic: "task",
|
|
92
|
-
|
|
104
|
+
data: {
|
|
93
105
|
registered: bootstrap.registered,
|
|
94
106
|
skipped: bootstrap.skipped,
|
|
95
107
|
errors: bootstrap.errors,
|
|
@@ -122,7 +134,7 @@ async function registerTasks({
|
|
|
122
134
|
if (includeRemote) {
|
|
123
135
|
logger.info("查詢遠程 Repo 列表", {
|
|
124
136
|
topic: "task",
|
|
125
|
-
|
|
137
|
+
data: { usage: TASK_REPO_USAGE },
|
|
126
138
|
});
|
|
127
139
|
const { data, error: remoteError } = await getScanRemoteRepoValues({ usage: TASK_REPO_USAGE });
|
|
128
140
|
if (remoteError) {
|
|
@@ -131,7 +143,7 @@ async function registerTasks({
|
|
|
131
143
|
remoteValues = data ?? [];
|
|
132
144
|
logger.info("遠程 Repo 查詢完成", {
|
|
133
145
|
topic: "task",
|
|
134
|
-
|
|
146
|
+
data: { count: remoteValues.length, values: remoteValues },
|
|
135
147
|
});
|
|
136
148
|
}
|
|
137
149
|
|
|
@@ -142,7 +154,7 @@ async function registerTasks({
|
|
|
142
154
|
|
|
143
155
|
logger.success("任務註冊完成", {
|
|
144
156
|
topic: "task",
|
|
145
|
-
|
|
157
|
+
data: {
|
|
146
158
|
availableTaskList,
|
|
147
159
|
localRegistered: local.registered,
|
|
148
160
|
remoteValues,
|
|
@@ -153,10 +165,11 @@ async function registerTasks({
|
|
|
153
165
|
availableTaskList,
|
|
154
166
|
local,
|
|
155
167
|
remote: { values: remoteValues },
|
|
168
|
+
includeRemote,
|
|
156
169
|
};
|
|
157
170
|
}
|
|
158
171
|
|
|
159
|
-
/** 将 taskFn 与 params 闭包绑定,供
|
|
172
|
+
/** 将 taskFn 与 params 闭包绑定,供 TaskNode 无参调用 `await taskFn()` */
|
|
160
173
|
function bindTaskFn(taskFn: TaskFn, taskParams: unknown): ExecutableTaskFn {
|
|
161
174
|
const run = async () => taskFn(taskParams);
|
|
162
175
|
return Object.assign(run, {
|
|
@@ -165,36 +178,23 @@ function bindTaskFn(taskFn: TaskFn, taskParams: unknown): ExecutableTaskFn {
|
|
|
165
178
|
});
|
|
166
179
|
}
|
|
167
180
|
|
|
168
|
-
/** 调用 Repo 模块导出的 taskParamsValidator;抛错视为校验失败 */
|
|
169
|
-
function validateTaskParams(
|
|
170
|
-
validator: TaskRepoContext["taskParamsValidator"],
|
|
171
|
-
taskParams: unknown,
|
|
172
|
-
logger: LoggerWithContext
|
|
173
|
-
): boolean {
|
|
174
|
-
try {
|
|
175
|
-
return validator(taskParams) === true;
|
|
176
|
-
} catch (error) {
|
|
177
|
-
logger.warn("taskParamsValidator 拋出異常", {
|
|
178
|
-
topic: "task",
|
|
179
|
-
context: { error: error instanceof Error ? error.message : error },
|
|
180
|
-
});
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
181
|
/**
|
|
186
182
|
* 將 Supabase 認領到的 Task 解析為可執行的 `ExecutableTaskFn`。
|
|
187
183
|
*
|
|
188
184
|
* 流程:
|
|
189
185
|
* 1. 從 task.details 取出 `params`;`task.value` 作為 taskTypeKey(同 {@link Repo.value})
|
|
190
|
-
* 2. {@link
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* 5. bindTaskFn,返回閉包後的無參執行函數
|
|
186
|
+
* 2. {@link TaskRepoValidation.validate} — 本地註冊優先;未命中時可選遠端(`includeRemote`)
|
|
187
|
+
* Worker 在 registry 未命中時會補掃本地(`local_bootstrap_on_miss`)
|
|
188
|
+
* 3. bindTaskFn,返回閉包後的無參執行函數
|
|
194
189
|
*
|
|
195
190
|
* 失敗時返回 `{ isSuccess: false, code, message, reason, step }`(不 throw)。
|
|
196
191
|
*/
|
|
197
|
-
const prepareTask = async ({
|
|
192
|
+
const prepareTask = async ({
|
|
193
|
+
logger,
|
|
194
|
+
task,
|
|
195
|
+
includeRemote = true,
|
|
196
|
+
bootstrapLocalOptions,
|
|
197
|
+
}: PrepareTaskParams): Promise<PrepareTaskResponse> => {
|
|
198
198
|
const fail = (params: {
|
|
199
199
|
reason: PrepareTaskFailureReason;
|
|
200
200
|
code: ResultCode;
|
|
@@ -203,7 +203,7 @@ const prepareTask = async ({ logger, task }: PrepareTaskParams): Promise<Prepare
|
|
|
203
203
|
}): PrepareTaskResponse => {
|
|
204
204
|
logger.warn(params.message, {
|
|
205
205
|
topic: "task",
|
|
206
|
-
|
|
206
|
+
data: {
|
|
207
207
|
taskId: task.id,
|
|
208
208
|
taskTypeKey: task.value,
|
|
209
209
|
reason: params.reason,
|
|
@@ -223,72 +223,60 @@ const prepareTask = async ({ logger, task }: PrepareTaskParams): Promise<Prepare
|
|
|
223
223
|
const { id: taskId, name: taskName, value: taskTypeKey, details } = task;
|
|
224
224
|
logger.info(`開始準備任務 ${taskName}-${taskTypeKey}`, {
|
|
225
225
|
topic: "task",
|
|
226
|
-
|
|
226
|
+
data: { taskId, taskName, taskTypeKey },
|
|
227
227
|
});
|
|
228
228
|
|
|
229
229
|
const { params: taskParams } = details;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
230
|
+
|
|
231
|
+
if (!getRegisteredLocalTaskTypeKeys().includes(taskTypeKey)) {
|
|
232
|
+
const bootstrap = await bootstrapLocalTasks({
|
|
233
|
+
...bootstrapLocalOptions,
|
|
234
|
+
forTaskTypeKey: taskTypeKey,
|
|
235
|
+
});
|
|
236
|
+
logger.debug("本地 registry 未命中,補掃本地任務", {
|
|
237
|
+
topic: "task",
|
|
238
|
+
data: {
|
|
239
|
+
taskId,
|
|
240
|
+
taskTypeKey,
|
|
241
|
+
step: "local_bootstrap_on_miss",
|
|
242
|
+
status: bootstrap.status,
|
|
243
|
+
registered: bootstrap.registered,
|
|
244
|
+
cached: bootstrap.cached ?? false,
|
|
245
|
+
message: bootstrap.message,
|
|
246
|
+
},
|
|
236
247
|
});
|
|
237
248
|
}
|
|
238
249
|
|
|
239
|
-
|
|
240
|
-
topic: "task",
|
|
241
|
-
context: { taskTypeKey, step: "repo_load" },
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const { context: repoContext, error: repoError } = await RepoManager.getRepoContext<TaskRepoContext>({
|
|
250
|
+
const validation = await TaskRepoValidation.validate({
|
|
245
251
|
logger,
|
|
246
252
|
taskTypeKey,
|
|
253
|
+
params: taskParams,
|
|
254
|
+
includeRemote,
|
|
247
255
|
});
|
|
248
|
-
|
|
249
|
-
if (repoContext == null) {
|
|
256
|
+
if (!validation.isValid) {
|
|
250
257
|
return fail({
|
|
251
|
-
reason:
|
|
252
|
-
code:
|
|
253
|
-
message:
|
|
254
|
-
step:
|
|
258
|
+
reason: validation.reason,
|
|
259
|
+
code: validation.code,
|
|
260
|
+
message: validation.message,
|
|
261
|
+
step: validation.step,
|
|
255
262
|
});
|
|
256
263
|
}
|
|
257
264
|
|
|
265
|
+
const { repoContext } = validation;
|
|
266
|
+
|
|
258
267
|
logger.info("Repo 上下文加載成功", {
|
|
259
268
|
topic: "task",
|
|
260
|
-
|
|
269
|
+
data: {
|
|
261
270
|
taskTypeKey,
|
|
262
271
|
taskFnDisplayName: repoContext.taskFn.displayName,
|
|
263
272
|
step: "repo_load",
|
|
264
273
|
},
|
|
265
274
|
});
|
|
266
275
|
|
|
267
|
-
if (repoContext.taskFn.taskTypeKey !== taskTypeKey) {
|
|
268
|
-
return fail({
|
|
269
|
-
reason: "TASK_TYPE_KEY_MISMATCH",
|
|
270
|
-
code: ResultCode.REPO_NOT_VALID,
|
|
271
|
-
message: `taskFn.taskTypeKey (${repoContext.taskFn.taskTypeKey}) 與 task.value (${taskTypeKey}) 不一致`,
|
|
272
|
-
step: "task_type_key",
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
logger.debug("開始校驗任務參數", { topic: "task", context: { taskTypeKey, step: "params_validation" } });
|
|
277
|
-
if (!validateTaskParams(repoContext.taskParamsValidator, taskParams, logger)) {
|
|
278
|
-
return fail({
|
|
279
|
-
reason: "PARAMS_VALIDATION_FAILED",
|
|
280
|
-
code: ResultCode.PARAMS_NOT_VALID,
|
|
281
|
-
message: "任務參數校驗失敗",
|
|
282
|
-
step: "params_validation",
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
logger.debug("任務參數校驗通過", { topic: "task", context: { taskTypeKey, step: "params_validation" } });
|
|
287
|
-
|
|
288
276
|
const boundTaskFn = bindTaskFn(repoContext.taskFn, taskParams);
|
|
289
277
|
logger.success("任務準備完成", {
|
|
290
278
|
topic: "task",
|
|
291
|
-
|
|
279
|
+
data: {
|
|
292
280
|
taskId,
|
|
293
281
|
taskTypeKey,
|
|
294
282
|
displayName: boundTaskFn.displayName,
|
|
@@ -307,6 +295,7 @@ const TaskManager = {
|
|
|
307
295
|
prepareTask,
|
|
308
296
|
bootstrapLocalTasks,
|
|
309
297
|
getRegisteredLocalTaskTypeKeys,
|
|
298
|
+
clearBootstrapLocalTasksCache,
|
|
310
299
|
};
|
|
311
300
|
|
|
312
301
|
export { TaskManager };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createTarget, validateWithSchema } from "../core.api";
|
|
2
|
+
import { generateResponse, type SupabaseResponse } from "../core.interface";
|
|
3
|
+
import { createApiLogger, logManager } from "../shared/log";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { CategoryTask, ResultCode, Task, TaskStatus } from "./task.interface";
|
|
6
|
+
import { TaskRepoValidation } from "./task-repo-validation";
|
|
7
|
+
|
|
8
|
+
const traceIdSchema = z.string().trim().min(1).optional();
|
|
9
|
+
const traceParentIdSchema = z.string().trim().min(1).nullable().optional();
|
|
10
|
+
|
|
11
|
+
/** Initial status for a new task — OPEN (draft) or TODO (claimable by workers). */
|
|
12
|
+
const postTaskInitialStatusSchema = z
|
|
13
|
+
.union([z.literal(TaskStatus.OPEN), z.literal(TaskStatus.TODO)])
|
|
14
|
+
.optional()
|
|
15
|
+
.default(TaskStatus.TODO);
|
|
16
|
+
|
|
17
|
+
export const postTaskSchema = z.object({
|
|
18
|
+
name: z.string().trim().min(1),
|
|
19
|
+
/** Task type key — matches `task.value`, {@link patchClaimTask}, {@link Repo.value}, and local Repo registry. */
|
|
20
|
+
value: z.string().trim().min(1),
|
|
21
|
+
params: z.unknown(),
|
|
22
|
+
taskStatus: postTaskInitialStatusSchema,
|
|
23
|
+
tagList: z.array(z.string()).optional().default([]),
|
|
24
|
+
extra: z.string().optional(),
|
|
25
|
+
traceId: traceIdSchema,
|
|
26
|
+
traceParentId: traceParentIdSchema,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export type PostTaskPayload = z.infer<typeof postTaskSchema>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a task row (scheduler / admin). **Node entry only** — requires local Repo bootstrap.
|
|
33
|
+
*
|
|
34
|
+
* @see target-supabase-sdk/node
|
|
35
|
+
*/
|
|
36
|
+
export const postTask = validateWithSchema(
|
|
37
|
+
postTaskSchema,
|
|
38
|
+
"postTaskSchema",
|
|
39
|
+
)(async ({
|
|
40
|
+
name,
|
|
41
|
+
value,
|
|
42
|
+
params,
|
|
43
|
+
taskStatus,
|
|
44
|
+
tagList,
|
|
45
|
+
extra,
|
|
46
|
+
traceId,
|
|
47
|
+
traceParentId,
|
|
48
|
+
}) => {
|
|
49
|
+
const taskTraceId = traceId?.trim() || logManager.generateTraceId();
|
|
50
|
+
const logger = createApiLogger("postTask", {
|
|
51
|
+
traceId: taskTraceId,
|
|
52
|
+
traceParentId: traceParentId ?? null,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const validation = await TaskRepoValidation.validate({
|
|
56
|
+
logger,
|
|
57
|
+
taskTypeKey: value,
|
|
58
|
+
params,
|
|
59
|
+
bootstrapLocal: true,
|
|
60
|
+
});
|
|
61
|
+
if (!validation.isValid) {
|
|
62
|
+
logger.warn(validation.message, {
|
|
63
|
+
topic: "task",
|
|
64
|
+
data: {
|
|
65
|
+
taskTypeKey: value,
|
|
66
|
+
reason: validation.reason,
|
|
67
|
+
step: validation.step,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
return generateResponse.error(
|
|
71
|
+
validation.message,
|
|
72
|
+
undefined,
|
|
73
|
+
String(validation.code),
|
|
74
|
+
) as SupabaseResponse<Task>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const result = await createTarget<Task, PostTaskPayload>({
|
|
78
|
+
payload: {
|
|
79
|
+
name,
|
|
80
|
+
value,
|
|
81
|
+
params,
|
|
82
|
+
taskStatus,
|
|
83
|
+
tagList,
|
|
84
|
+
extra,
|
|
85
|
+
traceId: taskTraceId,
|
|
86
|
+
traceParentId,
|
|
87
|
+
},
|
|
88
|
+
createFn: () => ({
|
|
89
|
+
name,
|
|
90
|
+
value,
|
|
91
|
+
category: CategoryTask.TASK,
|
|
92
|
+
tagList,
|
|
93
|
+
extra,
|
|
94
|
+
details: {
|
|
95
|
+
manifestVersion: 0,
|
|
96
|
+
status: taskStatus,
|
|
97
|
+
params,
|
|
98
|
+
progress: 0,
|
|
99
|
+
nodeId: null,
|
|
100
|
+
traceId: taskTraceId,
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
logger.info("任務已創建", {
|
|
106
|
+
topic: "task",
|
|
107
|
+
data: {
|
|
108
|
+
taskId: result.data?.id,
|
|
109
|
+
taskTypeKey: value,
|
|
110
|
+
status: taskStatus,
|
|
111
|
+
traceId: taskTraceId,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return result;
|
|
116
|
+
});
|
|
@@ -86,6 +86,8 @@ export interface BootstrapLocalTasksResult {
|
|
|
86
86
|
registered: string[];
|
|
87
87
|
skipped: { taskDir: string; reason: string }[];
|
|
88
88
|
errors: { taskDir: string; error: string }[];
|
|
89
|
+
/** True when scan was skipped due to unchanged config fingerprint */
|
|
90
|
+
cached?: boolean;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
/** Host app config directory (relative to `cwd`) — recommended for library consumers */
|