/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { ImplementationError } from "#MatterError.js"; import { Observable, ObservableValue } from "./Observable.js"; /** * A read-only set. */ export interface ImmutableSet { [Symbol.iterator]: () => Iterator; has(item: T): boolean; get size(): number; map(mapper: (item: T) => R): R[]; find(predicate: (item: T) => boolean | undefined): T | undefined; filter(predicate: (item: T) => boolean | undefined): T[]; } /** * A write-only set. */ export interface MutableSet { add(definition: AddT): void; delete(definition: T): boolean; clear(): void; } /** * Set change events. */ export interface ObservableSet { get added(): Observable<[T]>; get deleted(): Observable<[T]>; get empty(): ObservableValue; } /** * An interface for index set lookup. * * Note that this interface only supports a single item for each key. If multiple items associate with a key only the * first is returned. */ export interface IndexedSet { /** * Retrieve an item with the named {@link field} set to {@link value}. */ get(field: F, value: T[F]): T | undefined; /** * Obtain a {@link Map} of values in {@link field} to the associated item in the set. */ mapOf(field: F): Map; } /** * A generic set implementation supporting all interfaces in this module. * * Unused features have minimal performance impact. */ export class BasicSet implements ImmutableSet, MutableSet, ObservableSet, IndexedSet { #entries?: Set; #added?: Observable<[T]>; #deleted?: Observable<[T]>; #empty?: ObservableValue; #indices?: { [field in keyof T]?: Map; }; #maps?: { [field in keyof T]?: Map; }; constructor(...initialItems: AddT[]) { for (const item of initialItems) { this.add(item); } } [Symbol.iterator]() { return this.#definedEntries[Symbol.iterator](); } get size() { return this.#entries?.size ?? 0; } map(mapper: (item: T) => R) { return [...this].map(mapper); } find(predicate: (item: T) => boolean | undefined) { for (const item of this) { if (predicate(item)) { return item; } } } filter(predicate: (item: T) => boolean | undefined) { const result = new Array(); for (const item of this) { if (predicate(item)) { result.push(item); } } return result; } has(item: T) { return this.#entries?.has(item) ?? false; } add(item: AddT) { const created = this.create(item); if (this.#definedEntries.has(item as any)) { return; } this.#definedEntries.add(item as any); if (this.#indices) { for (const field in this.#indices) { const value = created[field]; if (value === undefined) { continue; } const index = this.#indices[field]; if (index === undefined || index.has(value)) { continue; } index.set(value, created); } } this.#added?.emit(created); if (this.#empty && this.#empty.value) { this.#empty.emit(false); } } get(field: F, value: T[F]) { return this.#indexOf(field).get(value); } get #definedEntries() { if (this.#entries === undefined) { this.#entries = new Set(); } return this.#entries; } #indexOf(field: F) { if (!this.#indices) { this.#indices = {}; } let index = this.#indices[field]; if (index === undefined) { index = new Map(); for (const item of this) { const value = item[field]; if (value === undefined || index.has(value)) { continue; } index.set(value, item); } this.#indices[field] = index; } return index!; // Need "!" due to (apparent) TS bug } /** * Obtain key/value map using specific field as key. * * Note we use {@link This} to constrain usage to sets where {@link T} === {@link AddT} as required by {@link Map}. */ mapOf, F extends keyof T>(this: This, field: F): Map { if (!this.#maps) { this.#maps = {}; } let map = this.#maps[field]; if (map === undefined) { map = new MapOfIndexedSet(this, field, this.#indexOf(field)); this.#maps[field] = map; } return map; } delete(item: T): boolean; delete(field: keyof T, value: T[F]): boolean; delete(itemOrField: T | F, value?: T[F]): boolean { let item: T | undefined; if (value === undefined) { item = itemOrField as T; } else { item = this.get(itemOrField as F, value); if (item === undefined) { return false; // Item not found } } if (!this.#entries?.delete(item)) { return false; } if (this.#indices) { for (const field in this.#indices) { const value = item[field]; if (value === undefined) { continue; } const index = this.#indices[field]; if (index !== undefined && index.get(value) === item) { index.delete(value); } } } this.#deleted?.emit(item); if (this.#empty && !this.size) { this.#empty.emit(true); } return true; } clear() { for (const entry of [...this]) { this.delete(entry); } } get added() { if (this.#added === undefined) { this.#added = Observable(); } return this.#added; } get deleted() { if (this.#deleted === undefined) { this.#deleted = Observable(); } return this.#deleted; } get empty() { if (this.#empty === undefined) { this.#empty = ObservableValue(!this.#entries?.size); } return this.#empty; } protected create(definition: AddT) { return definition as unknown as T; } } /** * A {@link Map} backed by an {@link IndexedSet}. * * This supports the common case where sets must be looked up by key. Implementations like {@link BasicSet} offer * efficient lookup by key using {@link IndexedSet#get}, but usage like a {@link Map} is still cumbersome. This class * works as an adapter to make key/value access patterns more natural. */ export class MapOfIndexedSet< T, S extends ImmutableSet & MutableSet & IndexedSet, K extends keyof T, > implements Map { #set: S; #key: K; // This is an optimization for lookup #index?: Map; /** * Create a new map. * * @param set the backing data * @param key a property of {@link T} used as the key * @param index optional index that optimizes lookup by bypassing {@link IndexedSet#get} */ constructor(set: S, key: K, index?: Map) { this.#set = set; this.#key = key; this.#index = index; } clear(): void { this.#set.clear(); } delete(key: T[K]): boolean { const item = this.get(key); if (item) { return this.#set.delete(item); } return false; } forEach(callbackfn: (value: T, key: T[K], map: Map) => void, thisArg?: any): void { if (thisArg) { callbackfn = callbackfn.bind(thisArg); } for (const [k, v] of this) { callbackfn(v, k, this); } } get(key: T[K]): T | undefined { if (this.#index) { return this.#index.get(key); } return this.#set.get(this.#key, key); } has(key: T[K]): boolean { return this.#index ? this.#index.has(key) : this.#set.get(this.#key, key) !== undefined; } set(key: T[K], value: T): this { if (value[this.#key] !== key) { throw new MapOfIndexedSet.KeyValueMismatchError( `Cannot set key "${key}" because value property ${String(this.#key)} is "${value[this.#key]}"`, ); } if (this.has(key)) { if (this.get(key) === value) { return this; } this.#set.delete((this.#index ? this.#index.get(key) : this.#set.get(this.#key, key)) as T); } this.#set.add(value); return this; } get size() { return this.#set.size; } entries(): MapIterator<[T[K], T]> { return this[Symbol.iterator](); } keys(): MapIterator { if (this.#index) { return this.#index.keys(); } const keys = [...this.#set].map(item => item[this.#key]).filter(key => key !== undefined); return keys[Symbol.iterator](); } values(): MapIterator { if (this.#index) { return this.#index.values(); } const values = [...this.#set].map(item => item); return values[Symbol.iterator](); } *[Symbol.iterator](): MapIterator<[T[K], T]> { for (const item of this.#set) { const k = item[this.#key]; if (k === undefined) { continue; } yield [k, item]; } } [Symbol.toStringTag] = "Map"; } export namespace MapOfIndexedSet { export class KeyValueMismatchError extends ImplementationError {} }