mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
94 lines
3.7 KiB
TypeScript
94 lines
3.7 KiB
TypeScript
// `TuiScope` (slice 11 §4, decision "Slice 11, P9"; p2's `TuiScope`): a terminal component's Chord resources, the
|
|
// counterpart of the React bindings for terminal UI libraries. It holds references, follows watchables and registries,
|
|
// calls one `changed` callback (the app's redraw) on every change, and releases everything at `dispose`. Its page state
|
|
// is the host's, shared by every scope (React's `usePageState`): a scope opened again after a plugin reload finds it.
|
|
// References wait and rebind by themselves (no retry loop, unlike p2's); no terminal library dependency.
|
|
|
|
import { BACKGROUND_CONTEXT, withAbortSignal } from "../context/index.ts";
|
|
import type { Contract, ParamsOf } from "../contracts/index.ts";
|
|
import type { Principal } from "../data/index.ts";
|
|
import type { Held, Host } from "../host/index.ts";
|
|
import type { Registry } from "../registries/index.ts";
|
|
import { type PageState, pageState } from "../surfaces/page-state.ts";
|
|
import type { NativeWatchable, Watchable } from "../watch.ts";
|
|
|
|
/** A component's scope: what it holds and follows, until `dispose`. */
|
|
export class TuiScope<P extends Principal = Principal> {
|
|
readonly #host: Host<P>;
|
|
readonly #changed: () => void;
|
|
readonly #abort = new AbortController();
|
|
readonly #releases: (() => void)[] = [];
|
|
|
|
constructor(host: Host<P>, changed: () => void) {
|
|
this.#host = host;
|
|
this.#changed = changed;
|
|
}
|
|
|
|
/** A reference to `Type` with `params`, held until `dispose`; its state changes call `changed`. */
|
|
hold<C extends Contract>(Type: C, params: ParamsOf<C>): Held<C> {
|
|
const held = this.#host.hold(Type, params, this.#context());
|
|
this.#releases.push(() => held.release());
|
|
this.watch(held.state);
|
|
return held;
|
|
}
|
|
|
|
/**
|
|
* Follows `source` (a member, a derive, `ref.state`) until `dispose`: its changes call `changed`. Returns a reader of
|
|
* its current value (`undefined` while it has none: nothing throws in a redraw).
|
|
*/
|
|
watch<T>(source: Watchable<T> | NativeWatchable<T>): () => T | undefined {
|
|
source.watch(this.#context()).then(
|
|
(watch) => {
|
|
(watch.start as (listener: () => Promise<void>) => void)(async () => {
|
|
if (!this.#abort.signal.aborted) this.#changed();
|
|
});
|
|
// The value it acquired may be newer than what was drawn (a reference bound since): draw it.
|
|
if (!this.#abort.signal.aborted) this.#changed();
|
|
},
|
|
// Disposed (or the source ended) before the watch started: nothing to follow.
|
|
() => {},
|
|
);
|
|
return () => {
|
|
try {
|
|
return source.value;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
};
|
|
}
|
|
|
|
/** Registry `R`'s composed value, followed until `dispose`; a reader, `undefined` until it is usable. */
|
|
registry<C, V>(R: Registry<C, V>): () => V | undefined {
|
|
let read: () => V | undefined = () => undefined;
|
|
this.#host.registry(R, this.#context()).then(
|
|
(value) => {
|
|
read = this.watch(value);
|
|
if (!this.#abort.signal.aborted) this.#changed();
|
|
},
|
|
() => {},
|
|
);
|
|
return () => read();
|
|
}
|
|
|
|
/**
|
|
* The host's page state at `key`, kept for the terminal's life, not the scope's (a new key starts at `initial`); its
|
|
* changes call `changed` until `dispose`.
|
|
*/
|
|
pageState<T>(key: string, initial: T): PageState<T> {
|
|
const state = pageState(this.#host, key, initial);
|
|
// A closure of its own: two scopes may share one `changed`, and subscribers are a set of functions.
|
|
this.#releases.push(state.subscribe(() => this.#changed()));
|
|
return state;
|
|
}
|
|
|
|
/** Releases every reference and ends every watch; `changed` is not called after it. */
|
|
dispose(): void {
|
|
this.#abort.abort();
|
|
for (const release of this.#releases.splice(0)) release();
|
|
}
|
|
|
|
/** The context of the scope's work: aborted at `dispose`. */
|
|
#context() {
|
|
return withAbortSignal(this.#abort.signal, BACKGROUND_CONTEXT);
|
|
}
|
|
}
|