@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 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
+ [![codecov](https://codecov.io/github/aedge-io/grugway/graph/badge.svg?token=9WTDQ8WOKW)](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
+ }