Skip to content

Commit

Permalink
Utilities updates (#31)
Browse files Browse the repository at this point in the history
* array: mapArray non-array error

* sheet: debug accepts functions for both info and data

* proxy, sheet: protect for invalid ids

* json: fix jsonStringify with cells

* cellify: _uncellify cleanup

* cellify: failOnCell option

* cellify: fix failOnCell

* cellify: export cellify and uncellify
  • Loading branch information
hbbio committed Apr 26, 2024
1 parent 4544c9d commit 06a3097
Show file tree
Hide file tree
Showing 10 changed files with 86 additions and 69 deletions.
50 changes: 25 additions & 25 deletions src/array.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
sort
} from "./array";
import type { AnyCell } from "./cell";
import { _uncellify } from "./cellify";
import { uncellify } from "./cellify";
import { delayed } from "./promise";
import { SheetProxy } from "./proxy";
import { Sheet } from "./sheet";
Expand All @@ -30,25 +30,25 @@ test("mapArray", async () => {
expect(sheet.stats.count).toBe(8); // +4 cells

// initial value
await expect(_uncellify(m)).resolves.toEqual([2, 3, 4]);
await expect(uncellify(m)).resolves.toEqual([2, 3, 4]);
expect(sheet.stats.count).toBe(8); // unchanged

// update one cell
(await l.get())[0].set(4);
await expect(_uncellify(m)).resolves.toEqual([5, 3, 4]);
await expect(uncellify(m)).resolves.toEqual([5, 3, 4]);
expect(sheet.stats.count).toBe(8); // @unchanged

// add one cell
l.update((arr) => [...arr, proxy.new(5)]);
await expect(_uncellify(m)).resolves.toEqual([5, 3, 4, 6]);
await expect(uncellify(m)).resolves.toEqual([5, 3, 4, 6]);
expect(sheet.stats.count).toBe(10); // +1 original cell, +1 new mapped

// get one cell
expect(await (await m.get())[3].get()).toBe(6);

// delete one cell
l.update((arr) => [...arr.slice(0, 1), ...arr.slice(1 + 1)]);
await expect(_uncellify(m)).resolves.toEqual([5, 4, 6]);
await expect(uncellify(m)).resolves.toEqual([5, 4, 6]);
expect(await (await m.get())[2].get()).toBe(6);
expect(sheet.stats.count).toBe(10); // @unchanged
});
Expand All @@ -61,16 +61,16 @@ test("mapArrayCell function change", async () => {
const fn = proxy.new((v: AnyCell<number>) => v.map((v) => v + 1));
const m = mapArrayCell(proxy, l, fn);
expect(sheet.stats.count).toBe(9); // +4 cells
await expect(_uncellify(m)).resolves.toEqual([2, 3, 4]);
await expect(uncellify(m)).resolves.toEqual([2, 3, 4]);

// update one cell
(await l.get())[0].set(4);
await expect(_uncellify(m)).resolves.toEqual([5, 3, 4]);
await expect(uncellify(m)).resolves.toEqual([5, 3, 4]);
expect(sheet.stats.count).toBe(9); // @unchanged

// function change
fn.set((v: AnyCell<number>) => v.map((v) => v - 1));
await expect(_uncellify(m)).resolves.toEqual([3, 1, 2]);
await expect(uncellify(m)).resolves.toEqual([3, 1, 2]);
expect(sheet.stats.count).toBe(12); // +3 new mapped cells
});

