state-event-target
Version:
Primary state with event-driven replicas.
115 lines (98 loc) • 2.96 kB
text/typescript
import { attachPromiseMeta, PromiseWithMeta } from "./PromiseWithMeta";
type PromiseResolver<Value> = (value: Value | PromiseLike<Value>) => void;
export class DataEventTarget<Value> extends EventTarget {
#abortController = new AbortController();
#promise?: PromiseWithMeta<Value>;
#pendingResolvers: PromiseResolver<Value>[] = [];
lastUpdated = performance.now();
constructor(initialValue?: Value) {
super();
if (initialValue !== undefined) {
this.set(initialValue);
}
this.addEventListener(
"state:prime",
(event) => {
const value = this.peek();
if (value !== undefined) {
this.dispatchEvent(
new CustomEvent("state:update", { detail: { value } }),
);
}
this.prime();
},
{ signal: this.#abortController.signal },
);
}
/**
* Read attempts to return the current value synchronously if one exists,
* but otherwise returns a Promise that will resolve when a value is set.
*/
read(): Value | PromiseWithMeta<Value> {
const p = this.getAsync();
return p.status === "fulfilled" ? p.value! : p;
}
/**
* Prime ensures there is a Promise ready to receive a future value.
* Useful for setting up the Promise before you need the value.
*/
prime() {
void this.getAsync();
}
/**
* Peek will always synchronously read the value.
* It will return undefined if no value has been set yet.
*/
peek(): Value | undefined {
return this.#promise?.value;
}
/**
* GetAsync will always return a *stable* Promise that resolves when a value is set.
*/
getAsync(): PromiseWithMeta<Value> {
if (!this.#promise) {
this.#promise = attachPromiseMeta(
new Promise((resolve) => {
this.#pendingResolvers.push(resolve);
}),
);
this.dispatchEvent(new CustomEvent("state:missing", { detail: {} }));
}
return this.#promise;
}
/**
* Sets the value, resolving any pending Promises.
*/
set(value: Value) {
for (const pendingResolver of this.#pendingResolvers) {
pendingResolver(value);
}
this.#pendingResolvers = [];
const existing = this.#promise;
if (!existing || existing.value !== value) {
this.#promise = attachPromiseMeta(
new Promise((resolve) => {
this.#pendingResolvers.push(resolve);
}),
);
this.#promise.status = "fulfilled";
this.#promise.value = value;
}
this.dispatchEvent(new CustomEvent("state:update", { detail: { value } }));
this.lastUpdated = performance.now();
}
onMissing(cb: EventListener) {
this.addEventListener("state:missing", cb, {
signal: this.#abortController.signal,
});
}
clear() {
this.#promise = undefined;
this.#pendingResolvers = [];
this.dispatchEvent(new CustomEvent("state:reset"));
}
destroy() {
this.clear();
this.#abortController.abort();
}
}