import type * as Debug from "@starbeam/debug"; import { Desc, Tree } from "@starbeam/debug"; import type * as interfaces from "@starbeam/interfaces"; import { REACTIVE } from "@starbeam/shared"; import { isPresent } from "@starbeam/verify"; import { Timestamp, zero } from "./timestamp.js"; interface ExhaustiveMatcher { mutable: (internals: interfaces.MutableInternals) => T; composite: (internals: interfaces.CompositeInternals) => T; delegate: (internals: interfaces.DelegateInternals) => T; static: (internals: interfaces.StaticInternals) => T; } interface DefaultMatcher extends Partial> { default: (internals: interfaces.ReactiveInternals) => T; } type Matcher = ExhaustiveMatcher | DefaultMatcher; export type ReactiveProtocol< I extends interfaces.ReactiveInternals = interfaces.ReactiveInternals > = interfaces.ReactiveProtocol; export const ReactiveProtocol = { description(this: void, reactive: ReactiveProtocol): Debug.Description { return ReactiveInternals.description(reactive[REACTIVE]); }, id(this: void, reactive: ReactiveProtocol): interfaces.ReactiveId { return ReactiveInternals.id(reactive[REACTIVE]); }, is( this: void, reactive: ReactiveProtocol, kind: T ): reactive is { [REACTIVE]: Extract; } { return ReactiveInternals.is(reactive[REACTIVE], kind); }, match(this: void, reactive: ReactiveProtocol, matcher: Matcher): T { return ReactiveInternals.match(reactive[REACTIVE], matcher); }, subscribesTo(this: void, reactive: ReactiveProtocol): ReactiveProtocol[] { const internals = reactive[REACTIVE]; if (internals.type === "delegate") { return internals.delegate.flatMap(ReactiveProtocol.subscribesTo); } else { return [reactive]; } }, dependencies( this: void, reactive: ReactiveProtocol ): Iterable { return ReactiveInternals.dependencies(reactive[REACTIVE]); }, *dependenciesInList( this: void, children: readonly ReactiveProtocol[] ): Iterable { for (const child of children) { yield* ReactiveInternals.dependencies(child[REACTIVE]); } }, lastUpdated(this: void, reactive: ReactiveProtocol): Timestamp { return ReactiveInternals.lastUpdated(reactive[REACTIVE]); }, lastUpdatedIn(this: void, reactives: ReactiveProtocol[]): Timestamp { let lastUpdatedTimestamp = zero(); for (const child of ReactiveProtocol.dependenciesInList(reactives)) { if (child.lastUpdated.gt(lastUpdatedTimestamp)) { lastUpdatedTimestamp = child.lastUpdated; } } return lastUpdatedTimestamp; }, log( this: void, reactive: ReactiveProtocol, options: { implementation?: boolean; source?: boolean; id?: boolean } = {} ): void { ReactiveInternals.log(reactive[REACTIVE], options); }, debug( this: void, reactive: ReactiveProtocol, { implementation = false, source = false, }: { implementation?: boolean; source?: boolean } = {} ): string { return ReactiveInternals.debug(reactive[REACTIVE], { implementation, source, }); }, } as const; export const ReactiveInternals = { is( this: void, internals: interfaces.ReactiveInternals, kind: T ): internals is Extract { return internals.type === kind; }, id( this: void, internals: interfaces.ReactiveInternals ): interfaces.ReactiveId { return internals.description.id; }, *dependencies( internals: interfaces.ReactiveInternals ): Iterable { switch (internals.type) { case "static": return; case "mutable": if (internals.isFrozen?.()) { break; } yield internals; break; case "delegate": for (const target of ReactiveInternals.subscribesTo(internals)) { yield* ReactiveInternals.dependencies(target); } break; case "composite": yield* ReactiveProtocol.dependenciesInList(internals.children()); break; } }, *dependenciesInList( this: void, children: readonly ReactiveInternals[] ): Iterable { for (const child of children) { yield* ReactiveInternals.dependencies(child); } }, subscribesTo( this: void, internals: interfaces.ReactiveInternals ): interfaces.ReactiveInternals[] { if (internals.type === "delegate") { return internals.delegate.flatMap((protocol) => ReactiveProtocol.subscribesTo(protocol).map((p) => p[REACTIVE]) ); } else { return [internals]; } }, lastUpdated(this: void, internals: ReactiveInternals): Timestamp { switch (internals.type) { case "static": return zero(); case "mutable": return internals.lastUpdated; case "delegate": { const delegates = ReactiveInternals.subscribesTo(internals); return ReactiveInternals.lastUpdatedIn(delegates); } case "composite": { let lastUpdatedTimestamp = zero(); for (const child of ReactiveInternals.dependencies(internals)) { if (child.lastUpdated.gt(lastUpdatedTimestamp)) { lastUpdatedTimestamp = child.lastUpdated; } } return lastUpdatedTimestamp; } } }, lastUpdatedIn(this: void, internals: ReactiveInternals[]): Timestamp { let lastUpdatedTimestamp = zero(); for (const child of ReactiveInternals.dependenciesInList(internals)) { if (child.lastUpdated.gt(lastUpdatedTimestamp)) { lastUpdatedTimestamp = child.lastUpdated; } } return lastUpdatedTimestamp; }, description(this: void, internals: ReactiveInternals): Debug.Description { return internals.description; }, debug( this: void, internals: interfaces.ReactiveInternals, { implementation = false, source = false, id = false, }: { implementation?: boolean; source?: boolean; id?: boolean } = {} ): string { const dependencies = [...ReactiveInternals.dependencies(internals)]; const descriptions = new Set( dependencies.map((dependency) => { return implementation ? dependency.description : dependency.description.userFacing; }) ); const nodes = [...descriptions] .map((d) => { const description = implementation ? d : d.userFacing; return description.describe({ source, id }); }) .filter(isPresent); return Tree(...nodes).format(); }, log( this: void, internals: interfaces.ReactiveInternals, options: { implementation?: boolean; source?: boolean; id?: boolean } = {} ): void { const debug = ReactiveInternals.debug(internals, options); console.group( ReactiveInternals.description(internals).describe({ id: options.id }), `(updated at ${ Timestamp.debug(ReactiveInternals.lastUpdated(internals)).at })` ); console.log(debug); console.groupEnd(); }, match(this: void, internals: ReactiveInternals, matcher: Matcher): T { const fn = matcher[internals.type]; if (typeof fn === "function") { return fn(internals as never); } return (matcher as DefaultMatcher).default(internals); }, }; export type ReactiveInternals = interfaces.ReactiveInternals; export type Reactive< T, I extends ReactiveInternals = ReactiveInternals > = interfaces.Reactive; export const Reactive = { is(this: void, value: unknown): value is interfaces.Reactive { return !!( value && (typeof value === "object" || typeof value === "function") && REACTIVE in value ); }, from( this: void, value: T | Reactive, description?: string | Debug.Description ): Reactive { if (Reactive.is(value)) { return value; } else { return new Static(value, Desc("static", description)); } }, }; export type ReactiveCore< T, I extends ReactiveInternals = ReactiveInternals > = interfaces.ReactiveCore; export const ReactiveCore = { is(value: unknown): value is interfaces.ReactiveCore { return !!( value && (typeof value === "object" || typeof value === "function") && REACTIVE in value && hasRead(value) ); }, }; function hasRead(value: object): value is { read: () => T } { return "read" in value && typeof value.read === "function"; } class Static { readonly #value: T; readonly [REACTIVE]: interfaces.StaticInternals; constructor(value: T, description: Debug.Description) { this.#value = value; this[REACTIVE] = { type: "static", description, }; } get current(): T { return this.#value; } read(): T { return this.#value; } }