Expand All @@ -84,27 +84,27 @@ test("mapArray async", async () => {
expect(sheet.stats.count).toBe(8); // +4 cells

// initial value
await expect(_uncellify(m)).resolves.toEqual([2, 3, 4]);
await expect(uncellify(m)).resolves.toEqual([2, 3, 4]);
expect(sheet.stats.count).toBe(8); // unchanged

// update one cell
(await l.get())[0].set(4);
await proxy.working.wait();
await expect(_uncellify(m)).resolves.toEqual([5, 3, 4]);
await expect(uncellify(m)).resolves.toEqual([5, 3, 4]);
expect(sheet.stats.count).toBe(8); // @unchanged

// add one cell
l.update((arr) => [...arr, proxy.new(5)]);
await proxy.working.wait();
await expect(_uncellify(m)).resolves.toEqual([5, 3, 4, 6]);
await expect(uncellify(m)).resolves.toEqual([5, 3, 4, 6]);
expect(sheet.stats.count).toBe(10); // +1 original cell, +1 new mapped

// get one cell
expect(await (await m.get())[3].get()).toBe(6);

// delete one cell
l.update((arr) => [...arr.slice(0, 1), ...arr.slice(1 + 1)]);
await expect(_uncellify(m)).resolves.toEqual([5, 4, 6]);
await expect(uncellify(m)).resolves.toEqual([5, 4, 6]);
expect(await (await m.get())[2].get()).toBe(6);
expect(sheet.stats.count).toBe(10); // @unchanged
});
Expand Down Expand Up @@ -159,18 +159,18 @@ test("sort array", async () => {
const l = proxy.new([1, 5, 3].map((v) => proxy.new(v)));
const s = sort(proxy, l);

await expect(_uncellify(s)).resolves.toEqual([1, 3, 5]);
await expect(uncellify(s)).resolves.toEqual([1, 3, 5]);
expect(sheet.stats.count).toBe(6); // 3+1 array +1 sorted +1 pointer

// update one cell
(await l.get())[0].set(4);
await expect(_uncellify(s)).resolves.toEqual([3, 4, 5]);
await expect(uncellify(s)).resolves.toEqual([3, 4, 5]);
expect(sheet.stats.count).toBe(6); // @unchanged

// add one cell
l.update((arr) => [...arr, proxy.new(1)]);
await expect(_uncellify(l)).resolves.toEqual([4, 5, 3, 1]);
await expect(_uncellify(s)).resolves.toEqual([1, 3, 4, 5]);
await expect(uncellify(l)).resolves.toEqual([4, 5, 3, 1]);
await expect(uncellify(s)).resolves.toEqual([1, 3, 4, 5]);
expect(sheet.stats.count).toBe(8); // +1 original cell +1 sorted pointer
});

Expand All @@ -187,18 +187,18 @@ test("filter array", async () => {

// we wait before expecting
await proxy.working.wait();
await expect(_uncellify(f)).resolves.toEqual([5, 3]);
await expect(uncellify(f)).resolves.toEqual([5, 3]);
// the removed cell is not deleted ("garbage collected" at proxy level)
expect(sheet.stats.count).toBe(7); // unchanged

// update one cell
(await l.get())[0].set(4);
await expect(_uncellify(f)).resolves.toEqual([4, 5, 3]);
await expect(uncellify(f)).resolves.toEqual([4, 5, 3]);
expect(sheet.stats.count).toBe(7); // unchanged

// change predicate
pred.set((v) => v > 3);
await expect(_uncellify(f)).resolves.toEqual([4, 5]);
await expect(uncellify(f)).resolves.toEqual([4, 5]);
expect(sheet.stats).toEqual({ size: 7, count: 8 }); // one pointer updated
});

Expand All @@ -215,11 +215,11 @@ test("filterPredicateCell array", async () => {

// we wait before expecting
await proxy.working.wait();
await expect(_uncellify(f)).resolves.toEqual([5, 3]);
await expect(uncellify(f)).resolves.toEqual([5, 3]);

pred.set((v: AnyCell<number>) => v.map((_v) => _v > 3));
await proxy.working.wait();
await expect(_uncellify(f)).resolves.toEqual([5]);
await expect(uncellify(f)).resolves.toEqual([5]);
expect(sheet.stats).toEqual({ size: 14, count: 15 }); // we should not recreate mapped cells
});

