import { isArray, isMap, isSet, type Constructable } from '@aurelia/kernel'; import { rtDef, rtDefineHiddenProp, ensureProto } from './utilities'; import { getArrayObserver } from './array-observer'; import { getSetObserver } from './set-observer'; import { getMapObserver } from './map-observer'; import type { IConnectable, ISubscribable, ISubscriber, Collection, CollectionObserver, ICollectionSubscriber, ICollectionSubscribable, } from './interfaces'; import type { IObserverLocator } from './observer-locator'; import { ErrorNames, createMappedError } from './errors'; export interface IObserverLocatorBasedConnectable extends IConnectable, ISubscriber, ICollectionSubscriber { oL: IObserverLocator; /** * A record storing observers that are currently subscribed to by this binding */ obs: IObserverRecord; } export interface IObserverRecord { version: number; count: number; add(observer: ISubscribable | ICollectionSubscribable): void; clear(): void; clearAll(): void; } export type DecoratedConnectable> = Constructable; const connectableDecorator = /*@__PURE__*/ (() => { class BindingObserverRecord implements IObserverRecord { public version: number = 0; public count: number = 0; // a map of the observers (subscribables) that the owning binding of this record // is currently subscribing to. The values are the version of the observers, // as the observers version may need to be changed during different evaluation /** @internal */ public o = new Map(); /** @internal */ public readonly b: IObserverLocatorBasedConnectable; public constructor(b: IObserverLocatorBasedConnectable) { this.b = b; } /** * Add, and subscribe to a given observer */ public add(observer: ISubscribable | ICollectionSubscribable): void { if (!this.o.has(observer)) { observer.subscribe(this.b); ++this.count; } this.o.set(observer, this.version); } /** * Unsubscribe the observers that are not up to date with the record version */ public clear(): void { this.o.forEach(unsubscribeStale, this); this.count = this.o.size; } public clearAll() { this.o.forEach(unsubscribeAll, this); this.o.clear(); this.count = 0; } } function unsubscribeAll(this: BindingObserverRecord, version: number, subscribable: ISubscribable | ICollectionSubscribable) { subscribable.unsubscribe(this.b); } function unsubscribeStale(this: BindingObserverRecord, version: number, subscribable: ISubscribable | ICollectionSubscribable) { if (this.version !== version) { subscribable.unsubscribe(this.b); this.o.delete(subscribable); } } function getObserverRecord(this: IObserverLocatorBasedConnectable): BindingObserverRecord { return rtDefineHiddenProp(this, 'obs', new BindingObserverRecord(this)); } function observe(this: IObserverLocatorBasedConnectable, obj: object, key: PropertyKey): void { this.obs.add(this.oL.getObserver(obj, key)); } function observeExpression(this: IObserverLocatorBasedConnectable, obj: object, expression: string): void { this.obs.add(this.oL.getExpressionObserver(obj, expression)); } function observeCollection(this: IObserverLocatorBasedConnectable, collection: Collection): void { let observer: CollectionObserver; if (isArray(collection)) { observer = getArrayObserver(collection); } else if (isSet(collection)) { observer = getSetObserver(collection); } else if (isMap(collection)) { observer = getMapObserver(collection); } else { throw createMappedError(ErrorNames.non_recognisable_collection_type, collection); } this.obs.add(observer); } function subscribeTo(this: IObserverLocatorBasedConnectable, subscribable: ISubscribable | ICollectionSubscribable): void { this.obs.add(subscribable); } function noopHandleChange() { throw createMappedError(ErrorNames.method_not_implemented, 'handleChange'); } function noopHandleCollectionChange() { throw createMappedError(ErrorNames.method_not_implemented, 'handleCollectionChange'); } return function connectableDecorator, C extends Constructable>(target: C, context: ClassDecoratorContext): DecoratedConnectable { const proto = target.prototype; ensureProto(proto, 'observe', observe); ensureProto(proto, 'observeExpression', observeExpression); ensureProto(proto, 'observeCollection', observeCollection); ensureProto(proto, 'subscribeTo', subscribeTo); rtDef(proto, 'obs', { get: getObserverRecord }); // optionally add these two methods to normalize a connectable impl // though don't override if it already exists ensureProto(proto, 'handleChange', noopHandleChange); ensureProto(proto, 'handleCollectionChange', noopHandleCollectionChange); return target as unknown as DecoratedConnectable; }; })(); export function connectable(): typeof connectableDecorator; export function connectable, C extends Constructable>(target: C, context: ClassDecoratorContext): DecoratedConnectable; export function connectable, C extends Constructable>(target?: C, context?: ClassDecoratorContext): DecoratedConnectable | typeof connectableDecorator { return target == null ? connectableDecorator : connectableDecorator(target, context!); }