diff --git a/src/api/become-observed.ts b/src/api/become-observed.ts index c7feca52a..968006fa4 100644 --- a/src/api/become-observed.ts +++ b/src/api/become-observed.ts @@ -5,11 +5,17 @@ import { Lambda, ObservableMap, fail, - getAtom + getAtom, + ObservableSet } from "../internal" export function onBecomeObserved( - value: IObservable | IComputedValue | IObservableArray | ObservableMap, + value: + | IObservable + | IComputedValue + | IObservableArray + | ObservableMap + | ObservableSet, listener: Lambda ): Lambda export function onBecomeObserved( @@ -22,7 +28,12 @@ export function onBecomeObserved(thing, arg2, arg3?): Lambda { } export function onBecomeUnobserved( - value: IObservable | IComputedValue | IObservableArray | ObservableMap, + value: + | IObservable + | IComputedValue + | IObservableArray + | ObservableMap + | ObservableSet, listener: Lambda ): Lambda export function onBecomeUnobserved( diff --git a/src/api/intercept-read.ts b/src/api/intercept-read.ts index d8be9d88f..72c33dee8 100644 --- a/src/api/intercept-read.ts +++ b/src/api/intercept-read.ts @@ -8,7 +8,8 @@ import { isObservableArray, isObservableMap, isObservableObject, - isObservableValue + isObservableValue, + ObservableSet } from "../internal" export type ReadInterceptor = (value: any) => T @@ -23,6 +24,10 @@ export function interceptReads( observableMap: ObservableMap, handler: ReadInterceptor ): Lambda +export function interceptReads( + observableSet: ObservableSet, + handler: ReadInterceptor +): Lambda export function interceptReads( object: Object, property: string, diff --git a/src/api/intercept.ts b/src/api/intercept.ts index b0835785a..8775b4584 100644 --- a/src/api/intercept.ts +++ b/src/api/intercept.ts @@ -9,7 +9,9 @@ import { IValueWillChange, Lambda, ObservableMap, - getAdministration + getAdministration, + ObservableSet, + ISetWillChange } from "../internal" export function intercept( @@ -24,6 +26,10 @@ export function intercept( observableMap: ObservableMap, handler: IInterceptor> ): Lambda +export function intercept( + observableMap: ObservableSet, + handler: IInterceptor> +): Lambda export function intercept( observableMap: ObservableMap, property: K, diff --git a/src/api/object-api.ts b/src/api/object-api.ts index 4c5f7e9f5..6f9c9e75d 100644 --- a/src/api/object-api.ts +++ b/src/api/object-api.ts @@ -3,6 +3,7 @@ import { IIsObservableObject, IObservableArray, ObservableMap, + ObservableSet, ObservableObjectAdministration, endBatch, fail, @@ -10,12 +11,14 @@ import { invariant, isObservableArray, isObservableMap, + isObservableSet, isObservableObject, startBatch } from "../internal" export function keys(map: ObservableMap): ReadonlyArray export function keys(ar: IObservableArray): ReadonlyArray +export function keys(set: ObservableSet): ReadonlyArray export function keys(obj: T): ReadonlyArray export function keys(obj: any): any { if (isObservableObject(obj)) { @@ -24,16 +27,20 @@ export function keys(obj: any): any { if (isObservableMap(obj)) { return Array.from(obj.keys()) } + if (isObservableSet(obj)) { + return Array.from(obj.keys()) + } if (isObservableArray(obj)) { return obj.map((_, index) => index) } return fail( process.env.NODE_ENV !== "production" && - "'keys()' can only be used on observable objects, arrays and maps" + "'keys()' can only be used on observable objects, arrays, sets and maps" ) } export function values(map: ObservableMap): ReadonlyArray +export function values(set: ObservableSet): ReadonlyArray export function values(ar: IObservableArray): ReadonlyArray export function values(obj: T): ReadonlyArray export function values(obj: any): string[] { @@ -43,16 +50,20 @@ export function values(obj: any): string[] { if (isObservableMap(obj)) { return keys(obj).map(key => obj.get(key)) } + if (isObservableSet(obj)) { + return Array.from(obj.values()) + } if (isObservableArray(obj)) { return obj.slice() } return fail( process.env.NODE_ENV !== "production" && - "'values()' can only be used on observable objects, arrays and maps" + "'values()' can only be used on observable objects, arrays, sets and maps" ) } export function entries(map: ObservableMap): ReadonlyArray<[K, T]> +export function entries(set: ObservableSet): ReadonlyArray<[T, T]> export function entries(ar: IObservableArray): ReadonlyArray<[number, T]> export function entries(obj: T): ReadonlyArray<[string, any]> export function entries(obj: any): any { @@ -62,6 +73,9 @@ export function entries(obj: any): any { if (isObservableMap(obj)) { return keys(obj).map(key => [key, obj.get(key)]) } + if (isObservableSet(obj)) { + return Array.from(obj.entries()) + } if (isObservableArray(obj)) { return obj.map((key, index) => [index, key]) } @@ -113,6 +127,7 @@ export function set(obj: any, key: any, value?: any): void { } export function remove(obj: ObservableMap, key: K) +export function remove(obj: ObservableSet, key: T) export function remove(obj: IObservableArray, index: number) export function remove(obj: T, key: string) export function remove(obj: any, key: any): void { @@ -120,6 +135,8 @@ export function remove(obj: any, key: any): void { ;((obj as any) as IIsObservableObject)[$mobx].remove(key) } else if (isObservableMap(obj)) { obj.delete(key) + } else if (isObservableSet(obj)) { + obj.delete(key) } else if (isObservableArray(obj)) { if (typeof key !== "number") key = parseInt(key, 10) invariant(key >= 0, `Not a valid index: '${key}'`) @@ -133,6 +150,7 @@ export function remove(obj: any, key: any): void { } export function has(obj: ObservableMap, key: K): boolean +export function has(obj: ObservableSet, key: T): boolean export function has(obj: IObservableArray, index: number): boolean export function has(obj: T, key: string): boolean export function has(obj: any, key: any): boolean { @@ -142,6 +160,8 @@ export function has(obj: any, key: any): boolean { return adm.has(key) } else if (isObservableMap(obj)) { return obj.has(key) + } else if (isObservableSet(obj)) { + return obj.has(key) } else if (isObservableArray(obj)) { return key >= 0 && key < obj.length } else { diff --git a/src/api/observable.ts b/src/api/observable.ts index b59ae12b0..d4fc1737a 100644 --- a/src/api/observable.ts +++ b/src/api/observable.ts @@ -3,9 +3,11 @@ import { IObservableArray, IObservableDecorator, IObservableMapInitialValues, + IObservableSetInitialValues, IObservableObject, IObservableValue, ObservableMap, + ObservableSet, ObservableValue, createDecoratorForEnhancer, createDynamicObservableObject, @@ -14,6 +16,7 @@ import { extendObservable, fail, isES6Map, + isES6Set, isObservable, isPlainObject, refStructEnhancer, @@ -88,7 +91,9 @@ function createObservable(v: any, arg2?: any, arg3?: any) { ? observable.array(v, arg2) : isES6Map(v) ? observable.map(v, arg2) - : v + : isES6Set(v) + ? observable.set(v, arg2) + : v // this value could be converted to a new observable data structure, return it if (res !== v) return res @@ -116,6 +121,10 @@ export interface IObservableFactory { export interface IObservableFactories { box(value?: T, options?: CreateObservableOptions): IObservableValue array(initialValues?: T[], options?: CreateObservableOptions): IObservableArray + set( + initialValues?: IObservableSetInitialValues, + options?: CreateObservableOptions + ): ObservableSet map( initialValues?: IObservableMapInitialValues, options?: CreateObservableOptions @@ -157,6 +166,14 @@ const observableFactories: IObservableFactories = { const o = asCreateObservableOptions(options) return new ObservableMap(initialValues, getEnhancerFromOptions(o), o.name) }, + set( + initialValues?: IObservableSetInitialValues, + options?: CreateObservableOptions + ): ObservableSet { + if (arguments.length > 2) incorrectlyUsedAsDecorator("set") + const o = asCreateObservableOptions(options) + return new ObservableSet(initialValues, getEnhancerFromOptions(o), o.name) + }, object( props: T, decorators?: { [K in keyof T]: Function }, diff --git a/src/api/observe.ts b/src/api/observe.ts index 4f71cee50..d9e919d4c 100644 --- a/src/api/observe.ts +++ b/src/api/observe.ts @@ -9,7 +9,9 @@ import { IValueDidChange, Lambda, ObservableMap, - getAdministration + getAdministration, + ObservableSet, + ISetDidChange } from "../internal" export function observe( @@ -22,6 +24,11 @@ export function observe( listener: (change: IArrayChange | IArraySplice) => void, fireImmediately?: boolean ): Lambda +export function observe( + observableMap: ObservableSet, + listener: (change: ISetDidChange) => void, + fireImmediately?: boolean +): Lambda export function observe( observableMap: ObservableMap, listener: (change: IMapDidChange) => void, diff --git a/src/api/tojs.ts b/src/api/tojs.ts index 8c13a38de..292c56a51 100644 --- a/src/api/tojs.ts +++ b/src/api/tojs.ts @@ -3,7 +3,8 @@ import { isObservable, isObservableArray, isObservableValue, - isObservableMap + isObservableMap, + isObservableSet } from "../internal" export type ToJSOptions = { @@ -27,7 +28,7 @@ function toJSHelper(source, options: ToJSOptions, __alreadySeen: Map) if (!options.recurseEverything && !isObservable(source)) return source if (typeof source !== "object") return source - + // Directly return null if source is null if (source === null) return null @@ -53,6 +54,22 @@ function toJSHelper(source, options: ToJSOptions, __alreadySeen: Map) return res } + if (isObservableSet(source) || Object.getPrototypeOf(source) === Set.prototype) { + if (options.exportMapsAsObjects === false) { + const res = cache(__alreadySeen, source, new Set(), options) + source.forEach(value => { + res.add(toJSHelper(value, options!, __alreadySeen)) + }) + return res + } else { + const res = cache(__alreadySeen, source, [] as any[], options) + source.forEach(value => { + res.push(toJSHelper(value, options!, __alreadySeen)) + }) + return res + } + } + if (isObservableMap(source) || Object.getPrototypeOf(source) === Map.prototype) { if (options.exportMapsAsObjects === false) { const res = cache(__alreadySeen, source, new Map(), options) diff --git a/src/internal.ts b/src/internal.ts index 1d9b12790..0002723fc 100644 --- a/src/internal.ts +++ b/src/internal.ts @@ -45,6 +45,7 @@ export * from "./types/intercept-utils" export * from "./types/listen-utils" export * from "./types/observablearray" export * from "./types/observablemap" +export * from "./types/observableset" export * from "./types/observableobject" export * from "./types/type-utils" export * from "./utils/eq" diff --git a/src/mobx.ts b/src/mobx.ts index 4688f5a6f..f679d7542 100644 --- a/src/mobx.ts +++ b/src/mobx.ts @@ -90,6 +90,11 @@ export { IMapDidChange, isObservableMap, IObservableMapInitialValues, + ObservableSet, + isObservableSet, + ISetDidChange, + ISetWillChange, + IObservableSetInitialValues, transaction, observable, IObservableFactory, diff --git a/src/types/modifiers.ts b/src/types/modifiers.ts index ab57650a7..b5575ea3b 100644 --- a/src/types/modifiers.ts +++ b/src/types/modifiers.ts @@ -2,9 +2,11 @@ import { deepEqual, fail, isES6Map, + isES6Set, isObservable, isObservableArray, isObservableMap, + isObservableSet, isObservableObject, isPlainObject, observable @@ -22,20 +24,23 @@ export function deepEnhancer(v, _, name) { if (Array.isArray(v)) return observable.array(v, { name }) if (isPlainObject(v)) return observable.object(v, undefined, { name }) if (isES6Map(v)) return observable.map(v, { name }) + if (isES6Set(v)) return observable.set(v, { name }) return v } export function shallowEnhancer(v, _, name): any { if (v === undefined || v === null) return v - if (isObservableObject(v) || isObservableArray(v) || isObservableMap(v)) return v + if (isObservableObject(v) || isObservableArray(v) || isObservableMap(v) || isObservableSet(v)) + return v if (Array.isArray(v)) return observable.array(v, { name, deep: false }) if (isPlainObject(v)) return observable.object(v, undefined, { name, deep: false }) if (isES6Map(v)) return observable.map(v, { name, deep: false }) + if (isES6Set(v)) return observable.set(v, { name, deep: false }) return fail( process.env.NODE_ENV !== "production" && - "The shallow modifier / decorator can only used in combination with arrays, objects and maps" + "The shallow modifier / decorator can only used in combination with arrays, objects, maps and sets" ) } diff --git a/src/types/observableset.ts b/src/types/observableset.ts new file mode 100644 index 000000000..959a796f7 --- /dev/null +++ b/src/types/observableset.ts @@ -0,0 +1,269 @@ +import { + $mobx, + createAtom, + deepEnhancer, + getNextId, + IEnhancer, + isSpyEnabled, + hasListeners, + IListenable, + invariant, + registerListener, + Lambda, + fail, + spyReportStart, + notifyListeners, + spyReportEnd, + createInstanceofPredicate, + hasInterceptors, + interceptChange, + IInterceptable, + IInterceptor, + registerInterceptor, + checkIfStateModificationsAreAllowed, + untracked, + makeIterable, + transaction, + isES6Set +} from "../internal" + +const ObservableSetMarker = {} + +export type IObservableSetInitialValues = Set | T[] + +export type ISetDidChange = + | { + object: ObservableSet + type: "add" + newValue: T + } + | { + object: ObservableSet + type: "delete" + oldValue: T + } + +export type ISetWillChange = + | { + type: "delete" + object: ObservableSet + oldValue: T + } + | { + type: "add" + object: ObservableSet + newValue: T + } + +export class ObservableSet implements Set, IInterceptable, IListenable { + [$mobx] = ObservableSetMarker + private _data: Set = new Set() + private _atom = createAtom(this.name) + changeListeners + interceptors + dehancer: any + enhancer: (newV: any, oldV: any | undefined) => any + + constructor( + initialData?: IObservableSetInitialValues, + enhancer: IEnhancer = deepEnhancer, + public name = "ObservableSet@" + getNextId() + ) { + if (typeof Set !== "function") { + throw new Error( + "mobx.set requires Set polyfill for the current browser. Check babel-polyfill or core-js/es6/set.js" + ) + } + + this.enhancer = (newV, oldV) => enhancer(newV, oldV, 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 + // TODO: ideally, value = change.value would be done here, so that values can be + // changed by interceptor. Same applies for other Set and Map api's. + } + if (!this.has(value)) { + transaction(() => { + this._data.add(this.enhancer(value, undefined)) + this._atom.reportChanged() + }) + const notifySpy = isSpyEnabled() + const notify = hasListeners(this) + const change = + notify || notifySpy + ? >{ + type: "add", + object: this, + newValue: value + } + : null + if (notifySpy && process.env.NODE_ENV !== "production") spyReportStart(change) + if (notify) notifyListeners(this, change) + if (notifySpy && process.env.NODE_ENV !== "production") spyReportEnd() + } + + return this + } + + delete(value: any) { + if (hasInterceptors(this)) { + const change = interceptChange>(this, { + type: "delete", + object: this, + oldValue: value + }) + if (!change) return false + } + if (this.has(value)) { + const notifySpy = isSpyEnabled() + const notify = hasListeners(this) + const change = + notify || notifySpy + ? >{ + type: "delete", + object: this, + oldValue: value + } + : null + + if (notifySpy && process.env.NODE_ENV !== "production") + spyReportStart({ ...change, name: this.name }) + transaction(() => { + this._atom.reportChanged() + this._data.delete(value) + }) + if (notify) notifyListeners(this, change) + if (notifySpy && process.env.NODE_ENV !== "production") spyReportEnd() + return true + } + return false + } + + has(value: any) { + this._atom.reportObserved() + return this._data.has(this.dehanceValue(value)) + } + + entries() { + let nextIndex = 0 + const keys = Array.from(this.keys()) + const values = Array.from(this.values()) + return makeIterable<[T, T]>({ + next() { + const index = nextIndex + nextIndex += 1 + return index < values.length + ? { value: [keys[index], values[index]], done: false } + : { done: true } + } + } as any) + } + + keys(): IterableIterator { + return this.values() + } + + values(): IterableIterator { + this._atom.reportObserved() + const self = this + let nextIndex = 0 + const observableValues = Array.from(this._data.values()) + return makeIterable({ + next() { + return nextIndex < observableValues.length + ? { value: self.dehanceValue(observableValues[nextIndex++]), done: false } + : { done: true } + } + } as any) + } + + replace(other: ObservableSet | IObservableSetInitialValues): ObservableSet { + if (isObservableSet(other)) { + other = other.toJS() + } + + transaction(() => { + if (Array.isArray(other)) { + this.clear() + other.forEach(value => this.add(value)) + } else if (isES6Set(other)) { + this.clear() + other.forEach(value => this.add(value)) + } else if (other !== null && other !== undefined) { + fail("Cannot initialize set from " + other) + } + }) + + return this + } + + observe(listener: (changes: ISetDidChange) => void, fireImmediately?: boolean): Lambda { + // TODO 'fireImmediately' can be true? + process.env.NODE_ENV !== "production" && + invariant( + fireImmediately !== true, + "`observe` doesn't support fireImmediately=true in combination with sets." + ) + return registerListener(this, listener) + } + + intercept(handler: IInterceptor>): Lambda { + return registerInterceptor(this, handler) + } + + toJS(): Set { + return new Set(this) + } + + toString(): string { + return this.name + "[ " + Array.from(this).join(", ") + " ]" + } + + [Symbol.iterator]() { + return this.values() + } + + [Symbol.toStringTag]: "Set" = "Set" +} + +export const isObservableSet = createInstanceofPredicate("ObservableSet", ObservableSet) as ( + thing: any +) => thing is ObservableSet diff --git a/src/types/type-utils.ts b/src/types/type-utils.ts index a843375c4..04bb45a4e 100644 --- a/src/types/type-utils.ts +++ b/src/types/type-utils.ts @@ -8,7 +8,8 @@ import { isObservableArray, isObservableMap, isObservableObject, - isReaction + isReaction, + isObservableSet } from "../internal" export function getAtom(thing: any, property?: string): IDepTreeNode { @@ -21,6 +22,9 @@ export function getAtom(thing: any, property?: string): IDepTreeNode { ) return (thing as any)[$mobx].atom } + if (isObservableSet(thing)) { + return (thing as any)[$mobx] + } if (isObservableMap(thing)) { const anyThing = thing as any if (property === undefined) return anyThing._keysAtom @@ -66,7 +70,7 @@ export function getAdministration(thing: any, property?: string) { if (!thing) fail("Expecting some object") if (property !== undefined) return getAdministration(getAtom(thing, property)) if (isAtom(thing) || isComputedValue(thing) || isReaction(thing)) return thing - if (isObservableMap(thing)) return thing + if (isObservableMap(thing) || isObservableSet(thing)) return thing // Initializers run lazily when transpiling to babel, so make sure they are run... initializeInstance(thing) if (thing[$mobx]) return thing[$mobx] @@ -76,7 +80,8 @@ export function getAdministration(thing: any, property?: string) { export function getDebugName(thing: any, property?: string): string { let named if (property !== undefined) named = getAtom(thing, property) - else if (isObservableObject(thing) || isObservableMap(thing)) named = getAdministration(thing) + else if (isObservableObject(thing) || isObservableMap(thing) || isObservableSet(thing)) + named = getAdministration(thing) else named = getAtom(thing) // valid for arrays as well return named.name } diff --git a/src/utils/eq.ts b/src/utils/eq.ts index fa3732ea5..721cf37ae 100644 --- a/src/utils/eq.ts +++ b/src/utils/eq.ts @@ -1,4 +1,10 @@ -import { isES6Map, isObservableArray, isObservableMap } from "../internal" +import { + isES6Map, + isObservableArray, + isObservableMap, + isES6Set, + isObservableSet +} from "../internal" declare var Symbol const toString = Object.prototype.toString @@ -127,6 +133,7 @@ function deepEq(a: any, b: any, aStack?: any[], bStack?: any[]) { function unwrap(a: any) { if (isObservableArray(a)) return a.slice() if (isES6Map(a) || isObservableMap(a)) return Array.from(a.entries()) + if (isES6Set(a) || isObservableSet(a)) return Array.from(a.entries()) return a } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 41e0a9b14..624339ae7 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -146,6 +146,10 @@ export function isES6Map(thing): boolean { return thing instanceof Map } +export function isES6Set(thing): thing is Set { + return thing instanceof Set +} + export function getMapLikeKeys(map: ObservableMap): ReadonlyArray export function getMapLikeKeys(map: IKeyValueMap | any): ReadonlyArray export function getMapLikeKeys(map: any): any { diff --git a/test/base/api.js b/test/base/api.js index f5094bf75..63de65d4a 100644 --- a/test/base/api.js +++ b/test/base/api.js @@ -40,10 +40,12 @@ test("correct api should be exposed", function() { "isObservable", "isObservableArray", "isObservableMap", + "isObservableSet", "isObservableObject", "isObservableProp", "keys", "ObservableMap", + "ObservableSet", "observable", "observe", "onReactionError", diff --git a/test/base/set.js b/test/base/set.js new file mode 100644 index 000000000..e04647ed2 --- /dev/null +++ b/test/base/set.js @@ -0,0 +1,268 @@ +"use strict" + +const mobx = require("../../src/mobx.ts") +const set = mobx.observable.set +const autorun = mobx.autorun +const iterall = require("iterall") + +test("set crud", function() { + const events = [] + const s = set([1]) + + s.observe(changes => { + events.push(changes) + }) + + expect(s.has(1)).toBe(true) + expect(s.has("1")).toBe(false) + expect(s.size).toBe(1) + + s.add("2") + + expect(s.has("2")).toBe(true) + expect(s.size).toBe(2) + expect(mobx.keys(s)).toEqual([1, "2"]) + expect(mobx.values(s)).toEqual([1, "2"]) + expect(mobx.entries(s)).toEqual([[1, 1], ["2", "2"]]) + expect(Array.from(s)).toEqual([1, "2"]) + expect(s.toJS()).toEqual(new Set([1, "2"])) + expect(s.toString()).toBe("ObservableSet@1[ 1, 2 ]") + + s.replace(new Set([3])) + + expect(mobx.keys(s)).toEqual([3]) + expect(mobx.values(s)).toEqual([3]) + expect(s.toJS()).toEqual(new Set([3])) + expect(s.toString()).toEqual("ObservableSet@1[ 3 ]") + expect(s.size).toBe(1) + expect(s.has(1)).toBe(false) + expect(s.has("2")).toBe(false) + expect(s.has(3)).toBe(true) + + s.replace(set([4])) + + expect(mobx.keys(s)).toEqual([4]) + expect(mobx.values(s)).toEqual([4]) + expect(s.toJS()).toEqual(new Set([4])) + expect(s.toString()).toEqual("ObservableSet@1[ 4 ]") + expect(s.size).toBe(1) + expect(s.has(1)).toBe(false) + expect(s.has("2")).toBe(false) + expect(s.has(3)).toBe(false) + expect(s.has(4)).toBe(true) + + expect(() => { + s.replace("") + }).toThrow(/Cannot initialize set from/) + + s.clear() + expect(mobx.keys(s)).toEqual([]) + expect(mobx.values(s)).toEqual([]) + expect(s.toJS()).toEqual(new Set()) + expect(s.toString()).toEqual("ObservableSet@1[ ]") + expect(s.size).toBe(0) + expect(s.has(1)).toBe(false) + expect(s.has("2")).toBe(false) + expect(s.has(3)).toBe(false) + expect(s.has(4)).toBe(false) + + expect(events).toEqual([ + { object: s, newValue: "2", type: "add" }, + { object: s, oldValue: 1, type: "delete" }, + { object: s, oldValue: "2", type: "delete" }, + { object: s, newValue: 3, type: "add" }, + { object: s, oldValue: 3, type: "delete" }, + { object: s, newValue: 4, type: "add" }, + { object: s, oldValue: 4, type: "delete" } + ]) +}) + +test("observe value", function() { + const s = set() + let hasX = false + let hasY = false + + autorun(function() { + hasX = s.has("x") + }) + autorun(function() { + hasY = s.has("y") + }) + + expect(hasX).toBe(false) + + s.add("x") + expect(hasX).toBe(true) + + s.delete("x") + expect(hasX).toBe(false) + + s.replace(["y"]) + expect(hasX).toBe(false) + expect(hasY).toBe(true) + expect(mobx.values(s)).toEqual(["y"]) +}) + +test("observe collections", function() { + const x = set() + let keys, values, entries + + autorun(function() { + keys = mobx.keys(x) + }) + autorun(function() { + values = Array.from(x.values()) + }) + autorun(function() { + entries = Array.from(x.entries()) + }) + + x.add("a") + expect(keys).toEqual(["a"]) + expect(values).toEqual(["a"]) + expect(entries).toEqual([["a", "a"]]) + + x.forEach(value => { + expect(x.has(value)).toBe(true) + }) + + // should not retrigger: + keys = null + values = null + entries = null + x.add("a") + expect(keys).toEqual(null) + expect(values).toEqual(null) + expect(entries).toEqual(null) + + x.add("b") + expect(keys).toEqual(["a", "b"]) + expect(values).toEqual(["a", "b"]) + expect(entries).toEqual([["a", "a"], ["b", "b"]]) + + x.delete("a") + expect(keys).toEqual(["b"]) + expect(values).toEqual(["b"]) + expect(entries).toEqual([["b", "b"]]) +}) + +test("set modifier", () => { + const x = set([{ a: 1 }]) + const y = mobx.observable({ a: x }) + + expect(mobx.isObservableSet(x)).toBe(true) + expect(mobx.isObservableObject(y)).toBe(true) + expect(mobx.isObservableObject(y.a)).toBe(false) + expect(mobx.isObservableSet(y.a)).toBe(true) +}) + +test("cleanup", function() { + const s = set(["a"]) + + let hasA + + const disposer = autorun(function() { + hasA = s.has("a") + }) + + expect(hasA).toBe(true) + expect(s.delete("a")).toBe(true) + expect(s.delete("not-existing")).toBe(false) + expect(hasA).toBe(false) +}) + +test("set should support iterall / iterable ", () => { + var a = set([1, 2]) + + function leech(iter) { + var values = [] + do { + var v = iter.next() + if (!v.done) values.push(v.value) + } while (!v.done) + return values + } + + expect(iterall.isIterable(a)).toBe(true) + + expect(leech(iterall.getIterator(a))).toEqual([1, 2]) + + expect(leech(a.entries())).toEqual([[1, 1], [2, 2]]) + + expect(leech(a.keys())).toEqual([1, 2]) + expect(leech(a.values())).toEqual([1, 2]) +}) + +test("support for ES6 Set", () => { + var x = new Set() + x.add(1) + x.add(2) + + var s = mobx.observable(x) + expect(mobx.isObservableSet(s)).toBe(true) + expect(Array.from(s)).toEqual([1, 2]) +}) + +test("deepEqual set", () => { + var x = new Set() + x.add(1) + x.add({ z: 1 }) + + var x2 = mobx.observable.set() + x2.add(1) + x2.add({ z: 2 }) + + expect(mobx.comparer.structural(x, x2)).toBe(false) + x2.replace([1, { z: 1 }]) + expect(mobx.comparer.structural(x, x2)).toBe(true) +}) + +test("set.clear should not be tracked", () => { + var x = set([1]) + var c = 0 + var d = mobx.autorun(() => { + c++ + x.clear() + }) + + expect(c).toBe(1) + x.add(2) + expect(c).toBe(1) + + d() +}) + +test("toStringTag", () => { + const x = set() + expect(x[Symbol.toStringTag]).toBe("Set") + expect(Object.prototype.toString.call(x)).toBe("[object Set]") +}) + +test("getAtom", () => { + var x = set([1]) + expect(mobx.getAtom(x)).toBeTruthy() + + expect(mobx.isObservableSet(x)).toBeTruthy() + expect(mobx.isObservable(x)).toBeTruthy() +}) + +test("observe", () => { + const vals = [] + var x = set([1]) + mobx.observe(x, v => { + vals.push(v) + }) + x.add(2) + x.add(1) + expect(vals).toEqual([{ newValue: 2, object: x, type: "add" }]) +}) + +test("toJS", () => { + const x = mobx.observable({ x: 1 }) + const y = set([x, 1]) + + const z = mobx.toJS(y) + expect(z).toEqual([{ x: 1 }, 1]) + expect(z.x).not.toBe(x) + expect(mobx.isObservable(z.x)).toBeFalsy() +})