Expand Down Expand Up @@ -257,20 +257,20 @@ test("sort array remapped", async () => {
Promise.resolve(0)
);

await expect(_uncellify(s)).resolves.toEqual([1, 3, 5]);
await expect(uncellify(s)).resolves.toEqual([1, 3, 5]);
expect(sheet.stats).toEqual({ size: 9, count: 9 }); // 3+1 array +1 sorted +1 pointer +1 sum +1 pointer
await expect(sum.get()).resolves.toBe(9);

// update one cell
(await l.get())[0].set(4);
await expect(_uncellify(s)).resolves.toEqual([3, 4, 5]);
await expect(uncellify(s)).resolves.toEqual([3, 4, 5]);
await expect(sum.get()).resolves.toBe(12);
expect(sheet.stats).toEqual({ size: 9, count: 10 }); // size unchanged, one pointer changed

// add one cell
l.update((arr) => [...arr, proxy.new(1)]);
await expect(_uncellify(l)).resolves.toEqual([4, 5, 3, 1]);
await expect(_uncellify(s)).resolves.toEqual([1, 3, 4, 5]);
await expect(uncellify(l)).resolves.toEqual([4, 5, 3, 1]);
await expect(uncellify(s)).resolves.toEqual([1, 3, 4, 5]);
await expect(sum.get()).resolves.toBe(13);
expect(sheet.stats).toEqual({ size: 10, count: 13 }); // +1 original cell, changed 2 pointers
});
Expand Down
2 changes: 2 additions & 0 deletions src/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const mapArray = <T, U>(
proxy.map(
[arr],
(cells, prev) => {
if (!Array.isArray(cells))
throw new Error(`not an array: ${typeof cells}`);
if (!cells) return [];
const set = new Set((prev || []).map((cell) => cell.id));
const res = cells.map((cell, index) => {
Expand Down
4 changes: 2 additions & 2 deletions src/cell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ export class Cell<
): void {
this.sheet.debug(
[this.id],
`Cell ${this.name} (${this.id}) <== ${newValue}`,
() => `Cell ${this.name} (${this.id}) <== ${newValue}`,
{
currentValue: this.value,
currentCompRank: this._currentComputationRank,
Expand Down Expand Up @@ -544,7 +544,7 @@ export class Cell<
}

const needUpdate = !this._sheet.equals(this.v, newValue);
this.sheet.debug([this.id], `Cell ${this.name} <== ${newValue}`, {
this.sheet.debug([this.id], () => `Cell ${this.name} <== ${newValue}`, {
currentValue: simplifier(this.value),
currentRank: this._currentComputationRank,
newValueRank: computationRank
Expand Down
23 changes: 15 additions & 8 deletions src/cellify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { expect, test } from "vitest";
import {
type Cellified,
type Uncellified,
_cellify,
_uncellify,
follow
cellify,
follow,
uncellify
} from "./cellify";
import { SheetProxy } from "./proxy";
import { Sheet } from "./sheet";
Expand Down Expand Up @@ -38,16 +38,16 @@ test("fix point", async () => {

for (let i = 0; i < tests.length; i++) {
const v = tests[i];
const c = _cellify(proxy, v);
const u = await _uncellify(c);
const c = cellify(proxy, v);
const u = await uncellify(c);
expect(u).toEqual(v);
}
});

test("_cellify one", async () => {
test("cellify one", async () => {
const sheet = new Sheet();
const proxy = new SheetProxy(sheet);
const res = _cellify(proxy, { a: 1 });
const res = cellify(proxy, { a: 1 });
const cell = await res.get();
await expect(cell.a.get()).resolves.toBe(1);
});
Expand All @@ -56,7 +56,7 @@ test("follow", async () => {
const sheet = new Sheet();
const proxy = new SheetProxy(sheet);
const v = { a: [1, 2, 3], b: { c: { foo: 1, bar: 1 } } };
const cv = _cellify(proxy, v);
const cv = cellify(proxy, v);
const f = follow(proxy, cv, ["a", 1]);
await expect(f.get()).resolves.toBe(2);
expect(sheet.stats).toEqual({ size: 12, count: 12 });
Expand All @@ -76,3 +76,10 @@ test("follow", async () => {
await expect(f.get()).resolves.toBeInstanceOf(Error);
expect(sheet.stats).toEqual({ size: 13, count: 14 }); // unchanged
});

test("cellify failOnCell", async () => {
const sheet = new Sheet();
const proxy = new SheetProxy(sheet);
const v = { a: [1, 2, 3], b: { c: { foo: proxy.new(1, "1"), bar: 1 } } };
expect(() => cellify(proxy, v, "cv", true)).toThrowError("value is cell");
});
30 changes: 19 additions & 11 deletions src/cellify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const isObject = <K extends string | number | symbol>(
): v is Record<K, unknown> =>
typeof v === "object" && v !== null && v.constructor?.name === "Object";

const errIsCell = new Error("value is cell");
/**
* cellify converts any value to a Cellified value where each array or record
* becomes a Cell in canonical form.
Expand All @@ -37,42 +38,49 @@ export const isObject = <K extends string | number | symbol>(
* @returns
* @todo cell reuses
*/
export const _cellify = <T>(
export const cellify = <T>(
proxy: SheetProxy,
v: T,
name = "cellify"
name = "cellify",
failOnCell = false
): Cellified<T> => {
if (v instanceof Cell) throw new Error("cell");
if (v instanceof Cell) {
if (failOnCell) throw errIsCell;
return v as Cellified<T>;
}
return proxy.new(
Array.isArray(v)
? v.map((vv) => _cellify(proxy, vv), "cellify.[]")
? v.map((vv) => cellify(proxy, vv, name, failOnCell), "cellify.[]")
: isObject(v)
? Object.fromEntries(
Object.entries(v).map(([k, vv]) => [k, _cellify(proxy, vv)], "ç{}")
Object.entries(v).map(
([k, vv]) => [k, cellify(proxy, vv, name, failOnCell)],
"ç{}"
)
)
: v,
name
) as Cellified<T>;
};

/**
* _uncellify is used in tests to flatten a value tree that contains multiple cells.
* uncellify is used in tests to flatten a value tree that contains multiple cells.
* @param v any value
* @returns value without cells
*/
export const _uncellify = async <T>(
export const uncellify = async <T>(
v: T | AnyCell<T>
): Promise<Uncellified<T>> => {
const value = v instanceof Cell ? await v.consolidatedValue : v;
if (value instanceof Error) throw value;
if (Array.isArray(value))
return Promise.all(
value.map(async (_element) => await _uncellify(_element))
) as Promise<Uncellified<T>>;
return Promise.all(value.map((_element) => uncellify(_element))) as Promise<
Uncellified<T>
>;
if (isObject(value))
return Object.fromEntries(
await Promise.all(
Object.entries(value).map(async ([k, vv]) => [k, await _uncellify(vv)])
Object.entries(value).map(async ([k, vv]) => [k, await uncellify(vv)])
)
);
// Classes, null or base types (string, number, ...)
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export {
type CellArray
} from "./array";
export {
_cellify,
_uncellify,
cellify,
uncellify,
follow,
isObject,
type Cellified,
Expand Down
8 changes: 4 additions & 4 deletions src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ export const jsonStringify = <T>(
obj: T,
options: { skipNull?: boolean; failOnCell?: boolean } = {}
) => {
if (obj instanceof Cell) {
if (options?.failOnCell) throw errorCell;
return jsonStringify(obj.value, options);
}
let out = "";
const aux = <T>(v: T) => {
if (v instanceof Cell) {
if (options?.failOnCell) throw errorCell;
return aux(v.value);
}
if (Array.isArray(v)) {
out += "[";
let first = true;
Expand Down
Loading

0 comments on commit 06a3097

Please sign in to comment.