@aedge-io/grugway 0.0.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.md +22 -0
- package/README.md +503 -0
- package/esm/_dnt.polyfills.js +1 -0
- package/esm/adapters/web/fetch/mod.js +118 -0
- package/esm/async/_internal.js +156 -0
- package/esm/async/task.js +578 -0
- package/esm/async/tasks.js +10 -0
- package/esm/core/assert.js +20 -0
- package/esm/core/errors.js +98 -0
- package/esm/core/option.js +651 -0
- package/esm/core/result.js +514 -0
- package/esm/core/type_utils.js +31 -0
- package/esm/mod.js +7 -0
- package/esm/package.json +3 -0
- package/package.json +47 -0
- package/types/_dnt.polyfills.d.ts +7 -0
- package/types/_dnt.polyfills.d.ts.map +1 -0
- package/types/adapters/web/fetch/mod.d.ts +98 -0
- package/types/adapters/web/fetch/mod.d.ts.map +1 -0
- package/types/async/_internal.d.ts +82 -0
- package/types/async/_internal.d.ts.map +1 -0
- package/types/async/task.d.ts +500 -0
- package/types/async/task.d.ts.map +1 -0
- package/types/async/tasks.d.ts +119 -0
- package/types/async/tasks.d.ts.map +1 -0
- package/types/core/assert.d.ts +9 -0
- package/types/core/assert.d.ts.map +1 -0
- package/types/core/errors.d.ts +85 -0
- package/types/core/errors.d.ts.map +1 -0
- package/types/core/option.d.ts +1672 -0
- package/types/core/option.d.ts.map +1 -0
- package/types/core/result.d.ts +1395 -0
- package/types/core/result.d.ts.map +1 -0
- package/types/core/type_utils.d.ts +78 -0
- package/types/core/type_utils.d.ts.map +1 -0
- package/types/mod.d.ts +11 -0
- package/types/mod.d.ts.map +1 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 - 2025 realpha <0xrealpha@proton.me> of the original
|
|
4
|
+
eitherway project (https://github.com/realpha/eitherway). Copyright (c) 2026
|
|
5
|
+
aedge-io <os@aedge.io> for all diverging modifications.
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
8
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
9
|
+
the Software without restriction, including without limitation the rights to
|
|
10
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
11
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
12
|
+
subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
19
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
20
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
21
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
22
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
# grugway
|
|
2
|
+
|
|
3
|
+
[](https://codecov.io/github/aedge-io/grugway)
|
|
4
|
+
|
|
5
|
+
> Safe abstractions for fallible flows — for humans and clankers alike.
|
|
6
|
+
|
|
7
|
+
This is a ~~fork~~ rework of an old, personal project
|
|
8
|
+
[eitherway](https://github.com/realpha/eitherway).
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why grugway?
|
|
13
|
+
|
|
14
|
+
This is for now mostly an experiment in human - agent collaboration and
|
|
15
|
+
in-context learning. The basic assumptions are:
|
|
16
|
+
|
|
17
|
+
- **Explicit error handling** makes code behavior predictable for both humans
|
|
18
|
+
and agents
|
|
19
|
+
- **Composable abstractions** allow agents to reason about data flow without
|
|
20
|
+
hidden exceptions
|
|
21
|
+
- **Type-safe operations** catch mistakes at compile time rather than runtime
|
|
22
|
+
- **Callibrated documentation** provides context that agents can leverage for
|
|
23
|
+
better performance
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Core Abstractions
|
|
28
|
+
|
|
29
|
+
| Abstraction | Purpose | Equivalent |
|
|
30
|
+
| -------------- | ------------------------------------- | ----------------------- |
|
|
31
|
+
| `Option<T>` | Handle presence/absence of values | `T \| undefined` |
|
|
32
|
+
| `Result<T, E>` | Handle success/failure explicitly | `T \| E` |
|
|
33
|
+
| `Task<T, E>` | Async operations with explicit errors | `Promise<Result<T, E>>` |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
### Installation
|
|
40
|
+
|
|
41
|
+
**Node.js:**
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
(bun | deno | (p)npm) add @aedge-io/grugway
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { Err, None, Ok, Option, Result, Some, Task } from "@aedge-io/grugway";
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Runtime Requirements
|
|
52
|
+
|
|
53
|
+
- Bun: tbd
|
|
54
|
+
- **Deno:** ≥1.14
|
|
55
|
+
- **Node.js:** ≥17.0.0
|
|
56
|
+
- **Browsers:** Support for `Error.cause` and `structuredClone`
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
|
|
62
|
+
### Option — Handling Optional Values
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { None, Option, Some } from "@aedge-io/grugway";
|
|
66
|
+
import { getUserById, id } from "grugway/examples";
|
|
67
|
+
import type { User } from "grugway/examples";
|
|
68
|
+
|
|
69
|
+
// Nullish values become None
|
|
70
|
+
const maybeUser = Option(getUserById(id)); // Option<User>
|
|
71
|
+
|
|
72
|
+
// Chain operations safely
|
|
73
|
+
const email = maybeUser
|
|
74
|
+
.filter((user: User) => user.isActive)
|
|
75
|
+
.map((user: User) => user.email)
|
|
76
|
+
.unwrapOr("no-email@example.com");
|
|
77
|
+
|
|
78
|
+
// Convert to Result for error handling
|
|
79
|
+
const userResult = maybeUser.okOrElse(() => new Error("User not found"));
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Result — Explicit Error Handling
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { Err, Ok, Result } from "@aedge-io/grugway";
|
|
86
|
+
|
|
87
|
+
function divide(a: number, b: number): Result<number, Error> {
|
|
88
|
+
if (b === 0) return Err(new Error("Division by zero"));
|
|
89
|
+
return Ok(a / b);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Compose operations
|
|
93
|
+
const result = divide(10, 2)
|
|
94
|
+
.map((n) => n * 2) // Only runs on Ok
|
|
95
|
+
.andThen((n) => divide(n, 3)) // Chain fallible operations
|
|
96
|
+
.mapErr((e) => new TypeError(e.message)); // Transform errors
|
|
97
|
+
|
|
98
|
+
// Unwrap with type narrowing
|
|
99
|
+
if (result.isOk()) {
|
|
100
|
+
console.log(result.unwrap()); // this is inferred as number
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Task — Async Operations
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { Err, Ok, Task } from "@aedge-io/grugway";
|
|
108
|
+
import { validateName } from "grugway/examples";
|
|
109
|
+
|
|
110
|
+
// Create tasks from promises
|
|
111
|
+
const fetchUser = Task.fromPromise(
|
|
112
|
+
fetch("/api/user").then((r) => r.json()),
|
|
113
|
+
(e: unknown) => new Error("Failed to fetch user", { cause: e }),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
// Compose async operations with the same API as Result
|
|
117
|
+
const userName = fetchUser
|
|
118
|
+
.map((user: { name: string }) => user.name)
|
|
119
|
+
.andThen(validateName)
|
|
120
|
+
.mapErr((e) => ({ code: "USER_ERROR", message: e.message }));
|
|
121
|
+
|
|
122
|
+
// Tasks are awaitable
|
|
123
|
+
const result = await userName;
|
|
124
|
+
result.inspect(console.log).inspectErr(console.error);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Lifting External Code
|
|
128
|
+
|
|
129
|
+
Integrate third-party libraries without manual wrapping:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { Option, Result, Task } from "@aedge-io/grugway";
|
|
133
|
+
import * as semver from "@std/semver";
|
|
134
|
+
|
|
135
|
+
// Lift sync functions
|
|
136
|
+
const tryParse = Result.liftFallible(
|
|
137
|
+
semver.parse,
|
|
138
|
+
(e: unknown) => new TypeError("Invalid version", { cause: e }),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
// Lift async functions
|
|
142
|
+
const tryFetch = Task.liftFallible(
|
|
143
|
+
(url: string) => fetch(url).then((r) => r.json()),
|
|
144
|
+
(e: unknown) => new Error("Fetch failed", { cause: e }),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Use in pipelines
|
|
148
|
+
const version = Option(Deno.args[0])
|
|
149
|
+
.okOr(new Error("No version provided"))
|
|
150
|
+
.andThen(tryParse);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## API Reference: Constructors & Helpers
|
|
156
|
+
|
|
157
|
+
Each abstraction provides multiple constructors and composability helpers for
|
|
158
|
+
different scenarios.
|
|
159
|
+
|
|
160
|
+
### Option<T>
|
|
161
|
+
|
|
162
|
+
#### Constructors
|
|
163
|
+
|
|
164
|
+
| Constructor | Returns | Use When |
|
|
165
|
+
| ----------------------------- | ------------- | ------------------------------------------ |
|
|
166
|
+
| `Option(value)` | `Option<T>` | General use — `null`/`undefined` → `None` |
|
|
167
|
+
| `Option.from(value)` | `Option<T>` | Alias for `Option()` |
|
|
168
|
+
| `Option.fromCoercible(value)` | `Option<T>` | Falsy values (`0`, `""`, `false`) → `None` |
|
|
169
|
+
| `Option.fromFallible(value)` | `Option<T>` | `Error` instances → `None` |
|
|
170
|
+
| `Some(value)` | `Some<T>` | Explicitly wrap a non-nullish value |
|
|
171
|
+
| `None` | `None` | The absent value singleton |
|
|
172
|
+
| `Some.empty()` | `Some<Empty>` | Signal success without a meaningful value |
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { Option } from "@aedge-io/grugway";
|
|
176
|
+
|
|
177
|
+
// Choose based on what should be "absent"
|
|
178
|
+
Option(0); // Some(0) — zero is a valid number
|
|
179
|
+
Option.fromCoercible(0); // None — zero is "empty" in this context
|
|
180
|
+
|
|
181
|
+
Option(new Error()); // Some(Error) — errors are values too
|
|
182
|
+
Option.fromFallible(new Error()); // None — errors mean absence
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### Composability Helpers
|
|
186
|
+
|
|
187
|
+
| Helper | Purpose |
|
|
188
|
+
| -------------------------------- | ---------------------------------------------------------------- |
|
|
189
|
+
| `Option.lift(fn, ctor?)` | Wrap a function to return `Option` (default ctor: `Option.from`) |
|
|
190
|
+
| `Option.liftFallible(fn, ctor?)` | Same as `lift`, but exceptions → `None` |
|
|
191
|
+
| `Option.apply(fn, arg)` | Apply `Option<Fn>` to `Option<Arg>` (applicative) |
|
|
192
|
+
| `Option.id(opt)` | Identity — useful for flattening `Option<Option<T>>` |
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { Option } from "@aedge-io/grugway";
|
|
196
|
+
|
|
197
|
+
// Lift a parser that might return undefined
|
|
198
|
+
const parseIntSafe = Option.lift(parseInt, Option.fromCoercible);
|
|
199
|
+
parseIntSafe("42"); // Some(42)
|
|
200
|
+
parseIntSafe("abc"); // None (NaN is falsy)
|
|
201
|
+
|
|
202
|
+
// Lift a function that throws
|
|
203
|
+
const parseJSON = Option.liftFallible(JSON.parse);
|
|
204
|
+
parseJSON('{"a":1}'); // Some({a: 1})
|
|
205
|
+
parseJSON("invalid"); // None
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
#### Collection Helpers (`Options` namespace)
|
|
209
|
+
|
|
210
|
+
| Helper | Returns | Behavior |
|
|
211
|
+
| ----------------------- | ------------- | --------------------------------------------- |
|
|
212
|
+
| `Options.all(opts)` | `Option<T[]>` | All `Some` → `Some<T[]>`, any `None` → `None` |
|
|
213
|
+
| `Options.any(opts)` | `Option<T>` | First `Some` found, or `None` |
|
|
214
|
+
| `Options.areSome(opts)` | `boolean` | Type predicate: all are `Some` |
|
|
215
|
+
| `Options.areNone(opts)` | `boolean` | Type predicate: all are `None` |
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### Result<T, E>
|
|
220
|
+
|
|
221
|
+
#### Constructors
|
|
222
|
+
|
|
223
|
+
| Constructor | Returns | Use When |
|
|
224
|
+
| ----------------------------------- | ------------------ | ----------------------------------------------------- |
|
|
225
|
+
| `Ok(value)` | `Ok<T>` | Explicit success |
|
|
226
|
+
| `Err(error)` | `Err<E>` | Explicit failure |
|
|
227
|
+
| `Result(value)` | `Result<T, E>` | Auto-detect — `Error` instances → `Err` |
|
|
228
|
+
| `Result.from(fn)` | `Result<T, never>` | Get value of infallible function (throws → propagate) |
|
|
229
|
+
| `Result.fromFallible(fn, errMapFn)` | `Result<T, E>` | Get value of fallible function (throws → `Err`) |
|
|
230
|
+
| `Ok.empty()` | `Ok<Empty>` | Signal success without a value |
|
|
231
|
+
| `Err.empty()` | `Err<Empty>` | Signal failure without details |
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
import { Result } from "@aedge-io/grugway";
|
|
235
|
+
import { getString, MathError, riskyDivision } from "grugway/examples";
|
|
236
|
+
|
|
237
|
+
// Auto-detection for union types
|
|
238
|
+
const value: string | TypeError = getString();
|
|
239
|
+
Result(value); // Ok<string> or Err<TypeError> based on runtime type
|
|
240
|
+
|
|
241
|
+
// Get the result of a fallible function
|
|
242
|
+
const safeDivide = Result.fromFallible(
|
|
243
|
+
riskyDivision,
|
|
244
|
+
(e: unknown) => new MathError("Division failed", { cause: e }),
|
|
245
|
+
);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
#### Composability Helpers
|
|
249
|
+
|
|
250
|
+
| Helper | Purpose |
|
|
251
|
+
| ------------------------------------------ | ------------------------------------------------------------------- |
|
|
252
|
+
| `Result.lift(fn, ctor?)` | Wrap function to return `Result` (panics propagate) |
|
|
253
|
+
| `Result.liftFallible(fn, errMapFn, ctor?)` | Wrap function, map exceptions to `Err<E>` |
|
|
254
|
+
| `asInfallible` | Error mapper that re-throws — marks function as "should never fail" |
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { asInfallible, Ok, Result } from "@aedge-io/grugway";
|
|
258
|
+
// Integrate a library function that throws
|
|
259
|
+
import * as semver from "@std/semver";
|
|
260
|
+
|
|
261
|
+
const tryParse = Result.liftFallible(
|
|
262
|
+
semver.parse,
|
|
263
|
+
(e: unknown) => new TypeError("Invalid semver", { cause: e }),
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
Ok("1.2.3").andThen(tryParse); // Ok<SemVer>
|
|
267
|
+
Ok("bad").andThen(tryParse); // Err<TypeError>
|
|
268
|
+
|
|
269
|
+
// Mark a function as infallible (will throw Panic if it actually fails)
|
|
270
|
+
const alwaysParses = Result.liftFallible(
|
|
271
|
+
(input: string) => JSON.parse(input),
|
|
272
|
+
asInfallible, // "I promise this won't throw"
|
|
273
|
+
);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
#### Collection Helpers (`Results` namespace)
|
|
277
|
+
|
|
278
|
+
| Helper | Returns | Behavior |
|
|
279
|
+
| ---------------------- | ---------------- | ------------------------------------------------ |
|
|
280
|
+
| `Results.all(results)` | `Result<T[], E>` | All `Ok` → `Ok<T[]>`, first `Err` short-circuits |
|
|
281
|
+
| `Results.any(results)` | `Result<T, E[]>` | First `Ok` found, or all `Err`s collected |
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
import { Results } from "@aedge-io/grugway";
|
|
285
|
+
import {
|
|
286
|
+
input,
|
|
287
|
+
loadDefaults,
|
|
288
|
+
loadFromEnv,
|
|
289
|
+
loadFromFile,
|
|
290
|
+
validateAge,
|
|
291
|
+
validateEmail,
|
|
292
|
+
validateName,
|
|
293
|
+
} from "grugway/examples";
|
|
294
|
+
|
|
295
|
+
// Validate multiple fields, fail on first error. Supports tuples!
|
|
296
|
+
const validated = Results.all(
|
|
297
|
+
[
|
|
298
|
+
validateName(input.name),
|
|
299
|
+
validateEmail(input.email),
|
|
300
|
+
validateAge(input.age),
|
|
301
|
+
] as const,
|
|
302
|
+
);
|
|
303
|
+
// Result<[string, string, number], ValidationError>
|
|
304
|
+
|
|
305
|
+
// Try multiple strategies, succeed on first
|
|
306
|
+
const config = Results.any([
|
|
307
|
+
loadFromEnv(),
|
|
308
|
+
loadFromFile(),
|
|
309
|
+
loadDefaults(),
|
|
310
|
+
]);
|
|
311
|
+
// Result<Config, EnvError|FileError|DefaultError[]>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
### Task<T, E>
|
|
317
|
+
|
|
318
|
+
#### Constructors
|
|
319
|
+
|
|
320
|
+
| Constructor | Returns | Use When |
|
|
321
|
+
| ------------------------------------- | -------------------- | ----------------------------------------------------- |
|
|
322
|
+
| `Task.succeed(value)` | `Task<T, never>` | Immediate success |
|
|
323
|
+
| `Task.fail(error)` | `Task<never, E>` | Immediate failure |
|
|
324
|
+
| `Task.of(result)` | `Task<T, E>` | From `Result<T,E>` or `Promise<Result<T,E>>` |
|
|
325
|
+
| `Task.from(fn)` | `Task<T, E>` | From function returning `Result` or `Promise<Result>` |
|
|
326
|
+
| `Task.fromPromise(promise, errMapFn)` | `Task<T, E>` | From `Promise<T>`, map rejections to `Err` |
|
|
327
|
+
| `Task.fromFallible(fn, errMapFn)` | `Task<T, E>` | From async function that might throw |
|
|
328
|
+
| `Task.deferred()` | `DeferredTask<T, E>` | For push-based APIs (callbacks, events) |
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
import { Task } from "@aedge-io/grugway";
|
|
332
|
+
import { Data, FetchError, legacyApi, TimeoutError } from "grugway/examples";
|
|
333
|
+
|
|
334
|
+
// Wrap fetch with proper error handling
|
|
335
|
+
const fetchJson = <T>(url: string): Task<T, FetchError> =>
|
|
336
|
+
Task.fromPromise(
|
|
337
|
+
fetch(url).then((r) => r.json()),
|
|
338
|
+
(e: unknown) => new FetchError(url, { cause: e }),
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// Deferred task for callback or push based APIs
|
|
342
|
+
const { task, succeed, fail } = Task.deferred<Data, TimeoutError>();
|
|
343
|
+
const timer = setTimeout(() => fail(new TimeoutError()), 5000);
|
|
344
|
+
legacyApi.fetch((err, data) => err ? fail(err) : succeed(data!));
|
|
345
|
+
await task; // Resolves when either callback fires
|
|
346
|
+
clearTimeout(timer);
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
#### Composability Helpers
|
|
350
|
+
|
|
351
|
+
| Helper | Purpose |
|
|
352
|
+
| ---------------------------------------- | ----------------------------------------------- |
|
|
353
|
+
| `Task.liftFallible(fn, errMapFn, ctor?)` | Wrap async function, map exceptions to `Err<E>` |
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
import { Task } from "@aedge-io/grugway";
|
|
357
|
+
import { ApiError } from "grugway/examples";
|
|
358
|
+
|
|
359
|
+
// Lift an async library function
|
|
360
|
+
// Check out the ready-made fetch adapter for a more thorough take on this
|
|
361
|
+
const tryFetch = Task.liftFallible(
|
|
362
|
+
async (url: string) => {
|
|
363
|
+
const res = await fetch(url);
|
|
364
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
365
|
+
return res.json();
|
|
366
|
+
},
|
|
367
|
+
(e: unknown) => new ApiError("Request failed", { cause: e }),
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// Use in pipelines
|
|
371
|
+
Task.succeed("/api/users")
|
|
372
|
+
.andThen(tryFetch)
|
|
373
|
+
.map((users: { active: boolean }[]) => users.filter((u) => u.active))
|
|
374
|
+
.inspectErr(console.error);
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
#### Collection Helpers (`Tasks` namespace)
|
|
378
|
+
|
|
379
|
+
| Helper | Returns | Behavior |
|
|
380
|
+
| ------------------ | -------------- | ----------------------------------------------------- |
|
|
381
|
+
| `Tasks.all(tasks)` | `Task<T[], E>` | All succeed → `Ok<T[]>`, first failure short-circuits |
|
|
382
|
+
| `Tasks.any(tasks)` | `Task<T, E[]>` | First success, or all failures collected |
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
import { Tasks } from "@aedge-io/grugway";
|
|
386
|
+
import {
|
|
387
|
+
fetchFromCache,
|
|
388
|
+
fetchFromPrimary,
|
|
389
|
+
fetchFromReplica,
|
|
390
|
+
fetchOrders,
|
|
391
|
+
fetchProducts,
|
|
392
|
+
fetchUsers,
|
|
393
|
+
} from "grugway/examples";
|
|
394
|
+
|
|
395
|
+
// These work for all iterables
|
|
396
|
+
// Parallel fetch with combined results
|
|
397
|
+
const allData = await Tasks.all(
|
|
398
|
+
[
|
|
399
|
+
fetchUsers(),
|
|
400
|
+
fetchProducts(),
|
|
401
|
+
fetchOrders(),
|
|
402
|
+
] as const,
|
|
403
|
+
);
|
|
404
|
+
// Result<[User[], Product[], Order[]], ApiError>
|
|
405
|
+
|
|
406
|
+
// Race multiple sources
|
|
407
|
+
const fastestResponse = await Tasks.any(
|
|
408
|
+
[
|
|
409
|
+
fetchFromPrimary(),
|
|
410
|
+
fetchFromReplica(),
|
|
411
|
+
fetchFromCache(),
|
|
412
|
+
] as const,
|
|
413
|
+
);
|
|
414
|
+
// Result<Data, [PrimaryError, ReplicaError, CacheError]>
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## Key Patterns
|
|
420
|
+
|
|
421
|
+
### Railway-Oriented Programming
|
|
422
|
+
|
|
423
|
+
Build pipelines where success flows forward and errors short-circuit:
|
|
424
|
+
|
|
425
|
+
```typescript
|
|
426
|
+
import { Task } from "@aedge-io/grugway";
|
|
427
|
+
import {
|
|
428
|
+
generateReceipt,
|
|
429
|
+
getOrder,
|
|
430
|
+
logError,
|
|
431
|
+
processPayment,
|
|
432
|
+
validateOrder,
|
|
433
|
+
} from "grugway/examples";
|
|
434
|
+
import type { OrderError, Receipt } from "grugway/examples";
|
|
435
|
+
|
|
436
|
+
function processOrder(orderId: string): Task<Receipt, OrderError> {
|
|
437
|
+
return getOrder(orderId) // Task<Order, NotFoundError>
|
|
438
|
+
.andThen(validateOrder) // Task<Order, ValidationError>
|
|
439
|
+
.andThen(processPayment) // Task<Payment, PaymentError>
|
|
440
|
+
.andThen(generateReceipt) // Task<Receipt, ReceiptError>
|
|
441
|
+
.inspectErr(logError);
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Pass-through Conditionals
|
|
446
|
+
|
|
447
|
+
Validate without consuming the value:
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
import { Result } from "@aedge-io/grugway";
|
|
451
|
+
import { isValid, isWritable, parse, writeFile } from "grugway/examples";
|
|
452
|
+
|
|
453
|
+
function saveFile(path: string): Result<void, Error> {
|
|
454
|
+
return parse(path)
|
|
455
|
+
.andEnsure(isValid) // Validate, but keep original path
|
|
456
|
+
.andEnsure(isWritable) // Check permissions, keep path
|
|
457
|
+
.andThen(writeFile);
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## Best Practices
|
|
464
|
+
|
|
465
|
+
1. **Computations, not data** — Use these abstractions for operation results,not
|
|
466
|
+
data models
|
|
467
|
+
2. **Embrace immutability** — Don't mutate wrapped values
|
|
468
|
+
3. **Unwrap at the edges** — Keep Result/Task types in your domain logic; unwrap
|
|
469
|
+
at API boundaries
|
|
470
|
+
4. **Some errors are fatal** — It's okay to throw for truly unrecoverable
|
|
471
|
+
states. Just make sure to catch at the top level and terminate gracefully.
|
|
472
|
+
5. **Lift external code** — Use `liftFallible` to integrate libraries cleanly
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
## Performance
|
|
477
|
+
|
|
478
|
+
These abstractions are _not totally performance prohibitive_. In benchmarks, the
|
|
479
|
+
linear return path often performs slightly better than nested try/catch blocks:
|
|
480
|
+
|
|
481
|
+
```
|
|
482
|
+
Synchronous: Result flow ~1.3x faster than exceptions
|
|
483
|
+
Asynchronous: Task flow ~1.0x (equivalent performance)
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**Your mileage will vary though.** Memory isn't free. Run benchmarks yourself:
|
|
487
|
+
`deno bench`
|
|
488
|
+
|
|
489
|
+
### License
|
|
490
|
+
|
|
491
|
+
MIT License — see [LICENSE.md](./LICENSE.md)
|
|
492
|
+
|
|
493
|
+
- Original eitherway: Copyright © 2023-2025 realpha
|
|
494
|
+
- grugway modifications: Copyright © 2026 aedge-io
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
## Resources
|
|
499
|
+
|
|
500
|
+
- [Original eitherway documentation](https://deno.land/x/eitherway)
|
|
501
|
+
- ["Railway-oriented programming" — Scott Wlaschin](https://vimeo.com/113707214)
|
|
502
|
+
- ["Boundaries" — Gary Bernhardt](https://www.destroyallsoftware.com/talks/boundaries)
|
|
503
|
+
- ["Errors are values" — Rob Pike](https://go.dev/blog/errors-are-values)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Task } from "../../../async/task.js";
|
|
2
|
+
import { Err, Ok } from "../../../core/result.js";
|
|
3
|
+
export class FailedRequest extends Error {
|
|
4
|
+
name = "FailedRequest";
|
|
5
|
+
status;
|
|
6
|
+
statusText;
|
|
7
|
+
response;
|
|
8
|
+
constructor(res) {
|
|
9
|
+
super(res.statusText);
|
|
10
|
+
this.response = res;
|
|
11
|
+
this.status = res.status;
|
|
12
|
+
this.statusText = res.statusText;
|
|
13
|
+
}
|
|
14
|
+
static from(res) {
|
|
15
|
+
return new FailedRequest(res);
|
|
16
|
+
}
|
|
17
|
+
static with(status, statusText) {
|
|
18
|
+
return new FailedRequest(new Response(null, { status, statusText }));
|
|
19
|
+
}
|
|
20
|
+
toJSON() {
|
|
21
|
+
return {
|
|
22
|
+
status: this.status,
|
|
23
|
+
statusText: this.statusText,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class FetchException extends Error {
|
|
28
|
+
name = "FetchException";
|
|
29
|
+
cause;
|
|
30
|
+
constructor(cause) {
|
|
31
|
+
super("Fetch panicked - operation aborted.");
|
|
32
|
+
this.cause = cause instanceof Error
|
|
33
|
+
? cause
|
|
34
|
+
: Error("Unknown exception", { cause });
|
|
35
|
+
}
|
|
36
|
+
static from(cause) {
|
|
37
|
+
return new FetchException(cause);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function isFetchException(err) {
|
|
41
|
+
return err != null && typeof err === "object" &&
|
|
42
|
+
err.constructor === FetchException;
|
|
43
|
+
}
|
|
44
|
+
export function toFetchResult(res) {
|
|
45
|
+
if (res.ok)
|
|
46
|
+
return Ok(res);
|
|
47
|
+
return Err(FailedRequest.from(res));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Use this to lift a fetch-like function into a Task context.
|
|
51
|
+
*
|
|
52
|
+
* This is a constrained wrapper over `Task.liftFallible` and comes with a default
|
|
53
|
+
* `ctorFn` and `errMapFn` for the 2nd and 3rd parameter respectively.
|
|
54
|
+
*
|
|
55
|
+
* @param fetchLike - Function to lift. Any function returning a `ResponseLike` object.
|
|
56
|
+
* @param ctorFn - Result or Task constructor function. Use this to distinguish successful from failed requests.
|
|
57
|
+
* @param errMapFn - Error map function. Maps any exception to a known error.
|
|
58
|
+
*
|
|
59
|
+
* @category Adapters
|
|
60
|
+
*
|
|
61
|
+
* @example Basic Usage
|
|
62
|
+
* ```typescript
|
|
63
|
+
* import { liftFetch } from "./mod.ts";
|
|
64
|
+
*
|
|
65
|
+
* interface Company {
|
|
66
|
+
* name: string;
|
|
67
|
+
* }
|
|
68
|
+
*
|
|
69
|
+
* interface User {
|
|
70
|
+
* id: number;
|
|
71
|
+
* company: Company;
|
|
72
|
+
* }
|
|
73
|
+
*
|
|
74
|
+
* const lifted = liftFetch(fetch);
|
|
75
|
+
* const resourceUrl = "https://jsonplaceholder.typicode.com/users/1";
|
|
76
|
+
*
|
|
77
|
+
* const result: Company = await lifted(resourceUrl)
|
|
78
|
+
* .map(async (resp) => (await resp.json()) as User) // Lazy, use validation!
|
|
79
|
+
* .inspect(console.log)
|
|
80
|
+
* .inspectErr(console.error) // FailedRequest<Response> | FetchException
|
|
81
|
+
* .mapOr(user => user.company, { name: "Acme Corp." })
|
|
82
|
+
* .unwrap();
|
|
83
|
+
* ```
|
|
84
|
+
*
|
|
85
|
+
* @example With custom Response type
|
|
86
|
+
* ```typescript
|
|
87
|
+
* import { liftFetch } from "./mod.ts";
|
|
88
|
+
*
|
|
89
|
+
* interface Company {
|
|
90
|
+
* name: string;
|
|
91
|
+
* }
|
|
92
|
+
*
|
|
93
|
+
* interface User {
|
|
94
|
+
* id: number;
|
|
95
|
+
* company: Company;
|
|
96
|
+
* }
|
|
97
|
+
*
|
|
98
|
+
* interface UserResponse extends Response {
|
|
99
|
+
* json(): Promise<User>
|
|
100
|
+
* }
|
|
101
|
+
*
|
|
102
|
+
* function fetchUserById(id: number): Promise<UserResponse> {
|
|
103
|
+
* return fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
|
|
104
|
+
* }
|
|
105
|
+
*
|
|
106
|
+
* const lifted = liftFetch(fetchUserById);
|
|
107
|
+
*
|
|
108
|
+
* const result: Company = await lifted(1)
|
|
109
|
+
* .map((resp) => resp.json()) // inferred as User
|
|
110
|
+
* .inspect(console.log)
|
|
111
|
+
* .inspectErr(console.error) // FailedRequest<UserResponse> | FetchException
|
|
112
|
+
* .mapOr(user => user.company, { name: "Acme Corp." })
|
|
113
|
+
* .unwrap();
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export function liftFetch(fetchLike, ctorFn, errMapFn) {
|
|
117
|
+
return Task.liftFallible(fetchLike, (errMapFn ?? FetchException.from), (ctorFn ?? (toFetchResult)));
|
|
118
|
+
}
|