Last active
March 11, 2025 07:10
-
-
Save rodydavis/3b5266da2cc07f6574d425f5ce6e1e31 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { ReadonlySignal, computed, effect, signal } from "@preact/signals-core"; | |
export class AsyncState<T> { | |
constructor() {} | |
get value(): T | null { | |
return null; | |
} | |
get requireValue(): T { | |
throw new Error("Value not set"); | |
} | |
get error(): any { | |
return null; | |
} | |
get isLoading(): boolean { | |
return false; | |
} | |
get hasValue(): boolean { | |
return false; | |
} | |
get hasError(): boolean { | |
return false; | |
} | |
map<R>(builders: { | |
onLoading: () => R; | |
onError: (error: any) => R; | |
onData: (data: T) => R; | |
}): R { | |
if (this.hasError) { | |
return builders.onError(this.error); | |
} | |
if (this.hasValue) { | |
return builders.onData(this.requireValue); | |
} | |
return builders.onLoading(); | |
} | |
} | |
export class AsyncData<T> extends AsyncState<T> { | |
private _value: T; | |
constructor(value: T) { | |
super(); | |
this._value = value; | |
} | |
get requireValue(): T { | |
return this._value; | |
} | |
get hasValue(): boolean { | |
return true; | |
} | |
toString() { | |
return `AsyncData{${this._value}}`; | |
} | |
} | |
export class AsyncLoading<T> extends AsyncState<T> { | |
get value(): T | null { | |
return null; | |
} | |
get isLoading(): boolean { | |
return true; | |
} | |
toString() { | |
return `AsyncLoading{}`; | |
} | |
} | |
export class AsyncError<T> extends AsyncState<T> { | |
private _error: any; | |
constructor(error: any) { | |
super(); | |
this._error = error; | |
} | |
get error(): any { | |
return this._error; | |
} | |
get hasError(): boolean { | |
return true; | |
} | |
toString() { | |
return `AsyncError{${this._error}}`; | |
} | |
} | |
export function asyncSignal<T>( | |
cb: () => Promise<T> | |
): ReadonlySignal<AsyncState<T>> { | |
const loading = new AsyncLoading<T>(); | |
const reset = Symbol("reset"); | |
const s = signal<AsyncState<T>>(loading); | |
const c = computed<Promise<T>>(cb); | |
let controller: AbortController | null; | |
let abortSignal: AbortSignal | null; | |
function execute(cb: Promise<T>, cancel: AbortSignal) { | |
(async () => { | |
s.value = loading; | |
try { | |
const result = await new Promise<T>(async (resolve, reject) => { | |
if (cancel.aborted) { | |
reject(cancel.reason); | |
} | |
cancel.addEventListener("abort", () => { | |
reject(cancel.reason); | |
}); | |
try { | |
const result = await cb; | |
if (cancel.aborted) { | |
reject(cancel.reason); | |
return; | |
} | |
resolve(result); | |
} catch (error) { | |
reject(error); | |
} | |
}); | |
s.value = new AsyncData<T>(result); | |
} catch (error) { | |
if (error === reset) { | |
s.value = loading; | |
} else { | |
s.value = new AsyncError<T>(error); | |
} | |
} | |
})(); | |
} | |
effect(() => { | |
if (controller != null) { | |
controller.abort(reset); | |
} | |
controller = new AbortController(); | |
abortSignal = controller.signal; | |
execute(c.value, abortSignal); | |
}); | |
return s; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage: