-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
300 additions
and
19 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 |
---|---|---|
|
@@ -2,6 +2,9 @@ node_modules | |
coverage | ||
dist | ||
*.dot | ||
*.png | ||
src/*.dot | ||
src/*.png | ||
README.html | ||
coverage | ||
.DS_Store |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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
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
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
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,91 @@ | ||
import { writeFileSync } from "fs"; | ||
import { expect, test } from "vitest"; | ||
|
||
import { _cellify, _uncellify } from "./cellify"; | ||
import { Debugger } from "./debug"; | ||
import { isEqual } from "./isEqual.test"; | ||
import { asyncReduce, mapObject, reduceObject } from "./object"; | ||
import { delayed } from "./promise"; | ||
import { SheetProxy } from "./proxy"; | ||
import { Sheet } from "./sheet"; | ||
|
||
test("mapObject", async () => { | ||
const sheet = new Sheet(); | ||
const proxy = new SheetProxy(sheet); | ||
|
||
const obj = _cellify(proxy, { a: 1, b: "foo", c: "bar" }); | ||
expect(sheet.stats).toEqual({ count: 4, size: 4 }); | ||
const m = mapObject(proxy, obj, (_k: string, _v: unknown): number => | ||
typeof _v === "string" ? _v.length : (_v as number) | ||
); | ||
|
||
// initial value | ||
await expect(_uncellify(m)).resolves.toEqual({ a: 1, b: 3, c: 3 }); | ||
expect(sheet.stats).toEqual({ count: 8, size: 8 }); | ||
|
||
// update a field | ||
(await (await obj.get()).a).set(4); | ||
await expect(_uncellify(m)).resolves.toEqual({ a: 4, b: 3, c: 3 }); | ||
expect(sheet.stats).toEqual({ count: 8, size: 8 }); | ||
|
||
// add a field | ||
obj.update((rec) => ({ ...rec, h: proxy.new("hello") })); | ||
await expect(_uncellify(obj)).resolves.toEqual({ | ||
a: 4, | ||
b: "foo", | ||
c: "bar", | ||
h: "hello" | ||
}); | ||
console.log(await _uncellify(m)); | ||
await expect(_uncellify(m)).resolves.toEqual({ a: 4, b: 3, c: 3, h: 5 }); | ||
expect(sheet.stats).toEqual({ count: 10, size: 10 }); | ||
|
||
// delete a field | ||
obj.update((rec) => { | ||
const copy = { ...rec }; | ||
// biome-ignore lint/performance/noDelete: we don't want an undefined field | ||
delete copy.a; | ||
return copy; | ||
}); | ||
await expect(_uncellify(m)).resolves.toEqual({ b: 3, c: 3, h: 5 }); | ||
expect(sheet.stats).toEqual({ count: 10, size: 9 }); // gc works | ||
}); | ||
|
||
test("asyncReduce", async () => { | ||
await expect( | ||
asyncReduce([1, 2, 3], (acc, v) => delayed(acc + v, 10), 0) | ||
).resolves.toBe(6); | ||
}); | ||
|
||
test( | ||
"reduceObject", | ||
async () => { | ||
const sheet = new Sheet(); | ||
const debug = new Debugger(sheet); | ||
const proxy = new SheetProxy(sheet); | ||
|
||
const l = _cellify(proxy, { a: 1, b: 2, c: 3 }); | ||
const v = reduceObject( | ||
proxy, | ||
l, | ||
async (acc, _key, v) => delayed(acc + (v as number), 1), | ||
0 | ||
); | ||
writeFileSync("reduceObject1.dot", debug.dot("reduceObject before")); | ||
await expect(v.consolidatedValue).resolves.toBe(6); | ||
expect(sheet.stats).toEqual({ count: 6, size: 6 }); // 3+1 array +1 sum +1 pointer | ||
|
||
// update one cell | ||
await (await l.get()).a.set(4); | ||
await expect(v.consolidatedValue).resolves.toBe(9); | ||
writeFileSync("reduceObject2.dot", debug.dot("reduceObject after update")); | ||
expect(sheet.stats).toEqual({ count: 6, size: 6 }); // unchanged | ||
|
||
// add one cell | ||
l.update((obj) => ({ ...obj, d: proxy.new(5, "n:5") })); | ||
await expect(v.consolidatedValue).resolves.toBe(14); | ||
writeFileSync("reduceObject3.dot", debug.dot("reduceObject after add")); | ||
expect(sheet.stats).toEqual({ count: 8, size: 7 }); // +1 cell, update pointer | ||
}, | ||
{ timeout: 1000 } | ||
); |
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,106 @@ | ||
import type { AnyCell, MapCell } from "./cell"; | ||
import { collector } from "./gc"; | ||
import type { SheetProxy } from "./proxy"; | ||
|
||
// @todo introduce a type variable that is a sum type of all possible field types? | ||
export type CellObject<T> = AnyCell<Record<string, AnyCell<T>>>; | ||
|
||
/** | ||
* mapObject applies a function to a CellObject. | ||
*/ | ||
export const mapObject = <NF extends boolean = false>( | ||
proxy: SheetProxy, | ||
obj: CellObject<unknown>, | ||
// @todo return type | ||
fn: ( | ||
key: string, | ||
value: unknown, | ||
valueCell: AnyCell<unknown> | ||
) => unknown | Promise<unknown>, | ||
name = "mapObject", | ||
nf?: NF | ||
): MapCell<Record<string, AnyCell<unknown>>, NF> => | ||
proxy.map( | ||
[obj], | ||
(cells, prev) => { | ||
const set = new Set(Object.keys(prev || {})); | ||
const res = Object.fromEntries( | ||
Object.entries(cells).map(([k, v]) => { | ||
// we reuse a previous cell if the key is the same and still maps to same v | ||
const reuse = | ||
(prev?.[k] && prev[k].dependencies?.[0] === v.id) || false; | ||
if (reuse) set.delete(k); | ||
// console.log({ k, reuse, prev: prev?.[k]?.id }); | ||
return [ | ||
k, | ||
reuse ? prev[k] : proxy.map([v], (_v) => fn(k, _v, v), `[${k}]µ`) | ||
]; | ||
}) | ||
); | ||
// collect unused previously mapped cells | ||
proxy._sheet.collect(...[...set].map((k) => prev[k])); | ||
return res; | ||
}, | ||
name, | ||
nf | ||
); | ||
|
||
export const asyncReduce = async <T, U>( | ||
array: T[], | ||
reducer: ( | ||
accumulator: U, | ||
currentValue: T, | ||
currentIndex: number, | ||
array: T[] | ||
) => U | Promise<U>, | ||
initialValue: U | ||
): Promise<U> => { | ||
let acc: U = initialValue; | ||
for (let index = 0; index < array.length; index++) { | ||
acc = await reducer(acc, array[index], index, array); | ||
} | ||
return acc; | ||
}; | ||
|
||
/** | ||
* reduceObject applies the reducer function `fn` for each | ||
* element in `obj`, starting from `init` value. | ||
*/ | ||
export const reduceObject = <T, R, NF extends boolean = false>( | ||
proxy: SheetProxy, | ||
obj: CellObject<T>, | ||
fn: ( | ||
acc: R, | ||
key: string, | ||
value: T, | ||
cell?: AnyCell<T>, | ||
index?: number | ||
) => R | Promise<R>, | ||
init: R, | ||
name = "reduceObject", | ||
nf?: NF | ||
): MapCell<R, NF> => { | ||
const coll = collector<MapCell<R, NF>>(proxy); | ||
return proxy.mapNoPrevious( | ||
[obj], | ||
(cells) => { | ||
const keys = Object.keys(cells); | ||
const values = Object.values(cells); | ||
// console.log({ reduce: keys, name, count: proxy._sheet.stats.count }); | ||
return coll( | ||
proxy.mapNoPrevious( | ||
values, | ||
(..._cells) => | ||
asyncReduce( | ||
_cells, | ||
(acc, _cell, i) => fn(acc, keys[i], _cell, values[i], i), | ||
init | ||
), | ||
"_reduce" | ||
) | ||
); | ||
}, | ||
name, | ||
nf | ||
); | ||
}; |
Oops, something went wrong.