Skip to content

Commit b2248f2

Browse files
authored
Merge pull request #4 from AegisJSProject/patch/types
Refactor attempt result handling and typings
2 parents b8fdf2e + 62aa04b commit b2248f2

5 files changed

Lines changed: 188 additions & 72 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [v1.0.1] - 2025-07-22
11+
12+
### Added
13+
- `getAttemptStatus`, `SUCCEEDED`, and `FAILED` exports for standardized status handling.
14+
15+
### Changed
16+
- Improved type safety and clarity of `AttemptResult`, `AttemptSuccess`, and `AttemptFailure`.
17+
- Enhanced `fail()` with additional overloads and better documentation.
18+
- Refined value and error extraction utilities.
19+
- Strengthened tests with stricter error handling and status checks.
20+
1021
## [v1.0.0] - 2025-07-16
1122

1223
Initial Release

attempt.js

Lines changed: 149 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,71 @@
66
*/
77
const ATTEMPT_STATUS = Symbol('attempt:status');
88

9+
const VALUE_INDEX = 0;
10+
11+
const ERROR_INDEX = 1;
12+
13+
export const SUCCEEDED = Symbol('attempt:status:succeeded');
14+
export const FAILED = Symbol('attempt:status:failed');
15+
16+
917
/**
10-
* Enum-like object holding internal status symbols for succeeded/failed results.
11-
*
12-
* @private
13-
* @readonly
18+
* Returns the status of an attempt result.
1419
* @enum {unique symbol}
20+
* @property {unique symbol} succeeded - Represents a successful attempt result.
21+
* @property {unique symbol} failed - Represents a failed attempt result.
1522
*/
16-
const ATTEMPT_STATUSES = Object.freeze({
17-
succeeded: Symbol('attempt:succeeded'),
18-
failed: Symbol('attempt:failed'),
23+
export const ATTEMPT_STATUSES = Object.freeze({
24+
succeeded: SUCCEEDED,
25+
failed: FAILED,
1926
});
2027

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+
43+
/**
44+
* Union of all error types.
45+
* @typedef {Error|DOMException|TypeError|RangeError|AggregateError|ReferenceError|EvalError|URIError|SyntaxError} AnyError
46+
*/
47+
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+
60+
/**
61+
* @template T
62+
* @template E
63+
* @typedef {AttemptSuccess<T> | AttemptFailure<E>} AttemptResult<T, E>
64+
* Union type for both possible attempt outcomes.
65+
*/
66+
2167
/**
2268
* Attach a hidden status symbol and freeze the result.
2369
*
24-
* @template {readonly [any, Error|null]} T
70+
* @template T
2571
* @param {T} value A tuple to tag with metadata.
2672
* @param {symbol} status Internal status symbol.
27-
* @returns {T & { [ATTEMPT_STATUS]: symbol }} The frozen and tagged tuple.
73+
* @returns {readonly T & { [ATTEMPT_STATUS]: symbol }} The frozen and tagged tuple.
2874
* @private
2975
*/
3076
function _createResult(value, status) {
@@ -38,6 +84,24 @@ function _createResult(value, status) {
3884
return Object.freeze(value);
3985
}
4086

87+
/**
88+
* @template T
89+
* @param {AttemptSuccess<T>} result
90+
* @returns {T}
91+
*/
92+
function _extractValue(result) {
93+
return result[VALUE_INDEX];
94+
}
95+
96+
/**
97+
* @template E
98+
* @param {AttemptFailure<E>} result
99+
* @returns {E}
100+
*/
101+
function _extractError(result) {
102+
return result[ERROR_INDEX];
103+
}
104+
41105
/**
42106
* @template T
43107
* @param {T} input
@@ -46,93 +110,112 @@ function _createResult(value, status) {
46110
const _successHandler = input => input;
47111

48112
/**
49-
*
50-
* @param {Error} err
51-
* @throws {Error}
113+
* @template E
114+
* @param {E} err
115+
* @throws {E}
52116
*/
53117
const _failHandler = err => {
54-
throw new Error('Unhandled error in result.', { cause: err });
118+
throw err;
55119
};
56120

57121
/**
58122
* Returns `true` if the given value is an AttemptResult (a frozen tuple with hidden metadata).
123+
*
59124
* @param {unknown} result The value to check.
60-
* @returns {result is AttemptResult<any>}
125+
* @returns {result is AttemptResult}
61126
*/
62127
export const isAttemptResult = result => Array.isArray(result) && Object.hasOwn(result, ATTEMPT_STATUS);
63128

64129
/**
65130
* Returns `true` if the given result is a successful AttemptResult.
131+
*
66132
* @param {unknown} result
67-
* @returns {result is AttemptSuccess<any>}
133+
* @returns {result is AttemptSuccess}
68134
*/
69135
export const succeeded = result => isAttemptResult(result) && result[ATTEMPT_STATUS] === ATTEMPT_STATUSES.succeeded;
70136

71137
/**
72138
* Returns `true` if the given result is a failed AttemptResult.
139+
*
73140
* @param {unknown} result
74-
* @returns {result is AttemptFailure}
141+
* @returns {result is AttemptFailure<AnyError>}
75142
*/
76143
export const failed = result => isAttemptResult(result) && result[ATTEMPT_STATUS] === ATTEMPT_STATUSES.failed;
77144

78145
/**
146+
* Creates an `AttemptSuccess`
147+
*
79148
* @template T
80-
* @typedef {readonly [T, null] & { [ATTEMPT_STATUS]: typeof ATTEMPT_STATUSES.succeeded }} AttemptSuccess
81-
* Represents a successful outcome tuple with hidden metadata.
149+
* @param {T} value
150+
* @returns {AttemptSuccess<T>}
82151
*/
152+
export const succeed = value => isAttemptResult(value) ? value : _createResult([value, null], ATTEMPT_STATUSES.succeeded);
83153

84154
/**
85-
* @typedef {readonly [null, Error] & { [ATTEMPT_STATUS]: typeof ATTEMPT_STATUSES.failed }} AttemptFailure
86-
* Represents a failed outcome tuple with hidden metadata.
155+
* @overload
156+
* @param {string} err
157+
* @returns {AttemptFailure<Error>}
87158
*/
88-
89159
/**
90-
* @template T
91-
* @typedef {AttemptSuccess<T> | AttemptFailure} AttemptResult
92-
* Union type for both possible attempt outcomes.
160+
* @overload
161+
* @template E
162+
* @param {AttemptFailure<E>} err
163+
* @returns {AttemptFailure<E>}
93164
*/
94-
95165
/**
96-
* Creates an `AttemptSuccess`
97-
*
98-
* @template T The type of the successful result.
99-
* @param {T} value
100-
* @returns {AttemptSuccess<T>}
166+
* @overload
167+
* @param {AnyError} err
168+
* @returns {AttemptFailure<AnyError>}
101169
*/
102-
export const succeed = value => isAttemptResult(value) ? value : _createResult([value, null], ATTEMPT_STATUSES.succeeded);
103-
104170
/**
105171
* Creates an `AttemptFailure`
106-
* @param {Error|string|AttemptFailure} err
107-
* @returns {AttemptFailure}
172+
*
173+
* @param {AnyError|AttemptFailure<AnyError>|string} err
174+
* @returns {AttemptFailure<AnyError>}
108175
*/
109-
110176
export function fail(err) {
111177
if (isAttemptResult(err)) {
112178
return err;
113179
} else if (Error.isError(err)) {
114180
return _createResult([null, err], ATTEMPT_STATUSES.failed);
115-
} else {
181+
} else if (typeof err === 'string') {
116182
return _createResult([null, new Error(err)], ATTEMPT_STATUSES.failed);
183+
} else {
184+
return _createResult([null, new TypeError('Invalid error type provided.')], ATTEMPT_STATUSES.failed);
117185
}
118186
}
119187

120188
/**
121-
* Extracts the value from a successful `AttemptResult`, or `null` if it failed or is invalid.
189+
* Extracts the value from a successful `AttemptResult`.
122190
*
123191
* @template T
124-
* @param {AttemptResult<T>} result The result to extract from.
125-
* @returns {T | null} The successful result value, or `null` if not a success.
192+
* @param {AttemptSuccess<T>} result The result to extract from.
193+
* @returns {T} The successful result value.
194+
* @throws {TypeError} If the result is not a successful `AttemptSuccess`.
126195
*/
127-
export const getResultValue = result => succeeded(result) ? result[0] : null;
196+
export function getResultValue(result) {
197+
if (succeeded(result)) {
198+
return _extractValue(result);
199+
} else {
200+
throw new TypeError('Result must be an `AttemptSuccess` tuple.');
201+
}
202+
}
128203

129204
/**
130-
* Extracts the error from a failed `AttemptResult`, or `null` if it succeeded or is invalid.
205+
* Extracts the error from a failed `AttemptResult`.
131206
*
132-
* @param {AttemptResult} result The result to extract from.
133-
* @returns {Error | null} The error object if the result is a failure, otherwise `null`.
207+
* @template E
208+
* @param {AttemptFailure<E>} result The result to extract from.
209+
* @returns {E} The error object if the result is a failure.
210+
* @throws {TypeError} If the result is not a failed `AttemptFailure`.
134211
*/
135-
export const getResultError = result => failed(result) ? result[1] : null;
212+
export function getResultError(result) {
213+
if (failed(result)){
214+
return _extractError(result);
215+
} else {
216+
throw new TypeError('Result must be an `AttemptFailure` tuple.');
217+
}
218+
}
136219

137220
/**
138221
* Attempts to execute a given callback function, catching any synchronous errors or Promise rejections,
@@ -143,7 +226,7 @@ export const getResultError = result => failed(result) ? result[1] : null;
143226
* @template T
144227
* @param {(...any) => T | PromiseLike<T>} callback The function to execute. It can be synchronous or return a Promise.
145228
* @param {(...any)} args Arguments to pass to the callback function.
146-
* @returns {Promise<AttemptResult<Awaited<T>>>} A Promise that resolves to a tuple:
229+
* @returns {Promise<AttemptResult<Awaited<T>|null, AnyError|null>>} A Promise that resolves to a tuple:
147230
* - `[result, null]` on success, where `result` is the resolved value of `T`.
148231
* - `[null, Error]` on failure, where `Error` is the caught error (normalized to an Error object).
149232
* @throws {TypeError} If `callback` is not a function.
@@ -152,7 +235,7 @@ export async function attemptAsync(callback, ...args) {
152235
if (typeof callback !== 'function') {
153236
throw new TypeError('callback must be a function.');
154237
} else {
155-
return await Promise.try(callback, ...args).then(succeed, fail);
238+
return await Promise.try(callback, ...args).then(succeed).catch(fail);
156239
}
157240
}
158241

@@ -165,7 +248,7 @@ export async function attemptAsync(callback, ...args) {
165248
* @template T
166249
* @param {(...any) => T} callback The function to execute.
167250
* @param {(...any)} args Arguments to pass to the callback function.
168-
* @returns {AttemptResult<T>} A tuple:
251+
* @returns {AttemptResult<T, AnyError|null>} A tuple:
169252
* - `[result, null]` on success, where `result` is the value of `T`.
170253
* - `[null, Error]` on failure, where `Error` is the caught error (normalized to an Error object).
171254
* @throws {TypeError} If `callback` is not a function or is an async function.
@@ -191,7 +274,7 @@ export function attemptSync(callback, ...args) {
191274
*
192275
* @template T
193276
* @param {(...any) => T | PromiseLike<T>} callback The function to execute.
194-
* @returns {(...any) => Promise<AttemptResult<Awaited<T>>>} An async wrapped function that returns to a tuple:
277+
* @returns {(...any) => Promise<AttemptResult<Awaited<T>|null, AnyError|null>>>} An async wrapped function that returns to a tuple:
195278
* - `[result, null]` on success, where `result` is the value of `T`.
196279
* - `[null, Error]` on failure, where `Error` is the caught error (normalized to an Error object).
197280
* @throws {TypeError} If `callback` is not a function.
@@ -203,7 +286,7 @@ export const createSafeAsyncCallback = callback => async (...args) => await atte
203286
*
204287
* @template T
205288
* @param {(...any) => T} callback The function to execute.
206-
* @returns {(...any) => AttemptResult<T>} A wrapped function that returns a tuple:
289+
* @returns {(...any) => AttemptResult<T, AnyError|null>} A wrapped function that returns a tuple:
207290
* - `[result, null]` on success, where `result` is the value of `T`.
208291
* - `[null, Error]` on failure, where `Error` is the caught error (normalized to an Error object).
209292
* @throws {TypeError} If `callback` is not a function or is an async function.
@@ -219,13 +302,16 @@ export const createSafeSyncCallback = callback => (...args) => attemptSync(callb
219302
*
220303
* All callbacks are wrapped in `attemptAsync()` to preserve consistent result formatting and error handling.
221304
*
222-
* @template T,U
223-
* @param {AttemptResult<T>} result The result to handle.
305+
* @template T
306+
* @template E
307+
* @template U
308+
* @template V
309+
* @param {AttemptResult<T, E>} result The result to handle.
224310
* @param {{
225311
* success?: (value: T) => U | PromiseLike<U>,
226-
* failure?: (err: Error) => U | PromiseLike<U>
312+
* failure?: (err: E) => V | PromiseLike<V>
227313
* }} callbacks Handlers for success or failure cases.
228-
* @returns {Promise<AttemptResult<Awaited<U>>>} A Promise resolving to a new `AttemptResult` from the callback execution,
314+
* @returns {Promise<AttemptResult<Awaited<U>|Awaited<V>, E>>} A Promise resolving to a new `AttemptResult` from the callback execution,
229315
* or a failure if the input is invalid.
230316
*/
231317
export async function handleResultAsync(result, {
@@ -250,13 +336,16 @@ export async function handleResultAsync(result, {
250336
*
251337
* All callbacks are wrapped in `attemptSync()` to preserve consistent result formatting and error handling.
252338
*
253-
* @template T,U
254-
* @param {AttemptResult<T>} result The result to handle.
339+
* @template T
340+
* @template E
341+
* @template U
342+
* @template V
343+
* @param {AttemptResult<T, E>} result The result to handle.
255344
* @param {{
256345
* success?: (value: T) => U,
257-
* failure?: (err: Error) => U
346+
* failure?: (err: E) => V
258347
* }} callbacks Handlers for success or failure cases.
259-
* @returns {AttemptResult<U>} A Promise resolving to a new `AttemptResult` from the callback execution,
348+
* @returns {AttemptSuccess<U>|AttemptSuccess<V>|AttemptFailure<E>>} A Promise resolving to a new `AttemptResult` from the callback execution,
260349
* or a failure if the input is invalid.
261350
*/
262351
export function handleResultSync(result, {
@@ -275,8 +364,9 @@ export function handleResultSync(result, {
275364
/**
276365
* Throws the error if `result` is an `AttemptFailure`.
277366
*
278-
* @param {AttemptResult} result The result tuple
279-
* @throws {Error} The error if result is an `AttemptFailure`
367+
* @template E
368+
* @param {AttemptResult<any, AnyError>} result The result tuple
369+
* @throws {AnyError} The error if result is an `AttemptFailure`
280370
*/
281371
export function throwIfFailed(result) {
282372
if (failed(result)) {

0 commit comments

Comments
 (0)