@@ -10,10 +10,11 @@ const VALUE_INDEX = 0;
1010
1111const ERROR_INDEX = 1 ;
1212
13+ export const NONE = null ;
14+
1315export const SUCCEEDED = Symbol ( 'attempt:status:succeeded' ) ;
1416export const FAILED = Symbol ( 'attempt:status:failed' ) ;
1517
16-
1718/**
1819 * Returns the status of an attempt result.
1920 * @enum {unique symbol}
@@ -25,81 +26,111 @@ export const ATTEMPT_STATUSES = Object.freeze({
2526 failed : FAILED ,
2627} ) ;
2728
28- /**
29- * Gets the status of an attempt result.
30- *
31- * @param {AttemptResult } result The result to check.
32- * @returns {ATTEMPT_STATUSES.succeeded|ATTEMPT_STATUSES.failed }
33- * @throws {TypeError } If the result is not an `AttemptResult`.
34- */
35- export function getAttemptStatus ( result ) {
36- if ( isAttemptResult ( result ) ) {
37- return result [ ATTEMPT_STATUS ] ;
38- } else {
39- throw new TypeError ( 'Result must be an `AttemptResult` tuple.' ) ;
40- }
41- }
42-
4329/**
4430 * Union of all error types.
4531 * @typedef {Error|DOMException|TypeError|RangeError|AggregateError|ReferenceError|EvalError|URIError|SyntaxError } AnyError
4632 */
4733
48- /**
49- * @template T
50- * @typedef {readonly [T, null] & { [ATTEMPT_STATUS]: typeof ATTEMPT_STATUSES.succeeded } } AttemptSuccess
51- * Represents a successful outcome tuple with hidden metadata.
52- */
53-
54- /**
55- * @template E
56- * @typedef {readonly [null, E] & { [ATTEMPT_STATUS]: typeof ATTEMPT_STATUSES.failed } } AttemptFailure
57- * Represents a failed outcome tuple with hidden metadata.
58- */
59-
6034/**
6135 * @template T
6236 * @template E
6337 * @typedef {AttemptSuccess<T> | AttemptFailure<E> } AttemptResult<T, E>
6438 * Union type for both possible attempt outcomes.
6539 */
40+ class ResultTuple extends Array {
41+ /**
42+ * @param {T } value
43+ * @param {E } error
44+ * @param {SUCCEEDED|FAILED } status
45+ */
46+ constructor ( value , error , status ) {
47+ super ( value , error ) ;
48+ Object . defineProperty ( this , ATTEMPT_STATUS , {
49+ value : status ,
50+ writable : false ,
51+ enumerable : false ,
52+ configurable : false ,
53+ } ) ;
54+
55+ Object . freeze ( this ) ;
56+ }
6657
67- /**
68- * Attach a hidden status symbol and freeze the result.
69- *
70- * @template T
71- * @param {T } value A tuple to tag with metadata.
72- * @param {symbol } status Internal status symbol.
73- * @returns {readonly T & { [ATTEMPT_STATUS]: symbol } } The frozen and tagged tuple.
74- * @private
75- */
76- function _createResult ( value , status ) {
77- Object . defineProperty ( value , ATTEMPT_STATUS , {
78- value : status ,
79- writable : false ,
80- enumerable : false ,
81- configurable : false ,
82- } ) ;
83-
84- return Object . freeze ( value ) ;
58+ /**
59+ * @returns {Error|NONE }
60+ */
61+ get error ( ) {
62+ return this [ ERROR_INDEX ] ;
63+ }
64+
65+ /**
66+ * @returns {SUCCEEDED|FAILED }
67+ */
68+ get status ( ) {
69+ return this [ ATTEMPT_STATUS ] ;
70+ }
71+
72+ /**
73+ * @returns {T }
74+ */
75+ get value ( ) {
76+ return this [ VALUE_INDEX ] ;
77+ }
78+
79+ /**
80+ * @returns {typeof SUCCEEDED }
81+ */
82+ static get SUCCEEDED ( ) {
83+ return SUCCEEDED ;
84+ }
85+
86+ /**
87+ * @returns {typeof FAILED }
88+ */
89+ static get FAILED ( ) {
90+ return FAILED ;
91+ }
8592}
8693
8794/**
8895 * @template T
89- * @param { AttemptSuccess<T> } result
90- * @returns { T }
96+ * @typedef { readonly [T, NONE] & { [ATTEMPT_STATUS]: typeof ATTEMPT_STATUSES.succeeded, value: T, error: NONE, status: typeof ATTEMPT_STATUSES.succeeded } } AttemptSuccess
97+ * Represents a successful outcome tuple with hidden metadata. Named differently in class to avoid JSDocs confusion.
9198 */
92- function _extractValue ( result ) {
93- return result [ VALUE_INDEX ] ;
99+ class SuccessTuple extends ResultTuple {
100+ /**
101+ *
102+ * @param {T } value
103+ */
104+ constructor ( value ) {
105+ super ( value , NONE , ResultTuple . SUCCEEDED ) ;
106+ }
94107}
95108
96109/**
97110 * @template E
98- * @param { AttemptFailure<E> } result
99- * @returns { E }
111+ * * @typedef { readonly [NONE, E] & { [ATTEMPT_STATUS]: typeof ATTEMPT_STATUSES.failed, value: NONE, error: E, status: typeof ATTEMPT_STATUSES.failed } } AttemptFailure
112+ * Represents a failed outcome tuple with hidden metadata. Named differently in class to avoid JSDocs confusion.
100113 */
101- function _extractError ( result ) {
102- return result [ ERROR_INDEX ] ;
114+ class FailureTuple extends ResultTuple {
115+ /**
116+ *
117+ * @param {E } error
118+ */
119+ constructor ( error ) {
120+ if ( typeof error === 'string' ) {
121+ super ( NONE , new Error ( error ) , ResultTuple . FAILED ) ;
122+ } else if ( Error . isError ( error ) ) {
123+ super ( NONE , error , ResultTuple . FAILED ) ;
124+ } else if ( ! ( error instanceof AbortSignal ) ) {
125+ super ( NONE , new TypeError ( 'Invalid error type provided.' ) , ResultTuple . FAILED ) ;
126+ } else if ( ! error . aborted ) {
127+ super ( NONE , new TypeError ( 'Failed with a non-aborted `AbortSignal`.' ) , ResultTuple . FAILED ) ;
128+ } else if ( typeof error . reason === 'string' ) {
129+ super ( NONE , new Error ( error . reason ) , ResultTuple . FAILED ) ;
130+ } else {
131+ super ( NONE , error . reason , ResultTuple . FAILED ) ;
132+ }
133+ }
103134}
104135
105136/**
@@ -124,23 +155,23 @@ const _failHandler = err => {
124155 * @param {unknown } result The value to check.
125156 * @returns {result is AttemptResult }
126157 */
127- export const isAttemptResult = result => Array . isArray ( result ) && Object . hasOwn ( result , ATTEMPT_STATUS ) ;
158+ export const isAttemptResult = result => result instanceof ResultTuple ;
128159
129160/**
130161 * Returns `true` if the given result is a successful AttemptResult.
131162 *
132163 * @param {unknown } result
133164 * @returns {result is AttemptSuccess }
134165 */
135- export const succeeded = result => isAttemptResult ( result ) && result [ ATTEMPT_STATUS ] === ATTEMPT_STATUSES . succeeded ;
166+ export const succeeded = result => result instanceof SuccessTuple ;
136167
137168/**
138169 * Returns `true` if the given result is a failed AttemptResult.
139170 *
140171 * @param {unknown } result
141172 * @returns {result is AttemptFailure<AnyError> }
142173 */
143- export const failed = result => isAttemptResult ( result ) && result [ ATTEMPT_STATUS ] === ATTEMPT_STATUSES . failed ;
174+ export const failed = result => result instanceof FailureTuple ;
144175
145176/**
146177 * Creates an `AttemptSuccess`
@@ -149,7 +180,7 @@ export const failed = result => isAttemptResult(result) && result[ATTEMPT_STATUS
149180 * @param {T } value
150181 * @returns {AttemptSuccess<T> }
151182 */
152- export const succeed = value => isAttemptResult ( value ) ? value : _createResult ( [ value , null ] , ATTEMPT_STATUSES . succeeded ) ;
183+ export const succeed = value => isAttemptResult ( value ) ? value : new SuccessTuple ( value ) ;
153184
154185/**
155186 * @overload
@@ -174,14 +205,10 @@ export const succeed = value => isAttemptResult(value) ? value : _createResult([
174205 * @returns {AttemptFailure<AnyError> }
175206 */
176207export function fail ( err ) {
177- if ( isAttemptResult ( err ) ) {
208+ if ( err instanceof ResultTuple ) {
178209 return err ;
179- } else if ( Error . isError ( err ) ) {
180- return _createResult ( [ null , err ] , ATTEMPT_STATUSES . failed ) ;
181- } else if ( typeof err === 'string' ) {
182- return _createResult ( [ null , new Error ( err ) ] , ATTEMPT_STATUSES . failed ) ;
183210 } else {
184- return _createResult ( [ null , new TypeError ( 'Invalid error type provided.' ) ] , ATTEMPT_STATUSES . failed ) ;
211+ return new FailureTuple ( err ) ;
185212 }
186213}
187214
@@ -194,8 +221,8 @@ export function fail(err) {
194221 * @throws {TypeError } If the result is not a successful `AttemptSuccess`.
195222 */
196223export function getResultValue ( result ) {
197- if ( succeeded ( result ) ) {
198- return _extractValue ( result ) ;
224+ if ( result instanceof SuccessTuple ) {
225+ return result . value ;
199226 } else {
200227 throw new TypeError ( 'Result must be an `AttemptSuccess` tuple.' ) ;
201228 }
@@ -210,13 +237,28 @@ export function getResultValue(result) {
210237 * @throws {TypeError } If the result is not a failed `AttemptFailure`.
211238 */
212239export function getResultError ( result ) {
213- if ( failed ( result ) ) {
214- return _extractError ( result ) ;
240+ if ( result instanceof FailureTuple ) {
241+ return result . error ;
215242 } else {
216243 throw new TypeError ( 'Result must be an `AttemptFailure` tuple.' ) ;
217244 }
218245}
219246
247+ /**
248+ * Gets the status of an attempt result.
249+ *
250+ * @param {AttemptResult } result The result to check.
251+ * @returns {ATTEMPT_STATUSES.succeeded|ATTEMPT_STATUSES.failed }
252+ * @throws {TypeError } If the result is not an `AttemptResult`.
253+ */
254+ export function getAttemptStatus ( result ) {
255+ if ( result instanceof ResultTuple ) {
256+ return result . status ;
257+ } else {
258+ throw new TypeError ( 'Result must be an `AttemptResult` tuple.' ) ;
259+ }
260+ }
261+
220262/**
221263 * Attempts to execute a given callback function, catching any synchronous errors or Promise rejections,
222264 * and returning the result in a consistent [value, error] tuple format.
@@ -226,9 +268,9 @@ export function getResultError(result) {
226268 * @template T
227269 * @param {(...any) => T | PromiseLike<T> } callback The function to execute. It can be synchronous or return a Promise.
228270 * @param {(...any) } args Arguments to pass to the callback function.
229- * @returns {Promise<AttemptResult<Awaited<T>|null , AnyError|null >> } A Promise that resolves to a tuple:
230- * - `[result, null ]` on success, where `result` is the resolved value of `T`.
231- * - `[null , Error]` on failure, where `Error` is the caught error (normalized to an Error object).
271+ * @returns {Promise<AttemptResult<Awaited<T>|NONE , AnyError|NONE >> } A Promise that resolves to a tuple:
272+ * - `[result, NONE ]` on success, where `result` is the resolved value of `T`.
273+ * - `[NONE , Error]` on failure, where `Error` is the caught error (normalized to an Error object).
232274 * @throws {TypeError } If `callback` is not a function.
233275 */
234276export async function attemptAsync ( callback , ...args ) {
@@ -248,9 +290,9 @@ export async function attemptAsync(callback, ...args) {
248290 * @template T
249291 * @param {(...any) => T } callback The function to execute.
250292 * @param {(...any) } args Arguments to pass to the callback function.
251- * @returns {AttemptResult<T, AnyError|null > } A tuple:
252- * - `[result, null ]` on success, where `result` is the value of `T`.
253- * - `[null , Error]` on failure, where `Error` is the caught error (normalized to an Error object).
293+ * @returns {AttemptResult<T, AnyError|NONE > } A tuple:
294+ * - `[result, NONE ]` on success, where `result` is the value of `T`.
295+ * - `[NONE , Error]` on failure, where `Error` is the caught error (normalized to an Error object).
254296 * @throws {TypeError } If `callback` is not a function or is an async function.
255297 */
256298export function attemptSync ( callback , ...args ) {
@@ -274,9 +316,9 @@ export function attemptSync(callback, ...args) {
274316 *
275317 * @template T
276318 * @param {(...any) => T | PromiseLike<T> } callback The function to execute.
277- * @returns {(...any) => Promise<AttemptResult <Awaited<T>|null, AnyError|null>>> } An async wrapped function that returns to a tuple:
278- * - `[result, null ]` on success, where `result` is the value of `T`.
279- * - `[null , Error]` on failure, where `Error` is the caught error (normalized to an Error object).
319+ * @returns {(...any) => Promise<AttemptSuccess <Awaited<T>>|AttemptFailure<Error>> } A wrapped function that returns a tuple:
320+ * - `[result, NONE ]` on success, where `result` is the value of `T`.
321+ * - `[NONE , Error]` on failure, where `Error` is the caught error (normalized to an Error object).
280322 * @throws {TypeError } If `callback` is not a function.
281323 */
282324export const createSafeAsyncCallback = callback => async ( ...args ) => await attemptAsync ( callback , ...args ) ;
@@ -286,9 +328,9 @@ export const createSafeAsyncCallback = callback => async (...args) => await atte
286328 *
287329 * @template T
288330 * @param {(...any) => T } callback The function to execute.
289- * @returns {(...any) => AttemptResult<T, AnyError|null > } A wrapped function that returns a tuple:
290- * - `[result, null ]` on success, where `result` is the value of `T`.
291- * - `[null , Error]` on failure, where `Error` is the caught error (normalized to an Error object).
331+ * @returns {(...any) => AttemptSuccess<T>|AttemptFailure<Error > } A wrapped function that returns a tuple:
332+ * - `[result, NONE ]` on success, where `result` is the value of `T`.
333+ * - `[NONE , Error]` on failure, where `Error` is the caught error (normalized to an Error object).
292334 * @throws {TypeError } If `callback` is not a function or is an async function.
293335 */
294336export const createSafeSyncCallback = callback => ( ...args ) => attemptSync ( callback , ...args ) ;
@@ -313,7 +355,7 @@ export const createSafeSyncCallback = callback => (...args) => attemptSync(callb
313355 * failure?: (err: E) => V | PromiseLike<V>
314356 * signal?: AbortSignal<R>
315357 * }} callbacks Handlers for success or failure cases.
316- * @returns {Promise<AttemptResult <Awaited<U>|Awaited<V>, E>|AttemptFailure<R> } A Promise resolving to a new `AttemptResult` from the callback execution,
358+ * @returns {Promise<AttemptSuccess <Awaited<U>|Awaited<V>, E>|AttemptFailure<R> } A Promise resolving to a new `AttemptResult` from the callback execution,
317359 * or a failure if the input is invalid.
318360 */
319361export async function handleResultAsync ( result , {
@@ -326,7 +368,7 @@ export async function handleResultAsync(result, {
326368 } else if ( typeof success !== 'function' || typeof failure !== 'function' ) {
327369 throw new TypeError ( 'Both success and failure handlers must be functions.' ) ;
328370 } else if ( signal instanceof AbortSignal && signal . aborted ) {
329- return fail ( signal . reason instanceof Error ? signal . reason : new Error ( signal . reason ) ) ;
371+ return fail ( signal . reason ) ;
330372 } else {
331373 switch ( getAttemptStatus ( result ) ) {
332374 case ATTEMPT_STATUSES . succeeded :
@@ -386,7 +428,7 @@ export async function attemptAll(...callbacks) {
386428 if ( callbacks . some ( cb => typeof cb !== 'function' ) ) {
387429 throw new TypeError ( 'All callbacks must be functions.' ) ;
388430 } else {
389- let result = succeed ( null ) ;
431+ let result = succeed ( NONE ) ;
390432
391433 for ( const cb of callbacks ) {
392434 if ( result [ ATTEMPT_STATUS ] === ATTEMPT_STATUSES . failed ) {
@@ -407,10 +449,13 @@ export async function attemptAll(...callbacks) {
407449 * @throws {AnyError } The error if result is an `AttemptFailure`
408450 */
409451export function throwIfFailed ( result ) {
410- if ( failed ( result ) ) {
411- throw getResultError ( result ) ;
452+ if ( result instanceof FailureTuple ) {
453+ throw result . error ;
412454 }
413455}
414456
415457export const createSafeCallback = createSafeAsyncCallback ;
416458export const attempt = attemptAsync ;
459+
460+ // Aliased to avoid confusion with types
461+ export { SuccessTuple as AttemptSuccess , FailureTuple as AttemptFailure } ;
0 commit comments