husbandry 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/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-07-22
9
+
10
+ ### Added
11
+ - Initial release with core structured concurrency functionality
12
+ - Implementation of 8 core invariants for task group management
13
+ - Concurrency limiting implementation with `options.concurrency` parameter
14
+ - Comprehensive demo in `examples/demo.ts` with 5 sections demonstrating all features
15
+ - Complete README.md documentation with usage examples and API documentation
16
+ - Repository field in package.json for proper npm publishing
17
+ - Basic test suite with invariant coverage
18
+ - Package scaffold with proper TypeScript configuration
19
+
20
+ ### Fixed
21
+ - Critical bug: Concurrency limiting was specified in Phase 2 spec but was completely unimplemented
22
+ - Missing repository field in package.json metadata
23
+ - Unhandled rejection when spawning tasks after abort
24
+
25
+ ### Changed
26
+ - Improved package metadata for production readiness
27
+ - Enhanced type safety and ESLint compliance in all source files
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
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,107 @@
1
+ # husbandry
2
+
3
+ [![npm version](https://badge.fury.io/js/husbandry.svg)](https://www.npmjs.com/package/husbandry)
4
+
5
+ Structured concurrency for async tasks — spawn siblings under one scope; first failure cancels the rest via AbortSignal, then the scope throws after all have settled.
6
+
7
+ ## The Problem
8
+
9
+ `Promise.all` starts everything but on first rejection it doesn't cancel the siblings — they keep running, leaking work, sockets, and money. Unhandled rejections surface later. Doing "run these together; if one fails, cancel the rest and clean up; await everything before returning" correctly is fiddly and re-invented constantly.
10
+
11
+ JavaScript has no native structured concurrency (unlike Python's `trio`/`asyncio.TaskGroup` or Kotlin coroutines). `AbortController` gives you the mechanism but not the lifecycle. Husbandry fills this gap with a tiny, zero-dependency package.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install husbandry
17
+ # or
18
+ pnpm add husbandry
19
+ # or
20
+ yarn add husbandry
21
+ ```
22
+
23
+ ## Use
24
+
25
+ ```typescript
26
+ import husbandry from "husbandry";
27
+
28
+ // Basic usage
29
+ const results = await husbandry(async (spawn, signal) => {
30
+ const task1 = spawn(() => fetchA(signal));
31
+ const task2 = spawn(() => fetchB(signal));
32
+ const task3 = spawn(() => fetchC(signal));
33
+ return await Promise.all([task1, task2, task3]);
34
+ });
35
+ ```
36
+
37
+ With timeout composition:
38
+
39
+ ```typescript
40
+ import husbandry from "husbandry";
41
+
42
+ const controller = new AbortController();
43
+ const timeout = AbortSignal.timeout(5000); // 5 second timeout
44
+ const combinedSignal = AbortSignal.any([controller.signal, timeout]);
45
+
46
+ const results = await husbandry(async (spawn, signal) => {
47
+ const response = await spawn(() => fetch("/api/data", { signal }));
48
+ return response.json();
49
+ }, { signal: combinedSignal });
50
+ ```
51
+
52
+ ## API
53
+
54
+ ### `husbandry<R>(body, options?): Promise<R>`
55
+
56
+ Creates a structured concurrency group for async tasks.
57
+
58
+ - **body**: Function that receives `spawn` and `signal`, returns the group result
59
+ - **options.signal**: External cancellation signal propagated into the group
60
+ - **options.concurrency**: Maximum concurrent tasks (default: Infinity)
61
+ - **Returns**: Promise that resolves with body result or rejects with GroupError
62
+
63
+ **Behavior**: Tasks spawned via `spawn()` run concurrently. On first failure or external abort, all tasks receive an abort signal. The scope waits for all tasks to settle before resolving or rejecting.
64
+
65
+ ### `Spawn<T>`
66
+
67
+ Type for the spawn function: `<T>(fn: (signal: AbortSignal) => Promise<T>) => Promise<T>`
68
+
69
+ ### `GroupError`
70
+
71
+ Extends `AggregateError`. Collects errors from all failed tasks. Includes a `primaryError` getter for the first non-AbortError.
72
+
73
+ ### `GroupOptions`
74
+
75
+ Configuration interface for the husbandry group.
76
+
77
+ ## Non-goals
78
+
79
+ Husbandry intentionally does NOT provide:
80
+
81
+ - **Forced task termination**: Tasks must cooperatively observe the signal (cannot force-kill promises in JS)
82
+ - **Retries**: Compose with retry logic separately
83
+ - **Timeouts**: Use `AbortSignal.timeout()` or compose with timeout promises
84
+ - **Worker pools**: Use dedicated worker pool libraries for heavy CPU work
85
+ - **Result streaming**: Return arrays or objects from the body function
86
+
87
+ ## TypeScript
88
+
89
+ Full TypeScript support with strict mode. All exports are properly typed:
90
+
91
+ ```typescript
92
+ import husbandry, { type GroupOptions, GroupError } from "husbandry";
93
+
94
+ async function example(): Promise<string[]> {
95
+ return await husbandry(async (spawn) => {
96
+ const results = await Promise.all([
97
+ spawn(() => Promise.resolve("one")),
98
+ spawn(() => Promise.resolve("two"))
99
+ ]);
100
+ return results;
101
+ });
102
+ }
103
+ ```
104
+
105
+ ## License
106
+
107
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ GroupError: () => GroupError,
24
+ default: () => index_default,
25
+ husbandry: () => husbandry
26
+ });
27
+ module.exports = __toCommonJS(index_exports);
28
+
29
+ // src/errors.ts
30
+ var GroupError = class extends AggregateError {
31
+ /**
32
+ * The first non-AbortError from the collected errors.
33
+ * If all errors are AbortErrors (from cancellation), this will be undefined.
34
+ */
35
+ primaryError;
36
+ constructor(errors, message) {
37
+ const definedErrors = errors.filter((e) => e instanceof Error);
38
+ super(
39
+ definedErrors,
40
+ message || "GroupError: One or more tasks in the group failed"
41
+ );
42
+ for (const error of definedErrors) {
43
+ const isAbortError = error instanceof DOMException && error.name === "AbortError";
44
+ const isErrorMessageAbort = error instanceof Error && error.message.includes("AbortError");
45
+ if (!isAbortError && !isErrorMessageAbort) {
46
+ this.primaryError = error;
47
+ break;
48
+ }
49
+ }
50
+ }
51
+ };
52
+
53
+ // src/group.ts
54
+ function createGroup(body, externalSignal, concurrency = Infinity) {
55
+ return new Promise((bodyResolve, bodyReject) => {
56
+ const controller = new AbortController();
57
+ const internalSignal = controller.signal;
58
+ const combinedSignal = externalSignal ? createCombinedSignal(externalSignal, internalSignal) : internalSignal;
59
+ const taskPromises = [];
60
+ const errors = [];
61
+ let hasError = false;
62
+ let abortInitiated = false;
63
+ let runningTasks = 0;
64
+ const pendingTasks = [];
65
+ const processQueue = () => {
66
+ while (pendingTasks.length > 0 && runningTasks < concurrency) {
67
+ const startNext = pendingTasks.shift();
68
+ startNext?.();
69
+ }
70
+ };
71
+ const spawn = (fn) => {
72
+ if (abortInitiated) {
73
+ const abortError = new DOMException(
74
+ "Task spawned after group abort",
75
+ "AbortError"
76
+ );
77
+ return Promise.reject(abortError);
78
+ }
79
+ let taskResolver = () => {
80
+ };
81
+ let taskRejecter = () => {
82
+ };
83
+ const taskPromise = new Promise((resolve, reject) => {
84
+ taskResolver = resolve;
85
+ taskRejecter = reject;
86
+ });
87
+ taskPromise.catch((err) => {
88
+ if (err instanceof Error && err.name !== "AbortError") {
89
+ handleFirstError(err);
90
+ }
91
+ });
92
+ taskPromises.push(taskPromise);
93
+ const startTask = () => {
94
+ runningTasks++;
95
+ if (abortInitiated) {
96
+ taskRejecter(
97
+ new DOMException("Task started after group abort", "AbortError")
98
+ );
99
+ runningTasks--;
100
+ processQueue();
101
+ return;
102
+ }
103
+ Promise.resolve().then(() => fn(combinedSignal)).then(taskResolver).catch(taskRejecter).finally(() => {
104
+ runningTasks--;
105
+ processQueue();
106
+ });
107
+ };
108
+ if (runningTasks < concurrency) {
109
+ startTask();
110
+ } else {
111
+ pendingTasks.push(startTask);
112
+ }
113
+ return taskPromise;
114
+ };
115
+ const handleFirstError = (error) => {
116
+ if (!abortInitiated) {
117
+ abortInitiated = true;
118
+ controller.abort();
119
+ const errorObj = error instanceof Error ? error : new Error(String(error));
120
+ if (errorObj.name !== "AbortError") {
121
+ errors.push(errorObj);
122
+ hasError = true;
123
+ }
124
+ processQueue();
125
+ }
126
+ };
127
+ void (async () => {
128
+ try {
129
+ const result = await body(spawn, combinedSignal);
130
+ await Promise.allSettled(taskPromises);
131
+ if (hasError) {
132
+ bodyReject(new GroupError(errors));
133
+ } else {
134
+ bodyResolve(result);
135
+ }
136
+ } catch (error) {
137
+ handleFirstError(error);
138
+ await Promise.allSettled(taskPromises);
139
+ bodyReject(new GroupError(errors));
140
+ }
141
+ })();
142
+ });
143
+ }
144
+ function createCombinedSignal(signal1, signal2) {
145
+ if (signal1.aborted || signal2.aborted) {
146
+ const controller = new AbortController();
147
+ controller.abort();
148
+ return controller.signal;
149
+ }
150
+ const aborted = false;
151
+ const listeners = /* @__PURE__ */ new Set();
152
+ return {
153
+ get aborted() {
154
+ return signal1.aborted || signal2.aborted || aborted;
155
+ },
156
+ addEventListener: (_type, listener) => {
157
+ if (_type === "abort") {
158
+ listeners.add(listener);
159
+ if (signal1.aborted || signal2.aborted || aborted) {
160
+ queueMicrotask(() => listener());
161
+ }
162
+ }
163
+ },
164
+ removeEventListener: (_type, listener) => {
165
+ if (_type === "abort") {
166
+ listeners.delete(listener);
167
+ }
168
+ }
169
+ };
170
+ }
171
+
172
+ // src/index.ts
173
+ function husbandry(body, options) {
174
+ const concurrency = options?.concurrency ?? Infinity;
175
+ return createGroup(body, options?.signal, concurrency);
176
+ }
177
+ var index_default = husbandry;
178
+ // Annotate the CommonJS export names for ESM import in node:
179
+ 0 && (module.exports = {
180
+ GroupError,
181
+ husbandry
182
+ });
183
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/group.ts"],"sourcesContent":["/**\n * Options for configuring the husbandry task group.\n */\nexport interface GroupOptions {\n /**\n * External cancellation signal propagated into the group.\n * When this signal aborts, all tasks in the group are cancelled.\n */\n signal?: AbortSignal;\n\n /**\n * Maximum number of tasks running concurrently.\n * Defaults to Infinity (no limit).\n */\n concurrency?: number;\n}\n\n/**\n * Spawn function type for creating tasks within the husbandry group.\n *\n * @template T - The type of value the task promise resolves to\n */\nexport type Spawn = <T>(fn: (signal: AbortSignal) => Promise<T>) => Promise<T>;\n\n/**\n * Creates a structured concurrency group for async tasks.\n *\n * This function manages a group of concurrent async tasks where:\n * - Tasks are spawned using the spawn function\n * - On first failure, all sibling tasks are cancelled via AbortSignal\n * - The scope only settles after all tasks have settled (cooperative cancellation)\n * - If any task fails, rejects with a GroupError aggregating all errors\n *\n * @template R - The type of value the body promise resolves to\n * @param body - Function that receives spawn and signal, returns the group result\n * @param options - Optional configuration for the group\n * @returns Promise that resolves with body result or rejects with GroupError\n *\n * @throws {GroupError} If any task or the body function fails\n *\n * @example\n * ```ts\n * import husbandry from \"husbandry\";\n *\n * const results = await husbandry(async (spawn, signal) => {\n * const task1 = spawn(() => fetchA(signal));\n * const task2 = spawn(() => fetchB(signal));\n * const task3 = spawn(() => fetchC(signal));\n * return await Promise.all([task1, task2, task3]);\n * });\n * ```\n */\nexport function husbandry<R>(\n body: (spawn: Spawn, signal: AbortSignal) => Promise<R>,\n options?: GroupOptions,\n): Promise<R> {\n const concurrency = options?.concurrency ?? Infinity;\n return createGroup(body, options?.signal, concurrency);\n}\n\n/**\n * Default export for convenience.\n * Allows both named import and default import:\n * - `import { husbandry } from \"husbandry\"`\n * - `import husbandry from \"husbandry\"`\n */\nexport default husbandry;\n\n// Re-export GroupError for users who want to check error types\nexport { GroupError } from \"./errors.js\";\n\n// Import the createGroup function\nimport { createGroup } from \"./group.js\";\n","/**\n * Custom error class for aggregating errors from failed tasks in a husbandry group.\n *\n * Extends the built-in AggregateError to collect all errors from tasks that failed\n * or were cancelled. Provides a primaryError getter to access the first non-AbortError.\n */\nexport class GroupError extends AggregateError {\n /**\n * The first non-AbortError from the collected errors.\n * If all errors are AbortErrors (from cancellation), this will be undefined.\n */\n public readonly primaryError?: unknown;\n\n constructor(errors: Array<unknown>, message?: string) {\n // Filter out undefined values\n const definedErrors = errors.filter((e): e is Error => e instanceof Error);\n super(\n definedErrors,\n message || \"GroupError: One or more tasks in the group failed\",\n );\n\n // Find the first non-AbortError\n for (const error of definedErrors) {\n // Check if it's not a DOMException AbortError or regular Error with \"Abort\" message\n const isAbortError =\n error instanceof DOMException && error.name === \"AbortError\";\n const isErrorMessageAbort =\n error instanceof Error && error.message.includes(\"AbortError\");\n\n if (!isAbortError && !isErrorMessageAbort) {\n this.primaryError = error;\n break;\n }\n }\n }\n}\n","/**\n * Internal group management for husbandry structured concurrency.\n *\n * @internal\n */\n\nimport { GroupError } from \"./errors.js\";\n\n/**\n * Creates a task group with structured concurrency management.\n *\n * @param body - The body function that receives spawn and signal\n * @param externalSignal - Optional external abort signal to propagate\n * @param concurrency - Maximum concurrent tasks (default: Infinity)\n * @returns Promise that resolves with body result or rejects with GroupError\n *\n * @internal\n */\nexport function createGroup<R>(\n body: (\n spawn: <T>(fn: (signal: AbortSignal) => Promise<T>) => Promise<T>,\n signal: AbortSignal,\n ) => Promise<R>,\n externalSignal?: AbortSignal,\n concurrency: number = Infinity,\n): Promise<R> {\n return new Promise((bodyResolve, bodyReject) => {\n // Create internal abort controller\n const controller = new AbortController();\n const internalSignal = controller.signal;\n\n // Combine with external signal if provided\n const combinedSignal = externalSignal\n ? createCombinedSignal(externalSignal, internalSignal)\n : internalSignal;\n\n // Track state\n const taskPromises: Promise<unknown>[] = [];\n const errors: Error[] = [];\n let hasError = false;\n let abortInitiated = false;\n let runningTasks = 0;\n const pendingTasks: Array<() => void> = [];\n\n /**\n * Process pending tasks queue\n */\n const processQueue = () => {\n while (pendingTasks.length > 0 && runningTasks < concurrency) {\n const startNext = pendingTasks.shift();\n startNext?.();\n }\n };\n\n /**\n * Spawn function for body to use\n */\n const spawn = <T>(fn: (signal: AbortSignal) => Promise<T>): Promise<T> => {\n if (abortInitiated) {\n // Return rejected promise if already aborted\n const abortError = new DOMException(\n \"Task spawned after group abort\",\n \"AbortError\",\n );\n return Promise.reject(abortError);\n }\n\n let taskResolver: (value: T) => void = () => {};\n let taskRejecter: (reason: unknown) => void = () => {};\n\n const taskPromise = new Promise<T>((resolve, reject) => {\n taskResolver = resolve;\n taskRejecter = reject;\n });\n\n // Track the promise and add error handler\n taskPromise.catch((err: unknown) => {\n if (err instanceof Error && err.name !== \"AbortError\") {\n handleFirstError(err);\n }\n });\n\n // Add to task list\n taskPromises.push(taskPromise);\n\n // Start task based on concurrency limit\n const startTask = () => {\n runningTasks++; // Always increment to track this task start\n\n if (abortInitiated) {\n taskRejecter(\n new DOMException(\"Task started after group abort\", \"AbortError\"),\n );\n runningTasks--; // Decrement since task never actually ran\n // Process remaining queue even when rejecting\n processQueue();\n return;\n }\n\n // Wrap function call in Promise.resolve to handle synchronous throws\n Promise.resolve()\n .then(() => fn(combinedSignal))\n .then(taskResolver)\n .catch(taskRejecter)\n .finally(() => {\n runningTasks--;\n processQueue();\n });\n };\n\n if (runningTasks < concurrency) {\n // Start immediately if under the limit\n startTask();\n } else {\n // Queue for later if at the limit\n pendingTasks.push(startTask);\n }\n\n return taskPromise;\n };\n\n /**\n * Handle first error - abort all tasks\n */\n const handleFirstError = (error: unknown) => {\n if (!abortInitiated) {\n abortInitiated = true;\n controller.abort(); // Abort internal signal\n\n const errorObj =\n error instanceof Error ? error : new Error(String(error));\n if (errorObj.name !== \"AbortError\") {\n errors.push(errorObj);\n hasError = true;\n }\n\n // Process queue to reject any pending tasks\n processQueue();\n }\n };\n\n /**\n * Execute the body function\n */\n void (async () => {\n try {\n const result = await body(spawn, combinedSignal);\n // Wait for all tasks to settle\n await Promise.allSettled(taskPromises);\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (hasError) {\n bodyReject(new GroupError(errors));\n } else {\n bodyResolve(result);\n }\n } catch (error) {\n handleFirstError(error);\n // Wait for all tasks to settle\n await Promise.allSettled(taskPromises);\n bodyReject(new GroupError(errors));\n }\n })();\n });\n}\n\n/**\n * Creates a combined AbortSignal that aborts when either input signal aborts.\n *\n * @internal\n */\nfunction createCombinedSignal(\n signal1: AbortSignal,\n signal2: AbortSignal,\n): AbortSignal {\n // Check if either signal is already aborted\n if (signal1.aborted || signal2.aborted) {\n const controller = new AbortController();\n controller.abort();\n return controller.signal;\n }\n\n // Create a wrapper that listens to both signals\n const aborted = false;\n const listeners = new Set<() => void>();\n\n return {\n get aborted() {\n return signal1.aborted || signal2.aborted || aborted;\n },\n addEventListener: (_type: string, listener: () => void) => {\n if (_type === \"abort\") {\n listeners.add(listener);\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (signal1.aborted || signal2.aborted || aborted) {\n queueMicrotask(() => listener());\n }\n }\n },\n removeEventListener: (_type: string, listener: () => void) => {\n if (_type === \"abort\") {\n listeners.delete(listener);\n }\n },\n } as AbortSignal;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Custom error class for aggregating errors from failed tasks in a husbandry group.
3
+ *
4
+ * Extends the built-in AggregateError to collect all errors from tasks that failed
5
+ * or were cancelled. Provides a primaryError getter to access the first non-AbortError.
6
+ */
7
+ declare class GroupError extends AggregateError {
8
+ /**
9
+ * The first non-AbortError from the collected errors.
10
+ * If all errors are AbortErrors (from cancellation), this will be undefined.
11
+ */
12
+ readonly primaryError?: unknown;
13
+ constructor(errors: Array<unknown>, message?: string);
14
+ }
15
+
16
+ /**
17
+ * Options for configuring the husbandry task group.
18
+ */
19
+ interface GroupOptions {
20
+ /**
21
+ * External cancellation signal propagated into the group.
22
+ * When this signal aborts, all tasks in the group are cancelled.
23
+ */
24
+ signal?: AbortSignal;
25
+ /**
26
+ * Maximum number of tasks running concurrently.
27
+ * Defaults to Infinity (no limit).
28
+ */
29
+ concurrency?: number;
30
+ }
31
+ /**
32
+ * Spawn function type for creating tasks within the husbandry group.
33
+ *
34
+ * @template T - The type of value the task promise resolves to
35
+ */
36
+ type Spawn = <T>(fn: (signal: AbortSignal) => Promise<T>) => Promise<T>;
37
+ /**
38
+ * Creates a structured concurrency group for async tasks.
39
+ *
40
+ * This function manages a group of concurrent async tasks where:
41
+ * - Tasks are spawned using the spawn function
42
+ * - On first failure, all sibling tasks are cancelled via AbortSignal
43
+ * - The scope only settles after all tasks have settled (cooperative cancellation)
44
+ * - If any task fails, rejects with a GroupError aggregating all errors
45
+ *
46
+ * @template R - The type of value the body promise resolves to
47
+ * @param body - Function that receives spawn and signal, returns the group result
48
+ * @param options - Optional configuration for the group
49
+ * @returns Promise that resolves with body result or rejects with GroupError
50
+ *
51
+ * @throws {GroupError} If any task or the body function fails
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * import husbandry from "husbandry";
56
+ *
57
+ * const results = await husbandry(async (spawn, signal) => {
58
+ * const task1 = spawn(() => fetchA(signal));
59
+ * const task2 = spawn(() => fetchB(signal));
60
+ * const task3 = spawn(() => fetchC(signal));
61
+ * return await Promise.all([task1, task2, task3]);
62
+ * });
63
+ * ```
64
+ */
65
+ declare function husbandry<R>(body: (spawn: Spawn, signal: AbortSignal) => Promise<R>, options?: GroupOptions): Promise<R>;
66
+
67
+ export { GroupError, type GroupOptions, type Spawn, husbandry as default, husbandry };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Custom error class for aggregating errors from failed tasks in a husbandry group.
3
+ *
4
+ * Extends the built-in AggregateError to collect all errors from tasks that failed
5
+ * or were cancelled. Provides a primaryError getter to access the first non-AbortError.
6
+ */
7
+ declare class GroupError extends AggregateError {
8
+ /**
9
+ * The first non-AbortError from the collected errors.
10
+ * If all errors are AbortErrors (from cancellation), this will be undefined.
11
+ */
12
+ readonly primaryError?: unknown;
13
+ constructor(errors: Array<unknown>, message?: string);
14
+ }
15
+
16
+ /**
17
+ * Options for configuring the husbandry task group.
18
+ */
19
+ interface GroupOptions {
20
+ /**
21
+ * External cancellation signal propagated into the group.
22
+ * When this signal aborts, all tasks in the group are cancelled.
23
+ */
24
+ signal?: AbortSignal;
25
+ /**
26
+ * Maximum number of tasks running concurrently.
27
+ * Defaults to Infinity (no limit).
28
+ */
29
+ concurrency?: number;
30
+ }
31
+ /**
32
+ * Spawn function type for creating tasks within the husbandry group.
33
+ *
34
+ * @template T - The type of value the task promise resolves to
35
+ */
36
+ type Spawn = <T>(fn: (signal: AbortSignal) => Promise<T>) => Promise<T>;
37
+ /**
38
+ * Creates a structured concurrency group for async tasks.
39
+ *
40
+ * This function manages a group of concurrent async tasks where:
41
+ * - Tasks are spawned using the spawn function
42
+ * - On first failure, all sibling tasks are cancelled via AbortSignal
43
+ * - The scope only settles after all tasks have settled (cooperative cancellation)
44
+ * - If any task fails, rejects with a GroupError aggregating all errors
45
+ *
46
+ * @template R - The type of value the body promise resolves to
47
+ * @param body - Function that receives spawn and signal, returns the group result
48
+ * @param options - Optional configuration for the group
49
+ * @returns Promise that resolves with body result or rejects with GroupError
50
+ *
51
+ * @throws {GroupError} If any task or the body function fails
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * import husbandry from "husbandry";
56
+ *
57
+ * const results = await husbandry(async (spawn, signal) => {
58
+ * const task1 = spawn(() => fetchA(signal));
59
+ * const task2 = spawn(() => fetchB(signal));
60
+ * const task3 = spawn(() => fetchC(signal));
61
+ * return await Promise.all([task1, task2, task3]);
62
+ * });
63
+ * ```
64
+ */
65
+ declare function husbandry<R>(body: (spawn: Spawn, signal: AbortSignal) => Promise<R>, options?: GroupOptions): Promise<R>;
66
+
67
+ export { GroupError, type GroupOptions, type Spawn, husbandry as default, husbandry };
package/dist/index.js ADDED
@@ -0,0 +1,155 @@
1
+ // src/errors.ts
2
+ var GroupError = class extends AggregateError {
3
+ /**
4
+ * The first non-AbortError from the collected errors.
5
+ * If all errors are AbortErrors (from cancellation), this will be undefined.
6
+ */
7
+ primaryError;
8
+ constructor(errors, message) {
9
+ const definedErrors = errors.filter((e) => e instanceof Error);
10
+ super(
11
+ definedErrors,
12
+ message || "GroupError: One or more tasks in the group failed"
13
+ );
14
+ for (const error of definedErrors) {
15
+ const isAbortError = error instanceof DOMException && error.name === "AbortError";
16
+ const isErrorMessageAbort = error instanceof Error && error.message.includes("AbortError");
17
+ if (!isAbortError && !isErrorMessageAbort) {
18
+ this.primaryError = error;
19
+ break;
20
+ }
21
+ }
22
+ }
23
+ };
24
+
25
+ // src/group.ts
26
+ function createGroup(body, externalSignal, concurrency = Infinity) {
27
+ return new Promise((bodyResolve, bodyReject) => {
28
+ const controller = new AbortController();
29
+ const internalSignal = controller.signal;
30
+ const combinedSignal = externalSignal ? createCombinedSignal(externalSignal, internalSignal) : internalSignal;
31
+ const taskPromises = [];
32
+ const errors = [];
33
+ let hasError = false;
34
+ let abortInitiated = false;
35
+ let runningTasks = 0;
36
+ const pendingTasks = [];
37
+ const processQueue = () => {
38
+ while (pendingTasks.length > 0 && runningTasks < concurrency) {
39
+ const startNext = pendingTasks.shift();
40
+ startNext?.();
41
+ }
42
+ };
43
+ const spawn = (fn) => {
44
+ if (abortInitiated) {
45
+ const abortError = new DOMException(
46
+ "Task spawned after group abort",
47
+ "AbortError"
48
+ );
49
+ return Promise.reject(abortError);
50
+ }
51
+ let taskResolver = () => {
52
+ };
53
+ let taskRejecter = () => {
54
+ };
55
+ const taskPromise = new Promise((resolve, reject) => {
56
+ taskResolver = resolve;
57
+ taskRejecter = reject;
58
+ });
59
+ taskPromise.catch((err) => {
60
+ if (err instanceof Error && err.name !== "AbortError") {
61
+ handleFirstError(err);
62
+ }
63
+ });
64
+ taskPromises.push(taskPromise);
65
+ const startTask = () => {
66
+ runningTasks++;
67
+ if (abortInitiated) {
68
+ taskRejecter(
69
+ new DOMException("Task started after group abort", "AbortError")
70
+ );
71
+ runningTasks--;
72
+ processQueue();
73
+ return;
74
+ }
75
+ Promise.resolve().then(() => fn(combinedSignal)).then(taskResolver).catch(taskRejecter).finally(() => {
76
+ runningTasks--;
77
+ processQueue();
78
+ });
79
+ };
80
+ if (runningTasks < concurrency) {
81
+ startTask();
82
+ } else {
83
+ pendingTasks.push(startTask);
84
+ }
85
+ return taskPromise;
86
+ };
87
+ const handleFirstError = (error) => {
88
+ if (!abortInitiated) {
89
+ abortInitiated = true;
90
+ controller.abort();
91
+ const errorObj = error instanceof Error ? error : new Error(String(error));
92
+ if (errorObj.name !== "AbortError") {
93
+ errors.push(errorObj);
94
+ hasError = true;
95
+ }
96
+ processQueue();
97
+ }
98
+ };
99
+ void (async () => {
100
+ try {
101
+ const result = await body(spawn, combinedSignal);
102
+ await Promise.allSettled(taskPromises);
103
+ if (hasError) {
104
+ bodyReject(new GroupError(errors));
105
+ } else {
106
+ bodyResolve(result);
107
+ }
108
+ } catch (error) {
109
+ handleFirstError(error);
110
+ await Promise.allSettled(taskPromises);
111
+ bodyReject(new GroupError(errors));
112
+ }
113
+ })();
114
+ });
115
+ }
116
+ function createCombinedSignal(signal1, signal2) {
117
+ if (signal1.aborted || signal2.aborted) {
118
+ const controller = new AbortController();
119
+ controller.abort();
120
+ return controller.signal;
121
+ }
122
+ const aborted = false;
123
+ const listeners = /* @__PURE__ */ new Set();
124
+ return {
125
+ get aborted() {
126
+ return signal1.aborted || signal2.aborted || aborted;
127
+ },
128
+ addEventListener: (_type, listener) => {
129
+ if (_type === "abort") {
130
+ listeners.add(listener);
131
+ if (signal1.aborted || signal2.aborted || aborted) {
132
+ queueMicrotask(() => listener());
133
+ }
134
+ }
135
+ },
136
+ removeEventListener: (_type, listener) => {
137
+ if (_type === "abort") {
138
+ listeners.delete(listener);
139
+ }
140
+ }
141
+ };
142
+ }
143
+
144
+ // src/index.ts
145
+ function husbandry(body, options) {
146
+ const concurrency = options?.concurrency ?? Infinity;
147
+ return createGroup(body, options?.signal, concurrency);
148
+ }
149
+ var index_default = husbandry;
150
+ export {
151
+ GroupError,
152
+ index_default as default,
153
+ husbandry
154
+ };
155
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/errors.ts","../src/group.ts","../src/index.ts"],"sourcesContent":["/**\n * Custom error class for aggregating errors from failed tasks in a husbandry group.\n *\n * Extends the built-in AggregateError to collect all errors from tasks that failed\n * or were cancelled. Provides a primaryError getter to access the first non-AbortError.\n */\nexport class GroupError extends AggregateError {\n /**\n * The first non-AbortError from the collected errors.\n * If all errors are AbortErrors (from cancellation), this will be undefined.\n */\n public readonly primaryError?: unknown;\n\n constructor(errors: Array<unknown>, message?: string) {\n // Filter out undefined values\n const definedErrors = errors.filter((e): e is Error => e instanceof Error);\n super(\n definedErrors,\n message || \"GroupError: One or more tasks in the group failed\",\n );\n\n // Find the first non-AbortError\n for (const error of definedErrors) {\n // Check if it's not a DOMException AbortError or regular Error with \"Abort\" message\n const isAbortError =\n error instanceof DOMException && error.name === \"AbortError\";\n const isErrorMessageAbort =\n error instanceof Error && error.message.includes(\"AbortError\");\n\n if (!isAbortError && !isErrorMessageAbort) {\n this.primaryError = error;\n break;\n }\n }\n }\n}\n","/**\n * Internal group management for husbandry structured concurrency.\n *\n * @internal\n */\n\nimport { GroupError } from \"./errors.js\";\n\n/**\n * Creates a task group with structured concurrency management.\n *\n * @param body - The body function that receives spawn and signal\n * @param externalSignal - Optional external abort signal to propagate\n * @param concurrency - Maximum concurrent tasks (default: Infinity)\n * @returns Promise that resolves with body result or rejects with GroupError\n *\n * @internal\n */\nexport function createGroup<R>(\n body: (\n spawn: <T>(fn: (signal: AbortSignal) => Promise<T>) => Promise<T>,\n signal: AbortSignal,\n ) => Promise<R>,\n externalSignal?: AbortSignal,\n concurrency: number = Infinity,\n): Promise<R> {\n return new Promise((bodyResolve, bodyReject) => {\n // Create internal abort controller\n const controller = new AbortController();\n const internalSignal = controller.signal;\n\n // Combine with external signal if provided\n const combinedSignal = externalSignal\n ? createCombinedSignal(externalSignal, internalSignal)\n : internalSignal;\n\n // Track state\n const taskPromises: Promise<unknown>[] = [];\n const errors: Error[] = [];\n let hasError = false;\n let abortInitiated = false;\n let runningTasks = 0;\n const pendingTasks: Array<() => void> = [];\n\n /**\n * Process pending tasks queue\n */\n const processQueue = () => {\n while (pendingTasks.length > 0 && runningTasks < concurrency) {\n const startNext = pendingTasks.shift();\n startNext?.();\n }\n };\n\n /**\n * Spawn function for body to use\n */\n const spawn = <T>(fn: (signal: AbortSignal) => Promise<T>): Promise<T> => {\n if (abortInitiated) {\n // Return rejected promise if already aborted\n const abortError = new DOMException(\n \"Task spawned after group abort\",\n \"AbortError\",\n );\n return Promise.reject(abortError);\n }\n\n let taskResolver: (value: T) => void = () => {};\n let taskRejecter: (reason: unknown) => void = () => {};\n\n const taskPromise = new Promise<T>((resolve, reject) => {\n taskResolver = resolve;\n taskRejecter = reject;\n });\n\n // Track the promise and add error handler\n taskPromise.catch((err: unknown) => {\n if (err instanceof Error && err.name !== \"AbortError\") {\n handleFirstError(err);\n }\n });\n\n // Add to task list\n taskPromises.push(taskPromise);\n\n // Start task based on concurrency limit\n const startTask = () => {\n runningTasks++; // Always increment to track this task start\n\n if (abortInitiated) {\n taskRejecter(\n new DOMException(\"Task started after group abort\", \"AbortError\"),\n );\n runningTasks--; // Decrement since task never actually ran\n // Process remaining queue even when rejecting\n processQueue();\n return;\n }\n\n // Wrap function call in Promise.resolve to handle synchronous throws\n Promise.resolve()\n .then(() => fn(combinedSignal))\n .then(taskResolver)\n .catch(taskRejecter)\n .finally(() => {\n runningTasks--;\n processQueue();\n });\n };\n\n if (runningTasks < concurrency) {\n // Start immediately if under the limit\n startTask();\n } else {\n // Queue for later if at the limit\n pendingTasks.push(startTask);\n }\n\n return taskPromise;\n };\n\n /**\n * Handle first error - abort all tasks\n */\n const handleFirstError = (error: unknown) => {\n if (!abortInitiated) {\n abortInitiated = true;\n controller.abort(); // Abort internal signal\n\n const errorObj =\n error instanceof Error ? error : new Error(String(error));\n if (errorObj.name !== \"AbortError\") {\n errors.push(errorObj);\n hasError = true;\n }\n\n // Process queue to reject any pending tasks\n processQueue();\n }\n };\n\n /**\n * Execute the body function\n */\n void (async () => {\n try {\n const result = await body(spawn, combinedSignal);\n // Wait for all tasks to settle\n await Promise.allSettled(taskPromises);\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (hasError) {\n bodyReject(new GroupError(errors));\n } else {\n bodyResolve(result);\n }\n } catch (error) {\n handleFirstError(error);\n // Wait for all tasks to settle\n await Promise.allSettled(taskPromises);\n bodyReject(new GroupError(errors));\n }\n })();\n });\n}\n\n/**\n * Creates a combined AbortSignal that aborts when either input signal aborts.\n *\n * @internal\n */\nfunction createCombinedSignal(\n signal1: AbortSignal,\n signal2: AbortSignal,\n): AbortSignal {\n // Check if either signal is already aborted\n if (signal1.aborted || signal2.aborted) {\n const controller = new AbortController();\n controller.abort();\n return controller.signal;\n }\n\n // Create a wrapper that listens to both signals\n const aborted = false;\n const listeners = new Set<() => void>();\n\n return {\n get aborted() {\n return signal1.aborted || signal2.aborted || aborted;\n },\n addEventListener: (_type: string, listener: () => void) => {\n if (_type === \"abort\") {\n listeners.add(listener);\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (signal1.aborted || signal2.aborted || aborted) {\n queueMicrotask(() => listener());\n }\n }\n },\n removeEventListener: (_type: string, listener: () => void) => {\n if (_type === \"abort\") {\n listeners.delete(listener);\n }\n },\n } as AbortSignal;\n}\n","/**\n * Options for configuring the husbandry task group.\n */\nexport interface GroupOptions {\n /**\n * External cancellation signal propagated into the group.\n * When this signal aborts, all tasks in the group are cancelled.\n */\n signal?: AbortSignal;\n\n /**\n * Maximum number of tasks running concurrently.\n * Defaults to Infinity (no limit).\n */\n concurrency?: number;\n}\n\n/**\n * Spawn function type for creating tasks within the husbandry group.\n *\n * @template T - The type of value the task promise resolves to\n */\nexport type Spawn = <T>(fn: (signal: AbortSignal) => Promise<T>) => Promise<T>;\n\n/**\n * Creates a structured concurrency group for async tasks.\n *\n * This function manages a group of concurrent async tasks where:\n * - Tasks are spawned using the spawn function\n * - On first failure, all sibling tasks are cancelled via AbortSignal\n * - The scope only settles after all tasks have settled (cooperative cancellation)\n * - If any task fails, rejects with a GroupError aggregating all errors\n *\n * @template R - The type of value the body promise resolves to\n * @param body - Function that receives spawn and signal, returns the group result\n * @param options - Optional configuration for the group\n * @returns Promise that resolves with body result or rejects with GroupError\n *\n * @throws {GroupError} If any task or the body function fails\n *\n * @example\n * ```ts\n * import husbandry from \"husbandry\";\n *\n * const results = await husbandry(async (spawn, signal) => {\n * const task1 = spawn(() => fetchA(signal));\n * const task2 = spawn(() => fetchB(signal));\n * const task3 = spawn(() => fetchC(signal));\n * return await Promise.all([task1, task2, task3]);\n * });\n * ```\n */\nexport function husbandry<R>(\n body: (spawn: Spawn, signal: AbortSignal) => Promise<R>,\n options?: GroupOptions,\n): Promise<R> {\n const concurrency = options?.concurrency ?? Infinity;\n return createGroup(body, options?.signal, concurrency);\n}\n\n/**\n * Default export for convenience.\n * Allows both named import and default import:\n * - `import { husbandry } from \"husbandry\"`\n * - `import husbandry from \"husbandry\"`\n */\nexport default husbandry;\n\n// Re-export GroupError for users who want to check error types\nexport { GroupError } from \"./errors.js\";\n\n// Import the createGroup function\nimport { createGroup } from \"./group.js\";\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "husbandry",
3
+ "version": "0.1.0",
4
+ "description": "Structured concurrency for async tasks — spawn siblings under one scope; first failure cancels the rest via AbortSignal, then the scope throws after all have settled.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "CHANGELOG.md"
21
+ ],
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
25
+ "scripts": {
26
+ "build": "tsup src/index.ts --format esm,cjs --dts --sourcemap --clean",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "typecheck": "tsc --noEmit",
30
+ "lint": "eslint src test examples",
31
+ "demo": "tsx examples/demo.ts",
32
+ "check": "npm run typecheck && npm run lint && npm run test && npm run build",
33
+ "prepublishOnly": "npm run check"
34
+ },
35
+ "keywords": [
36
+ "async",
37
+ "concurrency",
38
+ "promise",
39
+ "abort",
40
+ "abortcontroller",
41
+ "structured",
42
+ "task",
43
+ "group",
44
+ "cancellation",
45
+ "signal"
46
+ ],
47
+ "devDependencies": {
48
+ "@eslint/js": "^9.18.0",
49
+ "eslint": "^9.18.0",
50
+ "tsx": "^4.19.2",
51
+ "typescript": "^5.7.3",
52
+ "typescript-eslint": "^8.19.1",
53
+ "vitest": "^2.1.8",
54
+ "tsup": "^8.3.5"
55
+ }
56
+ }