diff --git a/mod.ts b/mod.ts index 5a0a893..8bed586 100644 --- a/mod.ts +++ b/mod.ts @@ -12,4 +12,4 @@ export * from "./src/signals/mod.ts"; export * from "./src/layout/mod.ts"; export * from "./src/canvas/mod.ts"; export * from "./src/utils/mod.ts"; -export * from "./src/input_reader/mod.ts"; +export * from "./src/input/mod.ts"; diff --git a/src/canvas/text.ts b/src/canvas/text.ts index 92f72dd..66701e0 100644 --- a/src/canvas/text.ts +++ b/src/canvas/text.ts @@ -141,19 +141,19 @@ export class TextObject extends Renderable<"text"> { }); } - draw(): void { + override draw(): void { this.#updateEffect.resume(); this.rectangle.subscribe(this.#rectangleSubscription); super.draw(); } - erase(): void { + override erase(): void { this.#updateEffect.pause(); this.rectangle.unsubscribe(this.#rectangleSubscription); super.erase(); } - updateMovement(): void { + override updateMovement(): void { const { objectsUnder, previousRectangle } = this; const rectangle = this.rectangle.peek(); @@ -204,7 +204,7 @@ export class TextObject extends Renderable<"text"> { } } - rerender(): void { + override rerender(): void { const { canvas, valueChars, omitCells, rerenderCells } = this; const { frameBuffer, rerenderQueue } = canvas; diff --git a/src/component.ts b/src/component.ts index 085939c..74ae80b 100644 --- a/src/component.ts +++ b/src/component.ts @@ -8,7 +8,7 @@ import type { Rectangle } from "./types.ts"; import { SortedArray } from "./utils/sorted_array.ts"; import type { Renderable } from "./canvas/renderable.ts"; import type { View } from "./view.ts"; -import type { InputEventRecord } from "./input_reader/mod.ts"; +import type { InputEventRecord } from "./input/mod.ts"; import { Computed, Signal, type SignalOfObject } from "./signals/mod.ts"; import { signalify } from "./signals/signalify.ts"; diff --git a/src/components/frame.ts b/src/components/frame.ts index ddd18ff..8603756 100644 --- a/src/components/frame.ts +++ b/src/components/frame.ts @@ -64,14 +64,12 @@ export interface FrameOptions extends ComponentOptions { * }); * ``` */ -export class Frame extends Component { - declare drawnObjects: { - top: TextObject; - bottom: TextObject; - left: BoxObject; - right: BoxObject; - }; - +export class Frame extends Component<{ + top: TextObject; + bottom: TextObject; + left: BoxObject; + right: BoxObject; +}> { charMap: Signal; constructor(options: FrameOptions) { diff --git a/src/components/input.ts b/src/components/input.ts index d2f074a..c8bfca5 100644 --- a/src/components/input.ts +++ b/src/components/input.ts @@ -91,12 +91,11 @@ export interface InputOptions extends Omit { * }); * ``` */ -export class Input extends Box { - declare drawnObjects: { - box: BoxObject; - text: TextObject; - cursor: TextObject; - }; +export class Input extends Box<{ + box: BoxObject; + text: TextObject; + cursor: TextObject; +}> { declare theme: InputTheme; text: Signal; diff --git a/src/components/label.ts b/src/components/label.ts index 7866442..03b5866 100644 --- a/src/components/label.ts +++ b/src/components/label.ts @@ -86,9 +86,7 @@ export interface LabelOptions extends Omit { * }) * ``` */ -export class Label extends Component { - declare drawnObjects: { texts: TextObject[] }; - +export class Label extends Component<{ texts: TextObject[] }> { #valueLines: Signal; text: Signal; diff --git a/src/components/slider.ts b/src/components/slider.ts index 70266b4..92eb790 100644 --- a/src/components/slider.ts +++ b/src/components/slider.ts @@ -58,8 +58,7 @@ export interface SliderOptions extends ComponentOptions { * }); * ``` */ -export class Slider extends Box { - declare drawnObjects: { box: BoxObject; thumb: BoxObject }; +export class Slider extends Box<{ box: BoxObject; thumb: BoxObject }> { declare theme: SliderTheme; min: Signal; diff --git a/src/components/table.ts b/src/components/table.ts index 79d472d..ba78180 100644 --- a/src/components/table.ts +++ b/src/components/table.ts @@ -100,20 +100,19 @@ export interface TableOptions extends Omit { * * ``` */ -export class Table extends Component { +export class Table extends Component<{ + frame: [ + top: TextObject, + bottom: TextObject, + spacer: TextObject, + left: BoxObject, + right: BoxObject, + ]; + + header: TextObject; + data: TextObject[]; +}> { declare theme: TableTheme; - declare drawnObjects: { - frame: [ - top: TextObject, - bottom: TextObject, - spacer: TextObject, - left: BoxObject, - right: BoxObject, - ]; - - header: TextObject; - data: TextObject[]; - }; data: Signal; headers: Signal[]>; diff --git a/src/components/text.ts b/src/components/text.ts index f4f630e..797f698 100644 --- a/src/components/text.ts +++ b/src/components/text.ts @@ -57,9 +57,7 @@ export interface TextOptions extends Omit { * }) * ``` */ -export class Text extends Component { - declare drawnObjects: { text: TextObject }; - +export class Text extends Component<{ text: TextObject }> { text: Signal; overwriteRectangle: Signal; multiCodePointSupport: Signal; diff --git a/src/components/textbox.ts b/src/components/textbox.ts index 9b5c1d1..8beaf32 100644 --- a/src/components/textbox.ts +++ b/src/components/textbox.ts @@ -9,7 +9,7 @@ import type { DeepPartial } from "../types.ts"; import { cropToWidth, insertAt } from "../utils/strings.ts"; import { clamp } from "../utils/numbers.ts"; import { Computed, Effect, Signal, signalify } from "../signals/mod.ts"; -import type { KeyPressEvent } from "../input_reader/types.ts"; +import type { KeyPressEvent } from "../input/types.ts"; export interface CursorPosition { x: number; diff --git a/src/event_emitter.ts b/src/event_emitter.ts index 088142c..689b652 100644 --- a/src/event_emitter.ts +++ b/src/event_emitter.ts @@ -13,7 +13,7 @@ export type EventListener< * Type for creating new arguments * - Required as a workaround for simple tuples and arrays types not working properly */ -export type EmitterEvent = { +export type EmitterEvent = { args: Args; }; diff --git a/src/input/mouse.ts b/src/input/mouse.ts index e8053fe..e76f08f 100644 --- a/src/input/mouse.ts +++ b/src/input/mouse.ts @@ -29,7 +29,6 @@ export function decodeMouseSGR( const action = code.at(-1); if (!code.startsWith("\x1b[<") || (action !== "m" && action !== "M")) { return undefined; - SSS; } const release = action === "m"; diff --git a/src/layout/mod.ts b/src/layout/mod.ts index e290dd0..4db0a66 100644 --- a/src/layout/mod.ts +++ b/src/layout/mod.ts @@ -1,6 +1,6 @@ // Copyright 2023 Im-Beast. MIT license. export * from "./errors.ts"; +export * from "./grid_layout.ts"; export * from "./horizontal_layout.ts"; export * from "./layout.ts"; export * from "./vertical_layout.ts"; -export * from "./grid_layout.ts"; diff --git a/src/signals/computed.ts b/src/signals/computed.ts index 88c7112..d0eb597 100644 --- a/src/signals/computed.ts +++ b/src/signals/computed.ts @@ -3,15 +3,7 @@ import { Signal } from "./signal.ts"; import type { Dependant, Dependency } from "./types.ts"; import { activeSignals, trackDependencies } from "./dependency_tracking.ts"; - -/** Thrown whenever someone tries to directly modify `Computed.value` */ -export class ComputedReadOnlyError extends Error { - constructor() { - super( - "Computed is read-only, you can't (and shouldn't!) directly modify its value", - ); - } -} +import { ComputedReadOnlyError } from "./errors.ts"; /** Function that's used to calculate `Computed`'s value */ export interface Computable { @@ -75,18 +67,19 @@ export class Computed extends Signal implements Dependant, Dependency { if ( this.$value !== (this.$value = this.computable(cause)) || this.forceUpdateValue - ) { - this.propagate(cause); - } + ) this.propagate(cause); } override dispose(): void { super.dispose(); - const { dependencies } = this; - for (const dependency of dependencies) { - dependency.dependants!.delete(this); - dependencies.delete(dependency); + for (const dependency of this.dependencies) { + dependency.dependants?.delete(this); + this.dependencies.delete(dependency); } } } + +export function computed(computable: Computable): Computed { + return new Computed(computable); +} diff --git a/src/signals/dependency_tracking.ts b/src/signals/dependency_tracking.ts index eb7ead9..3cc08b8 100644 --- a/src/signals/dependency_tracking.ts +++ b/src/signals/dependency_tracking.ts @@ -1,5 +1,5 @@ // Copyright 2023 Im-Beast. MIT license. -import type { Dependant, Dependency } from "./types.ts"; +import type { Dependency, Dependish } from "./types.ts"; export let activeSignals: Set | undefined; let incoming = 0; @@ -7,42 +7,44 @@ let incoming = 0; /** * Asynchronously tracks used signals for provided function */ -export async function trackDependencies( - dependencies: Set, +export async function track( + dependencies: Set, thisArg: unknown, // this is supposed to mean every function // deno-lint-ignore ban-types func: Function, ): Promise { - while (incoming !== 0) { - await Promise.resolve(); - } + while (incoming) await Promise.resolve(); ++incoming; activeSignals = dependencies; + try { func.call(thisArg); } catch (error) { incoming = 0; throw error; + } finally { + activeSignals = undefined; + --incoming; } - activeSignals = undefined; - --incoming; } /** * Replaces all dependencies with root dependencies to prevent multiple updates caused by the same change. */ -export function optimizeDependencies( - into: Set, - from = into, +export function optimize( + into: Set, + from: Iterable = into, ): void { for (const dependency of from) { if ("dependencies" in dependency) { into.delete(dependency); - optimizeDependencies(into, dependency.dependencies); + optimize(into, dependency.dependencies); } else { into.add(dependency); } } } + +export { optimize as optimizeDependencies, track as trackDependencies }; diff --git a/src/signals/effect.ts b/src/signals/effect.ts index 718a691..fdddebc 100644 --- a/src/signals/effect.ts +++ b/src/signals/effect.ts @@ -61,7 +61,7 @@ export class Effect implements Dependant, Disposable { update(cause: Dependency | Dependant): void { if (this.paused) { - throw "Something called update() on effect while being paused"; + throw new Error("Something called update() on effect while being paused"); } this.$effectable(cause); @@ -75,9 +75,7 @@ export class Effect implements Dependant, Disposable { if (this.disposed) { throw new ReferenceError("Effect already disposed"); } else { - for (const { dependants } of this.dependencies) { - dependants?.delete(this); - } + for (const dep of this.dependencies) dep.dependants?.delete(this); this.dependencies.clear(); this.disposed = true; } @@ -88,10 +86,12 @@ export class Effect implements Dependant, Disposable { * - Doesn't clear dependencies, can be resumed! */ pause(): void { - this.paused = true; - - for (const dependency of this.dependencies) { - dependency.dependants?.delete(this); + if (this.disposed) { + throw new ReferenceError("Effect already disposed"); + } + if (!this.paused) { + this.paused = true; + for (const dep of this.dependencies) dep.dependants?.delete(this); } } @@ -99,10 +99,12 @@ export class Effect implements Dependant, Disposable { * - Adds itself to all dependencies dependants */ resume(): void { - this.paused = false; - - for (const dependency of this.dependencies) { - dependency.depend(this); + if (this.disposed) { + throw new ReferenceError("Effect already disposed"); + } + if (this.paused) { + this.paused = false; + for (const dep of this.dependencies) dep.depend(this); } } @@ -110,3 +112,7 @@ export class Effect implements Dependant, Disposable { this.dispose(); } } + +export function effect(effectable: Effectable): Effect { + return new Effect(effectable); +} diff --git a/src/signals/errors.ts b/src/signals/errors.ts new file mode 100644 index 0000000..f013d54 --- /dev/null +++ b/src/signals/errors.ts @@ -0,0 +1,17 @@ +/** Thrown whenever someone tries to directly modify `Computed.value` */ +export class ComputedReadOnlyError extends Error { + constructor() { + super( + "Computed values are read-only and cannot be directly modified. If you need to change the value, change the signals it depends on instead.", + ); + this.name = "ComputedReadOnlyError"; + } +} + +/** Thrown whenever `deepObserve` is set and `typeof value !== "object"` */ +export class SignalDeepObserveTypeofError extends Error { + constructor() { + super("You can only deeply observe value with typeof 'object'"); + this.name = "SignalDeepObserveTypeofError"; + } +} diff --git a/src/signals/flusher.ts b/src/signals/flusher.ts index 11105d5..1fa5583 100644 --- a/src/signals/flusher.ts +++ b/src/signals/flusher.ts @@ -2,7 +2,11 @@ import type { Dependant, Dependency, LazyDependant } from "./types.ts"; /** - * Flusher tracks + * Flusher tracks dependants and updates them when `flush()` gets called. + * + * It can be used to delay updates of `LazyComputed` dependants until some + * condition is met, for example until next animation frame or until some + * expensive calculations get done. * * @example * ```ts @@ -36,9 +40,7 @@ export class Flusher implements Dependency { flush(): void { const { dependants } = this; - for (const dependant of dependants) { - dependant.update(this); - } + for (const dependant of dependants) dependant.update(this); dependants.clear(); } } diff --git a/src/signals/lazy_computed.ts b/src/signals/lazy_computed.ts index 8b35bad..2123e56 100644 --- a/src/signals/lazy_computed.ts +++ b/src/signals/lazy_computed.ts @@ -1,42 +1,47 @@ // Copyright 2023 Im-Beast. MIT license. +import { performance } from "../utils/performance.ts"; import { type Computable, Computed } from "./computed.ts"; -import type { Dependency } from "./types.ts"; import { Flusher } from "./flusher.ts"; - -import type { LazyDependant } from "./types.ts"; +import type { Dependency, LazyDependant } from "./types.ts"; // TODO: Tests -interface LazyComputedOptions { +export interface LazyComputedOptions { interval: number; flusher: Flusher; } /** * LazyComputed is a type of signal that depends on other signals and updates - * when any of them changes - * - If time between updates is smaller than given interval it gets delayed - * - If given `Flusher` instead, it will update after `Flusher.flush()` gets - * called + * when any of them changes. Be aware of its caveats, however, as it can delay + * updates to avoid too many of them in a short time. + * + * - If time between updates is smaller than given interval, it is delayed. + * - If given a `Flusher`, it will update after `Flusher.flush()` is called. * - Both interval and `Flusher` might be set at the same time. * * @example * ```ts * const multiplicand = new Signal(1); * const multiplier = new Signal(2); - * const product = new LazyComputed(() => multiplicand.value * multiplier.value, 16); + * const product = new LazyComputed( + * () => multiplicand.value * multiplier.value, + * 16, + * ); * * console.log(product.value); // 2 - * await Promise.resolve(); // Dependency tracking is asynchronous read more in `dependency_tracking.ts` + * + * // Dependency tracking is asynchronous (see `dependency_tracking.ts`) + * await Promise.resolve(); // wait for next tick * * multiplicand.value = 2; - * console.log(product.value); // 2 + * console.log(product.value); // => 2 + * * multiplier.value = 7; - * console.log(product.value); // 2 + * console.log(product.value); // => 2 * - * setTimeout(() => { - * console.log(product.value); // 14 - * }, 16) + * // wait until the timeout of LazyComputed gets executed + * setTimeout(() => console.log(product.value), 16); // => 14 * ``` */ export class LazyComputed extends Computed implements LazyDependant { @@ -79,9 +84,7 @@ export class LazyComputed extends Computed implements LazyDependant { return; } - if (flusher) { - flusher.depend(this); - } + flusher?.depend(this); if (interval) { const timeDifference = performance.now() - this.lastFired; diff --git a/src/signals/lazy_effect.ts b/src/signals/lazy_effect.ts index 6ce13ad..93bdd33 100644 --- a/src/signals/lazy_effect.ts +++ b/src/signals/lazy_effect.ts @@ -1,21 +1,23 @@ // Copyright 2023 Im-Beast. MIT license. +import { performance } from "../utils/performance.ts"; import { Effect, type Effectable } from "./effect.ts"; import { Flusher } from "./flusher.ts"; - import type { Dependant, Dependency, LazyDependant } from "./types.ts"; // TODO: Tests -interface LazyEffectOptions { +export interface LazyEffectOptions { interval: number; flusher: Flusher; } /** - * LazyEffect is an container for callback function, which runs every time any of its dependencies get updated. - * When initialized that functions gets ran and all dependencies for it are tracked. + * LazyEffect is an container for callback function, which runs every time any + * of its dependencies get updated. When initialized that functions gets ran + * and all dependencies for it are tracked. * - If time between updates is smaller than given interval it gets delayed - * - If given `Flusher` instead, it will update after `Flusher.flush()` gets called + * - If given `Flusher` instead, it will update after `Flusher.flush()` gets + * called * - Both interval and `Flusher` might be set at the same time. * * @example @@ -62,14 +64,14 @@ export class LazyEffect extends Effect implements LazyDependant { this.#updateCallback = () => this.update(this); } - [].slice(); this.lastFired = performance.now(); } override update(cause: Dependency | Dependant): void { const { flusher, interval } = this; - if (flusher) flusher.depend(this); + flusher?.depend(this); + if (interval) { const timeDifference = performance.now() - this.lastFired; if (timeDifference < interval) { diff --git a/src/signals/mod.ts b/src/signals/mod.ts index cb6547a..0246adc 100644 --- a/src/signals/mod.ts +++ b/src/signals/mod.ts @@ -2,6 +2,7 @@ export * from "./computed.ts"; export * from "./dependency_tracking.ts"; export * from "./effect.ts"; +export * from "./errors.ts"; export * from "./flusher.ts"; export * from "./lazy_computed.ts"; export * from "./lazy_effect.ts"; diff --git a/src/signals/reactivity.ts b/src/signals/reactivity.ts index 6cfd555..9e19fc5 100644 --- a/src/signals/reactivity.ts +++ b/src/signals/reactivity.ts @@ -70,23 +70,15 @@ export function makeMapMethodsReactive, S>( if (watchMapUpdates) { _$set = function $set(key: MapKeyType, value: MapValueType) { const previousValue = map.get(key); - set(key, value); - - if (value !== previousValue) { - signal.propagate(); - } + if (value !== previousValue) signal.propagate(); return map; }; } else { _$set = function $step(key: MapKeyType, value: MapValueType) { const { size } = map; - set(key, value); - - if (map.size > size) { - signal.propagate(); - } + if (map.size > size) signal.propagate(); return map; }; } @@ -98,9 +90,7 @@ export function makeMapMethodsReactive, S>( const del = map.delete.bind(map); function $delete(key: MapKeyType) { const removed = del(key); - if (removed) { - signal.propagate(); - } + if (removed) signal.propagate(); return removed; } @@ -108,9 +98,7 @@ export function makeMapMethodsReactive, S>( function $clear() { const { size } = map; clear(); - if (size > 0) { - signal.propagate(); - } + if (size > 0) signal.propagate(); } return map; @@ -137,33 +125,23 @@ export function makeSetMethodsReactive, S>( const add = set.add.bind(set); function $add(value: Parameters[0]) { const { size } = set; - add(value); - - if (set.size > size) { - signal.propagate(); - } + if (set.size > size) signal.propagate(); return set; } const del = set.delete.bind(set); function $delete(value: Parameters[0]) { const removed = del(value); - if (removed) { - signal.propagate(); - } + if (removed) signal.propagate(); return removed; } const clear = set.clear.bind(set); function $clear() { const { size } = set; - clear(); - - if (size > 0) { - signal.propagate(); - } + if (size > 0) signal.propagate(); } return set; @@ -201,21 +179,15 @@ export function makeArrayMethodsReactive, S>( const pop = array.pop.bind(array); function $pop() { const { length } = array; - const output = pop(); - - if (length > 0) { - signal.propagate(); - } + if (length > 0) signal.propagate(); return output; } const splice = array.splice.bind(array); function $splice(start: number, deleteCount: number, ...items: unknown[]) { const output = splice(start, deleteCount, ...items); - if (output.length > 0) { - signal.propagate(); - } + if (output.length > 0) signal.propagate(); return output; } diff --git a/src/signals/signal.ts b/src/signals/signal.ts index 87c0f9c..d9fd50d 100644 --- a/src/signals/signal.ts +++ b/src/signals/signal.ts @@ -1,4 +1,5 @@ // Copyright 2023 Im-Beast. MIT license. +// deno-lint-ignore-file no-explicit-any import { activeSignals } from "./dependency_tracking.ts"; import { makeMapMethodsReactive, @@ -7,16 +8,12 @@ import { ORIGINAL_REF, type Reactive, } from "./reactivity.ts"; +import { SignalDeepObserveTypeofError } from "./errors.ts"; import type { Dependant, Dependency, Subscription } from "./types.ts"; // TODO: Make dispose revert reactive value modifications -/** Thrown whenever `deepObserve` is set and `typeof value !== "object"` */ -export class SignalDeepObserveTypeofError extends Error { - constructor() { - super("You can only deeply observe value with typeof 'object'"); - } -} +type Collection = Map | Set; export interface SignalOptions { /** @@ -36,8 +33,7 @@ export interface SignalOptions { * - When set to `false` it uses `Object.defineProperty` to watch properties * that existed at the time of creating signal. */ - watchObjectIndex?: T extends Map | Set ? never - : boolean; + watchObjectIndex?: boolean & (T extends Collection ? never : unknown); /** * @requires T to be `instanceof Map` * @@ -46,9 +42,11 @@ export interface SignalOptions { * - When set to `true` it checks whether value changed. * - When set to `false` it checks whether map size changed (default). */ - watchMapUpdates?: T extends Map ? boolean : never; + watchMapUpdates?: boolean & (T extends Map ? unknown : never); } +let __is_signal = (it: unknown): boolean => (void it, false); + /** * Signal wraps value in a container. * @@ -63,7 +61,7 @@ export interface SignalOptions { * ``` */ export class Signal implements Dependency { - protected $value: T; + #value: T; // Dependant: something that depends on THIS dependants?: Set; @@ -88,7 +86,15 @@ export class Signal implements Dependency { ); } } - this.$value = value; + this.#value = value; + } + + protected get $value(): T { + return this.#value; + } + + protected set $value(value: T) { + this.#value = value; } /** Bind function to signal, it'll be called each time signal's value changes and is equal to {conditionValues} */ @@ -163,13 +169,13 @@ export class Signal implements Dependency { } } - if (!dependants?.size) return; - - for (const dependant of dependants) { - if ("forceUpdateValue" in dependant) { - dependant.forceUpdateValue = true; + if (dependants?.size) { + for (const dependant of dependants) { + if ("forceUpdateValue" in dependant) { + dependant.forceUpdateValue = true; + } + dependant.update(cause ?? this); } - dependant.update(cause ?? this); } } @@ -196,16 +202,15 @@ export class Signal implements Dependency { subscriptions?.clear(); - if (!dependants) return; - for (const dependant of dependants) { - dependants.delete(dependant); - dependant.dependencies.delete(this); + if (dependants?.size) { + for (const dependant of dependants) { + dependants.delete(dependant); + dependant.dependencies.delete(this); - // If dependant has no more dependencies then - // it means that it should be replaced with constant value, - // because nothing can update its value anymore - if (!dependant.dependencies) { - dependant.dispose(); + // If dependant has no more dependencies then + // it means that it should be replaced with constant value, + // because nothing can update its value anymore + if (!dependant.dependencies) dependant.dispose(); } } } @@ -248,9 +253,12 @@ export class Signal implements Dependency { [Symbol.toPrimitive](hint: "number"): number; [Symbol.toPrimitive](hint: "string" | "default"): string; [Symbol.toPrimitive](hint: "string" | "number" | "default"): string | number; - [Symbol.toPrimitive](hint: "string" | "number" | "default"): string | number { - if (hint === "number") return Number(this.$value); - return String(this.$value); + [Symbol.toPrimitive](hint: string): string | number { + return (hint === "number" ? Number : String)(this.$value); + } + + static { + __is_signal = (it) => #value in Object(it); } } @@ -278,3 +286,9 @@ export class Signal implements Dependency { * ``` */ export type SignalOfObject = Signal & { [key in keyof T]?: never }; + +export type SignalLike = T | Signal; + +export function isSignal(it: SignalLike): it is Signal { + return typeof it === "object" && it !== null && __is_signal(it); +} diff --git a/src/signals/types.ts b/src/signals/types.ts index 504aa53..df537b7 100644 --- a/src/signals/types.ts +++ b/src/signals/types.ts @@ -10,6 +10,7 @@ export interface Subscription { export interface Dependency { /** Attach dependant to this Dependency */ depend(dependant: Dependant): void; + /** All dependants that rely on this Dependency */ dependants?: Set; } @@ -18,10 +19,12 @@ export interface Dependency { export interface Dependant { /** Set of all dependencies Dependant relies on */ dependencies: Set; - /** Destroy dependant, clear its dependencies */ - dispose(): void; + /** Method which updates Dependants state/value */ update(cause: Dependency | Dependant): void; + + /** Destroy dependant, clear its dependencies */ + dispose(): void; } /** Element which relies on dependencies to function and updates either after specified interval or when flusher gets flushed */ @@ -31,3 +34,13 @@ export interface LazyDependant extends Dependant { flusher?: Flusher; lastFired: number; } + +export type Dependish = Dependency | (Dependency & Dependant); + +export interface Disposable { + [Symbol.dispose](): void; +} + +export interface AsyncDisposable { + [Symbol.asyncDispose](): void | Promise; +} diff --git a/src/stdio/stderr.ts b/src/stdio/stderr.ts index 0fcc838..56021c9 100644 --- a/src/stdio/stderr.ts +++ b/src/stdio/stderr.ts @@ -2,7 +2,7 @@ // Copyright 2018-2025 the Deno authors. MIT license. import { isDeno, isNodeLike } from "../utils/runtime.ts"; - +import { ObjectAssign, ObjectDefineProperty } from "../utils/primordials.ts"; // @ts-types="npm:@types/node@22/process" import p from "node:process"; @@ -72,8 +72,6 @@ export interface Stderr { isTerminal(): boolean; } -const { assign, defineProperty } = Object; - export class Stderr { constructor(impl?: Stderr) { if (!impl) { @@ -93,7 +91,7 @@ export class Stderr { } } - assign(this, impl); + ObjectAssign(this, impl); this.writeSync = this.writeSync.bind(impl); this.write ??= (p) => Promise.resolve(this.writeSync(p)); this.write = this.write.bind(impl); @@ -102,25 +100,30 @@ export class Stderr { this.isTerminal ??= () => false; this.isTerminal = this.isTerminal.bind(impl); - defineProperty(this, "writable", { - get() { - if (impl && "writable" in impl) { - return impl.writable; - } else { - return new WritableStream({ - write: async (chunk) => { - for ( - let n = 0; - n < chunk.byteLength; - n += await this.write(chunk.subarray(n)) - ); - }, - }); - } - }, - set() {}, - configurable: true, - enumerable: true, - }); + ObjectDefineProperty( + this, + "writable", + { + __proto__: impl, + get() { + if (impl && "writable" in impl) { + return impl.writable; + } else { + return new WritableStream({ + write: async (chunk) => { + for ( + let n = 0; + n < chunk.byteLength; + n += await this.write(chunk.subarray(n)) + ); + }, + }); + } + }, + set() {}, + configurable: true, + enumerable: true, + } as PropertyDescriptor & ThisType, + ); } } diff --git a/src/stdio/stdin.ts b/src/stdio/stdin.ts index 6ad33f5..2f17579 100644 --- a/src/stdio/stdin.ts +++ b/src/stdio/stdin.ts @@ -2,6 +2,8 @@ // Copyright 2018-2025 the Deno authors. MIT license. import { isDeno, isNodeLike } from "../utils/runtime.ts"; +import type { Reshape } from "../utils/types.ts"; +import { ObjectAssign, ObjectDefineProperty } from "../utils/primordials.ts"; import process from "node:process"; /** @category I/O */ @@ -10,6 +12,7 @@ export interface SetRawOptions { * The `cbreak` option can be used to indicate that characters that * correspond to a signal should still be generated. * + * @remarks * When disabling raw mode, this option is ignored. This functionality * currently only works on Linux and Mac OS. */ @@ -115,10 +118,6 @@ type StdinImpl = Reshape< Partial & Pick >; -type Reshape = Pick; - -const { assign, defineProperty } = Object; - export class Stdin { constructor(impl?: StdinImpl) { if (!impl) { @@ -137,14 +136,14 @@ export class Stdin { throw new Error("No Stdin implementation available"); } } - assign(this, impl); + ObjectAssign(this, impl); this.readSync = this.readSync.bind(impl); this.read = ( this.read ??= (p) => Promise.resolve(this.readSync(p)) ).bind(impl); this.close = (this.close ??= () => {}).bind(impl); this.isTerminal = (this.isTerminal ??= () => false).bind(impl); - defineProperty(this, "readable", { + ObjectDefineProperty(this, "readable", { get() { if (impl && "readable" in impl) { return impl.readable; diff --git a/src/stdio/stdout.ts b/src/stdio/stdout.ts index 29e1dff..6125dd8 100644 --- a/src/stdio/stdout.ts +++ b/src/stdio/stdout.ts @@ -2,6 +2,7 @@ // Copyright 2018-2025 the Deno authors. MIT license. import { isDeno, isNodeLike } from "../utils/runtime.ts"; +import { ObjectAssign, ObjectDefineProperty } from "../utils/primordials.ts"; // @ts-types="npm:@types/node@22/process" import p from "node:process"; @@ -72,8 +73,6 @@ export interface Stdout { isTerminal(): boolean; } -const { assign, defineProperty } = Object; - export class Stdout { constructor(impl?: Stdout) { if (!impl) { @@ -93,7 +92,7 @@ export class Stdout { } } - assign(this, impl); + ObjectAssign(this, impl); this.writeSync = this.writeSync.bind(impl); this.write ??= (p) => Promise.resolve(this.writeSync(p)); this.write = this.write.bind(impl); @@ -102,25 +101,30 @@ export class Stdout { this.isTerminal ??= () => false; this.isTerminal = this.isTerminal.bind(impl); - defineProperty(this, "writable", { - get() { - if (impl && "writable" in impl) { - return impl.writable; - } else { - return new WritableStream({ - write: async (chunk) => { - for ( - let n = 0; - n < chunk.byteLength; - n += await this.write(chunk.subarray(n)) - ); - }, - }); - } - }, - set() {}, - configurable: true, - enumerable: true, - }); + ObjectDefineProperty( + this, + "writable", + { + __proto__: impl, + get() { + if (impl && "writable" in impl) { + return impl.writable; + } else { + return new WritableStream({ + write: async (chunk) => { + for ( + let n = 0; + n < chunk.byteLength; + n += await this.write(chunk.subarray(n)) + ); + }, + }); + } + }, + set() {}, + configurable: true, + enumerable: true, + } as PropertyDescriptor & ThisType, + ); } } diff --git a/src/tui.ts b/src/tui.ts index 37e77d2..64cdb43 100644 --- a/src/tui.ts +++ b/src/tui.ts @@ -2,7 +2,7 @@ import { BoxObject, Canvas } from "./canvas/mod.ts"; import type { Component } from "./component.ts"; import { type EmitterEvent, EventEmitter } from "./event_emitter.ts"; -import type { InputEventRecord } from "./input_reader/mod.ts"; +import type { InputEventRecord } from "./input/mod.ts"; import { Computed, type Signal } from "./signals/mod.ts"; import type { Style } from "./theme.ts"; import { type Rectangle, Stdin, Stdout } from "./types.ts"; diff --git a/src/utils/performance.ts b/src/utils/performance.ts new file mode 100644 index 0000000..ac29fa0 --- /dev/null +++ b/src/utils/performance.ts @@ -0,0 +1,11 @@ +import { $global } from "./primordials.ts"; + +export interface Performance { + now(): number; +} + +const globalTiming: Performance = $global.performance || $global.Date; + +export const performance: Performance = { + now: globalTiming.now.bind(globalTiming), +}; diff --git a/src/utils/primordials.ts b/src/utils/primordials.ts new file mode 100644 index 0000000..d14151b --- /dev/null +++ b/src/utils/primordials.ts @@ -0,0 +1,29 @@ +export const $global: typeof globalThis = (() => { + try { + return (0, eval)("this"); + } catch { /* nah */ } + if (typeof self !== "undefined") return self; + if (typeof window !== "undefined") return window; + if (typeof globalThis !== "undefined") return globalThis; + // deno-lint-ignore no-node-globals + if (typeof global !== "undefined") return global; + throw new Error("Unable to locate global object"); +})(); + +const $O = $global.Object, + $defs = $O.defineProperties, + $gpds = $O.getOwnPropertyDescriptors; +// $create = $O.create, +// $def = $O.defineProperty, +// $gpd = $O.getOwnPropertyDescriptor, + +export const SafeObject = $defs($O.bind($O), $gpds($O)); + +export const ObjectAssign = SafeObject.assign; +export const ObjectDefineProperty = SafeObject.defineProperty; +export const ObjectDefineProperties = SafeObject.defineProperties; +export const ObjectGetOwnPropertyDescriptor = + SafeObject.getOwnPropertyDescriptor; +export const ObjectGetOwnPropertyDescriptors = + SafeObject.getOwnPropertyDescriptors; +export const ObjectCreate = SafeObject.create; diff --git a/src/utils/runtime.ts b/src/utils/runtime.ts index 017bc57..3920d4f 100644 --- a/src/utils/runtime.ts +++ b/src/utils/runtime.ts @@ -1,5 +1,13 @@ // deno-lint-ignore no-explicit-any -const _global: any = globalThis; +const _global: any = (() => { + try { + // deno-lint-ignore no-node-globals + return (0, eval)("this") || self || window || globalThis || global; + } catch (_) { + // deno-lint-ignore no-node-globals + return self || window || globalThis || global; + } +})(); export type Runtime = | "node" diff --git a/src/utils/sorted_array.ts b/src/utils/sorted_array.ts index 51ead72..be53934 100644 --- a/src/utils/sorted_array.ts +++ b/src/utils/sorted_array.ts @@ -51,8 +51,13 @@ export class SortedArray extends Array { return super.copyWithin(target, start, end).sort(this.compareFn); } - override sort(compareFn?: (a: T, b: T) => number): this { - return super.sort(compareFn ?? this.compareFn), this; + override sort( + compareFn?: (this: This, a: T, b: T) => number, + thisArg?: This, + ): this { + compareFn ??= this.compareFn; + if (thisArg) compareFn = compareFn?.bind(thisArg); + return super.sort(compareFn), this; } override fill(value: T, start?: number, end?: number): this { diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 0000000..8b36935 --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1 @@ +export type Reshape = Pick;