const $get = Symbol.for('FluidValue.get') const $observers = Symbol.for('FluidValue.observers') export { FluidValue, hasFluidValue, getFluidValue, getFluidObservers, callFluidObserver, callFluidObservers, // Mutations setFluidGetter, addFluidObserver, removeFluidObserver, } /** Returns true if `arg` can be observed. */ const hasFluidValue = (arg: any): arg is FluidValue => Boolean(arg && arg[$get]) /** * Get the current value. * If `arg` is not observable, `arg` is returned. */ const getFluidValue: GetFluidValue = (arg: any) => arg && arg[$get] ? arg[$get]() : arg /** Get the current observer set. Never mutate it directly! */ const getFluidObservers: GetFluidObservers = (target: any) => target[$observers] || null /** Send an event to an observer. */ function callFluidObserver( observer: FluidObserver, event: E ): void function callFluidObserver( observer: FluidObserver, event: UnsafeFluidEvent ): void function callFluidObserver( observer: any, event: FluidEvent | UnsafeFluidEvent ) { if (observer.eventObserved) { observer.eventObserved(event) } else { observer(event) } } /** Send an event to all observers. */ function callFluidObservers( target: FluidObservable, event: E ): void function callFluidObservers(target: object, event: UnsafeFluidEvent): void function callFluidObservers(target: any, event: FluidEvent | UnsafeFluidEvent) { let observers: Set = target[$observers] if (observers) { observers.forEach((observer) => { callFluidObserver(observer, event) }) } } type GetFluidValue = { (target: T | FluidValue): T } type GetFluidObservers = { (target: FluidObservable): ReadonlySet< FluidObserver > | null (target: object): ReadonlySet | null } /** An event from a `fluids` compatible object */ export interface FluidEvent { type: string parent: T } /** An unknown event from a `fluids` compatible object */ export interface UnsafeFluidEvent extends FluidEvent { [key: string]: any } /** * A source of `FluidEvent` objects that can be observed with * the `addFluidObserver` function. * * Use "interface merging" to mark your class as observable: * * class Foo {} * interface Foo extends FluidObservable {} * * Once that's done, you get the following benefits: * - observers added with `addFluidObserver` are type-checked * - events passed to `callFluidObservers` are type-checked */ export interface FluidObservable { // Never access this directly. Its only purpose is to enforce type safety. [$observers]?: E | null } /** * `FluidValue` objects can be unwrapped with `getFluidValue` to access * their current value. * * Libraries may support observable values by handling `FluidValue` objects * in their functions and classes. */ abstract class FluidValue { constructor(get?: () => T) { if (!get && !(get = this.get)) { throw Error('Unknown getter') } setFluidGetter(this, get) } /** Get the current value. */ protected get?(): T /** Called after an observer is added. */ protected observerAdded?(count: number, observer: FluidObserver): void /** Called after an observer is removed. */ protected observerRemoved?(count: number, observer: FluidObserver): void } /** Extract the value type from a `FluidValue` type */ export type FluidUnwrap = T extends FluidValue ? U : T /** Extract the event type from a `FluidObservable` type */ export type FluidEvents = T extends FluidObservable ? U : never /** An observer of `FluidValue` objects. */ export type FluidObserver = | { eventObserved(event: E): void } | { (event: E): void } /** Add the `FluidValue` type to every property. */ export type FluidProps = T extends object ? { [P in keyof T]: T[P] | FluidValue> } : unknown /** Remove the `FluidValue` type from every property. */ export type StaticProps = { [P in keyof T]: FluidUnwrap } /** Define the getter called by `getFluidValue`. */ const setFluidGetter = (target: object, get: () => any) => setHidden(target, $get, get) /** Observe a `fluids`-compatible object. */ function addFluidObserver( target: FluidObservable, observer: FluidObserver ): typeof observer function addFluidObserver( target: object, observer: FluidObserver ): typeof observer function addFluidObserver(target: any, observer: FluidObserver) { let observers: Set = target[$observers] if (!observers) { setHidden(target, $observers, (observers = new Set())) } if (!observers.has(observer)) { observers.add(observer) if (target.observerAdded) { target.observerAdded(observers.size, observer) } } return observer } /** Stop observing a `fluids`-compatible object. */ function removeFluidObserver( target: FluidObservable, observer: FluidObserver ): void function removeFluidObserver( target: object, observer: FluidObserver ): void function removeFluidObserver(target: any, observer: FluidObserver) { let observers: Set = target[$observers] if (observers && observers.has(observer)) { const count = observers.size - 1 if (count) { observers.delete(observer) } else { setHidden(target, $observers, null) } if (target.observerRemoved) { target.observerRemoved(count, observer) } } } const setHidden = (target: any, key: any, value: any) => Object.defineProperty(target, key, { value, writable: true, configurable: true, })