@headless-lms/adapter-workflow-hatchet 0.1.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/LICENSE +21 -0
- package/README.md +35 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +75 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mdwt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# @headless-lms/adapter-workflow-hatchet
|
|
2
|
+
|
|
3
|
+
Hatchet ([hatchet.run](https://hatchet.run)) implementation of the `AutomationEngine`
|
|
4
|
+
port from `@headless-lms/core/types` — durable execution for dispatched automations.
|
|
5
|
+
|
|
6
|
+
The adapter declares one workflow, `automation-run`: a durable parent task that
|
|
7
|
+
iterates the dispatched actions, spawning one child task per action
|
|
8
|
+
(`automation-run-action`, `retries: 3`) through Hatchet's durable event log. A
|
|
9
|
+
child that exhausts its retries stops the sequence; the parent then calls
|
|
10
|
+
`finalize` exactly once with the results collected so far. Because children are
|
|
11
|
+
spawned via the durable event log, a crash mid-run resumes after the last
|
|
12
|
+
completed action instead of restarting the sequence from scratch.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { HatchetAutomationEngine } from "@headless-lms/adapter-workflow-hatchet";
|
|
16
|
+
|
|
17
|
+
const container = await createContainer(config, {
|
|
18
|
+
adapters: { automationEngine: new HatchetAutomationEngine() },
|
|
19
|
+
});
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The Hatchet client reads its own environment (`HATCHET_CLIENT_TOKEN` and friends —
|
|
23
|
+
see the [Hatchet docs](https://docs.hatchet.run/home/setup)); this adapter reads
|
|
24
|
+
none of it itself. Pass a pre-built client (or a fake, satisfying
|
|
25
|
+
`HatchetClientLike`) as the first constructor argument for tests or non-default
|
|
26
|
+
setups.
|
|
27
|
+
|
|
28
|
+
## Environment variables (Hatchet client, read by the SDK)
|
|
29
|
+
|
|
30
|
+
| Variable | Required | Notes |
|
|
31
|
+
| --------------------- | -------- | ---------------------------------------- |
|
|
32
|
+
| `HATCHET_CLIENT_TOKEN` | yes | Hatchet API token for the target tenant. |
|
|
33
|
+
|
|
34
|
+
See the Hatchet TypeScript SDK docs for the full set of `HATCHET_CLIENT_*` variables
|
|
35
|
+
(host/port, TLS, namespace, …).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AutomationActionResult, AutomationDispatch, AutomationEngine, AutomationExecutor, Logger } from "@headless-lms/core/types";
|
|
2
|
+
//#region src/index.d.ts
|
|
3
|
+
interface ActionTaskInput {
|
|
4
|
+
dispatch: AutomationDispatch;
|
|
5
|
+
index: number;
|
|
6
|
+
}
|
|
7
|
+
/** Subset of `DurableContext` the parent task uses; hand-rolled so tests can fake it without a real Hatchet client. */
|
|
8
|
+
interface DurableCtxLike {
|
|
9
|
+
spawnChild(task: unknown, input: ActionTaskInput, options?: Record<string, unknown>): Promise<AutomationActionResult>;
|
|
10
|
+
}
|
|
11
|
+
interface RunnableTask<I> {
|
|
12
|
+
runNoWait(input: I): Promise<unknown>;
|
|
13
|
+
}
|
|
14
|
+
interface HatchetWorkerLike {
|
|
15
|
+
start(): Promise<void>;
|
|
16
|
+
stop(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
/** Subset of the Hatchet v1 client this adapter calls; hand-rolled so tests can inject a fake client. */
|
|
19
|
+
interface HatchetClientLike {
|
|
20
|
+
task(opts: {
|
|
21
|
+
name: string;
|
|
22
|
+
retries?: number;
|
|
23
|
+
fn: (input: ActionTaskInput, ctx: unknown) => Promise<AutomationActionResult> | AutomationActionResult;
|
|
24
|
+
}): unknown;
|
|
25
|
+
durableTask(opts: {
|
|
26
|
+
name: string;
|
|
27
|
+
fn: (input: AutomationDispatch, ctx: DurableCtxLike) => Promise<void>;
|
|
28
|
+
}): RunnableTask<AutomationDispatch>;
|
|
29
|
+
worker(name: string, opts: {
|
|
30
|
+
workflows: any[];
|
|
31
|
+
}): Promise<HatchetWorkerLike>;
|
|
32
|
+
}
|
|
33
|
+
declare class HatchetAutomationEngine implements AutomationEngine {
|
|
34
|
+
private readonly client;
|
|
35
|
+
private readonly logger?;
|
|
36
|
+
private executor;
|
|
37
|
+
private worker;
|
|
38
|
+
private readonly actionTask;
|
|
39
|
+
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);
|
|
42
|
+
register(executor: AutomationExecutor): void;
|
|
43
|
+
dispatch(d: AutomationDispatch): Promise<void>;
|
|
44
|
+
start(): Promise<void>;
|
|
45
|
+
stop(): Promise<void>;
|
|
46
|
+
/** Lets failures throw so Hatchet's per-task retry (retries: 3) applies before the parent gives up. */
|
|
47
|
+
private runAction;
|
|
48
|
+
private runDispatch;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { HatchetAutomationEngine, HatchetClientLike };
|
|
52
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Hatchet } from "@hatchet-dev/typescript-sdk";
|
|
2
|
+
//#region src/index.ts
|
|
3
|
+
const WORKFLOW_NAME = "automation-run";
|
|
4
|
+
const ACTION_TASK_NAME = "automation-run-action";
|
|
5
|
+
const WORKER_NAME = "automation-run-worker";
|
|
6
|
+
var HatchetAutomationEngine = class {
|
|
7
|
+
client;
|
|
8
|
+
logger;
|
|
9
|
+
executor;
|
|
10
|
+
worker;
|
|
11
|
+
actionTask;
|
|
12
|
+
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;
|
|
16
|
+
this.logger = logger;
|
|
17
|
+
this.actionTask = this.client.task({
|
|
18
|
+
name: ACTION_TASK_NAME,
|
|
19
|
+
retries: 3,
|
|
20
|
+
fn: async (input) => this.runAction(input)
|
|
21
|
+
});
|
|
22
|
+
this.automationRunTask = this.client.durableTask({
|
|
23
|
+
name: WORKFLOW_NAME,
|
|
24
|
+
fn: async (dispatch, ctx) => this.runDispatch(dispatch, ctx)
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
register(executor) {
|
|
28
|
+
this.executor = executor;
|
|
29
|
+
}
|
|
30
|
+
async dispatch(d) {
|
|
31
|
+
if (!this.executor) throw new Error("HatchetAutomationEngine.dispatch called before register()");
|
|
32
|
+
await this.automationRunTask.runNoWait(d);
|
|
33
|
+
}
|
|
34
|
+
async start() {
|
|
35
|
+
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] });
|
|
37
|
+
this.worker.start().catch((err) => {
|
|
38
|
+
this.logger?.error("hatchet worker stopped unexpectedly", { error: String(err) });
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async stop() {
|
|
42
|
+
await this.worker?.stop();
|
|
43
|
+
this.worker = void 0;
|
|
44
|
+
}
|
|
45
|
+
/** Lets failures throw so Hatchet's per-task retry (retries: 3) applies before the parent gives up. */
|
|
46
|
+
async runAction(input) {
|
|
47
|
+
if (!this.executor) throw new Error("HatchetAutomationEngine: automation-run-action invoked before register()");
|
|
48
|
+
return this.executor.runAction(input.dispatch, input.index);
|
|
49
|
+
}
|
|
50
|
+
async runDispatch(dispatch, ctx) {
|
|
51
|
+
if (!this.executor) throw new Error("HatchetAutomationEngine: automation-run invoked before register()");
|
|
52
|
+
const executor = this.executor;
|
|
53
|
+
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;
|
|
68
|
+
}
|
|
69
|
+
await executor.finalize(dispatch, results);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
//#endregion
|
|
73
|
+
export { HatchetAutomationEngine };
|
|
74
|
+
|
|
75
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@headless-lms/adapter-workflow-hatchet",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@hatchet-dev/typescript-sdk": "^1.28.0",
|
|
15
|
+
"@headless-lms/core": "0.1.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.10.2",
|
|
19
|
+
"tsdown": "^0.22.9",
|
|
20
|
+
"typescript": "^5.7.2",
|
|
21
|
+
"vitest": "^2.1.8"
|
|
22
|
+
},
|
|
23
|
+
"description": "Hatchet workflow adapter for the headless LMS",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/mdwt/headless-lms.git",
|
|
28
|
+
"directory": "adapters/workflow-hatchet"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/mdwt/headless-lms#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/mdwt/headless-lms/issues"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsdown",
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"test": "vitest run"
|
|
44
|
+
}
|
|
45
|
+
}
|