target-supabase-sdk 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/README.md +74 -6
  2. package/dist/auth/auth.api.d.ts.map +1 -1
  3. package/dist/auth/auth.api.js +2 -1
  4. package/dist/auth/auth.api.js.map +1 -1
  5. package/dist/browser.d.ts +52 -0
  6. package/dist/browser.d.ts.map +1 -0
  7. package/dist/browser.js +45 -0
  8. package/dist/browser.js.map +1 -0
  9. package/dist/command/command.api.d.ts +43 -0
  10. package/dist/command/command.api.d.ts.map +1 -0
  11. package/dist/command/command.api.js +58 -0
  12. package/dist/command/command.api.js.map +1 -0
  13. package/dist/command/command.interface.d.ts +14 -0
  14. package/dist/command/command.interface.d.ts.map +1 -0
  15. package/dist/command/command.interface.js +9 -0
  16. package/dist/command/command.interface.js.map +1 -0
  17. package/dist/core.api.d.ts +70 -6
  18. package/dist/core.api.d.ts.map +1 -1
  19. package/dist/core.api.js +191 -13
  20. package/dist/core.api.js.map +1 -1
  21. package/dist/file/file-manager.d.ts +1 -1
  22. package/dist/file/file-manager.d.ts.map +1 -1
  23. package/dist/file/file-manager.js +1 -1
  24. package/dist/file/file-manager.js.map +1 -1
  25. package/dist/index.d.ts +2 -40
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +2 -34
  28. package/dist/index.js.map +1 -1
  29. package/dist/link/link.interface.d.ts +1 -1
  30. package/dist/link/link.interface.d.ts.map +1 -1
  31. package/dist/node/node-log-scope.d.ts +11 -0
  32. package/dist/node/node-log-scope.d.ts.map +1 -0
  33. package/dist/node/node-log-scope.js +7 -0
  34. package/dist/node/node-log-scope.js.map +1 -0
  35. package/dist/node/node-runtime.base.d.ts +42 -0
  36. package/dist/node/node-runtime.base.d.ts.map +1 -0
  37. package/dist/node/node-runtime.base.js +397 -0
  38. package/dist/node/node-runtime.base.js.map +1 -0
  39. package/dist/node/node-task.preset.d.ts +1 -0
  40. package/dist/node/node-task.preset.d.ts.map +1 -0
  41. package/dist/node/node-task.preset.js +14 -0
  42. package/dist/node/node-task.preset.js.map +1 -0
  43. package/dist/node/node.api.d.ts +67 -31
  44. package/dist/node/node.api.d.ts.map +1 -1
  45. package/dist/node/node.api.js +65 -102
  46. package/dist/node/node.api.js.map +1 -1
  47. package/dist/node/node.interface.d.ts +7 -0
  48. package/dist/node/node.interface.d.ts.map +1 -1
  49. package/dist/node/node.interface.js +2 -0
  50. package/dist/node/node.interface.js.map +1 -1
  51. package/dist/node/node.utils.d.ts +5 -0
  52. package/dist/node/node.utils.d.ts.map +1 -0
  53. package/dist/node/node.utils.js +36 -0
  54. package/dist/node/node.utils.js.map +1 -0
  55. package/dist/node/task-node.d.ts +20 -0
  56. package/dist/node/task-node.d.ts.map +1 -0
  57. package/dist/node/task-node.js +245 -0
  58. package/dist/node/task-node.js.map +1 -0
  59. package/dist/node/trigger-node.d.ts +18 -0
  60. package/dist/node/trigger-node.d.ts.map +1 -0
  61. package/dist/node/trigger-node.js +150 -0
  62. package/dist/node/trigger-node.js.map +1 -0
  63. package/dist/node.d.ts +22 -0
  64. package/dist/node.d.ts.map +1 -0
  65. package/dist/node.js +19 -0
  66. package/dist/node.js.map +1 -0
  67. package/dist/parcel/parcel-manager.d.ts +1 -1
  68. package/dist/parcel/parcel-manager.d.ts.map +1 -1
  69. package/dist/parcel/parcel-manager.js +1 -1
  70. package/dist/parcel/parcel-manager.js.map +1 -1
  71. package/dist/repo/repo-context.utils.d.ts +24 -0
  72. package/dist/repo/repo-context.utils.d.ts.map +1 -0
  73. package/dist/repo/repo-context.utils.js +81 -0
  74. package/dist/repo/repo-context.utils.js.map +1 -0
  75. package/dist/repo/repo-manager.d.ts +42 -0
  76. package/dist/repo/repo-manager.d.ts.map +1 -0
  77. package/dist/repo/repo-manager.js +166 -0
  78. package/dist/repo/repo-manager.js.map +1 -0
  79. package/dist/repo/repo.api.d.ts +16 -0
  80. package/dist/repo/repo.api.d.ts.map +1 -0
  81. package/dist/repo/repo.api.js +40 -0
  82. package/dist/repo/repo.api.js.map +1 -0
  83. package/dist/repo/repo.interface.d.ts +12 -5
  84. package/dist/repo/repo.interface.d.ts.map +1 -1
  85. package/dist/repo/repo.interface.js +2 -2
  86. package/dist/repo/repo.interface.js.map +1 -1
  87. package/dist/repo/repo.script-loader.d.ts +19 -0
  88. package/dist/repo/repo.script-loader.d.ts.map +1 -0
  89. package/dist/repo/repo.script-loader.js +108 -0
  90. package/dist/repo/repo.script-loader.js.map +1 -0
  91. package/dist/shared/log/create-api-logger.d.ts +21 -0
  92. package/dist/shared/log/create-api-logger.d.ts.map +1 -0
  93. package/dist/shared/log/create-api-logger.js +19 -0
  94. package/dist/shared/log/create-api-logger.js.map +1 -0
  95. package/dist/shared/log/index.d.ts +10 -0
  96. package/dist/shared/log/index.d.ts.map +1 -0
  97. package/dist/shared/log/index.js +10 -0
  98. package/dist/shared/log/index.js.map +1 -0
  99. package/dist/shared/log/log-manager.d.ts +93 -0
  100. package/dist/shared/log/log-manager.d.ts.map +1 -0
  101. package/dist/shared/log/log-manager.js +237 -0
  102. package/dist/shared/log/log-manager.js.map +1 -0
  103. package/dist/shared/log/log-scope.d.ts +49 -0
  104. package/dist/shared/log/log-scope.d.ts.map +1 -0
  105. package/dist/shared/log/log-scope.js +70 -0
  106. package/dist/shared/log/log-scope.js.map +1 -0
  107. package/dist/shared/utils/config-path.utils.d.ts +38 -0
  108. package/dist/shared/utils/config-path.utils.d.ts.map +1 -0
  109. package/dist/shared/utils/config-path.utils.js +85 -0
  110. package/dist/shared/utils/config-path.utils.js.map +1 -0
  111. package/dist/shared/utils/error.utils.d.ts +11 -0
  112. package/dist/shared/utils/error.utils.d.ts.map +1 -0
  113. package/dist/shared/utils/error.utils.js +15 -0
  114. package/dist/shared/utils/error.utils.js.map +1 -0
  115. package/dist/shared/utils/id.utils.d.ts +3 -0
  116. package/dist/shared/utils/id.utils.d.ts.map +1 -0
  117. package/dist/shared/utils/id.utils.js +5 -0
  118. package/dist/shared/utils/id.utils.js.map +1 -0
  119. package/dist/supabase.js +1 -1
  120. package/dist/supabase.js.map +1 -1
  121. package/dist/task/index.d.ts +15 -0
  122. package/dist/task/index.d.ts.map +1 -0
  123. package/dist/task/index.js +14 -0
  124. package/dist/task/index.js.map +1 -0
  125. package/dist/task/local-task-registry.d.ts +37 -0
  126. package/dist/task/local-task-registry.d.ts.map +1 -0
  127. package/dist/task/local-task-registry.js +214 -0
  128. package/dist/task/local-task-registry.js.map +1 -0
  129. package/dist/task/task-manager.d.ts +64 -0
  130. package/dist/task/task-manager.d.ts.map +1 -0
  131. package/dist/task/task-manager.js +196 -0
  132. package/dist/task/task-manager.js.map +1 -0
  133. package/dist/task/task-post.api.d.ts +49 -0
  134. package/dist/task/task-post.api.d.ts.map +1 -0
  135. package/dist/task/task-post.api.js +91 -0
  136. package/dist/task/task-post.api.js.map +1 -0
  137. package/dist/task/task-repo-context.d.ts +97 -0
  138. package/dist/task/task-repo-context.d.ts.map +1 -0
  139. package/dist/task/task-repo-context.js +12 -0
  140. package/dist/task/task-repo-context.js.map +1 -0
  141. package/dist/task/task-repo-validation.d.ts +46 -0
  142. package/dist/task/task-repo-validation.d.ts.map +1 -0
  143. package/dist/task/task-repo-validation.js +96 -0
  144. package/dist/task/task-repo-validation.js.map +1 -0
  145. package/dist/task/task.api.d.ts +198 -0
  146. package/dist/task/task.api.d.ts.map +1 -1
  147. package/dist/task/task.api.js +302 -1
  148. package/dist/task/task.api.js.map +1 -1
  149. package/dist/task/task.interface.d.ts +11 -2
  150. package/dist/task/task.interface.d.ts.map +1 -1
  151. package/dist/task/task.interface.js +10 -0
  152. package/dist/task/task.interface.js.map +1 -1
  153. package/dist/trigger/trigger.api.d.ts +140 -0
  154. package/dist/trigger/trigger.api.d.ts.map +1 -0
  155. package/dist/trigger/trigger.api.js +125 -0
  156. package/dist/trigger/trigger.api.js.map +1 -0
  157. package/dist/trigger/trigger.interface.d.ts +43 -0
  158. package/dist/trigger/trigger.interface.d.ts.map +1 -0
  159. package/dist/trigger/trigger.interface.js +11 -0
  160. package/dist/trigger/trigger.interface.js.map +1 -0
  161. package/dist/trigger/trigger.utils.d.ts +6 -0
  162. package/dist/trigger/trigger.utils.d.ts.map +1 -0
  163. package/dist/trigger/trigger.utils.js +39 -0
  164. package/dist/trigger/trigger.utils.js.map +1 -0
  165. package/package.json +30 -8
  166. package/src/auth/auth.api.ts +3 -1
  167. package/src/browser.ts +100 -0
  168. package/src/command/command.api.ts +88 -0
  169. package/src/command/command.interface.ts +17 -0
  170. package/src/core.api.ts +305 -26
  171. package/src/file/file-manager.ts +1 -1
  172. package/src/index.ts +2 -64
  173. package/src/link/link.interface.ts +1 -1
  174. package/src/node/node-log-scope.ts +15 -0
  175. package/src/node/node-runtime.base.ts +450 -0
  176. package/src/node/node-task.preset.ts +13 -0
  177. package/src/node/node.api.ts +95 -117
  178. package/src/node/node.interface.ts +8 -0
  179. package/src/node/node.utils.ts +36 -0
  180. package/src/node/task-node.ts +284 -0
  181. package/src/node/trigger-node.ts +180 -0
  182. package/src/node.ts +62 -0
  183. package/src/parcel/parcel-manager.ts +1 -1
  184. package/src/repo/repo-context.utils.ts +111 -0
  185. package/src/repo/repo-manager.ts +245 -0
  186. package/src/repo/repo.api.ts +50 -0
  187. package/src/repo/repo.interface.ts +13 -5
  188. package/src/repo/repo.script-loader.ts +140 -0
  189. package/src/shared/log/create-api-logger.ts +36 -0
  190. package/src/shared/log/index.ts +23 -0
  191. package/src/shared/log/log-manager.ts +335 -0
  192. package/src/shared/log/log-scope.ts +123 -0
  193. package/src/shared/utils/config-path.utils.ts +104 -0
  194. package/src/shared/utils/error.utils.ts +15 -0
  195. package/src/shared/utils/id.utils.ts +4 -0
  196. package/src/supabase.ts +1 -1
  197. package/src/task/index.ts +48 -0
  198. package/src/task/local-task-registry.ts +282 -0
  199. package/src/task/task-manager.ts +301 -0
  200. package/src/task/task-post.api.ts +116 -0
  201. package/src/task/task-repo-context.ts +105 -0
  202. package/src/task/task-repo-validation.ts +171 -0
  203. package/src/task/task.api.ts +369 -0
  204. package/src/task/task.interface.ts +12 -2
  205. package/src/trigger/trigger.api.ts +166 -0
  206. package/src/trigger/trigger.interface.ts +50 -0
  207. package/src/trigger/trigger.utils.ts +56 -0
  208. package/dist/task/task-node-manager.d.ts +0 -1
  209. package/dist/task/task-node-manager.d.ts.map +0 -1
  210. package/dist/task/task-node-manager.js +0 -2
  211. package/dist/task/task-node-manager.js.map +0 -1
  212. package/src/task/task-node-manager.ts +0 -0
