Replies: 1 comment
-
This was super helpful. I changed the code a bit and added an extra utility but its been working great in my app for over a month now. import { computed, effect, type ReadonlySignal, signal } from "@preact/signals-core";
export class AsyncState<T> {
get value(): T | null {
return null;
}
get requireValue(): T {
throw new Error("Value not set");
}
get error(): unknown {
return null;
}
get isLoading(): boolean {
return false;
}
get hasValue(): boolean {
return false;
}
get hasError(): boolean {
return false;
}
map<R>(builders: {
onLoading: () => R;
onError: (error: unknown) => 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;
}
override get requireValue(): T {
return this._value;
}
override get hasValue(): boolean {
return true;
}
override toString() {
return `AsyncData{${this._value}}`;
}
}
export class AsyncLoading<T> extends AsyncState<T> {
override get value(): T | null {
return null;
}
override get isLoading(): boolean {
return true;
}
override toString() {
return "AsyncLoading{}";
}
}
export class AsyncError<T> extends AsyncState<T> {
private _error: unknown;
constructor(error: unknown) {
super();
this._error = error;
}
override get error(): unknown {
return this._error;
}
override get hasError(): boolean {
return true;
}
override toString() {
return `AsyncError{${this._error}}`;
}
}
function asyncCompute<T>(cb: () => Promise<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) {
s.value = loading;
new Promise<T>((resolve, reject) => {
if (cancel.aborted) {
reject(cancel.reason);
return;
}
cancel.addEventListener("abort", () => {
reject(cancel.reason);
});
cb.then((result) => {
if (cancel.aborted) {
reject(cancel.reason);
return;
}
resolve(result);
}).catch((error) => {
reject(error);
});
})
.then((result) => {
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;
}
function awaitAsync<T>(
asyncStateSignal: ReadonlySignal<AsyncState<T>>,
): Promise<T> {
if (asyncStateSignal.value.hasValue) {
return Promise.resolve(asyncStateSignal.value.requireValue);
}
if (asyncStateSignal.value.hasError) {
throw asyncStateSignal.value.error;
}
return new Promise((resolve, reject) => {
const dispose = effect(() => {
const signalState = asyncStateSignal.value;
if (signalState.hasValue) {
dispose(); // Clean up the effect when done
resolve(signalState.requireValue);
} else if (signalState.hasError) {
dispose(); // Clean up the effect when done
reject(signalState.error);
}
});
});
}
export { asyncCompute, awaitAsync }; This allows usage like this: class State {
units = signal<"SI" | "IP">("IP");
selectedCoordinate = signal(new Coordinate(30.2525, -97.7539));
selectedElevation = asyncCompute(async () => {
const data = await trpc.openMeteo.getElevation.query(
this.selectedCoordinate.value.toObject(),
);
return new Meters(data.elevation[0]);
});
openMeteoDataRange = signal<number>(1);
openMeteoData = asyncCompute(async () => {
const data = await trpc.openMeteo.getHistorical.query({
...this.selectedCoordinate.value.toObject(),
start_date: new Date(Date.now() - this.openMeteoDataRange.value * 365 * 86400000)
.toISOString().slice(0, 10),
...(this.units.value === "IP" ? { temperature_unit: "fahrenheit" } : {}),
});
return data;
});
openMeteoDataStats = asyncCompute(async () => {
const data = await awaitAsync(this.openMeteoData);
const unit = data.hourly_units.temperature_2m;
const temp = data.hourly.temperature_2m.filter((v) => v !== null);
const mean = v.parse(coerceToNumber, d3.mean(temp));
const median = v.parse(coerceToNumber, d3.median(temp));
const min = v.parse(coerceToNumber, d3.min(temp));
const max = v.parse(coerceToNumber, d3.max(temp));
const makeTemperature = (unit: "°C" | "°F", value: number) => {
return unit === "°C"
? new Temperature({ c: value, f: null })
: new Temperature({ c: null, f: value });
};
const transformToTemperatures = (
unit: "°C" | "°F",
data: ReturnType<typeof makeSatelliteData>,
) => {
return {
...data,
"Heating 99.6%": makeTemperature(unit, data["Heating 99.6%"] as number),
"Heating 99%": makeTemperature(unit, data["Heating 99%"] as number),
"Cooling 1%": makeTemperature(unit, data["Cooling 1%"] as number),
"Cooling 0.4%": makeTemperature(unit, data["Cooling 0.4%"] as number),
};
};
return {
mean: makeTemperature(unit, mean),
median: makeTemperature(unit, median),
min: makeTemperature(unit, min),
max: makeTemperature(unit, max),
data: this.openMeteoDataRange.value === 1
? [makeSatelliteData(1, temp)].map((v) => transformToTemperatures(unit, v))
: [
makeSatelliteData(5, temp),
makeSatelliteData(10, temp),
makeSatelliteData(15, temp),
].map((v) => transformToTemperatures(unit, v)),
};
});
... |
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
I have a Flutter fork of Preact signals and made an AsyncSignal for working with async data that still works the way you would expect with signals that are sync:
https://github.com/rodydavis/signals.dart/blob/main/packages/signals_core/lib/src/async/state.dart
I was able to port the logic to typescript including support for the native AbortController and would love to get some feedback:
Usage:
Beta Was this translation helpful? Give feedback.
All reactions