Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Utilities updates #31

Merged
merged 8 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading