Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Make AsyncResult a PromiseLike #23501

Merged
merged 6 commits into from
Jul 22, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 24 additions & 11 deletions lib/util/result.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,20 +225,34 @@ describe('util/result', () => {
});

describe('Transforming', () => {
it('transforms successful promise to value', async () => {
it('transforms AsyncResult to pure value', async () => {
const res = await AsyncResult.ok('foo').transform((x) =>
x.toUpperCase()
);
expect(res).toEqual(Result.ok('FOO'));
});

it('transforms successful promise to Result', async () => {
it('transforms AsyncResult to Result', async () => {
const res = await AsyncResult.ok('foo').transform((x) =>
Result.ok(x.toUpperCase())
);
expect(res).toEqual(Result.ok('FOO'));
});

it('transforms Result to AsyncResult', async () => {
const res = await Result.ok('foo').transform((x) =>
AsyncResult.ok(x.toUpperCase())
);
expect(res).toEqual(Result.ok('FOO'));
});

it('transforms AsyncResult to AsyncResult', async () => {
const res = await AsyncResult.ok('foo').transform((x) =>
AsyncResult.ok(x.toUpperCase())
);
expect(res).toEqual(Result.ok('FOO'));
});

it('skips transform for failed promises', async () => {
const res = AsyncResult.err('oops');
const fn = jest.fn((x: number) => x + 1);
Expand Down Expand Up @@ -289,15 +303,14 @@ describe('util/result', () => {
expect(fn).not.toHaveBeenCalled();
});

it('handles uncaught error from AsyncResult before transforming', async () => {
const res: AsyncResult<number, string> = new AsyncResult((_, reject) =>
reject('oops')
);
const fn = jest.fn((x: number) => Promise.resolve(x + 1));
await expect(res.transform(fn)).resolves.toEqual(
Result._uncaught('oops')
);
expect(fn).not.toHaveBeenCalled();
it('re-wraps error thrown via unwrapping in async transform', async () => {
const res = await AsyncResult.ok(42)
.transform(async (): Promise<number> => {
await Promise.resolve();
throw 'oops';
})
.transform((x) => x + 1);
expect(res).toEqual(Result._uncaught('oops'));
});

it('handles error thrown on Result async transform', async () => {
Expand Down
176 changes: 91 additions & 85 deletions lib/util/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,9 @@ export class Result<T, E = Error> {
transform<U, EE>(
fn: (value: NonNullable<T>) => Result<U, E | EE>
): Result<U, E | EE>;
transform<U, EE>(
fn: (value: NonNullable<T>) => AsyncResult<U, E | EE>
): AsyncResult<U, E | EE>;
transform<U, EE>(
fn: (value: NonNullable<T>) => Promise<Result<U, E | EE>>
): AsyncResult<U, E | EE>;
Expand All @@ -266,6 +269,7 @@ export class Result<T, E = Error> {
value: NonNullable<T>
) =>
| Result<U, E | EE>
| AsyncResult<U, E | EE>
| Promise<Result<U, E | EE>>
| Promise<NonNullable<U>>
| NonNullable<U>
Expand All @@ -275,28 +279,24 @@ export class Result<T, E = Error> {
}

try {
const res = fn(this.res.val);
const result = fn(this.res.val);

if (result instanceof Result) {
return result;
}

if (res instanceof Result) {
return res;
if (result instanceof AsyncResult) {
return result;
}

if (res instanceof Promise) {
return new AsyncResult((resolve) => {
res
.then((newResult) =>
newResult instanceof Result
? resolve(newResult)
: resolve(Result.ok(newResult))
)
.catch((err) => {
logger.warn({ err }, 'Result: unhandled async transform error');
resolve(Result._uncaught(err) as never);
});
if (result instanceof Promise) {
return AsyncResult.wrap(result, (err) => {
logger.warn({ err }, 'Result: unhandled async transform error');
return Result._uncaught(err);
});
}

return Result.ok(res);
return Result.ok(result);
} catch (err) {
logger.warn({ err }, 'Result: unhandled transform error');
return Result._uncaught(err);
Expand All @@ -310,66 +310,68 @@ export class Result<T, E = Error> {
*
* All the methods resemble `Result` methods, but work asynchronously.
*/
export class AsyncResult<T, E> extends Promise<Result<T, E>> {
constructor(
executor: (
resolve: (value: Result<T, E> | PromiseLike<Result<T, E>>) => void,
reject: (reason?: unknown) => void
) => void
) {
super(executor);
export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> {
private constructor(private asyncResult: Promise<Result<T, E>>) {}

then<TResult1 = Result<T, E>>(
onfulfilled?:
| ((value: Result<T, E>) => TResult1 | PromiseLike<TResult1>)
| undefined
| null
): PromiseLike<TResult1> {
return this.asyncResult.then(onfulfilled);
}

static ok<T>(val: NonNullable<T>): AsyncResult<T, never> {
return new AsyncResult((resolve) => resolve(Result.ok(val)));
return new AsyncResult(new Promise((resolve) => resolve(Result.ok(val))));
zharinov marked this conversation as resolved.
Show resolved Hide resolved
}

static err<E>(err: NonNullable<E>): AsyncResult<never, E> {
return new AsyncResult((resolve) => resolve(Result.err(err)));
return new AsyncResult(new Promise((resolve) => resolve(Result.err(err))));
zharinov marked this conversation as resolved.
Show resolved Hide resolved
}

static wrap<T, E = Error, EE = never>(
promise: Promise<Result<T, EE>>
): AsyncResult<T, E | EE>;
static wrap<T, E = Error>(
promise: Promise<NonNullable<T>>
): AsyncResult<T, E>;
static wrap<T, E = Error, EE = never>(
promise: Promise<NonNullable<T> | Result<T, E | EE>>
promise: Promise<Result<T, EE>> | Promise<NonNullable<T>>,
onErr?: (err: NonNullable<E>) => Result<T, E>
): AsyncResult<T, E | EE> {
return new AsyncResult((resolve) => {
promise
.then((value) => {
return new AsyncResult(
promise.then<Result<T, E | EE>, Result<T, E | EE>>(
(value) => {
if (value instanceof Result) {
return resolve(value);
return value;
}

return resolve(Result.ok(value));
})
.catch((err) => resolve(Result.err(err)));
});
return Result.ok(value);
},
(err) => {
zharinov marked this conversation as resolved.
Show resolved Hide resolved
if (onErr) {
return onErr(err);
}
return Result.err(err);
}
)
);
}

static wrapNullable<T, E, NullError, UndefinedError>(
promise: Promise<T>,
nullError: NonNullable<NullError>,
undefinedError: NonNullable<UndefinedError>
): AsyncResult<T, E | NullError | UndefinedError> {
return new AsyncResult((resolve) => {
return new AsyncResult(
promise
.then((value) => {
if (value === null) {
return resolve(Result.err(nullError));
return Result.err(nullError);
}

if (value === undefined) {
return resolve(Result.err(undefinedError));
return Result.err(undefinedError);
}

return resolve(Result.ok(value));
return Result.ok(value);
})
.catch((err) => resolve(Result.err(err)));
});
.catch((err) => Result.err(err))
);
}

/**
Expand All @@ -396,8 +398,8 @@ export class AsyncResult<T, E> extends Promise<Result<T, E>> {
fallback?: NonNullable<T>
): Promise<Res<T, E>> | Promise<NonNullable<T>> {
return fallback === undefined
? this.then<Res<T, E>>((res) => res.unwrap())
: this.then<NonNullable<T>>((res) => res.unwrap(fallback));
? this.asyncResult.then<Res<T, E>>((res) => res.unwrap())
: this.asyncResult.then<NonNullable<T>>((res) => res.unwrap(fallback));
}

/**
Expand All @@ -418,10 +420,13 @@ export class AsyncResult<T, E> extends Promise<Result<T, E>> {
* ```
*/
transform<U, EE>(
fn: (value: NonNullable<T>) => Result<U, EE>
fn: (value: NonNullable<T>) => Result<U, E | EE>
): AsyncResult<U, E | EE>;
transform<U, EE>(
fn: (value: NonNullable<T>) => AsyncResult<U, E | EE>
): AsyncResult<U, E | EE>;
transform<U, EE>(
fn: (value: NonNullable<T>) => Promise<Result<U, EE>>
fn: (value: NonNullable<T>) => Promise<Result<U, E | EE>>
): AsyncResult<U, E | EE>;
transform<U>(
fn: (value: NonNullable<T>) => Promise<NonNullable<U>>
Expand All @@ -433,50 +438,51 @@ export class AsyncResult<T, E> extends Promise<Result<T, E>> {
fn: (
value: NonNullable<T>
) =>
| Result<U, EE>
| Promise<Result<U, EE>>
| Result<U, E | EE>
| AsyncResult<U, E | EE>
| Promise<Result<U, E | EE>>
| Promise<NonNullable<U>>
| NonNullable<U>
): AsyncResult<U, E | EE> {
return new AsyncResult((resolve) => {
this.then((oldResult) => {
const { ok, val: value, err: error } = oldResult.unwrap();
if (!ok) {
return resolve(Result.err(error));
}
return new AsyncResult(
this.asyncResult
.then((oldResult) => {
const { ok, val: value, err: error } = oldResult.unwrap();
if (!ok) {
return Result.err(error);
}

try {
const newResult = fn(value);
try {
const result = fn(value);

if (newResult instanceof Result) {
return resolve(newResult);
}
if (result instanceof Result) {
return result;
}

if (newResult instanceof Promise) {
return newResult
.then((asyncRes) =>
asyncRes instanceof Result
? resolve(asyncRes)
: resolve(Result.ok(asyncRes))
)
.catch((err) => {
if (result instanceof AsyncResult) {
return result;
}

if (result instanceof Promise) {
return AsyncResult.wrap(result, (err) => {
logger.warn(
{ err },
'AsyncResult: unhandled async transform error'
);
return resolve(Result._uncaught(err));
return Result._uncaught(err);
});
}
}

return resolve(Result.ok(newResult));
} catch (err) {
logger.warn({ err }, 'AsyncResult: unhandled transform error');
return resolve(Result._uncaught(err));
}
}).catch((err) => {
// Happens when `.unwrap()` of `oldResult` throws
resolve(Result._uncaught(err));
});
});
return Result.ok(result);
} catch (err) {
logger.warn({ err }, 'AsyncResult: unhandled transform error');
return Result._uncaught(err);
}
})
.catch((err) => {
// Happens when `.unwrap()` of `oldResult` throws
return Result._uncaught(err);
})
);
}
}