@pencroff-lab/kore 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.
Files changed (51) hide show
  1. package/LICENSE +201 -0
  2. package/dist/cjs/index.d.ts +3 -0
  3. package/dist/cjs/index.d.ts.map +1 -0
  4. package/dist/cjs/index.js +19 -0
  5. package/dist/cjs/index.js.map +1 -0
  6. package/dist/cjs/package.json +3 -0
  7. package/dist/cjs/src/types/err.d.ts +1116 -0
  8. package/dist/cjs/src/types/err.d.ts.map +1 -0
  9. package/dist/cjs/src/types/err.js +1324 -0
  10. package/dist/cjs/src/types/err.js.map +1 -0
  11. package/dist/cjs/src/types/index.d.ts +3 -0
  12. package/dist/cjs/src/types/index.d.ts.map +1 -0
  13. package/dist/cjs/src/types/index.js +19 -0
  14. package/dist/cjs/src/types/index.js.map +1 -0
  15. package/dist/cjs/src/types/outcome.d.ts +1002 -0
  16. package/dist/cjs/src/types/outcome.d.ts.map +1 -0
  17. package/dist/cjs/src/types/outcome.js +958 -0
  18. package/dist/cjs/src/types/outcome.js.map +1 -0
  19. package/dist/cjs/src/utils/format_dt.d.ts +9 -0
  20. package/dist/cjs/src/utils/format_dt.d.ts.map +1 -0
  21. package/dist/cjs/src/utils/format_dt.js +29 -0
  22. package/dist/cjs/src/utils/format_dt.js.map +1 -0
  23. package/dist/cjs/src/utils/index.d.ts +2 -0
  24. package/dist/cjs/src/utils/index.d.ts.map +1 -0
  25. package/dist/cjs/src/utils/index.js +18 -0
  26. package/dist/cjs/src/utils/index.js.map +1 -0
  27. package/dist/esm/index.d.ts +3 -0
  28. package/dist/esm/index.d.ts.map +1 -0
  29. package/dist/esm/index.js +3 -0
  30. package/dist/esm/index.js.map +1 -0
  31. package/dist/esm/src/types/err.d.ts +1116 -0
  32. package/dist/esm/src/types/err.d.ts.map +1 -0
  33. package/dist/esm/src/types/err.js +1320 -0
  34. package/dist/esm/src/types/err.js.map +1 -0
  35. package/dist/esm/src/types/index.d.ts +3 -0
  36. package/dist/esm/src/types/index.d.ts.map +1 -0
  37. package/dist/esm/src/types/index.js +3 -0
  38. package/dist/esm/src/types/index.js.map +1 -0
  39. package/dist/esm/src/types/outcome.d.ts +1002 -0
  40. package/dist/esm/src/types/outcome.d.ts.map +1 -0
  41. package/dist/esm/src/types/outcome.js +954 -0
  42. package/dist/esm/src/types/outcome.js.map +1 -0
  43. package/dist/esm/src/utils/format_dt.d.ts +9 -0
  44. package/dist/esm/src/utils/format_dt.d.ts.map +1 -0
  45. package/dist/esm/src/utils/format_dt.js +26 -0
  46. package/dist/esm/src/utils/format_dt.js.map +1 -0
  47. package/dist/esm/src/utils/index.d.ts +2 -0
  48. package/dist/esm/src/utils/index.d.ts.map +1 -0
  49. package/dist/esm/src/utils/index.js +2 -0
  50. package/dist/esm/src/utils/index.js.map +1 -0
  51. package/package.json +56 -0
