@pogodisco/task-runner 0.0.1

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/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@pogodisco/task-runner",
3
+ "version": "0.0.1",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "author": "Jakub Bystroński",
8
+ "description": "run tasks organized in a hierarchical schema",
9
+ "keywords": [
10
+ "schema",
11
+ "task",
12
+ "typescript"
13
+ ],
14
+ "type": "module",
15
+ "license": "MIT",
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "import": "./dist/index.js",
21
+ "require": "./dist/index.cjs",
22
+ "types": "./dist/index.d.ts"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "prepublishOnly": "npm run build",
27
+ "build": "tsc",
28
+ "dev": "tsc -w -p tsconfig.json"
29
+ },
30
+ "peerDependencies": {
31
+ "@pogodisco/response": "^0.0.1",
32
+ "typescript": "^5.0.0"
33
+ },
34
+ "devDependecies": {
35
+ "@pogodisco/response": "^0.0.1"
36
+ }
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./main";
package/src/main.ts ADDED
@@ -0,0 +1,283 @@
1
+ import {
2
+ isFailure,
3
+ isSuccess,
4
+ withResponse,
5
+ type SuccessResponse,
6
+ } from "@pogodisco/response";
7
+
8
+ // --- logging / options ---
9
+ export type LogEvent =
10
+ | "start"
11
+ | "finish"
12
+ | "data"
13
+ | "deferred"
14
+ | "skipped"
15
+ | "background"
16
+ | "parallel"
17
+ | "success"
18
+ | "fail";
19
+
20
+ export type TaskLogger = (event: LogEvent, key: string, meta?: any) => void;
21
+
22
+ export interface SchemaOptions {
23
+ log?: TaskLogger;
24
+ }
25
+
26
+ // --- core types ---
27
+ export type TaskState = "pending" | "skipped" | "failed" | "success";
28
+
29
+ // --- task definition ---
30
+ export type TaskDefinition<
31
+ F extends (...args: any) => any,
32
+ T extends TaskMap,
33
+ I extends Record<string, any>,
34
+ > = {
35
+ fn: F;
36
+ dependencies?: string[];
37
+ abort?: boolean;
38
+ bg?: boolean;
39
+ runIf?: ((results: TaskResultsData<T, I>) => boolean | Promise<boolean>)[];
40
+ argMap?: (results: TaskResultsData<T, I>) => Parameters<F>[0];
41
+ };
42
+
43
+ export type TaskMap = Record<string, TaskDefinition<any, any, any>>;
44
+
45
+ type TaskFnResult<TD extends TaskDefinition<any, any, any>> = Awaited<
46
+ ReturnType<TD["fn"]>
47
+ >;
48
+
49
+ type SuccessData<T> = T extends { ok: true; data: infer D } ? D : never;
50
+
51
+ // --- **results only store unwrapped success data** ---
52
+ // export type TaskResultsData<T extends TaskMap, I> = { _init: I } & {
53
+ // [K in keyof T]: TDResultData<T[K]>;
54
+ // };
55
+
56
+ export type TaskResultsData<T extends TaskMap, I> = { _init: I } & {
57
+ [K in keyof T]: TDResultData<T[K]>;
58
+ };
59
+
60
+ type TDResultData<TD extends TaskDefinition<any, any, any>> =
61
+ TD extends TaskDefinition<infer F, any, any>
62
+ ? SuccessData<Awaited<ReturnType<F>>>
63
+ : never;
64
+
65
+ type ReturnTypeSuccessData<F extends (...args: any) => any> = Awaited<
66
+ ReturnType<F>
67
+ > extends SuccessResponse<infer D>
68
+ ? D
69
+ : never;
70
+
71
+ // --- helper to create task definitions from functions ---
72
+ export type TasksFromFns<T extends Record<string, (...args: any) => any>> = {
73
+ [K in keyof T]: TaskDefinition<T[K], any, any>;
74
+ };
75
+
76
+ // --- typed schema ---
77
+ export type TaskSchemaWithContracts<T extends TaskMap, I, O> = {
78
+ [K in keyof T]: T[K] extends TaskDefinition<infer F, any, any>
79
+ ? TaskDefinition<F, T, I>
80
+ : never;
81
+ } & {
82
+ _init?: I;
83
+ _output: (results: TaskResultsData<T, I>) => O;
84
+ };
85
+
86
+ // --- defineSchema ---
87
+ export function defineSchema<T extends TaskMap, I, O>(
88
+ schema: {
89
+ [K in keyof T]: T[K] extends TaskDefinition<infer F, any, any>
90
+ ? TaskDefinition<F, T, I>
91
+ : never;
92
+ } & { _output: (results: TaskResultsData<T, I>) => O },
93
+ ): TaskSchemaWithContracts<T, I, O> {
94
+ preflightCheck(schema);
95
+ return schema as any;
96
+ }
97
+
98
+ // --- preflight check ---
99
+ function preflightCheck<T extends TaskMap>(
100
+ schema: T | (T & { _output?: any }),
101
+ ) {
102
+ const keys = new Set<string>();
103
+ for (const key in schema) {
104
+ if (key === "_output" || key === "_init") continue;
105
+ if (keys.has(key)) throw new Error(`Duplicate task key: ${key}`);
106
+ keys.add(key);
107
+
108
+ const task = (schema as T)[key];
109
+ if (!task) continue;
110
+ if (task.abort === undefined) task.abort = true;
111
+ if (task.bg === undefined) task.bg = false;
112
+ if (task.runIf === undefined) task.runIf = [];
113
+ }
114
+
115
+ const visited = new Set<string>();
116
+ const stack = new Set<string>();
117
+ const visit = (taskKey: string) => {
118
+ if (taskKey === "_output" || taskKey === "_init") return;
119
+ if (stack.has(taskKey))
120
+ throw new Error(
121
+ `Circular dependency: ${[...stack, taskKey].join(" -> ")}`,
122
+ );
123
+ if (visited.has(taskKey)) return;
124
+
125
+ stack.add(taskKey);
126
+ const task = (schema as T)[taskKey];
127
+ const deps = task?.dependencies ?? [];
128
+ for (const dep of deps) {
129
+ if (!(dep in schema))
130
+ throw new Error(`Task "${taskKey}" depends on unknown "${dep}"`);
131
+ visit(dep);
132
+ }
133
+ stack.delete(taskKey);
134
+ visited.add(taskKey);
135
+ };
136
+
137
+ for (const key of Object.keys(schema)) visit(key);
138
+ }
139
+
140
+ // --- mark dependents skipped ---
141
+ function markDependentsSkipped<T extends TaskMap>(
142
+ schema: T,
143
+ status: Record<keyof T, TaskState>,
144
+ key: keyof T,
145
+ ) {
146
+ for (const k of Object.keys(schema) as (keyof T)[]) {
147
+ const task = schema[k];
148
+ if (
149
+ (task.dependencies ?? []).includes(key as string) &&
150
+ status[k] === "pending"
151
+ ) {
152
+ status[k] = "skipped";
153
+ markDependentsSkipped(schema, status, k);
154
+ }
155
+ }
156
+ }
157
+
158
+ // --- runSchema, storing **only success data** ---
159
+ export const runSchema = withResponse(
160
+ async <T extends TaskMap, I, O>(
161
+ schema: TaskSchemaWithContracts<T, I, O>,
162
+ initArgs: I,
163
+ options?: SchemaOptions,
164
+ ): Promise<{ _output: O; _status: Record<keyof T, TaskState> }> => {
165
+ try {
166
+ const logger: TaskLogger | undefined = options?.log;
167
+
168
+ const localSchema = {
169
+ ...schema,
170
+ _init: { fn: (x: I) => x, argMap: () => initArgs },
171
+ } as unknown as T & { _init: TaskDefinition<(x: I) => I, T, I> };
172
+
173
+ const taskKeys = Object.keys(localSchema).filter(
174
+ (k) => k !== "_output" && k !== "_init",
175
+ ) as (keyof typeof localSchema)[];
176
+
177
+ const results: Partial<TaskResultsData<T, I>> = {
178
+ _init: initArgs,
179
+ } as any;
180
+
181
+ const status: { [K in keyof typeof localSchema]?: TaskState } = {};
182
+ for (const key of taskKeys) status[key] = "pending";
183
+
184
+ let progress = true;
185
+ let safety = 0;
186
+ const maxIterations = taskKeys.length * 2;
187
+
188
+ while (progress && safety < maxIterations) {
189
+ progress = false;
190
+ safety++;
191
+
192
+ for (const key of taskKeys) {
193
+ const task = localSchema[key];
194
+ if (status[key] !== "pending") continue;
195
+
196
+ const deps = task.dependencies ?? [];
197
+ if (
198
+ !deps.every(
199
+ (d) => status[d as keyof typeof localSchema] === "success",
200
+ )
201
+ )
202
+ continue;
203
+
204
+ if (task.runIf?.length) {
205
+ const runIfResults = await Promise.all(
206
+ task.runIf.map((fn) => fn(results as TaskResultsData<T, I>)),
207
+ );
208
+ if (!runIfResults.every(Boolean)) {
209
+ status[key] = "skipped";
210
+ logger?.("skipped", String(key), {
211
+ reason: "runIf",
212
+ results: runIfResults,
213
+ });
214
+ markDependentsSkipped(localSchema, status as any, key as any);
215
+ continue;
216
+ }
217
+ }
218
+
219
+ const args = task.argMap?.(results as TaskResultsData<T, I>);
220
+
221
+ if (task.bg) {
222
+ logger?.("background", String(key), args);
223
+ Promise.resolve().then(async () => {
224
+ try {
225
+ await (args !== undefined ? task.fn(args) : task.fn());
226
+ logger?.("success", String(key));
227
+ } catch (err) {
228
+ logger?.("fail", String(key), err);
229
+ }
230
+ });
231
+ status[key] = "success";
232
+ (results as any)[key] = undefined;
233
+ progress = true;
234
+ continue;
235
+ }
236
+
237
+ logger?.("start", String(key), args);
238
+
239
+ try {
240
+ const result =
241
+ args !== undefined ? await task.fn(args) : await task.fn();
242
+
243
+ if (isFailure(result)) {
244
+ status[key] = "failed";
245
+ logger?.("fail", String(key), result);
246
+ if (task.abort) throw result;
247
+ } else if (isSuccess(result)) {
248
+ status[key] = "success";
249
+ (results as any)[key] = result.data; // ✅ unwrap data automatically
250
+ logger?.("success", String(key), result);
251
+ }
252
+ } catch (err) {
253
+ status[key] = "failed";
254
+ logger?.("fail", String(key), err);
255
+ if (task.abort) throw err;
256
+ }
257
+
258
+ progress = true;
259
+ }
260
+ }
261
+
262
+ const pendingKeys = taskKeys.filter(
263
+ (k) => status[k as keyof typeof status] === "pending",
264
+ );
265
+ if (pendingKeys.length)
266
+ throw new Error(
267
+ `Unresolved tasks (possible circular deps): ${pendingKeys.join(", ")}`,
268
+ );
269
+
270
+ const output = schema._output(results as TaskResultsData<T, I>);
271
+ logger?.("finish", "_schema", { results, status, output });
272
+
273
+ return { _output: output, _status: status as Record<keyof T, TaskState> };
274
+ } catch (err) {
275
+ console.error("🔥 runSchema crashed before finish:", err);
276
+ throw err;
277
+ }
278
+ },
279
+ );
280
+
281
+ /// utility fn wrapper around runSchema
282
+ //
283
+ //
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "declaration": true,
5
+ "declarationMap": true,
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "module": "ESNext",
9
+ "target": "ESNext",
10
+ "moduleResolution": "Node",
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src"],
14
+ "exclude": ["dist", "node_modules"]
15
+ }