import { $mobx, createAtom, deepEnhancer, getNextId, IEnhancer, isSpyEnabled, hasListeners, IListenable, spyReportStart, notifyListeners, spyReportEnd, createInstanceofPredicate, makeIterable, hasInterceptors, interceptChange, IInterceptable, checkIfStateModificationsAreAllowed, untracked, transaction, isES6Set, IAtom, DELETE, ADD, die, initObservable } from "../internal" const ObservableSetMarker = {} export type IObservableSetInitialValues = Set | readonly T[] export type ISetDidChange = | { object: ObservableSet observableKind: "set" debugObjectName: string type: "add" newValue: T } | { object: ObservableSet observableKind: "set" debugObjectName: string type: "delete" oldValue: T } export type ISetWillDeleteChange = { type: "delete" object: ObservableSet oldValue: T } export type ISetWillAddChange = { type: "add" object: ObservableSet newValue: T } export type ISetWillChange = ISetWillDeleteChange | ISetWillAddChange export class ObservableSet implements Set, IInterceptable, IListenable { [$mobx] = ObservableSetMarker private data_: Set = new Set() atom_!: IAtom changeListeners_ interceptors_ dehancer: any enhancer_: (newV: any, oldV: any | undefined) => any constructor( initialData?: IObservableSetInitialValues, enhancer: IEnhancer = deepEnhancer, public name_ = __DEV__ ? "ObservableSet@" + getNextId() : "ObservableSet" ) { this.enhancer_ = (newV, oldV) => enhancer(newV, oldV, name_) initObservable(() => { this.atom_ = createAtom(this.name_) if (initialData) { this.replace(initialData) } }) } private dehanceValue_(value: X): X { if (this.dehancer !== undefined) { return this.dehancer(value) } return value } clear() { transaction(() => { untracked(() => { for (const value of this.data_.values()) { this.delete(value) } }) }) } forEach(callbackFn: (value: T, value2: T, set: Set) => void, thisArg?: any) { for (const value of this) { callbackFn.call(thisArg, value, value, this) } } get size() { this.atom_.reportObserved() return this.data_.size } add(value: T) { checkIfStateModificationsAreAllowed(this.atom_) if (hasInterceptors(this)) { const change = interceptChange>(this, { type: ADD, object: this, newValue: value }) if (!change) { return this } // implemented reassignment same as it's done for ObservableMap value = change.newValue! } if (!this.has(value)) { transaction(() => { value = this.enhancer_(value, undefined) // report the stored value, like map and array this.data_.add(value) this.atom_.reportChanged() }) const notifySpy = __DEV__ && isSpyEnabled() const notify = hasListeners(this) const change = notify || notifySpy ? >{ observableKind: "set", debugObjectName: this.name_, type: ADD, object: this, newValue: value } : null if (notifySpy && __DEV__) { spyReportStart(change!) } if (notify) { notifyListeners(this, change) } if (notifySpy && __DEV__) { spyReportEnd() } } return this } delete(value: T) { checkIfStateModificationsAreAllowed(this.atom_) if (hasInterceptors(this)) { const change = interceptChange>(this, { type: DELETE, object: this, oldValue: value }) if (!change) { return false } } if (this.has(value)) { const notifySpy = __DEV__ && isSpyEnabled() const notify = hasListeners(this) const change = notify || notifySpy ? >{ observableKind: "set", debugObjectName: this.name_, type: DELETE, object: this, oldValue: value } : null if (notifySpy && __DEV__) { spyReportStart(change!) } transaction(() => { this.atom_.reportChanged() this.data_.delete(value) }) if (notify) { notifyListeners(this, change) } if (notifySpy && __DEV__) { spyReportEnd() } return true } return false } has(value: T) { this.atom_.reportObserved() return this.data_.has(this.dehanceValue_(value)) } entries() { const values = this.values() return makeIterableForSet<[T, T]>({ next() { const { value, done } = values.next() return !done ? { value: [value, value], done } : { value: undefined, done } } }) } keys(): SetIterator { return this.values() } values(): SetIterator { this.atom_.reportObserved() const self = this const values = this.data_.values() return makeIterableForSet({ next() { const { value, done } = values.next() return !done ? { value: self.dehanceValue_(value), done } : { value: undefined, done } } }) } intersection(otherSet: ReadonlySetLike | Set): Set { return new Set(this).intersection(otherSet) } union(otherSet: ReadonlySetLike | Set): Set { return new Set(this).union(otherSet) } difference(otherSet: ReadonlySetLike): Set { return new Set(this).difference(otherSet) } symmetricDifference(otherSet: ReadonlySetLike | Set): Set { return new Set(this).symmetricDifference(otherSet) } isSubsetOf(otherSet: ReadonlySetLike): boolean { return new Set(this).isSubsetOf(otherSet) } isSupersetOf(otherSet: ReadonlySetLike): boolean { return new Set(this).isSupersetOf(otherSet) } isDisjointFrom(otherSet: ReadonlySetLike | Set): boolean { return new Set(this).isDisjointFrom(otherSet) } replace(other: ObservableSet | IObservableSetInitialValues): ObservableSet { if (isObservableSet(other)) { other = new Set(other) } if (Array.isArray(other) || isES6Set(other)) { // Only emit `delete`/`add` events (and `reportChanged`) for values that // actually change, instead of clearing and re-adding everything. `add` and // `delete` are already no-ops for values that are respectively already // present or already absent, so we just need to avoid deleting values that // are part of the replacement. See #3761. transaction(() => { // Collect the desired values for quick lookup. `other` is already a Set // here when it was passed (or snapshotted from an observable set) as one, // so reuse it rather than allocating another; arrays are wrapped (which // also dedupes them). const replacementValues: Set = isES6Set(other) ? other : new Set(other as Iterable) // Short-circuit the trivial cases: an empty replacement is just a clear, // and replacing into an empty set only needs the adds. if (replacementValues.size === 0) { this.clear() return } if (this.data_.size === 0) { replacementValues.forEach(value => this.add(value)) return } // Delete values that are not part of the replacement. for (const value of this.data_.values()) { if (!replacementValues.has(this.dehanceValue_(value))) { this.delete(value) } } // Add new values; values that are already present are a no-op. replacementValues.forEach(value => this.add(value)) }) } else if (other !== null && other !== undefined) { die(41, other) } return this } toJSON(): T[] { return Array.from(this) } toString(): string { return "[object ObservableSet]" } [Symbol.iterator]() { return this.values() } get [Symbol.toStringTag]() { return "Set" } } // eslint-disable-next-line export var isObservableSet = createInstanceofPredicate("ObservableSet", ObservableSet) as ( thing: any ) => thing is ObservableSet function makeIterableForSet(iterator: Iterator): SetIterator { iterator[Symbol.toStringTag] = "SetIterator" return makeIterable(iterator) }