@@ -0,0 +1,954 @@
1
+ /**
2
+ * @fileoverview
3
+ * Monadic container for handling success and error states using tuple-first API design.
4
+ *
5
+ * This module provides the `Outcome<T>` class and related types for implementing
6
+ * type-safe error handling without exceptions. All operations favor immutability.
7
+ *
8
+ * @example Basic usage
9
+ * ```typescript
10
+ * import { Outcome } from './outcome';
11
+ *
12
+ * const [val, err] = Outcome.from(() => [42, null]).toTuple();
13
+ * ```
14
+ *
15
+ * @example Migration from throwing functions
16
+ * ```typescript
17
+ * // Before (throwing):
18
+ * function getUser(id: string): User {
19
+ * const user = db.find(id);
20
+ * if (!user) throw new Error("Not found");
21
+ * return user;
22
+ * }
23
+ * try {
24
+ * const user = getUser("123");
25
+ * console.log(user.name);
26
+ * } catch (e) {
27
+ * console.error(e.message);
28
+ * }
29
+ *
30
+ * // After (Outcome):
31
+ * function getUser(id: string): Outcome<User> {
32
+ * return Outcome.from(() => {
33
+ * const user = db.find(id);
34
+ * if (!user) return Err.from("Not found", "NOT_FOUND");
35
+ * return [user, null];
36
+ * });
37
+ * }
38
+ * const [user, err] = getUser("123").toTuple();
39
+ * if (err) {
40
+ * console.error(err.message);
41
+ * return;
42
+ * }
43
+ * console.log(user.name);
44
+ * ```
45
+ */
46
+ import { Err } from "./err";
47
+ /**
48
+ * A monadic container for handling success and error states.
49
+ *
50
+ * `Outcome<T>` provides a type-safe way to handle operations that can fail,
51
+ * using tuples as the primary interface. All instances are immutable.
52
+ *
53
+ * ## Core Patterns
54
+ *
55
+ * - **Construction**: Use static methods `ok()`, `err()`, `from()`, `fromAsync()`
56
+ * - **Inspection**: Use `isOk`, `isErr`, `value`, `error` properties
57
+ * - **Transformation**: Use `map()`, `mapErr()` for chained operations
58
+ * - **Extraction**: Use `toTuple()` for final value extraction
59
+ *
60
+ * @example Basic usage
61
+ * ```typescript
62
+ * const outcome = Outcome.from(() => {
63
+ * if (Math.random() > 0.5) return [42, null];
64
+ * return Err.from('Bad luck');
65
+ * });
66
+ *
67
+ * const [value, err] = outcome.toTuple();
68
+ * if (err) {
69
+ * console.error('Failed:', err.message);
70
+ * } else {
71
+ * console.log('Success:', value);
72
+ * }
73
+ * ```
74
+ *
75
+ * @example Chaining transformations
76
+ * ```typescript
77
+ * const result = Outcome.ok(5)
78
+ * .map(n => [n * 2, null])
79
+ * .map(n => [n.toString(), null])
80
+ * .toTuple();
81
+ * // result: ['10', null]
82
+ * ```
83
+ *
84
+ * @typeParam T - The type of the success value
85
+ */
86
+ export class Outcome {
87
+ /**
88
+ * Discriminator property for type narrowing.
89
+ * `true` for success outcomes, `false` for error outcomes.
90
+ */
91
+ isOk;
92
+ /** Internal tuple storage */
93
+ _tuple;
94
+ /**
95
+ * Private constructor - use static factory methods.
96
+ * @internal
97
+ */
98
+ constructor(tuple) {
99
+ this._tuple = tuple;
100
+ this.isOk = tuple[1] === null;
101
+ }
102
+ /**
103
+ * Process a CallbackReturn value into an Outcome.
104
+ * Handles discrimination: Err → null (void) → tuple destructure.
105
+ * @internal
106
+ */
107
+ static _processCallbackReturn(result) {
108
+ // Case 1: Direct Err return (shorthand)
109
+ if (Err.isErr(result)) {
110
+ return new Outcome([null, result]);
111
+ }
112
+ // Case 2: null = void success
113
+ if (result === null) {
114
+ return new Outcome([null, null]);
115
+ }
116
+ // Case 3: Tuple [T, null] | [null, Err]
117
+ const [value, error] = result;
118
+ if (Err.isErr(error)) {
119
+ return new Outcome([null, error]);
120
+ }
121
+ return new Outcome([value, null]);
122
+ }
123
+ // ══════════════════════════════════════════════════════════════════════════
124
+ // Static Constructors
125
+ // ══════════════════════════════════════════════════════════════════════════
126
+ /**
127
+ * Create a success Outcome with the given value.
128
+ *
129
+ * @param value - The success value
130
+ * @returns Outcome containing the success value
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * const outcome = Outcome.ok(42);
135
+ * console.log(outcome.isOk); // true
136
+ * console.log(outcome.value); // 42
137
+ *
138
+ * const [val, err] = outcome.toTuple();
139
+ * // val: 42, err: null
140
+ * ```
141
+ */
142
+ static ok(value) {
143
+ return new Outcome([value, null]);
144
+ }
145
+ /**
146
+ * Implementation signature for err().
147
+ * @internal
148
+ */
149
+ static err(messageOrErr, codeOrOptionsOrErr, options) {
150
+ // If first arg is already an Err, use it directly
151
+ if (Err.isErr(messageOrErr)) {
152
+ return new Outcome([null, messageOrErr]);
153
+ }
154
+ const message = messageOrErr;
155
+ // If second arg is Err or Error, wrap it
156
+ if (Err.isErr(codeOrOptionsOrErr) || codeOrOptionsOrErr instanceof Error) {
157
+ const wrapped = Err.wrap(message, codeOrOptionsOrErr, options);
158
+ return new Outcome([null, wrapped]);
159
+ }
160
+ // Otherwise, create new Err with message and options/code
161
+ // biome-ignore lint/suspicious/noExplicitAny: overloaded argument handling
162
+ const err = Err.from(message, codeOrOptionsOrErr);
163
+ return new Outcome([null, err]);
164
+ }
165
+ /**
166
+ * Create a success Outcome with null value (void success).
167
+ *
168
+ * Use for operations that succeed but have no meaningful return value.
169
+ *
170
+ * @returns Outcome<null> representing void success
171
+ *
172
+ * @remarks
173
+ * Returns `Outcome<null>` (not `Outcome<undefined>` or `Outcome<void>`).
174
+ * This is intentional for consistency with the tuple pattern where `null`
175
+ * indicates absence of error in `[value, null]`.
176
+ *
177
+ * @example
178
+ * ```typescript
179
+ * function logMessage(msg: string): Outcome<null> {
180
+ * console.log(msg);
181
+ * return Outcome.unit();
182
+ * }
183
+ *
184
+ * const outcome = logMessage('Hello');
185
+ * console.log(outcome.isOk); // true
186
+ * console.log(outcome.value); // null
187
+ * ```
188
+ */
189
+ static unit() {
190
+ return new Outcome([null, null]);
191
+ }
192
+ /**
193
+ * Create an Outcome from a callback that returns `CallbackReturn<T>`.
194
+ *
195
+ * The callback can return:
196
+ * - `[value, null]` - success with value
197
+ * - `[null, Err]` - error as tuple
198
+ * - `null` - void success
199
+ * - `Err` - error directly
200
+ *
201
+ * If the callback throws, the exception is caught and wrapped in an error Outcome.
202
+ *
203
+ * @param fn - Callback returning CallbackReturn<T>
204
+ * @returns Outcome<T>
205
+ *
206
+ * @see {@link fromAsync} for the async version
207
+ *
208
+ * @example Success with value
209
+ * ```typescript
210
+ * const outcome = Outcome.from(() => {
211
+ * return [42, null];
212
+ * });
213
+ * console.log(outcome.value); // 42
214
+ * ```
215
+ *
216
+ * @example Error shorthand
217
+ * ```typescript
218
+ * const outcome = Outcome.from(() => {
219
+ * if (invalid) return Err.from('Invalid input');
220
+ * return [result, null];
221
+ * });
222
+ * ```
223
+ *
224
+ * @example Catching throws from external libraries
225
+ * ```typescript
226
+ * const outcome = Outcome.from(() => {
227
+ * const data = JSON.parse(untrustedInput); // may throw
228
+ * return [data, null];
229
+ * });
230
+ * // If JSON.parse throws, outcome.isErr === true
231
+ * ```
232
+ */
233
+ static from(fn) {
234
+ try {
235
+ const result = fn();
236
+ return Outcome._processCallbackReturn(result);
237
+ }
238
+ catch (e) {
239
+ return new Outcome([null, Err.from(e)]);
240
+ }
241
+ }
242
+ /**
243
+ * Create an Outcome from an async callback that returns `Promise<CallbackReturn<T>>`.
244
+ *
245
+ * Async version of `from()` with identical semantics.
246
+ *
247
+ * @param fn - Async callback returning Promise<CallbackReturn<T>>
248
+ * @returns Promise<Outcome<T>>
249
+ *
250
+ * @see {@link from} for the synchronous version
251
+ *
252
+ * @example Async operation
253
+ * ```typescript
254
+ * const outcome = await Outcome.fromAsync(async () => {
255
+ * const response = await fetch('/api/data');
256
+ * if (!response.ok) {
257
+ * return Err.from('Request failed', { code: 'HTTP_ERROR' });
258
+ * }
259
+ * const data = await response.json();
260
+ * return [data, null];
261
+ * });
262
+ * ```
263
+ *
264
+ * @example With error aggregation
265
+ * ```typescript
266
+ * const outcome = await Outcome.fromAsync(async () => {
267
+ * let errors = Err.aggregate('Batch failed');
268
+ *
269
+ * const [a, errA] = await taskA().toTuple();
270
+ * if (errA) errors = errors.add(errA);
271
+ *
272
+ * const [b, errB] = await taskB().toTuple();
273
+ * if (errB) errors = errors.add(errB);
274
+ *
275
+ * if (errors.count > 0) return errors;
276
+ * return [{ a, b }, null];
277
+ * });
278
+ * ```
279
+ */
280
+ static async fromAsync(fn) {
281
+ try {
282
+ const result = await fn();
283
+ return Outcome._processCallbackReturn(result);
284
+ }
285
+ catch (e) {
286
+ return new Outcome([null, Err.from(e)]);
287
+ }
288
+ }
289
+ /**
290
+ * Create an Outcome from an existing ResultTuple.
291
+ *
292
+ * Useful for deserializing Outcomes or converting from external tuple sources.
293
+ *
294
+ * @param tuple - A ResultTuple<T>
295
+ * @returns Outcome<T>
296
+ *
297
+ * @see {@link toTuple} for extracting the tuple from an Outcome
298
+ *
299
+ * @example Deserializing from JSON
300
+ * ```typescript
301
+ * const json = '["hello", null]';
302
+ * const tuple = JSON.parse(json) as ResultTuple<string>;
303
+ * const outcome = Outcome.fromTuple(tuple);
304
+ * console.log(outcome.value); // 'hello'
305
+ * ```
306
+ *
307
+ * @example Round-trip serialization
308
+ * ```typescript
309
+ * const original = Outcome.ok(42);
310
+ * const json = JSON.stringify(original.toJSON());
311
+ * const restored = Outcome.fromTuple(JSON.parse(json));
312
+ * console.log(restored.value); // 42
313
+ * ```
314
+ */
315
+ static fromTuple(tuple) {
316
+ return new Outcome(tuple);
317
+ }
318
+ static fromJSON(payload) {
319
+ return Outcome.from(() => {
320
+ if (!Array.isArray(payload) || payload.length !== 2) {
321
+ return Err.from("Invalid Outcome JSON");
322
+ }
323
+ const [value, error] = payload;
324
+ if (error === null) {
325
+ return [value, null];
326
+ }
327
+ return [null, Err.fromJSON(error)];
328
+ });
329
+ }
330
+ // ══════════════════════════════════════════════════════════════════════════
331
+ // Combinators
332
+ // ══════════════════════════════════════════════════════════════════════════
333
+ /**
334
+ * Combines multiple Outcomes, succeeding if all succeed with an array of values.
335
+ * If any Outcome fails, returns an Err containing all failures aggregated via Err.aggregate().
336
+ *
337
+ * This is useful for validation scenarios where you need to collect all errors.
338
+ *
339
+ * For empty arrays, returns `Outcome.ok([])` (vacuous truth).
340
+ *
341
+ * @param outcomes - Array of Outcomes to combine
342
+ * @returns Outcome containing array of all success values, or aggregate error
343
+ *
344
+ * @remarks
345
+ * Time complexity: O(n) where n is the number of outcomes.
346
+ * All outcomes are evaluated (non-short-circuiting) to collect all errors.
347
+ *
348
+ * @example All succeed
349
+ * ```typescript
350
+ * const outcomes = [Outcome.ok(1), Outcome.ok(2), Outcome.ok(3)];
351
+ * const combined = Outcome.all(outcomes);
352
+ * console.log(combined.value); // [1, 2, 3]
353
+ * ```
354
+ *
355
+ * @example One fails
356
+ * ```typescript
357
+ * const outcomes = [
358
+ * Outcome.ok(1),
359
+ * Outcome.err('Failed'),
360
+ * Outcome.ok(3)
361
+ * ];
362
+ * const combined = Outcome.all(outcomes);
363
+ * console.log(combined.isErr); // true
364
+ * console.log(combined.error?.message); // 'Failed'
365
+ * ```
366
+ *
367
+ * @example Many fails
368
+ * ```typescript
369
+ * const mixed = [
370
+ * Outcome.ok(1),
371
+ * Outcome.err("Error A"),
372
+ * Outcome.err("Error B")
373
+ * ];
374
+ * const failed = Outcome.all(mixed);
375
+ * console.log(failed.isErr); // true
376
+ * // Error contains both "Error A" and "Error B"
377
+ * ```
378
+ *
379
+ * @example Empty array
380
+ * ```typescript
381
+ * const combined = Outcome.all([]);
382
+ * console.log(combined.value); // []
383
+ * ```
384
+ */
385
+ static all(outcomes) {
386
+ const values = [];
387
+ const errors = [];
388
+ for (const outcome of outcomes) {
389
+ if (outcome.isErr) {
390
+ //
391
+ errors.push(outcome._tuple[1]);
392
+ continue;
393
+ }
394
+ values.push(outcome._tuple[0]);
395
+ }
396
+ if (errors.length === 1) {
397
+ return new Outcome([null, errors[0]]);
398
+ }
399
+ if (errors.length > 0) {
400
+ return Outcome.err(Err.aggregate("Multiple failed", errors));
401
+ }
402
+ return new Outcome([values, null]);
403
+ }
404
+ /**
405
+ * Return the first successful Outcome from an array.
406
+ *
407
+ * Returns the first success encountered.
408
+ * Returns an aggregate error if ALL outcomes fail.
409
+ *
410
+ * For empty arrays, returns an error (no value to return).
411
+ *
412
+ * @param outcomes - Array of Outcomes to check
413
+ * @returns First successful Outcome, or aggregate of all errors
414
+ *
415
+ * @remarks
416
+ * Time complexity: O(n) worst case, but short-circuits on first success.
417
+ * Best case: O(1) if first outcome is successful.
418
+ *
419
+ * @example First success wins
420
+ * ```typescript
421
+ * const outcomes = [
422
+ * Outcome.err('First failed'),
423
+ * Outcome.ok(42),
424
+ * Outcome.ok(100)
425
+ * ];
426
+ * const result = Outcome.any(outcomes);
427
+ * console.log(result.value); // 42
428
+ * ```
429
+ *
430
+ * @example All fail
431
+ * ```typescript
432
+ * const outcomes = [
433
+ * Outcome.err('Error 1'),
434
+ * Outcome.err('Error 2')
435
+ * ];
436
+ * const result = Outcome.any(outcomes);
437
+ * console.log(result.isErr); // true
438
+ * console.log(result.error?.isAggregate); // true
439
+ * ```
440
+ *
441
+ * @example Empty array
442
+ * ```typescript
443
+ * const result = Outcome.any([]);
444
+ * console.log(result.isErr); // true
445
+ * console.log(result.error?.message); // 'No outcomes provided'
446
+ * ```
447
+ */
448
+ static any(outcomes) {
449
+ if (outcomes.length === 0) {
450
+ return Outcome.err("No outcomes provided", "EMPTY_INPUT");
451
+ }
452
+ const errors = [];
453
+ for (const outcome of outcomes) {
454
+ if (outcome.isOk) {
455
+ return outcome;
456
+ }
457
+ errors.push(outcome._tuple[1]);
458
+ }
459
+ const aggregate = Err.aggregate("All failed", errors);
460
+ return new Outcome([null, aggregate]);
461
+ }
462
+ // ══════════════════════════════════════════════════════════════════════════
463
+ // Instance Accessors
464
+ // ══════════════════════════════════════════════════════════════════════════
465
+ /**
466
+ * Whether this Outcome is in error state.
467
+ *
468
+ * @example
469
+ * ```typescript
470
+ * const success = Outcome.ok(42);
471
+ * const failure = Outcome.err('Failed');
472
+ *
473
+ * console.log(success.isErr); // false
474
+ * console.log(failure.isErr); // true
475
+ * ```
476
+ */
477
+ get isErr() {
478
+ return !this.isOk;
479
+ }
480
+ /**
481
+ * The success value, or null if in error state.
482
+ *
483
+ * @example
484
+ * ```typescript
485
+ * const success = Outcome.ok(42);
486
+ * const failure = Outcome.err('Failed');
487
+ *
488
+ * console.log(success.value); // 42
489
+ * console.log(failure.value); // null
490
+ * ```
491
+ */
492
+ get value() {
493
+ return this._tuple[0];
494
+ }
495
+ /**
496
+ * The error, or null if in success state.
497
+ *
498
+ * @example
499
+ * ```typescript
500
+ * const success = Outcome.ok(42);
501
+ * const failure = Outcome.err('Failed');
502
+ *
503
+ * console.log(success.error); // null
504
+ * console.log(failure.error?.message); // 'Failed'
505
+ * ```
506
+ */
507
+ get error() {
508
+ return this._tuple[1];
509
+ }
510
+ // ══════════════════════════════════════════════════════════════════════════
511
+ // Transformation
512
+ // ══════════════════════════════════════════════════════════════════════════
513
+ /**
514
+ * Transform the success value using a callback.
515
+ *
516
+ * Only called if this Outcome is successful. Errors pass through unchanged.
517
+ * The callback can return any `CallbackReturn<U>` pattern.
518
+ * If the callback throws, the exception is caught and wrapped.
519
+ *
520
+ * @param fn - Transformation function receiving the success value
521
+ * @returns New Outcome with transformed value or original/new error
522
+ *
523
+ * @see {@link mapAsync} for the async version
524
+ * @see {@link mapErr} for transforming or recovering from errors
525
+ *
526
+ * @example Simple transformation
527
+ * ```typescript
528
+ * const outcome = Outcome.ok(5)
529
+ * .map(n => [n * 2, null])
530
+ * .map(n => [n.toString(), null]);
531
+ *
532
+ * console.log(outcome.value); // '10'
533
+ * ```
534
+ *
535
+ * @example Transformation that can fail
536
+ * ```typescript
537
+ * const outcome = Outcome.ok('{"name":"John"}')
538
+ * .map(json => {
539
+ * try {
540
+ * return [JSON.parse(json), null];
541
+ * } catch {
542
+ * return Err.from('Invalid JSON');
543
+ * }
544
+ * });
545
+ * ```
546
+ *
547
+ * @example Error passes through
548
+ * ```typescript
549
+ * const outcome = Outcome.err('Original error')
550
+ * .map(v => [v * 2, null]); // Never called
551
+ *
552
+ * console.log(outcome.error?.message); // 'Original error'
553
+ * ```
554
+ */
555
+ map(fn) {
556
+ if (this.isErr) {
557
+ return new Outcome([null, this._tuple[1]]);
558
+ }
559
+ try {
560
+ const result = fn(this._tuple[0]);
561
+ return Outcome._processCallbackReturn(result);
562
+ }
563
+ catch (e) {
564
+ return new Outcome([null, Err.from(e)]);
565
+ }
566
+ }
567
+ /**
568
+ * Async version of `map()`.
569
+ *
570
+ * @param fn - Async transformation function
571
+ * @returns Promise of new Outcome
572
+ *
573
+ * @see {@link map} for the synchronous version
574
+ *
575
+ * @example
576
+ * ```typescript
577
+ * const outcome = await Outcome.ok(userId)
578
+ * .mapAsync(async id => {
579
+ * const user = await fetchUser(id);
580
+ * return [user, null];
581
+ * });
582
+ * ```
583
+ */
584
+ async mapAsync(fn) {
585
+ if (this.isErr) {
586
+ return new Outcome([null, this._tuple[1]]);
587
+ }
588
+ try {
589
+ const result = await fn(this._tuple[0]);
590
+ return Outcome._processCallbackReturn(result);
591
+ }
592
+ catch (e) {
593
+ return new Outcome([null, Err.from(e)]);
594
+ }
595
+ }
596
+ /**
597
+ * Transform or recover from an error using a callback.
598
+ *
599
+ * Only called if this Outcome is in error state. Success passes through unchanged.
600
+ * The callback can return any `CallbackReturn<U>` pattern, allowing:
601
+ * - Recovery: return `[value, null]` to convert error to success
602
+ * - Transform: return `Err` or `[null, Err]` to change the error
603
+ *
604
+ * @param fn - Function receiving the error
605
+ * @returns New Outcome with transformed error or recovered value
606
+ *
607
+ * @see {@link mapErrAsync} for the async version
608
+ * @see {@link map} for transforming success values
609
+ *
610
+ * @example Recovery
611
+ * ```typescript
612
+ * const outcome = Outcome.err('Not found')
613
+ * .mapErr(err => {
614
+ * if (err.hasCode('NOT_FOUND')) {
615
+ * return [defaultValue, null]; // recover with default
616
+ * }
617
+ * return err; // pass through other errors
618
+ * });
619
+ * ```
620
+ *
621
+ * @example Error transformation
622
+ * ```typescript
623
+ * const outcome = Outcome.err('Low-level error')
624
+ * .mapErr(err => err.wrap('High-level context'));
625
+ * ```
626
+ *
627
+ * @example Logging and pass-through
628
+ * ```typescript
629
+ * const outcome = Outcome.err('Something failed')
630
+ * .mapErr(err => {
631
+ * console.error('Error occurred:', err.message);
632
+ * return err; // pass through unchanged
633
+ * });
634
+ * ```
635
+ */
636
+ mapErr(fn) {
637
+ if (this.isOk) {
638
+ return this;
639
+ }
640
+ try {
641
+ const result = fn(this._tuple[1]);
642
+ return Outcome._processCallbackReturn(result);
643
+ }
644
+ catch (e) {
645
+ return new Outcome([null, Err.from(e)]);
646
+ }
647
+ }
648
+ /**
649
+ * Async version of `mapErr()`.
650
+ *
651
+ * @param fn - Async function receiving the error
652
+ * @returns Promise of new Outcome
653
+ *
654
+ * @see {@link mapErr} for the synchronous version
655
+ *
656
+ * @example Async recovery with fallback fetch
657
+ * ```typescript
658
+ * const outcome = await Outcome.err('Primary failed')
659
+ * .mapErrAsync(async err => {
660
+ * const fallback = await fetchFromBackup();
661
+ * if (fallback) return [fallback, null];
662
+ * return err.wrap('Backup also failed');
663
+ * });
664
+ * ```
665
+ */
666
+ async mapErrAsync(fn) {
667
+ if (this.isOk) {
668
+ return this;
669
+ }
670
+ try {
671
+ const result = await fn(this._tuple[1]);
672
+ return Outcome._processCallbackReturn(result);
673
+ }
674
+ catch (e) {
675
+ return new Outcome([null, Err.from(e)]);
676
+ }
677
+ }
678
+ // ══════════════════════════════════════════════════════════════════════════
679
+ // Side Effects
680
+ // ══════════════════════════════════════════════════════════════════════════
681
+ /**
682
+ * Execute a side effect with access to the full tuple.
683
+ *
684
+ * The callback receives the tuple `[value, error]` regardless of state.
685
+ * Returns `this` unchanged for chaining.
686
+ * If the callback throws, the exception is caught and the Outcome becomes an error.
687
+ *
688
+ * @param fn - Side effect function receiving the tuple
689
+ * @returns This Outcome (for chaining), or error Outcome if callback throws
690
+ *
691
+ * @see {@link effectAsync} for the async version
692
+ *
693
+ * @example Logging
694
+ * ```typescript
695
+ * const outcome = Outcome.ok(42)
696
+ * .effect(([val, err]) => {
697
+ * if (err) console.error('Failed:', err.message);
698
+ * else console.log('Success:', val);
699
+ * })
700
+ * .map(v => [v * 2, null]);
701
+ * ```
702
+ *
703
+ * @example Metrics
704
+ * ```typescript
705
+ * outcome.effect(([val, err]) => {
706
+ * metrics.record({
707
+ * success: !err,
708
+ * value: val,
709
+ * errorCode: err?.code
710
+ * });
711
+ * });
712
+ * ```
713
+ */
714
+ effect(fn) {
715
+ try {
716
+ const t = this.toTuple();
717
+ fn(t);
718
+ return this;
719
+ }
720
+ catch (e) {
721
+ return new Outcome([null, Err.from(e)]);
722
+ }
723
+ }
724
+ /**
725
+ * Async version of `effect()`.
726
+ *
727
+ * @param fn - Async side effect function
728
+ * @returns Promise of this Outcome
729
+ *
730
+ * @see {@link effect} for the synchronous version
731
+ *
732
+ * @example Async logging
733
+ * ```typescript
734
+ * const outcome = await Outcome.ok(data)
735
+ * .effectAsync(async ([val, err]) => {
736
+ * await logger.log({ value: val, error: err?.toJSON() });
737
+ * });
738
+ * ```
739
+ */
740
+ async effectAsync(fn) {
741
+ try {
742
+ const t = this.toTuple();
743
+ await fn(t);
744
+ return this;
745
+ }
746
+ catch (e) {
747
+ return new Outcome([null, Err.from(e)]);
748
+ }
749
+ }
750
+ /**
751
+ * Implementation for defaultTo overloads.
752
+ * @internal
753
+ */
754
+ defaultTo(fallbackOrHandler, asValue) {
755
+ if (this.isOk) {
756
+ return this._tuple[0];
757
+ }
758
+ if (asValue === true) {
759
+ return fallbackOrHandler;
760
+ }
761
+ if (typeof fallbackOrHandler === "function") {
762
+ return fallbackOrHandler(this._tuple[1]);
763
+ }
764
+ return fallbackOrHandler;
765
+ }
766
+ /**
767
+ * Transform the Outcome into a final value by handling both cases.
768
+ *
769
+ * This is a terminal operation that exits the Outcome chain, similar to
770
+ * `toTuple()` but with transformation logic applied.
771
+ *
772
+ * Each handler receives only its relevant type with full type safety:
773
+ * - `onOk` receives `T` (guaranteed non-null value)
774
+ * - `onErr` receives `Err` (guaranteed error)
775
+ *
776
+ * @param onOk - Function to transform success value into final result
777
+ * @param onErr - Function to transform error into final result
778
+ * @returns The transformed value (not wrapped in Outcome)
779
+ * @throws If either callback throws, the exception propagates to the caller
780
+ *
781
+ * @see {@link defaultTo} for simple value extraction with fallback
782
+ * @see {@link toTuple} for raw tuple extraction
783
+ * @see {@link toJSON} for JSON serialization
784
+ *
785
+ * @example Basic transformation
786
+ * ```typescript
787
+ * const message = fetchUser(id).either(
788
+ * user => `Welcome, ${user.name}!`,
789
+ * err => `Error: ${err.message}`
790
+ * );
791
+ * // message is string, not Outcome<string>
792
+ * ```
793
+ *
794
+ * @example HTTP response building
795
+ * ```typescript
796
+ * const response = processOrder(orderId).either(
797
+ * order => ({ status: 200, body: { id: order.id, total: order.total } }),
798
+ * err => ({
799
+ * status: err.hasCode('NOT_FOUND') ? 404 : 500,
800
+ * body: { error: err.message }
801
+ * })
802
+ * );
803
+ * ```
804
+ *
805
+ * @example Default value on error
806
+ * ```typescript
807
+ * const count = parseNumber(input).either(n => n, () => 0);
808
+ * ```
809
+ *
810
+ * @example Type transformation
811
+ * ```typescript
812
+ * const status: 'success' | 'error' = outcomeEntity.either(
813
+ * () => 'success',
814
+ * () => 'error'
815
+ * );
816
+ * ```
817
+ */
818
+ either(onOk, onErr) {
819
+ if (this.isOk) {
820
+ return onOk(this._tuple[0]);
821
+ }
822
+ return onErr(this._tuple[1]);
823
+ }
824
+ /**
825
+ * Implementation for pipe overloads.
826
+ * @internal
827
+ */
828
+ // biome-ignore lint/suspicious/noExplicitAny: implementation signature needs any
829
+ pipe(...fns) {
830
+ // biome-ignore lint/suspicious/noExplicitAny: implementation signature needs any
831
+ let current = this;
832
+ for (const fn of fns) {
833
+ try {
834
+ const result = fn(current.toTuple());
835
+ current = Outcome._processCallbackReturn(result);
836
+ }
837
+ catch (e) {
838
+ // biome-ignore lint/suspicious/noExplicitAny: implementation signature needs any
839
+ current = new Outcome([null, Err.from(e)]);
840
+ }
841
+ }
842
+ return current;
843
+ }
844
+ /**
845
+ * Implementation for pipeAsync overloads.
846
+ * @internal
847
+ */
848
+ // biome-ignore lint/suspicious/noExplicitAny: implementation signature needs any
849
+ async pipeAsync(...fns) {
850
+ // biome-ignore lint/suspicious/noExplicitAny: implementation signature needs any
851
+ let current = this;
852
+ for (const fn of fns) {
853
+ try {
854
+ const result = await fn(current.toTuple());
855
+ current = Outcome._processCallbackReturn(result);
856
+ }
857
+ catch (e) {
858
+ // biome-ignore lint/suspicious/noExplicitAny: implementation signature needs any
859
+ current = new Outcome([null, Err.from(e)]);
860
+ }
861
+ }
862
+ return current;
863
+ }
864
+ // ══════════════════════════════════════════════════════════════════════════
865
+ // Conversion
866
+ // ══════════════════════════════════════════════════════════════════════════
867
+ /**
868
+ * Extract the internal tuple.
869
+ *
870
+ * Primary method for extracting values from an Outcome.
871
+ * Use destructuring for ergonomic access.
872
+ *
873
+ * @returns The internal ResultTuple<T>
874
+ *
875
+ * @see {@link fromTuple} for creating an Outcome from a tuple
876
+ *
877
+ * @example
878
+ * ```typescript
879
+ * const outcome = Outcome.ok(42);
880
+ * const [value, error] = outcome.toTuple();
881
+ *
882
+ * if (error) {
883
+ * console.error('Failed:', error.message);
884
+ * return;
885
+ * }
886
+ * console.log('Value:', value); // 42
887
+ * ```
888
+ */
889
+ toTuple() {
890
+ const [v, e] = this._tuple;
891
+ return [v, e];
892
+ }
893
+ /**
894
+ * Convert to JSON-serializable tuple.
895
+ *
896
+ * For success: returns `[value, null]`
897
+ * For error: returns `[null, errJSON]` where errJSON is from `Err.toJSON()`
898
+ *
899
+ * @returns JSON-serializable representation
900
+ *
901
+ * @see {@link fromJSON} for deserializing an Outcome from JSON
902
+ *
903
+ * @example
904
+ * ```typescript
905
+ * const outcome = Outcome.ok({ name: 'John' });
906
+ * const json = JSON.stringify(outcome.toJSON());
907
+ * // '[{"name":"John"},null]'
908
+ *
909
+ * // Deserialize
910
+ * const restored = Outcome.fromJSON(JSON.parse(json));
911
+ * ```
912
+ */
913
+ toJSON() {
914
+ if (this.isOk) {
915
+ return [this._tuple[0], null];
916
+ }
917
+ return [null, this._tuple[1].toJSON()];
918
+ }
919
+ /**
920
+ * Convert to a human-readable string.
921
+ *
922
+ * @returns String representation
923
+ *
924
+ * @example
925
+ * ```typescript
926
+ * console.log(Outcome.ok(42).toString());
927
+ * // 'Outcome.ok(42)'
928
+ *
929
+ * console.log(Outcome.err('Failed').toString());
930
+ * // 'Outcome.err([ERROR] Failed)'
931
+ * ```
932
+ */
933
+ toString() {
934
+ if (this.isOk) {
935
+ return `Outcome.ok(${fmt(this._tuple[0])})`;
936
+ }
937
+ return `Outcome.err(${this._tuple[1].toString()})`;
938
+ }
939
+ }
940
+ function fmt(v) {
941
+ if (v === null)
942
+ return "null";
943
+ if (v === undefined)
944
+ return "undefined";
945
+ if (typeof v === "string")
946
+ return JSON.stringify(v);
947
+ try {
948
+ return JSON.stringify(v);
949
+ }
950
+ catch {
951
+ return String(v);
952
+ }
953
+ }
954
+ //# sourceMappingURL=outcome.js.map