-
-
Notifications
You must be signed in to change notification settings - Fork 126
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
1 changed file
with
184 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } |