import { CollectionSizeObserver } from './collection-length-observer'; import { atObserver, createIndexMap, type AccessorType, type ICollectionObserver, type ICollectionSubscriberCollection } from './interfaces'; import { addCollectionBatch, batching } from './subscriber-batch'; import { subscriberCollection } from './subscriber-collection'; import { rtDefineHiddenProp } from './utilities'; export interface SetObserver extends ICollectionObserver<'set'>, ICollectionSubscriberCollection { } export const getSetObserver = /*@__PURE__*/ (() => { const observerLookup = new WeakMap, SetObserverImpl>(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const { add: $add, clear: $clear, delete: $delete } = Set.prototype as { [K in keyof Set]: Set[K] & { observing?: boolean } }; const methods: ['add', 'clear', 'delete'] = ['add', 'clear', 'delete']; // note: we can't really do much with Set due to the internal data structure not being accessible so we're just using the native calls // fortunately, add/delete/clear are easy to reconstruct for the indexMap const observe = { // https://tc39.github.io/ecma262/#sec-set.prototype.add add: function (this: Set, value: unknown): ReturnType { const o = observerLookup.get(this); if (o === undefined) { $add.call(this, value); return this; } const oldSize = this.size; $add.call(this, value); const newSize = this.size; if (newSize === oldSize) { return this; } o.indexMap[oldSize] = -2; o.notify(); return this; }, // https://tc39.github.io/ecma262/#sec-set.prototype.clear clear: function (this: Set): ReturnType { const o = observerLookup.get(this); if (o === undefined) { return $clear.call(this); } const size = this.size; if (size > 0) { const indexMap = o.indexMap; let i = 0; // deepscan-disable-next-line for (const key of this.keys()) { if (indexMap[i] > -1) { indexMap.deletedIndices.push(indexMap[i]); indexMap.deletedItems.push(key); } i++; } $clear.call(this); indexMap.length = 0; o.notify(); } return undefined; }, // https://tc39.github.io/ecma262/#sec-set.prototype.delete delete: function (this: Set, value: unknown): ReturnType { const o = observerLookup.get(this); if (o === undefined) { return $delete.call(this, value); } const size = this.size; if (size === 0) { return false; } let i = 0; const indexMap = o.indexMap; for (const entry of this.keys()) { if (entry === value) { if (indexMap[i] > -1) { indexMap.deletedIndices.push(indexMap[i]); indexMap.deletedItems.push(entry); } indexMap.splice(i, 1); const deleteResult = $delete.call(this, value); if (deleteResult === true) { o.notify(); } return deleteResult; } i++; } return false; } }; function enableSetObservation(set: Set): void { for (const method of methods) { rtDefineHiddenProp(set, method, observe[method]); } } interface SetObserverImpl extends SetObserver {} class SetObserverImpl { public type: AccessorType = atObserver; private lenObs?: CollectionSizeObserver; public constructor(observedSet: Set) { this.collection = observedSet; this.indexMap = createIndexMap(observedSet.size); this.lenObs = void 0; } public notify(): void { const subs = this.subs; subs.notifyDirty(); const indexMap = this.indexMap; if (batching) { addCollectionBatch(subs, this.collection, indexMap); return; } const set = this.collection; const size = set.size; this.indexMap = createIndexMap(size); subs.notifyCollection(set, indexMap); } public getLengthObserver(): CollectionSizeObserver { return this.lenObs ??= new CollectionSizeObserver(this); } } subscriberCollection(SetObserverImpl, null!); return function getSetObserver(set: Set): SetObserver { let observer = observerLookup.get(set); if (observer === void 0) { observerLookup.set(set, observer = new SetObserverImpl(set)); enableSetObservation(set); } return observer; }; })();