mobx-bonsai
Version:
A fast lightweight alternative to MobX-State-Tree + Y.js two-way binding
262 lines (219 loc) • 6.32 kB
text/typescript
import {
action,
IObservableArray,
ISetWillChange,
intercept,
isObservableArray,
ObservableSet,
observable,
observe,
runInAction,
transaction,
untracked,
} from "mobx"
import { failure } from "../error/failure"
import { isArray } from "../plainTypes/checks"
import { getMobxVersion } from "../utils/getMobxVersion"
class PlainArraySet<T> implements Set<T> {
constructor(private readonly data: T[]) {}
add(value: T): this {
if (!this.has(value)) {
this.data.push(value)
}
return this
}
clear(): void {
this.data.length = 0
}
delete(value: T): boolean {
const index = this.data.indexOf(value)
if (index >= 0) {
this.data.splice(index, 1)
return true
}
return false
}
has(value: T): boolean {
return this.data.indexOf(value) !== -1
}
forEach(callbackfn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any): void {
for (const v of this.data) {
callbackfn.call(thisArg, v, v, this)
}
}
get size(): number {
return this.data.length
}
*entries(): ReturnType<Set<T>["entries"]> {
for (const v of this.data) {
yield [v, v]
}
}
*keys(): ReturnType<Set<T>["keys"]> {
yield* this.data
}
*values(): ReturnType<Set<T>["values"]> {
yield* this.data
}
[Symbol.iterator](): ReturnType<Set<T>[typeof Symbol.iterator]> {
return this.values()
}
readonly [Symbol.toStringTag] = "PlainArraySet"
union<U>(other: ReadonlySetLike<U>): Set<T | U> {
const s = new Set(this)
return s.union(other)
}
intersection<U>(other: ReadonlySetLike<U>): Set<T & U> {
const s = new Set(this)
return s.intersection(other)
}
difference<U>(other: ReadonlySetLike<U>): Set<T> {
const s = new Set(this)
return s.difference(other)
}
symmetricDifference<U>(other: ReadonlySetLike<U>): Set<T | U> {
const s = new Set(this)
return s.symmetricDifference(other)
}
isSubsetOf(other: ReadonlySetLike<unknown>): boolean {
const s = new Set(this)
return s.isSubsetOf(other)
}
isSupersetOf(other: ReadonlySetLike<unknown>): boolean {
const s = new Set(this)
return s.isSupersetOf(other)
}
isDisjointFrom(other: ReadonlySetLike<unknown>): boolean {
const s = new Set(this)
return s.isDisjointFrom(other)
}
}
const observableSetBackedByObservableArray = <T>(
array: IObservableArray<T>
): ObservableSet<T> & { dataObject: typeof array } => {
if (!isObservableArray(array)) {
throw failure("assertion failed: expected an observable array")
}
const set = transaction(() =>
untracked(() => {
if (getMobxVersion() >= 6) {
return observable.set(array)
} else {
// In MobX 5, we need to create the set and add items within an action
const set = observable.set()
// Use action to avoid strict mode errors in MobX 5
runInAction(() => {
array.forEach((item) => {
set.add(item)
})
})
return set
}
})
)
;(set as ObservableSet<T> & { dataObject: typeof array }).dataObject = array
if (set!.size !== array.length) {
throw failure("arrays backing a set cannot contain duplicate values")
}
let setAlreadyChanged = false
let arrayAlreadyChanged = false
// for speed reasons we will just assume distinct values are only once in the array
// when the array changes the set changes
observe(
array,
action((change: any /*IArrayDidChange<T>*/) => {
if (setAlreadyChanged) {
return
}
arrayAlreadyChanged = true
try {
switch (change.type) {
case "splice": {
{
const removed = change.removed
for (let i = 0; i < removed.length; i++) {
set.delete(removed[i])
}
}
{
const added = change.added
for (let i = 0; i < added.length; i++) {
set.add(added[i])
}
}
break
}
case "update": {
set.delete(change.oldValue)
set.add(change.newValue)
break
}
default:
throw failure("assertion error: unsupported array change type")
}
} finally {
arrayAlreadyChanged = false
}
})
)
// when the set changes also change the array
intercept(
set!,
action((change: ISetWillChange<T>) => {
if (setAlreadyChanged) {
return null
}
if (arrayAlreadyChanged) {
return change
}
setAlreadyChanged = true
try {
switch (change.type) {
case "add": {
array.push(change.newValue)
break
}
case "delete": {
const i = array.indexOf(change.oldValue)
if (i >= 0) {
array.splice(i, 1)
}
break
}
default:
throw failure("assertion error: unsupported set change type")
}
return change
} finally {
setAlreadyChanged = false
}
})
)
return set! as ObservableSet<T> & { dataObject: typeof array }
}
const setCache = new WeakMap<any[], Set<any>>()
/**
* Returns a Set-like view of the given array.
*
* For plain arrays, the set interface is backed by the array,
* ensuring values appear only once. For observable arrays, mutations
* are wrapped in actions using MobX.
*
* @template T The type of the values.
* @param data An array to be wrapped as a Set.
* @returns A Set-like view over the array.
* @throws When data is not an array.
*/
export function asSet<T>(data: T[]): Set<T> {
if (!isArray(data)) {
throw failure("asSet expects an array")
}
let setInstance = setCache.get(data)
if (!setInstance) {
setInstance = isObservableArray(data)
? observableSetBackedByObservableArray<T>(data)
: new PlainArraySet<T>(data)
setCache.set(data, setInstance)
}
return setInstance
}