Skip to content

Commit

Permalink
Replace AsyncAction with AsyncFetch
Browse files Browse the repository at this point in the history
  • Loading branch information
lemonmade committed May 15, 2024
1 parent c275207 commit 97aea8b
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 130 deletions.
126 changes: 0 additions & 126 deletions packages/async/source/AsyncAction.ts

This file was deleted.

200 changes: 200 additions & 0 deletions packages/async/source/AsyncFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import {signal} from '@quilted/signals';

export interface AsyncFetchCallResult<Data = unknown, Input = unknown> {
readonly value?: Data;
readonly error?: unknown;
readonly input?: Input;
}

export interface AsyncFetchFunction<Data = unknown, Input = unknown> {
(
input: Input,
options: {
signal?: AbortSignal;
},
): PromiseLike<Data>;
}

export class AsyncFetch<Data = unknown, Input = unknown> {
get status() {
return this.finished.value?.status ?? 'pending';
}

get isRunning() {
return this.running.value != null;
}

get value() {
return this.finished.value?.value;
}

get error() {
return this.finished.value?.error;
}

get promise(): AsyncFetchPromise<Data, Input> {
return (
this.running.value?.promise ??
this.finished.value?.promise ??
this.initial.promise
);
}

private readonly running = signal<AsyncFetchCall<Data, Input> | undefined>(
undefined,
);
private readonly finished = signal<AsyncFetchCall<Data, Input> | undefined>(
undefined,
);
private readonly function: AsyncFetchFunction<Data, Input>;
private readonly initial: AsyncFetchCall<Data, Input>;

constructor(
fetchFunction: AsyncFetchFunction<Data, Input>,
{initial}: {initial?: AsyncFetchCallResult<Data, Input>} = {},
) {
this.function = fetchFunction;
this.initial = new AsyncFetchCall(fetchFunction, initial);
}

call = (
input?: Input,
{signal}: {signal?: AbortSignal} = {},
): AsyncFetchPromise<Data, Input> => {
const fetchCall =
this.running.peek() == null &&
this.finished.peek() == null &&
!this.initial.signal.aborted
? this.initial
: new AsyncFetchCall(this.function);

const finalizeFetchCall = () => () => {
if (this.running.peek() === fetchCall) {
this.running.value = undefined;
}

if (fetchCall.signal.aborted) return;

this.finished.value = fetchCall;
};

fetchCall.call(input, {signal}).then(finalizeFetchCall, finalizeFetchCall);

this.running.value = fetchCall;

return fetchCall.promise;
};
}

export class AsyncFetchCall<Data = unknown, Input = unknown> {
readonly promise: AsyncFetchPromise<Data, Input>;
readonly function: AsyncFetchFunction<Data, Input>;
readonly input!: Input;

get signal() {
return this.abortController.signal;
}

get isRunning() {
return this.runningSignal.value;
}

get status() {
return this.promise.status;
}

get value() {
return this.promise.status === 'fulfilled' ? this.promise.value : undefined;
}

get error() {
return this.promise.status === 'rejected' ? this.promise.reason : undefined;
}

private readonly resolve: (value: Data) => void;
private readonly reject: (cause: unknown) => void;
private readonly abortController = new AbortController();
private readonly runningSignal = signal(false);

constructor(
fetchFunction: AsyncFetchFunction<Data, Input>,
initial?: AsyncFetchCallResult<Data, Input>,
) {
this.function = fetchFunction;

let resolve!: (value: Data) => void;
let reject!: (cause: unknown) => void;

this.promise = new AsyncFetchPromise(this, (res, rej) => {
resolve = res;
reject = rej;
});

this.resolve = resolve;
this.reject = reject;

if (initial) {
this.input = initial.input!;

if (initial.error) {
this.reject(initial.error);
} else {
this.resolve(initial.value!);
}
}
}

abort = () => {
this.abortController.abort();
};

call = (input?: Input, {signal}: {signal?: AbortSignal} = {}) => {
if (
this.runningSignal.peek() ||
this.promise.status === 'pending' ||
this.signal.aborted
) {
throw new Error(`Can’t perform fetch()`);
}

if (signal) {
signal.addEventListener('abort', () => {
this.abortController.abort();
});
}

this.function(input!, {signal}).then(this.resolve, this.reject);

return this.promise;
};
}

export class AsyncFetchPromise<
Data = unknown,
Input = unknown,
> extends Promise<Data> {
readonly status: 'pending' | 'fulfilled' | 'rejected' = 'pending';
readonly value?: Data;
readonly reason?: unknown;
readonly source: AsyncFetchCall<Data, Input>;

constructor(
source: AsyncFetchCall<Data, Input>,
executor: ConstructorParameters<typeof Promise<Data>>[0],
) {
super((resolve, reject) => {
executor(
(value) => {
Object.assign(this, {status: 'fulfilled', value});
resolve(value);
},
(reason) => {
Object.assign(this, {status: 'rejected', reason});
reject(reason);
},
);
});

this.source = source;
}
}
10 changes: 6 additions & 4 deletions packages/async/source/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export {AsyncModulesGlobal, type AsyncModulesOptions} from './global.ts';
export {
AsyncAction,
AsyncActionPromise,
AsyncActionDeferred,
} from './AsyncAction.ts';
AsyncFetch,
AsyncFetchCall,
AsyncFetchPromise,
type AsyncFetchFunction,
type AsyncFetchCallResult,
} from './AsyncFetch.ts';
export {
AsyncModule,
type AsyncModuleLoader,
Expand Down

0 comments on commit 97aea8b

Please sign in to comment.