import { DI, IIndexable, isArray } from '@aurelia/kernel'; import { rtObjectFreeze } from './utilities'; import type { CollectionLengthObserver, CollectionSizeObserver } from './collection-length-observer'; export const ICoercionConfiguration = /*@__PURE__*/DI.createInterface('ICoercionConfiguration'); export interface ICoercionConfiguration { /** When set to `true`, enables the automatic type-coercion for bindables globally. */ enableCoercion: boolean; /** When set to `true`, coerces the `null` and `undefined` values to the target types. This is ineffective when `disableCoercion` is set to `true.` */ coerceNullish: boolean; } export type InterceptorFunc = (value: TInput, coercionConfig?: ICoercionConfiguration) => TOutput; export interface IConnectable { observe(obj: object, key: PropertyKey): void; observeExpression(obj: object, expression: string): void; observeCollection(obj: Collection): void; subscribeTo(subscribable: ISubscribable | ICollectionSubscribable): void; } export interface IDirtySubscriber { handleDirty(): void; } /** * Interface of a subscriber or property change handler */ export interface ISubscriber extends Partial { handleChange(newValue: TValue, previousValue: TValue): void; } /** * Interface of a collection subscriber or mutation handler */ export interface ICollectionSubscriber { handleCollectionChange(collection: Collection, indexMap: IndexMap): void; } export interface ISubscribable { subscribe(subscriber: T): void; unsubscribe(subscriber: T): void; } export interface ICollectionSubscribable { subscribe(subscriber: ICollectionSubscriber): void; unsubscribe(subscriber: ICollectionSubscriber): void; } /** * An interface describing the contract of a subscriber list, * with the ability to propagate values to those subscribers */ export interface ISubscriberRecord { readonly count: number; add(subscriber: T): boolean; remove(subscriber: T): boolean; notify(value: unknown, oldValue: unknown): void; notifyCollection(collection: Collection, indexMap: IndexMap): void; notifyDirty(): void; } /** * An internal interface describing the implementation of a ISubscribable of Aurelia that supports batching * * This is usually mixed into a class via the import `subscriberCollection` import from Aurelia. * The `subscriberCollection` import can be used as either a decorator, or a function call. */ export interface ISubscriberCollection extends ISubscribable { /** * The backing subscriber record for all subscriber methods of this collection */ readonly subs: ISubscriberRecord; } /** * An internal interface describing the implementation of a ICollectionSubscribable of Aurelia that supports batching * * This is usually mixed into a class via the import `subscriberCollection` import from Aurelia. * The `subscriberCollection` import can be used as either a decorator, or a function call. */ export interface ICollectionSubscriberCollection extends ICollectionSubscribable { /** * The backing subscriber record for all subscriber methods of this collection */ readonly subs: ISubscriberRecord; } /** * A collection (array, set or map) */ export type Collection = unknown[] | Set | Map; export type CollectionKind = 'indexed' | 'keyed' | 'array' | 'map' | 'set'; export type LengthPropertyName = T extends unknown[] ? 'length' : T extends Set ? 'size' : T extends Map ? 'size' : never; export type CollectionKindToType = T extends 'array' ? unknown[] : T extends 'indexed' ? unknown[] : T extends 'map' ? Map : T extends 'set' ? Set : T extends 'keyed' ? Set | Map : never; export type ObservedCollectionKindToType = T extends 'array' ? unknown[] : T extends 'indexed' ? unknown[] : T extends 'map' ? Map : T extends 'set' ? Set : T extends 'keyed' ? Map | Set : never; /** @internal */ export const atNone = 0b0_000_000; /** @internal */ export const atObserver = 0b0_000_001; /** @internal */ export const atNode = 0b0_000_010; /** @internal */ export const atLayout = 0b0_000_100; export const AccessorType = /*@__PURE__*/rtObjectFreeze({ None : atNone, Observer : atObserver, Node : atNode, // misc characteristic of accessors/observers when update // // by default, everything is synchronous // except changes that are supposed to cause reflow/heavy computation // an observer can use this flag to signal binding that don't carelessly tell it to update // queue it instead // todo: https://gist.github.com/paulirish/5d52fb081b3570c81e3a // todo: https://csstriggers.com/ Layout : atLayout, } as const); export type AccessorType = typeof AccessorType[keyof typeof AccessorType]; /** * Basic interface to normalize getting/setting a value of any property on any object */ export interface IAccessor { type: AccessorType; getValue(obj?: object, key?: PropertyKey): TValue; setValue(newValue: TValue, obj?: object, key?: PropertyKey): void; } /** * An interface describing a standard contract of an observer in Aurelia binding & observation system */ export interface IObserver extends IAccessor, ISubscribable { doNotCache?: boolean; useCallback?(callback: (newValue: TValue, oldValue: TValue) => void): boolean; useCoercer?(coercer: InterceptorFunc, coercionConfig?: ICoercionConfiguration): boolean; useFlush?(flush: 'sync' | 'async'): boolean; } export type AccessorOrObserver = (IAccessor | IObserver) & { doNotCache?: boolean; }; /** * An array of indices, where the index of an element represents the index to map FROM, and the numeric value of the element itself represents the index to map TO * * The deletedIndices property contains the items (in case of an array) or keys (in case of map or set) that have been deleted. */ export type IndexMap = number[] & { deletedIndices: number[]; deletedItems: T[]; isIndexMap: true; }; export const hasChanges = (indexMap: IndexMap) => { if (indexMap.deletedIndices.length > 0) { return true; } for (let i = 0; i < indexMap.length; ++i) { if (indexMap[i] !== i) { return true; } } return false; }; export function copyIndexMap( existing: number[] & { deletedIndices?: number[]; deletedItems?: T[] }, deletedIndices?: number[], deletedItems?: T[], ): IndexMap { const { length } = existing; const arr = Array(length) as IndexMap; let i = 0; while (i < length) { arr[i] = existing[i]; ++i; } if (deletedIndices !== void 0) { arr.deletedIndices = deletedIndices.slice(0); } else if (existing.deletedIndices !== void 0) { arr.deletedIndices = existing.deletedIndices.slice(0); } else { arr.deletedIndices = []; } if (deletedItems !== void 0) { arr.deletedItems = deletedItems.slice(0); } else if (existing.deletedItems !== void 0) { arr.deletedItems = existing.deletedItems.slice(0); } else { arr.deletedItems = []; } arr.isIndexMap = true; return arr; } export function createIndexMap(length: number = 0): IndexMap { const arr = Array(length) as IndexMap; let i = 0; while (i < length) { arr[i] = i++; } arr.deletedIndices = []; arr.deletedItems = []; arr.isIndexMap = true; return arr; } export function cloneIndexMap(indexMap: IndexMap): IndexMap { const clone = indexMap.slice() as IndexMap; clone.deletedIndices = indexMap.deletedIndices.slice(); clone.deletedItems = indexMap.deletedItems.slice(); clone.isIndexMap = true; return clone; } export function isIndexMap(value: unknown): value is IndexMap { return isArray(value) && (value as IndexMap).isIndexMap === true; } /** * Describes a type that specifically tracks changes in a collection (map, set or array) */ export interface ICollectionChangeTracker { collection: T; indexMap: IndexMap; } /** * An observer that tracks collection mutations and notifies subscribers (either directly or in batches) */ export interface ICollectionObserver extends ICollectionChangeTracker>, ICollectionSubscribable { type: AccessorType; collection: ObservedCollectionKindToType; getLengthObserver(): T extends 'array' ? CollectionLengthObserver : CollectionSizeObserver; notify(): void; } export type CollectionObserver = ICollectionObserver; export type IObservable = T & { $observers?: IIndexable<{}, AccessorOrObserver>; };