@@ -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";
@@ -206,4 +206,4 @@ const ParcelManager = {
206
206
  reassembly,
207
207
  };
208
208
 
209
- export default ParcelManager;
209
+ export { ParcelManager };
@@ -0,0 +1,111 @@
1
+ import {
2
+ TASK_REPO_SCRIPT_CATEGORY,
3
+ type TaskRepoContext,
4
+ type TaskRepoScriptDetails,
5
+ type TaskRepoScriptRecord,
6
+ } from "../task/task-repo-context";
7
+
8
+ export { TASK_REPO_SCRIPT_CATEGORY };
9
+
10
+ export interface LoadedRepoContext {
11
+ context: TaskRepoContext;
12
+ /** Cache key segment — typically repo hash or script hash */
13
+ contentHash: string;
14
+ }
15
+
16
+ export function isTaskRepoContext(value: unknown): value is TaskRepoContext {
17
+ if (value == null || typeof value !== "object") {
18
+ return false;
19
+ }
20
+ const record = value as TaskRepoContext;
21
+ return typeof record.taskParamsValidator === "function" && typeof record.taskFn === "function";
22
+ }
23
+
24
+ /**
25
+ * Normalize a dynamically imported module into {@link TaskRepoContext}.
26
+ * Supported shapes:
27
+ * - `export default { taskParamsValidator, taskFn }`
28
+ * - `export { taskParamsValidator, taskFn }`
29
+ * - `export default taskFn` (validator becomes `() => true`)
30
+ */
31
+ export function normalizeRepoContextModule(
32
+ moduleExports: unknown,
33
+ taskTypeKey: string,
34
+ exportName?: string
35
+ ): TaskRepoContext | null {
36
+ if (moduleExports == null) {
37
+ return null;
38
+ }
39
+
40
+ const record = moduleExports as Record<string, unknown>;
41
+ const namedExport =
42
+ exportName != null && exportName !== "" ? record[exportName] : undefined;
43
+ const defaultExport = record.default;
44
+ const candidate = namedExport ?? defaultExport ?? moduleExports;
45
+
46
+ let taskParamsValidator: unknown;
47
+ let taskFn: unknown;
48
+
49
+ if (typeof candidate === "function") {
50
+ taskFn = candidate;
51
+ taskParamsValidator = record.taskParamsValidator;
52
+ } else if (typeof candidate === "object" && candidate != null) {
53
+ const ctx = candidate as Record<string, unknown>;
54
+ taskParamsValidator = ctx.taskParamsValidator;
55
+ taskFn = ctx.taskFn;
56
+ }
57
+
58
+ taskParamsValidator ??= record.taskParamsValidator;
59
+ taskFn ??= record.taskFn;
60
+
61
+ if (typeof taskFn !== "function") {
62
+ return null;
63
+ }
64
+
65
+ if (typeof taskParamsValidator !== "function") {
66
+ taskParamsValidator = () => true;
67
+ }
68
+
69
+ const fn = taskFn as TaskRepoContext["taskFn"];
70
+ if (fn.displayName == null || fn.displayName === "") {
71
+ fn.displayName = taskTypeKey;
72
+ }
73
+ if (fn.taskTypeKey == null || fn.taskTypeKey === "") {
74
+ fn.taskTypeKey = taskTypeKey;
75
+ }
76
+
77
+ return {
78
+ taskParamsValidator: taskParamsValidator as TaskRepoContext["taskParamsValidator"],
79
+ taskFn: fn,
80
+ };
81
+ }
82
+
83
+ export function pickEntryScript(scripts: TaskRepoScriptRecord[]): TaskRepoScriptRecord | null {
84
+ if (scripts.length === 0) {
85
+ return null;
86
+ }
87
+ const entry = scripts.find((script) => script.details.isEntry === true);
88
+ return entry ?? scripts[0];
89
+ }
90
+
91
+ export function getScriptLoadKey(script: TaskRepoScriptRecord): string {
92
+ const { source, modulePath, hash } = script.details;
93
+ if (hash != null && hash !== "") {
94
+ return hash;
95
+ }
96
+ if (source != null && source !== "") {
97
+ return `source:${source.length}:${source.slice(0, 64)}`;
98
+ }
99
+ if (modulePath != null && modulePath !== "") {
100
+ return `path:${modulePath}`;
101
+ }
102
+ return script.id;
103
+ }
104
+
105
+ export function assertScriptLoadable(
106
+ details: TaskRepoScriptDetails
107
+ ): details is TaskRepoScriptDetails & ({ source: string } | { modulePath: string }) {
108
+ const hasSource = details.source != null && details.source.trim() !== "";
109
+ const hasModulePath = details.modulePath != null && details.modulePath.trim() !== "";
110
+ return hasSource || hasModulePath;
111
+ }
@@ -0,0 +1,245 @@
1
+ import { getPossibleTarget } from "../core.api";
2
+ import { SupabaseInitializer } from "../supabase";
3
+ import { LoggerWithScope } from "../shared/log";
4
+ import type { TaskRepoContext, TaskRepoScriptRecord } from "../task/task-repo-context";
5
+ import { TASK_REPO_SCRIPT_CATEGORY } from "../task/task-repo-context";
6
+ import {
7
+ assertScriptLoadable,
8
+ isTaskRepoContext,
9
+ pickEntryScript,
10
+ } from "./repo-context.utils";
11
+ import { CategoryRepo, Repo } from "./repo.interface";
12
+ import {
13
+ clearRepoScriptModuleCache,
14
+ loadRepoContextFromScript,
15
+ loadRepoContextFromUrl,
16
+ } from "./repo.script-loader";
17
+
18
+ const supabase = SupabaseInitializer.getInstance();
19
+
20
+ type LocalRepoEntry =
21
+ | { kind: "context"; context: TaskRepoContext }
22
+ | { kind: "modulePath"; modulePath: string; exportName?: string };
23
+
24
+ const localRepoRegistry = new Map<string, LocalRepoEntry>();
25
+ const remoteContextCache = new Map<string, TaskRepoContext>();
26
+
27
+ export 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
+
35
+ export interface GetRepoContextParams {
36
+ logger: LoggerWithScope;
37
+ /** Task type key (`task.value`) — matches {@link Repo.value} for remote lookup; local registry key */
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;
44
+ }
45
+
46
+ export interface GetRepoContextResult<T extends TaskRepoContext = TaskRepoContext> {
47
+ context: T | null;
48
+ /** Present when `context` is null — import / DB / format failure at repo layer */
49
+ error?: string;
50
+ failureReason?: RepoContextFailureReason;
51
+ resolvedVia?: RepoContextResolvedVia;
52
+ }
53
+
54
+ function buildRemoteCacheKey(taskTypeKey: string, contentHash: string): string {
55
+ return `${taskTypeKey}@${contentHash}`;
56
+ }
57
+
58
+ async function fetchRemoteRepo(taskTypeKey: string): Promise<Repo | null> {
59
+ const { data } = await getPossibleTarget({
60
+ filterList: [
61
+ { field: "category", operator: "eq", value: CategoryRepo.REPO },
62
+ { field: "value", operator: "eq", value: taskTypeKey },
63
+ ],
64
+ });
65
+ return (data as Repo | null) ?? null;
66
+ }
67
+
68
+ async function fetchRemoteScripts(taskTypeKey: string): Promise<TaskRepoScriptRecord[]> {
69
+ const { data, error } = await supabase.client
70
+ .from("target")
71
+ .select("id, value, details")
72
+ .eq("category", TASK_REPO_SCRIPT_CATEGORY)
73
+ .eq("value", taskTypeKey);
74
+
75
+ if (error) {
76
+ throw error;
77
+ }
78
+
79
+ return (data ?? []) as TaskRepoScriptRecord[];
80
+ }
81
+
82
+ async function loadLocalEntry(
83
+ logger: LoggerWithScope,
84
+ entry: LocalRepoEntry,
85
+ taskTypeKey: string
86
+ ): Promise<TaskRepoContext | null> {
87
+ if (entry.kind === "context") {
88
+ return entry.context;
89
+ }
90
+
91
+ const loaded = await loadRepoContextFromScript({
92
+ logger,
93
+ script: {
94
+ id: `local:${taskTypeKey}`,
95
+ value: taskTypeKey,
96
+ details: {
97
+ manifestVersion: 0,
98
+ modulePath: entry.modulePath,
99
+ exportName: entry.exportName,
100
+ isEntry: true,
101
+ },
102
+ },
103
+ taskTypeKey,
104
+ });
105
+ return loaded?.context ?? null;
106
+ }
107
+
108
+ async function loadRemoteRepoContext(
109
+ logger: LoggerWithScope,
110
+ taskTypeKey: string
111
+ ): Promise<GetRepoContextResult> {
112
+ logger.info("從 Supabase 加載 Repo", { topic: "repo", data: { taskTypeKey } });
113
+
114
+ const remoteRepo = await fetchRemoteRepo(taskTypeKey);
115
+ if (remoteRepo == null) {
116
+ const error = `Supabase 未找到 Repo (value=${taskTypeKey})`;
117
+ logger.warn(error, { topic: "repo", data: { taskTypeKey } });
118
+ return { context: null, error, failureReason: "REMOTE_REPO_NOT_FOUND" };
119
+ }
120
+
121
+ const repoUrl = remoteRepo.details?.url?.trim() ?? "";
122
+ const repoHash = remoteRepo.details?.hash?.trim() ?? repoUrl;
123
+ const cacheKey = buildRemoteCacheKey(taskTypeKey, repoHash);
124
+ const cached = remoteContextCache.get(cacheKey);
125
+ if (cached != null) {
126
+ logger.info("命中遠程 Repo 上下文緩存", { topic: "repo", data: { cacheKey } });
127
+ return { context: cached, resolvedVia: "remote" };
128
+ }
129
+
130
+ const scripts = await fetchRemoteScripts(taskTypeKey);
131
+ const entryScript = pickEntryScript(scripts);
132
+
133
+ let loaded =
134
+ entryScript != null && assertScriptLoadable(entryScript.details)
135
+ ? await loadRepoContextFromScript({ logger, script: entryScript, taskTypeKey })
136
+ : null;
137
+
138
+ if (loaded == null && repoUrl !== "") {
139
+ logger.info("未找到 script 行,嘗試從 Repo.details.url 加載", {
140
+ topic: "repo",
141
+ data: { taskTypeKey, url: repoUrl },
142
+ });
143
+ loaded = await loadRepoContextFromUrl({ logger, url: repoUrl, taskTypeKey });
144
+ }
145
+
146
+ if (loaded == null) {
147
+ const error = `無法從 Repo 加載 TaskRepoContext (hasUrl=${repoUrl !== ""}, scriptCount=${scripts.length})`;
148
+ logger.warn(error, {
149
+ topic: "repo",
150
+ data: { taskTypeKey, hasUrl: repoUrl !== "", scriptCount: scripts.length },
151
+ });
152
+ return { context: null, error, failureReason: "REMOTE_REPO_LOAD_FAILED" };
153
+ }
154
+
155
+ remoteContextCache.set(cacheKey, loaded.context);
156
+ return { context: loaded.context, resolvedVia: "remote" };
157
+ }
158
+
159
+ const getRepoContext = async <RepoContext extends TaskRepoContext>({
160
+ logger,
161
+ taskTypeKey,
162
+ includeRemote = true,
163
+ }: GetRepoContextParams): Promise<GetRepoContextResult<RepoContext>> => {
164
+ const localEntry = localRepoRegistry.get(taskTypeKey);
165
+ if (localEntry != null) {
166
+ logger.info("使用本地註冊的 Repo", { topic: "repo", data: { taskTypeKey } });
167
+ const context = await loadLocalEntry(logger, localEntry, taskTypeKey);
168
+ if (context != null && isTaskRepoContext(context)) {
169
+ return { context: context as RepoContext, resolvedVia: "local" };
170
+ }
171
+ const error = "本地 Repo 模組加載失敗或未導出有效的 TaskRepoContext";
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" };
180
+ }
181
+
182
+ try {
183
+ const result = await loadRemoteRepoContext(logger, taskTypeKey);
184
+ if (result.context != null && isTaskRepoContext(result.context)) {
185
+ return { context: result.context as RepoContext, resolvedVia: "remote" };
186
+ }
187
+ return {
188
+ context: null,
189
+ error: result.error ?? "遠程 Repo 上下文無效",
190
+ failureReason: result.failureReason ?? "REMOTE_REPO_LOAD_FAILED",
191
+ };
192
+ } catch (error) {
193
+ const message = error instanceof Error ? error.message : String(error);
194
+ logger.error("從 Supabase 加載 Repo 失敗", {
195
+ topic: "repo",
196
+ data: { taskTypeKey, error: message },
197
+ });
198
+ return { context: null, error: message, failureReason: "REMOTE_REPO_LOAD_FAILED" };
199
+ }
200
+ };
201
+
202
+ /** Register an in-process TaskRepoContext (highest priority — no Supabase fetch). */
203
+ function registerLocalRepoContext(taskTypeKey: string, context: TaskRepoContext): void {
204
+ if (!isTaskRepoContext(context)) {
205
+ throw new Error("[RepoManager] Invalid TaskRepoContext");
206
+ }
207
+ localRepoRegistry.set(taskTypeKey, { kind: "context", context });
208
+ }
209
+
210
+ /** Register a local ESM module path; loaded via dynamic import when the task runs. */
211
+ function registerLocalModule(taskTypeKey: string, modulePath: string, exportName?: string): void {
212
+ if (modulePath.trim() === "") {
213
+ throw new Error("[RepoManager] modulePath must not be empty");
214
+ }
215
+ localRepoRegistry.set(taskTypeKey, { kind: "modulePath", modulePath, exportName });
216
+ }
217
+
218
+ function unregisterLocalRepo(taskTypeKey: string): void {
219
+ localRepoRegistry.delete(taskTypeKey);
220
+ }
221
+
222
+ function hasLocalRepo(taskTypeKey: string): boolean {
223
+ return localRepoRegistry.has(taskTypeKey);
224
+ }
225
+
226
+ function getRegisteredLocalTaskTypeKeys(): string[] {
227
+ return [...localRepoRegistry.keys()];
228
+ }
229
+
230
+ function clearRemoteContextCache(): void {
231
+ remoteContextCache.clear();
232
+ }
233
+
234
+ const RepoManager = {
235
+ getRepoContext,
236
+ registerLocalRepoContext,
237
+ registerLocalModule,
238
+ unregisterLocalRepo,
239
+ hasLocalRepo,
240
+ getRegisteredLocalTaskTypeKeys,
241
+ clearRemoteContextCache,
242
+ clearRepoScriptModuleCache,
243
+ };
244
+
245
+ export { RepoManager };
@@ -0,0 +1,50 @@
1
+ import { scanTargetList, validateWithSchema } from "../core.api";
2
+ import { generateResponse, SupabaseResponse } from "../core.interface";
3
+ import { z } from "zod";
4
+ import { CategoryRepo, Repo } from "./repo.interface";
5
+
6
+ const REPO_USAGE_FIELD = "details->>usage" as const;
7
+
8
+ export const getScanRemoteRepoValuesSchema = z.object({
9
+ usage: z.string().trim().min(1),
10
+ });
11
+
12
+ export type GetScanRemoteRepoValuesPayload = z.infer<typeof getScanRemoteRepoValuesSchema>;
13
+
14
+ /**
15
+ * Scan `Repo.value` keys for `category=repo` with matching `details.usage`.
16
+ * Returns `{ data: string[] }` on success; validation and DB failures use `{ error }` (never throws).
17
+ */
18
+ export const getScanRemoteRepoValues = async (
19
+ payload: GetScanRemoteRepoValuesPayload
20
+ ): Promise<SupabaseResponse<string[]>> => {
21
+ try {
22
+ return await getScanRemoteRepoValuesValidated(payload);
23
+ } catch (error) {
24
+ const message = error instanceof Error ? error.message : String(error);
25
+ return generateResponse.error(message) as SupabaseResponse<string[]>;
26
+ }
27
+ };
28
+
29
+ const getScanRemoteRepoValuesValidated = validateWithSchema(
30
+ getScanRemoteRepoValuesSchema,
31
+ "getScanRemoteRepoValuesSchema"
32
+ )(async ({ usage }): Promise<SupabaseResponse<string[]>> => {
33
+ try {
34
+ const { data } = await scanTargetList<Repo>({
35
+ category: CategoryRepo.REPO,
36
+ selectFields: "value",
37
+ orderBy: { field: "value", ascending: true },
38
+ filterList: [{ field: REPO_USAGE_FIELD, operator: "eq", value: usage }],
39
+ });
40
+
41
+ const values = (data ?? [])
42
+ .map((row) => row.value?.trim())
43
+ .filter((value): value is string => value != null && value !== "");
44
+
45
+ return generateResponse.success([...new Set(values)]);
46
+ } catch (error) {
47
+ const message = error instanceof Error ? error.message : String(error);
48
+ return generateResponse.error(message) as SupabaseResponse<string[]>;
49
+ }
50
+ });
@@ -3,13 +3,15 @@ import { Target } from "../core.interface";
3
3
  export enum CategoryRepo {
4
4
  /** Project */
5
5
  REPO = "repo",
6
- SCRIPT = "script",
7
6
  }
8
7
 
8
+ /** `Repo.details.usage` for task worker remote discovery ({@link registerTasks}). */
9
+ export const TASK_REPO_USAGE = "task";
10
+
9
11
  export interface Repo extends Target {
10
- /** Key */
12
+ /** Human readable name */
11
13
  name: string;
12
- /** Url */
14
+ /** Unique key, consumers use this to determine which repo to use */
13
15
  value: string;
14
16
  details: RepoDetails;
15
17
  category: CategoryRepo;
@@ -17,7 +19,13 @@ export interface Repo extends Target {
17
19
 
18
20
  export interface RepoDetails {
19
21
  manifestVersion: number;
20
- context?: unknown;
22
+ /**
23
+ * Business usage partition for discovery and load (caller-defined string, e.g. `"task"`).
24
+ * Used by {@link getScanRemoteRepoValues} to narrow scan scope; validity is the caller's responsibility.
25
+ */
26
+ usage: string;
27
+ /** Repo entry module URL or filesystem path — used to dynamic-import TaskRepoContext. */
28
+ url: string;
21
29
  hash: string;
22
- };
30
+ }
23
31