Skip to content

Commit

Permalink
Add ObservableGroupMap
Browse files Browse the repository at this point in the history
An ObservableGroupMap is constructed from a base IObservableArray and a `groupBy` function.
It maps `groupBy` values to IObservableArrays of the items in the base array which have groupBy(item) == key
The IObservableArrays are updated reactively when items in the base array are removed, changed or added.
Updates are done without iterating through the base array (except once at the beginning)
  • Loading branch information
NaridaL committed Apr 4, 2020
1 parent 6454b14 commit 6142fd3
Showing 1 changed file with 184 additions and 0 deletions.
184 changes: 184 additions & 0 deletions src/observable-group-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import {
observable,
IReactionDisposer,
autorun,
reaction,
observe,
IObservableArray,
transaction,
untracked,
trace,
getDependencyTree,
} from "mobx"
import { createTransformer } from "./mobx-utils"

function bagRemove(arr: any[], index: number) {
arr[index] = arr[arr.length - 1]
arr.length--
}

interface GrouperItemInfo {
groupByValue: any
reaction: IReactionDisposer
grouperArrIndex: number
}

export class ObservableGroupMap<G, T> extends Map<G, IObservableArray<T>> {
has(key: G): boolean {
throw new Error("unsupported operation")
}
private keyToName: (group: G) => string
private groupBy: (x: T) => G
private grouperInfoKey: symbol
private base: IObservableArray<T>
clear(): void {
throw new Error("Method not implemented.")
}
delete(_key: G): boolean {
throw new Error("Method not implemented.")
}
set(_key: G, _value: IObservableArray<T>): this {
throw new Error("Method not implemented.")
}
get(key: G) {
let result = super.get(key)
if (undefined === result) {
result = observable([], { name: `GroupArray[${this.keyToName(key)}]` })
super.set(key, result)
}
return result
}
private addItem(item: any) {
const groupByValue = this.groupBy(item)
const groupArr = this.get(groupByValue)
const value: GrouperItemInfo = {
groupByValue: groupByValue,
grouperArrIndex: groupArr.length,
reaction: reaction(
() => this.groupBy(item),
(newGroupByValue, r) => {
const grouperItemInfo = (item as any)[this.grouperInfoKey]
this.get(grouperItemInfo.groupByValue)!.splice(
grouperItemInfo.grouperArrIndex,
1
)

const newGroupArr = this.get(groupByValue)
const newGrouperArrIndex = newGroupArr.length
newGroupArr.push(item)
grouperItemInfo.groupByValue = newGroupByValue
grouperItemInfo.grouperArrIndex = newGrouperArrIndex
}
),
}
Object.defineProperty(item, this.grouperInfoKey, {
configurable: true,
enumerable: false,
value,
})
groupArr.push(item)
}
private removeItem(item: any) {
const grouperItemInfo: GrouperItemInfo = (item as any)[this.grouperInfoKey]
bagRemove(this.get(grouperItemInfo.groupByValue)!, grouperItemInfo.grouperArrIndex)
grouperItemInfo.reaction()

delete (item as any)[this.grouperInfoKey]
}

constructor(
base: IObservableArray<T>,
groupBy: (x: T) => G,
{
name,
keyToName = (x) => "" + x,
}: { name?: string; keyToName?: (group: G) => string } = {}
) {
super()
this.keyToName = keyToName
this.groupBy = groupBy
this.grouperInfoKey = Symbol("grouperInfo")
this.base = base

for (let i = 0; i < base.length; i++) {
const item = base[i]
this.addItem(item)
}

observe(base, (change) => {
if ("splice" === change.type) {
transaction(() => {
for (const removed of change.removed) {
this.removeItem(removed)
}
for (const added of change.added) {
this.addItem(added)
}
})
} else if ("update" === change.type) {
transaction(() => {
this.removeItem(change.oldValue)
this.addItem(change.newValue)
})
} else {
throw new Error("illegal state")
}
})
}
dispose() {
for (let i = 0; i < this.base.length; i++) {
const item = this.base[i]
const grouperItemInfo: GrouperItemInfo = (item as any)[this.grouperInfoKey]
grouperItemInfo.reaction()

delete (item as any)[this.grouperInfoKey]
this.addItem(item)
}
}
}

type Slice = { day: string; hours: number }

const base = observable(
[
{ day: "mo", hours: 3 },
{ day: "mo", hours: 3 },
{ day: "tu", hours: 3 },
{ day: "we", hours: 3 },
],
{ name: "base" }
)

const slicesByDay: Map<string, Slice[]> = new ObservableGroupMap(base, (s) => s.day)

const dayHours = createTransformer(
(day: string) => {
trace()
return slicesByDay.get(day)!.reduce((a, b) => a + b.hours, 0)
},
{ debugNameGenerator: (x) => x + "Hours" }
)

const mapToObj = (map: Map<any, any>) =>
Array.from(map.keys()).reduce((r, key) => ((r[key] = slicesByDay.get(key)), r), {} as any)

const dispose = autorun(() => {
trace()
untracked(() => console.log(JSON.stringify(mapToObj(slicesByDay))))
console.log("moHours", dayHours("mo"))
console.log("tuHours", dayHours("tu"))
})

console.log("getDependencyTree", JSON.stringify(getDependencyTree(dispose), null, " "))

console.log(">> add tu 4")
base.push({ day: "tu", hours: 4 })

console.log(">> remove we slice")
base.splice(3, 1)

console.log(">> set first mo slice hours")
base[0].hours = 12

console.log(">> replace tu slice")
base[2] = { day: "tu", hours: 4.5 }

0 comments on commit 6142fd3

Please sign in to comment.