import { type Subscribable, type Connectable, kebabify } from "astal/binding"; export class Derived< ResultType, ObjectType extends Connectable, PropKeys extends keyof ObjectType & string, > implements Subscribable { private upstreamSubscriptions: number[]; private downstreamSubscriptions: ((value: ResultType) => void)[] = []; private properties: PropKeys[]; // @ts-expect-error - value _is_ definitively defined in the constructor, thorugh the call to refresh() private value: ResultType; constructor( private object: ObjectType, private transform: (...args: any) => ResultType, ...properties: PropKeys[] ) { this.upstreamSubscriptions = properties.map((prop) => object.connect(kebabify(prop), () => this.refresh()), ); this.properties = properties; this.refresh(true); } private refresh(initial = false) { this.value = this.transform( this.properties.map((prop) => this.object[prop]), ); if (!initial) { this.downstreamSubscriptions.forEach((callback) => callback(this.value)); } } subscribe(callback: (value: ResultType) => void): () => void { this.downstreamSubscriptions.push(callback); return () => { const index = this.downstreamSubscriptions.findIndex( (func) => func === callback, ); this.downstreamSubscriptions.splice(index, 1); }; } get(): ResultType { return this.value; } drop(): void { this.upstreamSubscriptions.forEach((sub) => this.object.disconnect(sub)); } } export function nestedBind< ObjectType extends Connectable | undefined, KeyType extends keyof ObjectType & string, >( bound: Subscribable, key: KeyType, ): Subscribable[KeyType] | undefined> { let subscription: number | undefined; let lastObject: ObjectType = bound.get(); let value: ObjectType[KeyType] | undefined = bound.get()?.[key]; const subscriptions: ((value: ObjectType[KeyType]) => void)[] = []; const unsubscribeUpstream = bound.subscribe((obj) => { if (subscription !== undefined) { lastObject?.disconnect(subscription); } subscription = obj?.connect(`notify:${key}`, () => { subscriptions.forEach((sub) => sub(obj![key])); }); lastObject = obj; }); }