@headless-lms/adapter-workflow-hatchet 0.1.0 → 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 CHANGED
@@ -1,7 +1,10 @@
1
1
  # @headless-lms/adapter-workflow-hatchet
2
2
 
3
3
  Hatchet ([hatchet.run](https://hatchet.run)) implementation of the `AutomationEngine`
4
- port from `@headless-lms/core/types` — durable execution for dispatched automations.
4
+ port from `@headless-lms/core/types` and the `Scheduler` port from
5
+ `@headless-lms/core/shared/ports` — durable execution for
6
+ dispatched automations, notification deliveries and scheduled jobs. One object
7
+ implements both ports; the container exposes it as `automationEngine` and `scheduler`.
5
8
 
6
9
  The adapter declares one workflow, `automation-run`: a durable parent task that
7
10
  iterates the dispatched actions, spawning one child task per action
@@ -11,11 +14,33 @@ child that exhausts its retries stops the sequence; the parent then calls
11
14
  spawned via the durable event log, a crash mid-run resumes after the last
12
15
  completed action instead of restarting the sequence from scratch.
13
16
 
17
+ Webhook deliveries run through the same workflow: a webhook is an automation whose
18
+ one action delivers the event, so a failed delivery is retried like any other action.
19
+
20
+ Two more tasks run on the same worker:
21
+
22
+ - `notification-delivery` sends one notification delivery (an email or a push) on its
23
+ channel, with `retries: 3` and `backoff: { factor: 5, maxSeconds: 300 }` (retries after
24
+ 5 s, 25 s and 125 s). Its concurrency key is the delivery id with
25
+ `maxRuns: 1` and `GROUP_ROUND_ROBIN`, so a second run for the same delivery queues
26
+ instead of overlapping; on the last attempt a failing delivery is marked `failed`.
27
+ - `scheduled-job` runs the job a context registered under the input's `job` name with
28
+ its payload, with the same retries and backoff and a concurrency key of `job:key`. A one-off job
29
+ (`Scheduler.at`) is a Hatchet scheduled run tagged with metadata `job` and `key`;
30
+ a second `at` with the same key moves it, and `cancel` deletes it. A recurring
31
+ product job is a cron named `hlms-job:{job}`, created, replaced or removed by
32
+ `start()` to match the registered jobs. A firing whose job has no handler is
33
+ logged and dropped.
34
+
35
+ `start()` registers every task on one worker, reconciles the product job crons and
36
+ starts the worker; call it after the server listens. The `automation-schedule` crons
37
+ for an org's own `schedule:` triggers are unchanged.
38
+
14
39
  ```ts
15
40
  import { HatchetAutomationEngine } from "@headless-lms/adapter-workflow-hatchet";
16
41
 
17
42
  const container = await createContainer(config, {
18
- adapters: { automationEngine: new HatchetAutomationEngine() },
43
+ adapters: { workflows: (logger) => new HatchetAutomationEngine(undefined, logger) },
19
44
  });
20
45
  ```
21
46
 
@@ -27,9 +52,9 @@ setups.
27
52
 
28
53
  ## Environment variables (Hatchet client, read by the SDK)
29
54
 
30
- | Variable | Required | Notes |
31
- | --------------------- | -------- | ---------------------------------------- |
32
- | `HATCHET_CLIENT_TOKEN` | yes | Hatchet API token for the target tenant. |
55
+ | Variable | Required | Notes |
56
+ | ---------------------- | -------- | ---------------------------------------- |
57
+ | `HATCHET_CLIENT_TOKEN` | yes | Hatchet API token for the target tenant. |
33
58
 
34
59
  See the Hatchet TypeScript SDK docs for the full set of `HATCHET_CLIENT_*` variables
35
60
  (host/port, TLS, namespace, …).
package/dist/index.d.ts CHANGED
@@ -1,10 +1,12 @@
1
- import { AutomationActionResult, AutomationDispatch, AutomationEngine, AutomationExecutor, Logger } from "@headless-lms/core/types";
1
+ import { APIContracts } from "@hatchet-dev/typescript-sdk/clients/rest/index.js";
2
+ import { ConcurrencyLimitStrategy } from "@hatchet-dev/typescript-sdk/v1/index.js";
3
+ import { AutomationActionResult, AutomationDispatch, AutomationEngine, AutomationExecutor, DeliveryDispatch, JsonValue, Logger, ScheduledAutomation } from "@headless-lms/core/types";
4
+ import { RecurringJob, ScheduledJob, Scheduler } from "@headless-lms/core/shared/ports";
2
5
  //#region src/index.d.ts
3
6
  interface ActionTaskInput {
4
7
  dispatch: AutomationDispatch;
5
8
  index: number;
6
9
  }
7
- /** Subset of `DurableContext` the parent task uses; hand-rolled so tests can fake it without a real Hatchet client. */
8
10
  interface DurableCtxLike {
9
11
  spawnChild(task: unknown, input: ActionTaskInput, options?: Record<string, unknown>): Promise<AutomationActionResult>;
10
12
  }
@@ -15,38 +17,125 @@ interface HatchetWorkerLike {
15
17
  start(): Promise<void>;
16
18
  stop(): Promise<void>;
17
19
  }
18
- /** Subset of the Hatchet v1 client this adapter calls; hand-rolled so tests can inject a fake client. */
20
+ interface HatchetCronLike {
21
+ metadata: {
22
+ id: string;
23
+ };
24
+ name?: string;
25
+ cron: string;
26
+ }
27
+ interface HatchetCronClientLike {
28
+ create(workflow: unknown, cron: {
29
+ name: string;
30
+ expression: string;
31
+ input?: Record<string, unknown>;
32
+ }): Promise<HatchetCronLike>;
33
+ delete(cron: string | HatchetCronLike): Promise<void>;
34
+ list(query: {
35
+ workflow?: unknown;
36
+ limit?: number;
37
+ offset?: number;
38
+ }): Promise<{
39
+ rows?: HatchetCronLike[];
40
+ }>;
41
+ }
42
+ interface HatchetScheduledRunLike {
43
+ metadata: {
44
+ id: string;
45
+ };
46
+ }
47
+ interface HatchetScheduledClientLike {
48
+ list(query: {
49
+ workflow?: unknown;
50
+ additionalMetadata?: string[];
51
+ statuses?: APIContracts.ScheduledRunStatus[];
52
+ }): Promise<{
53
+ rows?: HatchetScheduledRunLike[];
54
+ }>;
55
+ update(scheduledRun: string, update: {
56
+ triggerAt: Date;
57
+ }): Promise<unknown>;
58
+ bulkDelete(opts: {
59
+ scheduledRuns?: string[];
60
+ filter?: {
61
+ additionalMetadata?: string[];
62
+ };
63
+ }): Promise<unknown>;
64
+ }
65
+ interface HatchetTaskLike<I> {
66
+ runNoWait(input: I): Promise<unknown>;
67
+ schedule(enqueueAt: Date, input: I, options?: {
68
+ additionalMetadata?: Record<string, string>;
69
+ }): Promise<unknown>;
70
+ }
19
71
  interface HatchetClientLike {
20
- task(opts: {
72
+ task<I>(opts: {
21
73
  name: string;
22
74
  retries?: number;
23
- fn: (input: ActionTaskInput, ctx: unknown) => Promise<AutomationActionResult> | AutomationActionResult;
24
- }): unknown;
75
+ backoff?: {
76
+ factor?: number;
77
+ maxSeconds?: number;
78
+ };
79
+ concurrency?: {
80
+ expression: string;
81
+ maxRuns?: number;
82
+ limitStrategy?: ConcurrencyLimitStrategy;
83
+ };
84
+ fn: (input: I, ctx: unknown) => Promise<unknown> | unknown;
85
+ }): HatchetTaskLike<I>;
25
86
  durableTask(opts: {
26
87
  name: string;
27
88
  fn: (input: AutomationDispatch, ctx: DurableCtxLike) => Promise<void>;
28
89
  }): RunnableTask<AutomationDispatch>;
29
90
  worker(name: string, opts: {
30
91
  workflows: any[];
92
+ handleKill?: boolean;
31
93
  }): Promise<HatchetWorkerLike>;
94
+ crons: HatchetCronClientLike;
95
+ scheduled: HatchetScheduledClientLike;
32
96
  }
33
- declare class HatchetAutomationEngine implements AutomationEngine {
34
- private readonly client;
97
+ declare function cronName(schedule: ScheduledAutomation): string;
98
+ declare class HatchetAutomationEngine implements AutomationEngine, Scheduler {
35
99
  private readonly logger?;
100
+ private readonly reconcileEveryMs;
36
101
  private executor;
102
+ private jobs;
103
+ private recurring;
37
104
  private worker;
105
+ private syncing;
106
+ private syncingJobs;
107
+ private reconcileTimer;
38
108
  private readonly actionTask;
109
+ private readonly scheduleTask;
110
+ private readonly jobTask;
111
+ private readonly deliveryTask;
39
112
  private readonly automationRunTask;
40
- /** `client` defaults to a Hatchet client built from env (`HATCHET_CLIENT_TOKEN`); pass a fake for tests. */
41
- constructor(client?: HatchetClientLike, logger?: Logger | undefined);
113
+ private readonly client;
114
+ constructor(client?: HatchetClientLike, logger?: Logger | undefined, reconcileEveryMs?: number);
42
115
  register(executor: AutomationExecutor): void;
116
+ registerJobs(handlers: ScheduledJob[], recurring: RecurringJob[]): void;
117
+ at(job: string, runAt: Date, key: string, payload: JsonValue): Promise<void>;
118
+ private pendingRuns;
119
+ private scheduleJob;
120
+ cancel(job: string, key: string): Promise<void>;
43
121
  dispatch(d: AutomationDispatch): Promise<void>;
122
+ deliver(d: DeliveryDispatch): Promise<void>;
44
123
  start(): Promise<void>;
45
124
  stop(): Promise<void>;
46
- /** Lets failures throw so Hatchet's per-task retry (retries: 3) applies before the parent gives up. */
125
+ private reconcileAll;
126
+ private reconcileJobsLogged;
127
+ private syncLogged;
128
+ syncSchedules(): Promise<void>;
129
+ private reconcile;
130
+ private reconcileJobs;
131
+ private runJob;
132
+ private listCrons;
133
+ private runDelivery;
134
+ private runSchedule;
47
135
  private runAction;
136
+ private logActionError;
48
137
  private runDispatch;
49
138
  }
50
139
  //#endregion
51
- export { HatchetAutomationEngine, HatchetClientLike };
140
+ export { HatchetAutomationEngine, HatchetClientLike, HatchetCronClientLike, HatchetCronLike, HatchetScheduledClientLike, HatchetScheduledRunLike, HatchetTaskLike, cronName };
52
141
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,23 +1,110 @@
1
- import { Hatchet } from "@hatchet-dev/typescript-sdk";
1
+ import { APIContracts } from "@hatchet-dev/typescript-sdk/clients/rest/index.js";
2
+ import { ConcurrencyLimitStrategy, HatchetClient } from "@hatchet-dev/typescript-sdk/v1/index.js";
3
+ import { ACTION_FAILED, ActionFailedError, actionFailureMessage } from "@headless-lms/core/automations";
2
4
  //#region src/index.ts
3
5
  const WORKFLOW_NAME = "automation-run";
4
6
  const ACTION_TASK_NAME = "automation-run-action";
7
+ const SCHEDULE_TASK_NAME = "automation-schedule";
8
+ const JOB_TASK_NAME = "scheduled-job";
9
+ const DELIVERY_TASK_NAME = "notification-delivery";
5
10
  const WORKER_NAME = "automation-run-worker";
11
+ const CRON_PREFIX = "hlms-schedule:";
12
+ const JOB_CRON_PREFIX = "hlms-job:";
13
+ const RECONCILE_EVERY_MS = 5 * 6e4;
14
+ const ACTION_RETRIES = 3;
15
+ const JOB_RETRIES = 3;
16
+ const RETRY_BACKOFF = {
17
+ factor: 5,
18
+ maxSeconds: 300
19
+ };
20
+ function floorMinute(at) {
21
+ const copy = new Date(at);
22
+ copy.setUTCSeconds(0, 0);
23
+ return copy;
24
+ }
25
+ function cronName(schedule) {
26
+ return `${CRON_PREFIX}${schedule.orgId}:${schedule.automationId}:${schedule.cron}`;
27
+ }
28
+ function jobMetadata(job, key) {
29
+ return {
30
+ job: encodeURIComponent(job),
31
+ key: encodeURIComponent(key)
32
+ };
33
+ }
34
+ function jobMetadataFilter(job, key) {
35
+ const metadata = jobMetadata(job, key);
36
+ return [`job:${metadata.job}`, `key:${metadata.key}`];
37
+ }
38
+ function sdkLogger(logger) {
39
+ return (context) => {
40
+ const log = logger.child({ hatchet: context });
41
+ const failure = (error, extra) => ({
42
+ ...extra,
43
+ ...error && { err: error }
44
+ });
45
+ return {
46
+ debug: (message, extra) => log.debug(message, extra),
47
+ info: (message, extra) => log.info(message, extra),
48
+ green: (message, extra) => log.info(message, extra),
49
+ warn: (message, error, extra) => log.warn(message, failure(error, extra)),
50
+ error: (message, error, extra) => log.error(message, failure(error, extra)),
51
+ util: (key, message, extra) => log.debug(message, {
52
+ ...extra,
53
+ key
54
+ })
55
+ };
56
+ };
57
+ }
6
58
  var HatchetAutomationEngine = class {
7
- client;
8
59
  logger;
60
+ reconcileEveryMs;
9
61
  executor;
62
+ jobs = /* @__PURE__ */ new Map();
63
+ recurring = [];
10
64
  worker;
65
+ syncing;
66
+ syncingJobs;
67
+ reconcileTimer;
11
68
  actionTask;
69
+ scheduleTask;
70
+ jobTask;
71
+ deliveryTask;
12
72
  automationRunTask;
13
- /** `client` defaults to a Hatchet client built from env (`HATCHET_CLIENT_TOKEN`); pass a fake for tests. */
14
- constructor(client = new Hatchet(), logger) {
15
- this.client = client;
73
+ client;
74
+ constructor(client, logger, reconcileEveryMs = RECONCILE_EVERY_MS) {
16
75
  this.logger = logger;
76
+ this.reconcileEveryMs = reconcileEveryMs;
77
+ this.client = client ?? new HatchetClient(logger ? { logger: sdkLogger(logger) } : {});
17
78
  this.actionTask = this.client.task({
18
79
  name: ACTION_TASK_NAME,
19
- retries: 3,
20
- fn: async (input) => this.runAction(input)
80
+ retries: ACTION_RETRIES,
81
+ fn: async (input, ctx) => this.runAction(input, ctx)
82
+ });
83
+ this.scheduleTask = this.client.task({
84
+ name: SCHEDULE_TASK_NAME,
85
+ fn: async (input) => this.runSchedule(input)
86
+ });
87
+ this.jobTask = this.client.task({
88
+ name: JOB_TASK_NAME,
89
+ retries: JOB_RETRIES,
90
+ backoff: RETRY_BACKOFF,
91
+ concurrency: {
92
+ expression: "input.job + ':' + input.key",
93
+ maxRuns: 1,
94
+ limitStrategy: ConcurrencyLimitStrategy.GROUP_ROUND_ROBIN
95
+ },
96
+ fn: async (input, ctx) => this.runJob(input, ctx)
97
+ });
98
+ this.deliveryTask = this.client.task({
99
+ name: DELIVERY_TASK_NAME,
100
+ retries: ACTION_RETRIES,
101
+ backoff: RETRY_BACKOFF,
102
+ concurrency: {
103
+ expression: "input.deliveryId",
104
+ maxRuns: 1,
105
+ limitStrategy: ConcurrencyLimitStrategy.GROUP_ROUND_ROBIN
106
+ },
107
+ fn: async (input, ctx) => this.runDelivery(input, ctx)
21
108
  });
22
109
  this.automationRunTask = this.client.durableTask({
23
110
  name: WORKFLOW_NAME,
@@ -27,49 +114,252 @@ var HatchetAutomationEngine = class {
27
114
  register(executor) {
28
115
  this.executor = executor;
29
116
  }
117
+ registerJobs(handlers, recurring) {
118
+ this.jobs = new Map(handlers.map((handler) => [handler.job, handler]));
119
+ this.recurring = [...recurring];
120
+ }
121
+ async at(job, runAt, key, payload) {
122
+ const [found, ...extra] = await this.pendingRuns(job, key);
123
+ if (!found) {
124
+ await this.scheduleJob(job, runAt, key, payload);
125
+ return;
126
+ }
127
+ try {
128
+ await this.client.scheduled.update(found.metadata.id, { triggerAt: runAt });
129
+ } catch (err) {
130
+ if ((await this.pendingRuns(job, key)).some((run) => run.metadata.id === found.metadata.id)) throw err;
131
+ await this.scheduleJob(job, runAt, key, payload);
132
+ }
133
+ if (extra.length > 0) await this.client.scheduled.bulkDelete({ scheduledRuns: extra.map((run) => run.metadata.id) });
134
+ }
135
+ async pendingRuns(job, key) {
136
+ return (await this.client.scheduled.list({
137
+ workflow: this.jobTask,
138
+ additionalMetadata: jobMetadataFilter(job, key),
139
+ statuses: [APIContracts.ScheduledRunStatus.SCHEDULED]
140
+ })).rows ?? [];
141
+ }
142
+ async scheduleJob(job, runAt, key, payload) {
143
+ await this.jobTask.schedule(runAt, {
144
+ job,
145
+ key,
146
+ payload
147
+ }, { additionalMetadata: jobMetadata(job, key) });
148
+ }
149
+ async cancel(job, key) {
150
+ await this.client.scheduled.bulkDelete({ filter: { additionalMetadata: jobMetadataFilter(job, key) } });
151
+ }
30
152
  async dispatch(d) {
31
153
  if (!this.executor) throw new Error("HatchetAutomationEngine.dispatch called before register()");
32
154
  await this.automationRunTask.runNoWait(d);
33
155
  }
156
+ async deliver(d) {
157
+ if (!this.executor) throw new Error("HatchetAutomationEngine.deliver called before register()");
158
+ await this.deliveryTask.runNoWait(d);
159
+ }
34
160
  async start() {
35
161
  if (!this.executor) throw new Error("HatchetAutomationEngine.start called before register()");
36
- this.worker = await this.client.worker(WORKER_NAME, { workflows: [this.actionTask, this.automationRunTask] });
162
+ this.worker = await this.client.worker(WORKER_NAME, {
163
+ handleKill: false,
164
+ workflows: [
165
+ this.actionTask,
166
+ this.automationRunTask,
167
+ this.scheduleTask,
168
+ this.jobTask,
169
+ this.deliveryTask
170
+ ]
171
+ });
172
+ await this.reconcileJobsLogged();
37
173
  this.worker.start().catch((err) => {
38
- this.logger?.error("hatchet worker stopped unexpectedly", { error: String(err) });
174
+ this.logger?.error("hatchet worker stopped unexpectedly", { err });
39
175
  });
176
+ if (this.reconcileTimer) clearInterval(this.reconcileTimer);
177
+ this.reconcileTimer = setInterval(() => void this.reconcileAll(), this.reconcileEveryMs);
178
+ this.reconcileTimer.unref?.();
179
+ await this.syncLogged();
40
180
  }
41
181
  async stop() {
42
- await this.worker?.stop();
182
+ if (this.reconcileTimer) clearInterval(this.reconcileTimer);
183
+ this.reconcileTimer = void 0;
184
+ if (!this.worker) return;
185
+ await this.worker.stop();
43
186
  this.worker = void 0;
187
+ this.logger?.info("hatchet worker stopped");
188
+ }
189
+ async reconcileAll() {
190
+ await this.reconcileJobsLogged();
191
+ await this.syncLogged();
192
+ }
193
+ async reconcileJobsLogged() {
194
+ if (this.syncingJobs) return this.syncingJobs;
195
+ this.syncingJobs = this.reconcileJobs().catch((err) => {
196
+ this.logger?.error("hatchet job crons not reconciled", { err });
197
+ }).finally(() => {
198
+ this.syncingJobs = void 0;
199
+ });
200
+ return this.syncingJobs;
201
+ }
202
+ async syncLogged() {
203
+ try {
204
+ await this.syncSchedules();
205
+ } catch (err) {
206
+ this.logger?.error("hatchet schedules not reconciled", { err });
207
+ }
208
+ }
209
+ async syncSchedules() {
210
+ if (this.syncing) {
211
+ await this.syncing;
212
+ return this.syncSchedules();
213
+ }
214
+ this.syncing = this.reconcile().finally(() => {
215
+ this.syncing = void 0;
216
+ });
217
+ await this.syncing;
218
+ }
219
+ async reconcile() {
220
+ if (!this.executor?.listSchedules) return;
221
+ const wanted = /* @__PURE__ */ new Map();
222
+ for (const schedule of await this.executor.listSchedules()) wanted.set(cronName(schedule), schedule);
223
+ const existing = await this.listCrons(this.scheduleTask);
224
+ const seen = /* @__PURE__ */ new Set();
225
+ for (const cron of existing) {
226
+ if (!cron.name?.startsWith(CRON_PREFIX)) continue;
227
+ const schedule = wanted.get(cron.name);
228
+ if (schedule && schedule.cron === cron.cron && !seen.has(cron.name)) {
229
+ seen.add(cron.name);
230
+ continue;
231
+ }
232
+ await this.client.crons.delete(cron);
233
+ }
234
+ for (const [name, schedule] of wanted) {
235
+ if (seen.has(name)) continue;
236
+ await this.client.crons.create(this.scheduleTask, {
237
+ name,
238
+ expression: schedule.cron,
239
+ input: {
240
+ orgId: schedule.orgId,
241
+ automationId: schedule.automationId
242
+ }
243
+ });
244
+ }
245
+ this.logger?.info("hatchet schedules synced", { count: wanted.size });
246
+ }
247
+ async reconcileJobs() {
248
+ const wanted = /* @__PURE__ */ new Map();
249
+ for (const recurring of this.recurring) wanted.set(`${JOB_CRON_PREFIX}${recurring.job}`, recurring);
250
+ const existing = await this.listCrons(this.jobTask);
251
+ const seen = /* @__PURE__ */ new Set();
252
+ for (const cron of existing) {
253
+ if (!cron.name?.startsWith(JOB_CRON_PREFIX)) continue;
254
+ const recurring = wanted.get(cron.name);
255
+ if (recurring && recurring.cron === cron.cron && !seen.has(cron.name)) {
256
+ seen.add(cron.name);
257
+ continue;
258
+ }
259
+ await this.client.crons.delete(cron);
260
+ }
261
+ for (const [name, recurring] of wanted) {
262
+ if (seen.has(name)) continue;
263
+ await this.client.crons.create(this.jobTask, {
264
+ name,
265
+ expression: recurring.cron,
266
+ input: {
267
+ job: recurring.job,
268
+ key: recurring.job,
269
+ payload: null
270
+ }
271
+ });
272
+ }
273
+ this.logger?.info("hatchet job crons synced", { count: wanted.size });
274
+ }
275
+ async runJob(input, ctx) {
276
+ const handler = this.jobs.get(input.job);
277
+ if (!handler) {
278
+ this.logger?.error("scheduled job has no handler", {
279
+ job: input.job,
280
+ key: input.key
281
+ });
282
+ return;
283
+ }
284
+ await handler.run(input.payload, {
285
+ id: ctx.workflowRunId(),
286
+ startedAt: /* @__PURE__ */ new Date()
287
+ });
288
+ }
289
+ async listCrons(workflow) {
290
+ const rows = [];
291
+ const limit = 100;
292
+ for (let offset = 0;; offset += limit) {
293
+ const batch = (await this.client.crons.list({
294
+ workflow,
295
+ limit,
296
+ offset
297
+ })).rows ?? [];
298
+ rows.push(...batch);
299
+ if (batch.length < limit) return rows;
300
+ }
301
+ }
302
+ async runDelivery(input, ctx) {
303
+ if (!this.executor) throw new Error("HatchetAutomationEngine: notification-delivery invoked before register()");
304
+ const lastAttempt = (ctx?.retryCount?.() ?? 0) >= ACTION_RETRIES;
305
+ await this.executor.runDelivery(input, lastAttempt);
44
306
  }
45
- /** Lets failures throw so Hatchet's per-task retry (retries: 3) applies before the parent gives up. */
46
- async runAction(input) {
307
+ async runSchedule(input) {
308
+ if (!this.executor?.fireSchedule) throw new Error("HatchetAutomationEngine: automation-schedule invoked before register()");
309
+ await this.executor.fireSchedule(input.orgId, input.automationId, floorMinute(/* @__PURE__ */ new Date()));
310
+ }
311
+ async runAction(input, ctx) {
47
312
  if (!this.executor) throw new Error("HatchetAutomationEngine: automation-run-action invoked before register()");
48
- return this.executor.runAction(input.dispatch, input.index);
313
+ const { dispatch, index } = input;
314
+ try {
315
+ return await this.executor.runAction(dispatch, index);
316
+ } catch (err) {
317
+ if ((ctx?.retryCount?.() ?? ACTION_RETRIES) < ACTION_RETRIES) throw err;
318
+ if (!(err instanceof ActionFailedError)) this.logActionError(dispatch, index, err);
319
+ return {
320
+ index,
321
+ type: dispatch.actions[index].type,
322
+ status: "failed",
323
+ error: actionFailureMessage(err)
324
+ };
325
+ }
326
+ }
327
+ logActionError(dispatch, index, err) {
328
+ this.logger?.error("automation action failed", {
329
+ orgId: dispatch.orgId,
330
+ automationId: dispatch.automationId,
331
+ runId: dispatch.runId,
332
+ index,
333
+ err
334
+ });
49
335
  }
50
336
  async runDispatch(dispatch, ctx) {
51
337
  if (!this.executor) throw new Error("HatchetAutomationEngine: automation-run invoked before register()");
52
338
  const executor = this.executor;
53
339
  const results = [];
54
- for (let index = 0; index < dispatch.actions.length; index++) try {
55
- results.push(await ctx.spawnChild(this.actionTask, {
56
- dispatch,
57
- index
58
- }));
59
- } catch (err) {
60
- const message = err instanceof Error ? err.message : String(err);
61
- results.push({
62
- index,
63
- type: dispatch.actions[index].type,
64
- status: "failed",
65
- error: message
66
- });
67
- break;
340
+ for (let index = 0; index < dispatch.actions.length; index++) {
341
+ let result;
342
+ try {
343
+ result = await ctx.spawnChild(this.actionTask, {
344
+ dispatch,
345
+ index
346
+ });
347
+ } catch (err) {
348
+ this.logActionError(dispatch, index, err);
349
+ result = {
350
+ index,
351
+ type: dispatch.actions[index].type,
352
+ status: "failed",
353
+ error: ACTION_FAILED
354
+ };
355
+ }
356
+ results.push(result);
357
+ if (result.status === "failed") break;
68
358
  }
69
359
  await executor.finalize(dispatch, results);
70
360
  }
71
361
  };
72
362
  //#endregion
73
- export { HatchetAutomationEngine };
363
+ export { HatchetAutomationEngine, cronName };
74
364
 
75
365
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["// Hatchet implementation of the AutomationEngine port — durable execution via\n// hatchet.run. The \"automation-run\" durable parent task spawns one child task\n// (\"automation-run-action\", retries: 3) per action through the durable event\n// log, stopping on the first exhausted child; the parent then finalizes once\n// with the results collected so far.\nimport type { Logger, AutomationDispatch, AutomationExecutor, AutomationEngine, AutomationActionResult } from \"@headless-lms/core/types\";\nimport { Hatchet } from \"@hatchet-dev/typescript-sdk\";\n\nconst WORKFLOW_NAME = \"automation-run\";\nconst ACTION_TASK_NAME = \"automation-run-action\";\nconst WORKER_NAME = \"automation-run-worker\";\n\ninterface ActionTaskInput {\n dispatch: AutomationDispatch;\n index: number;\n}\n\n/** Subset of `DurableContext` the parent task uses; hand-rolled so tests can fake it without a real Hatchet client. */\ninterface DurableCtxLike {\n spawnChild(task: unknown, input: ActionTaskInput, options?: Record<string, unknown>): Promise<AutomationActionResult>;\n}\n\ninterface RunnableTask<I> {\n runNoWait(input: I): Promise<unknown>;\n}\n\ninterface HatchetWorkerLike {\n start(): Promise<void>;\n stop(): Promise<void>;\n}\n\n/** Subset of the Hatchet v1 client this adapter calls; hand-rolled so tests can inject a fake client. */\nexport interface HatchetClientLike {\n task(opts: {\n name: string;\n retries?: number;\n fn: (input: ActionTaskInput, ctx: unknown) => Promise<AutomationActionResult> | AutomationActionResult;\n }): unknown;\n durableTask(opts: {\n name: string;\n fn: (input: AutomationDispatch, ctx: DurableCtxLike) => Promise<void>;\n }): RunnableTask<AutomationDispatch>;\n worker(name: string, opts: { workflows: any[] }): Promise<HatchetWorkerLike>; // eslint-disable-line @typescript-eslint/no-explicit-any -- bridges to the SDK's own workflow union type without importing it\n}\n\nexport class HatchetAutomationEngine implements AutomationEngine {\n private executor: AutomationExecutor | undefined;\n private worker: HatchetWorkerLike | undefined;\n private readonly actionTask: unknown;\n private readonly automationRunTask: RunnableTask<AutomationDispatch>;\n\n /** `client` defaults to a Hatchet client built from env (`HATCHET_CLIENT_TOKEN`); pass a fake for tests. */\n constructor(\n private readonly client: HatchetClientLike = new Hatchet(),\n private readonly logger?: Logger,\n ) {\n this.actionTask = this.client.task({\n name: ACTION_TASK_NAME,\n retries: 3,\n fn: async (input) => this.runAction(input),\n });\n\n this.automationRunTask = this.client.durableTask({\n name: WORKFLOW_NAME,\n fn: async (dispatch, ctx) => this.runDispatch(dispatch, ctx),\n });\n }\n\n register(executor: AutomationExecutor): void {\n this.executor = executor;\n }\n\n async dispatch(d: AutomationDispatch): Promise<void> {\n if (!this.executor) {\n throw new Error(\"HatchetAutomationEngine.dispatch called before register()\");\n }\n await this.automationRunTask.runNoWait(d);\n }\n\n async start(): Promise<void> {\n if (!this.executor) {\n throw new Error(\"HatchetAutomationEngine.start called before register()\");\n }\n this.worker = await this.client.worker(WORKER_NAME, {\n workflows: [this.actionTask, this.automationRunTask],\n });\n // worker.start() resolves only once the worker stops — run it in the background.\n void this.worker.start().catch((err: unknown) => {\n this.logger?.error(\"hatchet worker stopped unexpectedly\", { error: String(err) });\n });\n }\n\n async stop(): Promise<void> {\n await this.worker?.stop();\n this.worker = undefined;\n }\n\n /** Lets failures throw so Hatchet's per-task retry (retries: 3) applies before the parent gives up. */\n private async runAction(input: ActionTaskInput): Promise<AutomationActionResult> {\n if (!this.executor) {\n throw new Error(\"HatchetAutomationEngine: automation-run-action invoked before register()\");\n }\n return this.executor.runAction(input.dispatch, input.index);\n }\n\n private async runDispatch(dispatch: AutomationDispatch, ctx: DurableCtxLike): Promise<void> {\n if (!this.executor) {\n throw new Error(\"HatchetAutomationEngine: automation-run invoked before register()\");\n }\n const executor = this.executor;\n\n const results: AutomationActionResult[] = [];\n for (let index = 0; index < dispatch.actions.length; index++) {\n try {\n results.push(await ctx.spawnChild(this.actionTask, { dispatch, index }));\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n results.push({ index, type: dispatch.actions[index]!.type, status: \"failed\", error: message });\n break;\n }\n }\n\n // A crash before this task completes replays finalize on resume — finalize and its downstream events must tolerate duplicates.\n await executor.finalize(dispatch, results);\n }\n}\n"],"mappings":";;AAQA,MAAM,gBAAgB;AACtB,MAAM,mBAAmB;AACzB,MAAM,cAAc;AAmCpB,IAAa,0BAAb,MAAiE;CAQ5C;CACA;CARnB;CACA;CACA;CACA;;CAGA,YACE,SAA6C,IAAI,QAAQ,GACzD,QACA;EAFiB,KAAA,SAAA;EACA,KAAA,SAAA;EAEjB,KAAK,aAAa,KAAK,OAAO,KAAK;GACjC,MAAM;GACN,SAAS;GACT,IAAI,OAAO,UAAU,KAAK,UAAU,KAAK;EAC3C,CAAC;EAED,KAAK,oBAAoB,KAAK,OAAO,YAAY;GAC/C,MAAM;GACN,IAAI,OAAO,UAAU,QAAQ,KAAK,YAAY,UAAU,GAAG;EAC7D,CAAC;CACH;CAEA,SAAS,UAAoC;EAC3C,KAAK,WAAW;CAClB;CAEA,MAAM,SAAS,GAAsC;EACnD,IAAI,CAAC,KAAK,UACR,MAAM,IAAI,MAAM,2DAA2D;EAE7E,MAAM,KAAK,kBAAkB,UAAU,CAAC;CAC1C;CAEA,MAAM,QAAuB;EAC3B,IAAI,CAAC,KAAK,UACR,MAAM,IAAI,MAAM,wDAAwD;EAE1E,KAAK,SAAS,MAAM,KAAK,OAAO,OAAO,aAAa,EAClD,WAAW,CAAC,KAAK,YAAY,KAAK,iBAAiB,EACrD,CAAC;EAED,KAAU,OAAO,MAAM,CAAC,CAAC,OAAO,QAAiB;GAC/C,KAAK,QAAQ,MAAM,uCAAuC,EAAE,OAAO,OAAO,GAAG,EAAE,CAAC;EAClF,CAAC;CACH;CAEA,MAAM,OAAsB;EAC1B,MAAM,KAAK,QAAQ,KAAK;EACxB,KAAK,SAAS,KAAA;CAChB;;CAGA,MAAc,UAAU,OAAyD;EAC/E,IAAI,CAAC,KAAK,UACR,MAAM,IAAI,MAAM,0EAA0E;EAE5F,OAAO,KAAK,SAAS,UAAU,MAAM,UAAU,MAAM,KAAK;CAC5D;CAEA,MAAc,YAAY,UAA8B,KAAoC;EAC1F,IAAI,CAAC,KAAK,UACR,MAAM,IAAI,MAAM,mEAAmE;EAErF,MAAM,WAAW,KAAK;EAEtB,MAAM,UAAoC,CAAC;EAC3C,KAAK,IAAI,QAAQ,GAAG,QAAQ,SAAS,QAAQ,QAAQ,SACnD,IAAI;GACF,QAAQ,KAAK,MAAM,IAAI,WAAW,KAAK,YAAY;IAAE;IAAU;GAAM,CAAC,CAAC;EACzE,SAAS,KAAK;GACZ,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;GAC/D,QAAQ,KAAK;IAAE;IAAO,MAAM,SAAS,QAAQ,MAAM,CAAE;IAAM,QAAQ;IAAU,OAAO;GAAQ,CAAC;GAC7F;EACF;EAIF,MAAM,SAAS,SAAS,UAAU,OAAO;CAC3C;AACF"}
1
+ {"version":3,"file":"index.js","names":["Hatchet"],"sources":["../src/index.ts"],"sourcesContent":["import type {\n Logger,\n AutomationDispatch,\n AutomationExecutor,\n AutomationEngine,\n AutomationActionResult,\n DeliveryDispatch,\n ScheduledAutomation,\n JsonValue,\n} from \"@headless-lms/core/types\";\nimport type { RecurringJob, ScheduledJob, Scheduler } from \"@headless-lms/core/shared/ports\";\nimport { APIContracts } from \"@hatchet-dev/typescript-sdk/clients/rest/index.js\";\nimport {\n ConcurrencyLimitStrategy,\n HatchetClient as Hatchet,\n} from \"@hatchet-dev/typescript-sdk/v1/index.js\";\nimport type { LogConstructor } from \"@hatchet-dev/typescript-sdk/clients/hatchet-client/client-config.js\";\nimport {\n ACTION_FAILED,\n ActionFailedError,\n actionFailureMessage,\n} from \"@headless-lms/core/automations\";\n\nconst WORKFLOW_NAME = \"automation-run\";\nconst ACTION_TASK_NAME = \"automation-run-action\";\nconst SCHEDULE_TASK_NAME = \"automation-schedule\";\nconst JOB_TASK_NAME = \"scheduled-job\";\nconst DELIVERY_TASK_NAME = \"notification-delivery\";\nconst WORKER_NAME = \"automation-run-worker\";\nconst CRON_PREFIX = \"hlms-schedule:\";\nconst JOB_CRON_PREFIX = \"hlms-job:\";\nconst RECONCILE_EVERY_MS = 5 * 60_000;\nconst ACTION_RETRIES = 3;\nconst JOB_RETRIES = 3;\nconst RETRY_BACKOFF = { factor: 5, maxSeconds: 300 };\n\ninterface ActionTaskInput {\n dispatch: AutomationDispatch;\n index: number;\n}\n\ninterface ScheduleTaskInput {\n orgId: string;\n automationId: string;\n}\n\ninterface JobTaskInput {\n job: string;\n key: string;\n payload: JsonValue;\n}\n\ninterface TaskCtxLike {\n retryCount?(): number;\n}\n\ninterface JobCtxLike {\n workflowRunId(): string;\n}\n\ninterface DurableCtxLike {\n spawnChild(\n task: unknown,\n input: ActionTaskInput,\n options?: Record<string, unknown>,\n ): Promise<AutomationActionResult>;\n}\n\ninterface RunnableTask<I> {\n runNoWait(input: I): Promise<unknown>;\n}\n\ninterface HatchetWorkerLike {\n start(): Promise<void>;\n stop(): Promise<void>;\n}\n\nexport interface HatchetCronLike {\n metadata: { id: string };\n name?: string;\n cron: string;\n}\n\nexport interface HatchetCronClientLike {\n create(\n workflow: unknown,\n cron: { name: string; expression: string; input?: Record<string, unknown> },\n ): Promise<HatchetCronLike>;\n delete(cron: string | HatchetCronLike): Promise<void>;\n list(query: { workflow?: unknown; limit?: number; offset?: number }): Promise<{\n rows?: HatchetCronLike[];\n }>;\n}\n\nexport interface HatchetScheduledRunLike {\n metadata: { id: string };\n}\n\nexport interface HatchetScheduledClientLike {\n list(query: {\n workflow?: unknown;\n additionalMetadata?: string[];\n statuses?: APIContracts.ScheduledRunStatus[];\n }): Promise<{ rows?: HatchetScheduledRunLike[] }>;\n update(scheduledRun: string, update: { triggerAt: Date }): Promise<unknown>;\n bulkDelete(opts: {\n scheduledRuns?: string[];\n filter?: { additionalMetadata?: string[] };\n }): Promise<unknown>;\n}\n\nexport interface HatchetTaskLike<I> {\n runNoWait(input: I): Promise<unknown>;\n schedule(\n enqueueAt: Date,\n input: I,\n options?: { additionalMetadata?: Record<string, string> },\n ): Promise<unknown>;\n}\n\nexport interface HatchetClientLike {\n task<I>(opts: {\n name: string;\n retries?: number;\n backoff?: { factor?: number; maxSeconds?: number };\n concurrency?: {\n expression: string;\n maxRuns?: number;\n limitStrategy?: ConcurrencyLimitStrategy;\n };\n fn: (input: I, ctx: unknown) => Promise<unknown> | unknown;\n }): HatchetTaskLike<I>;\n durableTask(opts: {\n name: string;\n fn: (input: AutomationDispatch, ctx: DurableCtxLike) => Promise<void>;\n }): RunnableTask<AutomationDispatch>;\n worker(\n name: string,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- bridges to the SDK's own workflow union type without importing it\n opts: { workflows: any[]; handleKill?: boolean },\n ): Promise<HatchetWorkerLike>;\n crons: HatchetCronClientLike;\n scheduled: HatchetScheduledClientLike;\n}\n\nfunction floorMinute(at: Date): Date {\n const copy = new Date(at);\n copy.setUTCSeconds(0, 0);\n return copy;\n}\n\nexport function cronName(schedule: ScheduledAutomation): string {\n return `${CRON_PREFIX}${schedule.orgId}:${schedule.automationId}:${schedule.cron}`;\n}\n\nfunction jobMetadata(job: string, key: string): { job: string; key: string } {\n return { job: encodeURIComponent(job), key: encodeURIComponent(key) };\n}\n\nfunction jobMetadataFilter(job: string, key: string): string[] {\n const metadata = jobMetadata(job, key);\n return [`job:${metadata.job}`, `key:${metadata.key}`];\n}\n\nfunction sdkLogger(logger: Logger): LogConstructor {\n return (context) => {\n const log = logger.child({ hatchet: context });\n const failure = (error?: Error, extra?: object) => ({ ...extra, ...(error && { err: error }) });\n return {\n debug: (message, extra) => log.debug(message, extra),\n info: (message, extra) => log.info(message, extra),\n green: (message, extra) => log.info(message, extra),\n warn: (message, error, extra) => log.warn(message, failure(error, extra)),\n error: (message, error, extra) => log.error(message, failure(error, extra)),\n util: (key, message, extra) => log.debug(message, { ...extra, key }),\n };\n };\n}\n\nexport class HatchetAutomationEngine implements AutomationEngine, Scheduler {\n private executor: AutomationExecutor | undefined;\n private jobs = new Map<string, ScheduledJob>();\n private recurring: RecurringJob[] = [];\n private worker: HatchetWorkerLike | undefined;\n private syncing: Promise<void> | undefined;\n private syncingJobs: Promise<void> | undefined;\n private reconcileTimer: ReturnType<typeof setInterval> | undefined;\n private readonly actionTask: unknown;\n private readonly scheduleTask: unknown;\n private readonly jobTask: HatchetTaskLike<JobTaskInput>;\n private readonly deliveryTask: HatchetTaskLike<DeliveryDispatch>;\n private readonly automationRunTask: RunnableTask<AutomationDispatch>;\n\n private readonly client: HatchetClientLike;\n\n constructor(\n client?: HatchetClientLike,\n private readonly logger?: Logger,\n private readonly reconcileEveryMs: number = RECONCILE_EVERY_MS,\n ) {\n this.client =\n client ??\n (new Hatchet(logger ? { logger: sdkLogger(logger) } : {}) as unknown as HatchetClientLike);\n this.actionTask = this.client.task<ActionTaskInput>({\n name: ACTION_TASK_NAME,\n retries: ACTION_RETRIES,\n fn: async (input, ctx) => this.runAction(input, ctx as TaskCtxLike | undefined),\n });\n\n this.scheduleTask = this.client.task<ScheduleTaskInput>({\n name: SCHEDULE_TASK_NAME,\n fn: async (input) => this.runSchedule(input),\n });\n\n this.jobTask = this.client.task<JobTaskInput>({\n name: JOB_TASK_NAME,\n retries: JOB_RETRIES,\n backoff: RETRY_BACKOFF,\n concurrency: {\n expression: \"input.job + ':' + input.key\",\n maxRuns: 1,\n limitStrategy: ConcurrencyLimitStrategy.GROUP_ROUND_ROBIN,\n },\n fn: async (input, ctx) => this.runJob(input, ctx as JobCtxLike),\n });\n\n this.deliveryTask = this.client.task<DeliveryDispatch>({\n name: DELIVERY_TASK_NAME,\n retries: ACTION_RETRIES,\n backoff: RETRY_BACKOFF,\n concurrency: {\n expression: \"input.deliveryId\",\n maxRuns: 1,\n limitStrategy: ConcurrencyLimitStrategy.GROUP_ROUND_ROBIN,\n },\n fn: async (input, ctx) => this.runDelivery(input, ctx as TaskCtxLike | undefined),\n });\n\n this.automationRunTask = this.client.durableTask({\n name: WORKFLOW_NAME,\n fn: async (dispatch, ctx) => this.runDispatch(dispatch, ctx),\n });\n }\n\n register(executor: AutomationExecutor): void {\n this.executor = executor;\n }\n\n registerJobs(handlers: ScheduledJob[], recurring: RecurringJob[]): void {\n this.jobs = new Map(handlers.map((handler) => [handler.job, handler]));\n this.recurring = [...recurring];\n }\n\n async at(job: string, runAt: Date, key: string, payload: JsonValue): Promise<void> {\n const [found, ...extra] = await this.pendingRuns(job, key);\n if (!found) {\n await this.scheduleJob(job, runAt, key, payload);\n return;\n }\n try {\n await this.client.scheduled.update(found.metadata.id, { triggerAt: runAt });\n } catch (err) {\n const pending = await this.pendingRuns(job, key);\n if (pending.some((run) => run.metadata.id === found.metadata.id)) {\n throw err;\n }\n await this.scheduleJob(job, runAt, key, payload);\n }\n if (extra.length > 0) {\n await this.client.scheduled.bulkDelete({\n scheduledRuns: extra.map((run) => run.metadata.id),\n });\n }\n }\n\n private async pendingRuns(job: string, key: string): Promise<HatchetScheduledRunLike[]> {\n const page = await this.client.scheduled.list({\n workflow: this.jobTask,\n additionalMetadata: jobMetadataFilter(job, key),\n statuses: [APIContracts.ScheduledRunStatus.SCHEDULED],\n });\n return page.rows ?? [];\n }\n\n private async scheduleJob(\n job: string,\n runAt: Date,\n key: string,\n payload: JsonValue,\n ): Promise<void> {\n await this.jobTask.schedule(\n runAt,\n { job, key, payload },\n { additionalMetadata: jobMetadata(job, key) },\n );\n }\n\n async cancel(job: string, key: string): Promise<void> {\n await this.client.scheduled.bulkDelete({\n filter: { additionalMetadata: jobMetadataFilter(job, key) },\n });\n }\n\n async dispatch(d: AutomationDispatch): Promise<void> {\n if (!this.executor) {\n throw new Error(\"HatchetAutomationEngine.dispatch called before register()\");\n }\n await this.automationRunTask.runNoWait(d);\n }\n\n async deliver(d: DeliveryDispatch): Promise<void> {\n if (!this.executor) {\n throw new Error(\"HatchetAutomationEngine.deliver called before register()\");\n }\n await this.deliveryTask.runNoWait(d);\n }\n\n async start(): Promise<void> {\n if (!this.executor) {\n throw new Error(\"HatchetAutomationEngine.start called before register()\");\n }\n this.worker = await this.client.worker(WORKER_NAME, {\n handleKill: false,\n workflows: [\n this.actionTask,\n this.automationRunTask,\n this.scheduleTask,\n this.jobTask,\n this.deliveryTask,\n ],\n });\n await this.reconcileJobsLogged();\n void this.worker.start().catch((err: unknown) => {\n this.logger?.error(\"hatchet worker stopped unexpectedly\", { err });\n });\n if (this.reconcileTimer) {\n clearInterval(this.reconcileTimer);\n }\n this.reconcileTimer = setInterval(() => void this.reconcileAll(), this.reconcileEveryMs);\n this.reconcileTimer.unref?.();\n await this.syncLogged();\n }\n\n async stop(): Promise<void> {\n if (this.reconcileTimer) {\n clearInterval(this.reconcileTimer);\n }\n this.reconcileTimer = undefined;\n if (!this.worker) {\n return;\n }\n await this.worker.stop();\n this.worker = undefined;\n this.logger?.info(\"hatchet worker stopped\");\n }\n\n private async reconcileAll(): Promise<void> {\n await this.reconcileJobsLogged();\n await this.syncLogged();\n }\n\n private async reconcileJobsLogged(): Promise<void> {\n if (this.syncingJobs) {\n return this.syncingJobs;\n }\n this.syncingJobs = this.reconcileJobs()\n .catch((err: unknown) => {\n this.logger?.error(\"hatchet job crons not reconciled\", { err });\n })\n .finally(() => {\n this.syncingJobs = undefined;\n });\n return this.syncingJobs;\n }\n\n private async syncLogged(): Promise<void> {\n try {\n await this.syncSchedules();\n } catch (err) {\n this.logger?.error(\"hatchet schedules not reconciled\", { err });\n }\n }\n\n async syncSchedules(): Promise<void> {\n if (this.syncing) {\n await this.syncing;\n return this.syncSchedules();\n }\n this.syncing = this.reconcile().finally(() => {\n this.syncing = undefined;\n });\n await this.syncing;\n }\n\n private async reconcile(): Promise<void> {\n if (!this.executor?.listSchedules) {\n return;\n }\n const wanted = new Map<string, ScheduledAutomation>();\n for (const schedule of await this.executor.listSchedules()) {\n wanted.set(cronName(schedule), schedule);\n }\n const existing = await this.listCrons(this.scheduleTask);\n const seen = new Set<string>();\n for (const cron of existing) {\n if (!cron.name?.startsWith(CRON_PREFIX)) {\n continue;\n }\n const schedule = wanted.get(cron.name);\n if (schedule && schedule.cron === cron.cron && !seen.has(cron.name)) {\n seen.add(cron.name);\n continue;\n }\n await this.client.crons.delete(cron);\n }\n for (const [name, schedule] of wanted) {\n if (seen.has(name)) {\n continue;\n }\n await this.client.crons.create(this.scheduleTask, {\n name,\n expression: schedule.cron,\n input: { orgId: schedule.orgId, automationId: schedule.automationId },\n });\n }\n this.logger?.info(\"hatchet schedules synced\", { count: wanted.size });\n }\n\n private async reconcileJobs(): Promise<void> {\n const wanted = new Map<string, RecurringJob>();\n for (const recurring of this.recurring) {\n wanted.set(`${JOB_CRON_PREFIX}${recurring.job}`, recurring);\n }\n const existing = await this.listCrons(this.jobTask);\n const seen = new Set<string>();\n for (const cron of existing) {\n if (!cron.name?.startsWith(JOB_CRON_PREFIX)) {\n continue;\n }\n const recurring = wanted.get(cron.name);\n if (recurring && recurring.cron === cron.cron && !seen.has(cron.name)) {\n seen.add(cron.name);\n continue;\n }\n await this.client.crons.delete(cron);\n }\n for (const [name, recurring] of wanted) {\n if (seen.has(name)) {\n continue;\n }\n await this.client.crons.create(this.jobTask, {\n name,\n expression: recurring.cron,\n input: { job: recurring.job, key: recurring.job, payload: null },\n });\n }\n this.logger?.info(\"hatchet job crons synced\", { count: wanted.size });\n }\n\n private async runJob(input: JobTaskInput, ctx: JobCtxLike): Promise<void> {\n const handler = this.jobs.get(input.job);\n if (!handler) {\n this.logger?.error(\"scheduled job has no handler\", { job: input.job, key: input.key });\n return;\n }\n await handler.run(input.payload, { id: ctx.workflowRunId(), startedAt: new Date() });\n }\n\n private async listCrons(workflow: unknown): Promise<HatchetCronLike[]> {\n const rows: HatchetCronLike[] = [];\n const limit = 100;\n for (let offset = 0; ; offset += limit) {\n const page = await this.client.crons.list({ workflow, limit, offset });\n const batch = page.rows ?? [];\n rows.push(...batch);\n if (batch.length < limit) {\n return rows;\n }\n }\n }\n\n private async runDelivery(input: DeliveryDispatch, ctx: TaskCtxLike | undefined): Promise<void> {\n if (!this.executor) {\n throw new Error(\"HatchetAutomationEngine: notification-delivery invoked before register()\");\n }\n const lastAttempt = (ctx?.retryCount?.() ?? 0) >= ACTION_RETRIES;\n await this.executor.runDelivery(input, lastAttempt);\n }\n\n private async runSchedule(input: ScheduleTaskInput): Promise<void> {\n if (!this.executor?.fireSchedule) {\n throw new Error(\"HatchetAutomationEngine: automation-schedule invoked before register()\");\n }\n await this.executor.fireSchedule(input.orgId, input.automationId, floorMinute(new Date()));\n }\n\n private async runAction(\n input: ActionTaskInput,\n ctx: TaskCtxLike | undefined,\n ): Promise<AutomationActionResult> {\n if (!this.executor) {\n throw new Error(\"HatchetAutomationEngine: automation-run-action invoked before register()\");\n }\n const { dispatch, index } = input;\n try {\n return await this.executor.runAction(dispatch, index);\n } catch (err) {\n if ((ctx?.retryCount?.() ?? ACTION_RETRIES) < ACTION_RETRIES) {\n throw err;\n }\n if (!(err instanceof ActionFailedError)) {\n this.logActionError(dispatch, index, err);\n }\n return {\n index,\n type: dispatch.actions[index]!.type,\n status: \"failed\",\n error: actionFailureMessage(err),\n };\n }\n }\n\n private logActionError(dispatch: AutomationDispatch, index: number, err: unknown): void {\n this.logger?.error(\"automation action failed\", {\n orgId: dispatch.orgId,\n automationId: dispatch.automationId,\n runId: dispatch.runId,\n index,\n err,\n });\n }\n\n private async runDispatch(dispatch: AutomationDispatch, ctx: DurableCtxLike): Promise<void> {\n if (!this.executor) {\n throw new Error(\"HatchetAutomationEngine: automation-run invoked before register()\");\n }\n const executor = this.executor;\n\n const results: AutomationActionResult[] = [];\n for (let index = 0; index < dispatch.actions.length; index++) {\n let result: AutomationActionResult;\n try {\n result = await ctx.spawnChild(this.actionTask, { dispatch, index });\n } catch (err) {\n this.logActionError(dispatch, index, err);\n result = {\n index,\n type: dispatch.actions[index]!.type,\n status: \"failed\",\n error: ACTION_FAILED,\n };\n }\n results.push(result);\n if (result.status === \"failed\") {\n break;\n }\n }\n\n await executor.finalize(dispatch, results);\n }\n}\n"],"mappings":";;;;AAuBA,MAAM,gBAAgB;AACtB,MAAM,mBAAmB;AACzB,MAAM,qBAAqB;AAC3B,MAAM,gBAAgB;AACtB,MAAM,qBAAqB;AAC3B,MAAM,cAAc;AACpB,MAAM,cAAc;AACpB,MAAM,kBAAkB;AACxB,MAAM,qBAAqB,IAAI;AAC/B,MAAM,iBAAiB;AACvB,MAAM,cAAc;AACpB,MAAM,gBAAgB;CAAE,QAAQ;CAAG,YAAY;AAAI;AA+GnD,SAAS,YAAY,IAAgB;CACnC,MAAM,OAAO,IAAI,KAAK,EAAE;CACxB,KAAK,cAAc,GAAG,CAAC;CACvB,OAAO;AACT;AAEA,SAAgB,SAAS,UAAuC;CAC9D,OAAO,GAAG,cAAc,SAAS,MAAM,GAAG,SAAS,aAAa,GAAG,SAAS;AAC9E;AAEA,SAAS,YAAY,KAAa,KAA2C;CAC3E,OAAO;EAAE,KAAK,mBAAmB,GAAG;EAAG,KAAK,mBAAmB,GAAG;CAAE;AACtE;AAEA,SAAS,kBAAkB,KAAa,KAAuB;CAC7D,MAAM,WAAW,YAAY,KAAK,GAAG;CACrC,OAAO,CAAC,OAAO,SAAS,OAAO,OAAO,SAAS,KAAK;AACtD;AAEA,SAAS,UAAU,QAAgC;CACjD,QAAQ,YAAY;EAClB,MAAM,MAAM,OAAO,MAAM,EAAE,SAAS,QAAQ,CAAC;EAC7C,MAAM,WAAW,OAAe,WAAoB;GAAE,GAAG;GAAO,GAAI,SAAS,EAAE,KAAK,MAAM;EAAG;EAC7F,OAAO;GACL,QAAQ,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;GACnD,OAAO,SAAS,UAAU,IAAI,KAAK,SAAS,KAAK;GACjD,QAAQ,SAAS,UAAU,IAAI,KAAK,SAAS,KAAK;GAClD,OAAO,SAAS,OAAO,UAAU,IAAI,KAAK,SAAS,QAAQ,OAAO,KAAK,CAAC;GACxE,QAAQ,SAAS,OAAO,UAAU,IAAI,MAAM,SAAS,QAAQ,OAAO,KAAK,CAAC;GAC1E,OAAO,KAAK,SAAS,UAAU,IAAI,MAAM,SAAS;IAAE,GAAG;IAAO;GAAI,CAAC;EACrE;CACF;AACF;AAEA,IAAa,0BAAb,MAA4E;CAkBvD;CACA;CAlBnB;CACA,uBAAe,IAAI,IAA0B;CAC7C,YAAoC,CAAC;CACrC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CAEA,YACE,QACA,QACA,mBAA4C,oBAC5C;EAFiB,KAAA,SAAA;EACA,KAAA,mBAAA;EAEjB,KAAK,SACH,UACC,IAAIA,cAAQ,SAAS,EAAE,QAAQ,UAAU,MAAM,EAAE,IAAI,CAAC,CAAC;EAC1D,KAAK,aAAa,KAAK,OAAO,KAAsB;GAClD,MAAM;GACN,SAAS;GACT,IAAI,OAAO,OAAO,QAAQ,KAAK,UAAU,OAAO,GAA8B;EAChF,CAAC;EAED,KAAK,eAAe,KAAK,OAAO,KAAwB;GACtD,MAAM;GACN,IAAI,OAAO,UAAU,KAAK,YAAY,KAAK;EAC7C,CAAC;EAED,KAAK,UAAU,KAAK,OAAO,KAAmB;GAC5C,MAAM;GACN,SAAS;GACT,SAAS;GACT,aAAa;IACX,YAAY;IACZ,SAAS;IACT,eAAe,yBAAyB;GAC1C;GACA,IAAI,OAAO,OAAO,QAAQ,KAAK,OAAO,OAAO,GAAiB;EAChE,CAAC;EAED,KAAK,eAAe,KAAK,OAAO,KAAuB;GACrD,MAAM;GACN,SAAS;GACT,SAAS;GACT,aAAa;IACX,YAAY;IACZ,SAAS;IACT,eAAe,yBAAyB;GAC1C;GACA,IAAI,OAAO,OAAO,QAAQ,KAAK,YAAY,OAAO,GAA8B;EAClF,CAAC;EAED,KAAK,oBAAoB,KAAK,OAAO,YAAY;GAC/C,MAAM;GACN,IAAI,OAAO,UAAU,QAAQ,KAAK,YAAY,UAAU,GAAG;EAC7D,CAAC;CACH;CAEA,SAAS,UAAoC;EAC3C,KAAK,WAAW;CAClB;CAEA,aAAa,UAA0B,WAAiC;EACtE,KAAK,OAAO,IAAI,IAAI,SAAS,KAAK,YAAY,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC;EACrE,KAAK,YAAY,CAAC,GAAG,SAAS;CAChC;CAEA,MAAM,GAAG,KAAa,OAAa,KAAa,SAAmC;EACjF,MAAM,CAAC,OAAO,GAAG,SAAS,MAAM,KAAK,YAAY,KAAK,GAAG;EACzD,IAAI,CAAC,OAAO;GACV,MAAM,KAAK,YAAY,KAAK,OAAO,KAAK,OAAO;GAC/C;EACF;EACA,IAAI;GACF,MAAM,KAAK,OAAO,UAAU,OAAO,MAAM,SAAS,IAAI,EAAE,WAAW,MAAM,CAAC;EAC5E,SAAS,KAAK;GAEZ,KAAI,MADkB,KAAK,YAAY,KAAK,GAAG,EAAA,CACnC,MAAM,QAAQ,IAAI,SAAS,OAAO,MAAM,SAAS,EAAE,GAC7D,MAAM;GAER,MAAM,KAAK,YAAY,KAAK,OAAO,KAAK,OAAO;EACjD;EACA,IAAI,MAAM,SAAS,GACjB,MAAM,KAAK,OAAO,UAAU,WAAW,EACrC,eAAe,MAAM,KAAK,QAAQ,IAAI,SAAS,EAAE,EACnD,CAAC;CAEL;CAEA,MAAc,YAAY,KAAa,KAAiD;EAMtF,QAAO,MALY,KAAK,OAAO,UAAU,KAAK;GAC5C,UAAU,KAAK;GACf,oBAAoB,kBAAkB,KAAK,GAAG;GAC9C,UAAU,CAAC,aAAa,mBAAmB,SAAS;EACtD,CAAC,EAAA,CACW,QAAQ,CAAC;CACvB;CAEA,MAAc,YACZ,KACA,OACA,KACA,SACe;EACf,MAAM,KAAK,QAAQ,SACjB,OACA;GAAE;GAAK;GAAK;EAAQ,GACpB,EAAE,oBAAoB,YAAY,KAAK,GAAG,EAAE,CAC9C;CACF;CAEA,MAAM,OAAO,KAAa,KAA4B;EACpD,MAAM,KAAK,OAAO,UAAU,WAAW,EACrC,QAAQ,EAAE,oBAAoB,kBAAkB,KAAK,GAAG,EAAE,EAC5D,CAAC;CACH;CAEA,MAAM,SAAS,GAAsC;EACnD,IAAI,CAAC,KAAK,UACR,MAAM,IAAI,MAAM,2DAA2D;EAE7E,MAAM,KAAK,kBAAkB,UAAU,CAAC;CAC1C;CAEA,MAAM,QAAQ,GAAoC;EAChD,IAAI,CAAC,KAAK,UACR,MAAM,IAAI,MAAM,0DAA0D;EAE5E,MAAM,KAAK,aAAa,UAAU,CAAC;CACrC;CAEA,MAAM,QAAuB;EAC3B,IAAI,CAAC,KAAK,UACR,MAAM,IAAI,MAAM,wDAAwD;EAE1E,KAAK,SAAS,MAAM,KAAK,OAAO,OAAO,aAAa;GAClD,YAAY;GACZ,WAAW;IACT,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;IACL,KAAK;GACP;EACF,CAAC;EACD,MAAM,KAAK,oBAAoB;EAC/B,KAAU,OAAO,MAAM,CAAC,CAAC,OAAO,QAAiB;GAC/C,KAAK,QAAQ,MAAM,uCAAuC,EAAE,IAAI,CAAC;EACnE,CAAC;EACD,IAAI,KAAK,gBACP,cAAc,KAAK,cAAc;EAEnC,KAAK,iBAAiB,kBAAkB,KAAK,KAAK,aAAa,GAAG,KAAK,gBAAgB;EACvF,KAAK,eAAe,QAAQ;EAC5B,MAAM,KAAK,WAAW;CACxB;CAEA,MAAM,OAAsB;EAC1B,IAAI,KAAK,gBACP,cAAc,KAAK,cAAc;EAEnC,KAAK,iBAAiB,KAAA;EACtB,IAAI,CAAC,KAAK,QACR;EAEF,MAAM,KAAK,OAAO,KAAK;EACvB,KAAK,SAAS,KAAA;EACd,KAAK,QAAQ,KAAK,wBAAwB;CAC5C;CAEA,MAAc,eAA8B;EAC1C,MAAM,KAAK,oBAAoB;EAC/B,MAAM,KAAK,WAAW;CACxB;CAEA,MAAc,sBAAqC;EACjD,IAAI,KAAK,aACP,OAAO,KAAK;EAEd,KAAK,cAAc,KAAK,cAAc,CAAC,CACpC,OAAO,QAAiB;GACvB,KAAK,QAAQ,MAAM,oCAAoC,EAAE,IAAI,CAAC;EAChE,CAAC,CAAC,CACD,cAAc;GACb,KAAK,cAAc,KAAA;EACrB,CAAC;EACH,OAAO,KAAK;CACd;CAEA,MAAc,aAA4B;EACxC,IAAI;GACF,MAAM,KAAK,cAAc;EAC3B,SAAS,KAAK;GACZ,KAAK,QAAQ,MAAM,oCAAoC,EAAE,IAAI,CAAC;EAChE;CACF;CAEA,MAAM,gBAA+B;EACnC,IAAI,KAAK,SAAS;GAChB,MAAM,KAAK;GACX,OAAO,KAAK,cAAc;EAC5B;EACA,KAAK,UAAU,KAAK,UAAU,CAAC,CAAC,cAAc;GAC5C,KAAK,UAAU,KAAA;EACjB,CAAC;EACD,MAAM,KAAK;CACb;CAEA,MAAc,YAA2B;EACvC,IAAI,CAAC,KAAK,UAAU,eAClB;EAEF,MAAM,yBAAS,IAAI,IAAiC;EACpD,KAAK,MAAM,YAAY,MAAM,KAAK,SAAS,cAAc,GACvD,OAAO,IAAI,SAAS,QAAQ,GAAG,QAAQ;EAEzC,MAAM,WAAW,MAAM,KAAK,UAAU,KAAK,YAAY;EACvD,MAAM,uBAAO,IAAI,IAAY;EAC7B,KAAK,MAAM,QAAQ,UAAU;GAC3B,IAAI,CAAC,KAAK,MAAM,WAAW,WAAW,GACpC;GAEF,MAAM,WAAW,OAAO,IAAI,KAAK,IAAI;GACrC,IAAI,YAAY,SAAS,SAAS,KAAK,QAAQ,CAAC,KAAK,IAAI,KAAK,IAAI,GAAG;IACnE,KAAK,IAAI,KAAK,IAAI;IAClB;GACF;GACA,MAAM,KAAK,OAAO,MAAM,OAAO,IAAI;EACrC;EACA,KAAK,MAAM,CAAC,MAAM,aAAa,QAAQ;GACrC,IAAI,KAAK,IAAI,IAAI,GACf;GAEF,MAAM,KAAK,OAAO,MAAM,OAAO,KAAK,cAAc;IAChD;IACA,YAAY,SAAS;IACrB,OAAO;KAAE,OAAO,SAAS;KAAO,cAAc,SAAS;IAAa;GACtE,CAAC;EACH;EACA,KAAK,QAAQ,KAAK,4BAA4B,EAAE,OAAO,OAAO,KAAK,CAAC;CACtE;CAEA,MAAc,gBAA+B;EAC3C,MAAM,yBAAS,IAAI,IAA0B;EAC7C,KAAK,MAAM,aAAa,KAAK,WAC3B,OAAO,IAAI,GAAG,kBAAkB,UAAU,OAAO,SAAS;EAE5D,MAAM,WAAW,MAAM,KAAK,UAAU,KAAK,OAAO;EAClD,MAAM,uBAAO,IAAI,IAAY;EAC7B,KAAK,MAAM,QAAQ,UAAU;GAC3B,IAAI,CAAC,KAAK,MAAM,WAAW,eAAe,GACxC;GAEF,MAAM,YAAY,OAAO,IAAI,KAAK,IAAI;GACtC,IAAI,aAAa,UAAU,SAAS,KAAK,QAAQ,CAAC,KAAK,IAAI,KAAK,IAAI,GAAG;IACrE,KAAK,IAAI,KAAK,IAAI;IAClB;GACF;GACA,MAAM,KAAK,OAAO,MAAM,OAAO,IAAI;EACrC;EACA,KAAK,MAAM,CAAC,MAAM,cAAc,QAAQ;GACtC,IAAI,KAAK,IAAI,IAAI,GACf;GAEF,MAAM,KAAK,OAAO,MAAM,OAAO,KAAK,SAAS;IAC3C;IACA,YAAY,UAAU;IACtB,OAAO;KAAE,KAAK,UAAU;KAAK,KAAK,UAAU;KAAK,SAAS;IAAK;GACjE,CAAC;EACH;EACA,KAAK,QAAQ,KAAK,4BAA4B,EAAE,OAAO,OAAO,KAAK,CAAC;CACtE;CAEA,MAAc,OAAO,OAAqB,KAAgC;EACxE,MAAM,UAAU,KAAK,KAAK,IAAI,MAAM,GAAG;EACvC,IAAI,CAAC,SAAS;GACZ,KAAK,QAAQ,MAAM,gCAAgC;IAAE,KAAK,MAAM;IAAK,KAAK,MAAM;GAAI,CAAC;GACrF;EACF;EACA,MAAM,QAAQ,IAAI,MAAM,SAAS;GAAE,IAAI,IAAI,cAAc;GAAG,2BAAW,IAAI,KAAK;EAAE,CAAC;CACrF;CAEA,MAAc,UAAU,UAA+C;EACrE,MAAM,OAA0B,CAAC;EACjC,MAAM,QAAQ;EACd,KAAK,IAAI,SAAS,IAAK,UAAU,OAAO;GAEtC,MAAM,SAAQ,MADK,KAAK,OAAO,MAAM,KAAK;IAAE;IAAU;IAAO;GAAO,CAAC,EAAA,CAClD,QAAQ,CAAC;GAC5B,KAAK,KAAK,GAAG,KAAK;GAClB,IAAI,MAAM,SAAS,OACjB,OAAO;EAEX;CACF;CAEA,MAAc,YAAY,OAAyB,KAA6C;EAC9F,IAAI,CAAC,KAAK,UACR,MAAM,IAAI,MAAM,0EAA0E;EAE5F,MAAM,eAAe,KAAK,aAAa,KAAK,MAAM;EAClD,MAAM,KAAK,SAAS,YAAY,OAAO,WAAW;CACpD;CAEA,MAAc,YAAY,OAAyC;EACjE,IAAI,CAAC,KAAK,UAAU,cAClB,MAAM,IAAI,MAAM,wEAAwE;EAE1F,MAAM,KAAK,SAAS,aAAa,MAAM,OAAO,MAAM,cAAc,4BAAY,IAAI,KAAK,CAAC,CAAC;CAC3F;CAEA,MAAc,UACZ,OACA,KACiC;EACjC,IAAI,CAAC,KAAK,UACR,MAAM,IAAI,MAAM,0EAA0E;EAE5F,MAAM,EAAE,UAAU,UAAU;EAC5B,IAAI;GACF,OAAO,MAAM,KAAK,SAAS,UAAU,UAAU,KAAK;EACtD,SAAS,KAAK;GACZ,KAAK,KAAK,aAAa,KAAK,kBAAkB,gBAC5C,MAAM;GAER,IAAI,EAAE,eAAe,oBACnB,KAAK,eAAe,UAAU,OAAO,GAAG;GAE1C,OAAO;IACL;IACA,MAAM,SAAS,QAAQ,MAAM,CAAE;IAC/B,QAAQ;IACR,OAAO,qBAAqB,GAAG;GACjC;EACF;CACF;CAEA,eAAuB,UAA8B,OAAe,KAAoB;EACtF,KAAK,QAAQ,MAAM,4BAA4B;GAC7C,OAAO,SAAS;GAChB,cAAc,SAAS;GACvB,OAAO,SAAS;GAChB;GACA;EACF,CAAC;CACH;CAEA,MAAc,YAAY,UAA8B,KAAoC;EAC1F,IAAI,CAAC,KAAK,UACR,MAAM,IAAI,MAAM,mEAAmE;EAErF,MAAM,WAAW,KAAK;EAEtB,MAAM,UAAoC,CAAC;EAC3C,KAAK,IAAI,QAAQ,GAAG,QAAQ,SAAS,QAAQ,QAAQ,SAAS;GAC5D,IAAI;GACJ,IAAI;IACF,SAAS,MAAM,IAAI,WAAW,KAAK,YAAY;KAAE;KAAU;IAAM,CAAC;GACpE,SAAS,KAAK;IACZ,KAAK,eAAe,UAAU,OAAO,GAAG;IACxC,SAAS;KACP;KACA,MAAM,SAAS,QAAQ,MAAM,CAAE;KAC/B,QAAQ;KACR,OAAO;IACT;GACF;GACA,QAAQ,KAAK,MAAM;GACnB,IAAI,OAAO,WAAW,UACpB;EAEJ;EAEA,MAAM,SAAS,SAAS,UAAU,OAAO;CAC3C;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@headless-lms/adapter-workflow-hatchet",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -12,7 +12,8 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "@hatchet-dev/typescript-sdk": "^1.28.0",
15
- "@headless-lms/core": "0.1.0"
15
+ "@headless-lms/adapter-defaults": "0.5.0",
16
+ "@headless-lms/core": "0.5.0"
16
17
  },
17
18
  "devDependencies": {
18
19
  "@types/node": "^22.10.2",