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.
Files changed (135) hide show
  1. package/README.md +66 -0
  2. package/dist/browser.d.ts +52 -0
  3. package/dist/browser.d.ts.map +1 -0
  4. package/dist/browser.js +45 -0
  5. package/dist/browser.js.map +1 -0
  6. package/dist/command/command.api.js +5 -5
  7. package/dist/command/command.api.js.map +1 -1
  8. package/dist/index.d.ts +2 -48
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +2 -41
  11. package/dist/index.js.map +1 -1
  12. package/dist/link/link.interface.d.ts +1 -1
  13. package/dist/link/link.interface.d.ts.map +1 -1
  14. package/dist/node/node-log-scope.d.ts +11 -0
  15. package/dist/node/node-log-scope.d.ts.map +1 -0
  16. package/dist/node/node-log-scope.js +7 -0
  17. package/dist/node/node-log-scope.js.map +1 -0
  18. package/dist/node/node-runtime.base.d.ts +42 -0
  19. package/dist/node/node-runtime.base.d.ts.map +1 -0
  20. package/dist/node/node-runtime.base.js +397 -0
  21. package/dist/node/node-runtime.base.js.map +1 -0
  22. package/dist/node/node.api.d.ts +3 -3
  23. package/dist/node/node.api.js +3 -3
  24. package/dist/node/node.api.js.map +1 -1
  25. package/dist/node/task-node.d.ts +20 -0
  26. package/dist/node/task-node.d.ts.map +1 -0
  27. package/dist/node/task-node.js +245 -0
  28. package/dist/node/task-node.js.map +1 -0
  29. package/dist/node/trigger-node.d.ts +18 -0
  30. package/dist/node/trigger-node.d.ts.map +1 -0
  31. package/dist/node/trigger-node.js +150 -0
  32. package/dist/node/trigger-node.js.map +1 -0
  33. package/dist/node.d.ts +22 -0
  34. package/dist/node.d.ts.map +1 -0
  35. package/dist/node.js +19 -0
  36. package/dist/node.js.map +1 -0
  37. package/dist/repo/repo-manager.d.ts +14 -3
  38. package/dist/repo/repo-manager.d.ts.map +1 -1
  39. package/dist/repo/repo-manager.js +27 -17
  40. package/dist/repo/repo-manager.js.map +1 -1
  41. package/dist/repo/repo.script-loader.d.ts +3 -3
  42. package/dist/repo/repo.script-loader.d.ts.map +1 -1
  43. package/dist/repo/repo.script-loader.js +10 -10
  44. package/dist/repo/repo.script-loader.js.map +1 -1
  45. package/dist/shared/log/create-api-logger.d.ts +10 -6
  46. package/dist/shared/log/create-api-logger.d.ts.map +1 -1
  47. package/dist/shared/log/create-api-logger.js +10 -6
  48. package/dist/shared/log/create-api-logger.js.map +1 -1
  49. package/dist/shared/log/index.d.ts +10 -0
  50. package/dist/shared/log/index.d.ts.map +1 -0
  51. package/dist/shared/log/index.js +10 -0
  52. package/dist/shared/log/index.js.map +1 -0
  53. package/dist/shared/log/log-manager.d.ts +25 -46
  54. package/dist/shared/log/log-manager.d.ts.map +1 -1
  55. package/dist/shared/log/log-manager.js +59 -40
  56. package/dist/shared/log/log-manager.js.map +1 -1
  57. package/dist/shared/log/log-scope.d.ts +49 -0
  58. package/dist/shared/log/log-scope.d.ts.map +1 -0
  59. package/dist/shared/log/log-scope.js +70 -0
  60. package/dist/shared/log/log-scope.js.map +1 -0
  61. package/dist/task/index.d.ts +15 -0
  62. package/dist/task/index.d.ts.map +1 -0
  63. package/dist/task/index.js +14 -0
  64. package/dist/task/index.js.map +1 -0
  65. package/dist/task/local-task-registry.d.ts +10 -0
  66. package/dist/task/local-task-registry.d.ts.map +1 -1
  67. package/dist/task/local-task-registry.js +83 -22
  68. package/dist/task/local-task-registry.js.map +1 -1
  69. package/dist/task/task-manager.d.ts +18 -6
  70. package/dist/task/task-manager.d.ts.map +1 -1
  71. package/dist/task/task-manager.js +46 -67
  72. package/dist/task/task-manager.js.map +1 -1
  73. package/dist/task/task-post.api.d.ts +49 -0
  74. package/dist/task/task-post.api.d.ts.map +1 -0
  75. package/dist/task/task-post.api.js +91 -0
  76. package/dist/task/task-post.api.js.map +1 -0
  77. package/dist/task/task-repo-context.d.ts +2 -0
  78. package/dist/task/task-repo-context.d.ts.map +1 -1
  79. package/dist/task/task-repo-context.js.map +1 -1
  80. package/dist/task/task-repo-validation.d.ts +46 -0
  81. package/dist/task/task-repo-validation.d.ts.map +1 -0
  82. package/dist/task/task-repo-validation.js +96 -0
  83. package/dist/task/task-repo-validation.js.map +1 -0
  84. package/dist/task/task.api.d.ts +11 -53
  85. package/dist/task/task.api.d.ts.map +1 -1
  86. package/dist/task/task.api.js +13 -61
  87. package/dist/task/task.api.js.map +1 -1
  88. package/dist/task/task.interface.d.ts +2 -0
  89. package/dist/task/task.interface.d.ts.map +1 -1
  90. package/dist/task/task.interface.js.map +1 -1
  91. package/dist/trigger/trigger.api.d.ts +140 -0
  92. package/dist/trigger/trigger.api.d.ts.map +1 -0
  93. package/dist/trigger/trigger.api.js +125 -0
  94. package/dist/trigger/trigger.api.js.map +1 -0
  95. package/dist/trigger/trigger.interface.d.ts +43 -0
  96. package/dist/trigger/trigger.interface.d.ts.map +1 -0
  97. package/dist/trigger/trigger.interface.js +11 -0
  98. package/dist/trigger/trigger.interface.js.map +1 -0
  99. package/dist/trigger/trigger.utils.d.ts +6 -0
  100. package/dist/trigger/trigger.utils.d.ts.map +1 -0
  101. package/dist/trigger/trigger.utils.js +39 -0
  102. package/dist/trigger/trigger.utils.js.map +1 -0
  103. package/package.json +21 -6
  104. package/src/browser.ts +100 -0
  105. package/src/command/command.api.ts +5 -5
  106. package/src/index.ts +2 -79
  107. package/src/link/link.interface.ts +1 -1
  108. package/src/node/node-log-scope.ts +15 -0
  109. package/src/node/node-runtime.base.ts +450 -0
  110. package/src/node/node.api.ts +3 -3
  111. package/src/node/task-node.ts +284 -0
  112. package/src/node/trigger-node.ts +180 -0
  113. package/src/node.ts +62 -0
  114. package/src/repo/repo-manager.ts +49 -21
  115. package/src/repo/repo.script-loader.ts +13 -13
  116. package/src/shared/log/create-api-logger.ts +23 -12
  117. package/src/shared/log/index.ts +23 -0
  118. package/src/shared/log/log-manager.ts +85 -90
  119. package/src/shared/log/log-scope.ts +123 -0
  120. package/src/task/index.ts +48 -0
  121. package/src/task/local-task-registry.ts +110 -23
  122. package/src/task/task-manager.ts +73 -84
  123. package/src/task/task-post.api.ts +116 -0
  124. package/src/task/task-repo-context.ts +2 -0
  125. package/src/task/task-repo-validation.ts +171 -0
  126. package/src/task/task.api.ts +13 -73
  127. package/src/task/task.interface.ts +2 -0
  128. package/src/trigger/trigger.api.ts +166 -0
  129. package/src/trigger/trigger.interface.ts +50 -0
  130. package/src/trigger/trigger.utils.ts +56 -0
  131. package/dist/node/node-manager.d.ts +0 -48
  132. package/dist/node/node-manager.d.ts.map +0 -1
  133. package/dist/node/node-manager.js +0 -619
  134. package/dist/node/node-manager.js.map +0 -1
  135. 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 };
@@ -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 LoggerWithContext } from "../shared/log/log-manager";
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. NodeManager loop) passes its trace to correlate logs. */
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: LoggerWithContext,
163
+ logger: LoggerWithScope,
182
164
  action: TaskStatusAction | "patchTaskProgress" | "patchClaimTask",
183
165
  task: Task,
184
- context?: Record<string, unknown>
166
+ data?: Record<string, unknown>
185
167
  ): void {
186
168
  logger.info("任务状态迁移成功", {
187
169
  topic: "task",
188
- context: {
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
- ...context,
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
- throw new Error(`[patchChangeTaskStatus] Unknown action: ${_exhaustive}`);
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", context: { error: message } });
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
- });
@@ -53,6 +53,8 @@ export interface TaskDetails {
53
53
  /** [0,100] */
54
54
  progress: number;
55
55
  nodeId: null | string;
56
+ /** Task lifecycle correlation id (written at postTask). */
57
+ traceId?: string;
56
58
  result?: {
57
59
  cost: number;
58
60
  code: ResultCode;
@@ -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"}