Skip to content

Commit

Permalink
Merge 8eac607 into e41cee7
Browse files Browse the repository at this point in the history
  • Loading branch information
hbbio authored Apr 4, 2024
2 parents e41cee7 + 8eac607 commit 9a9f138
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 17 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ node_modules
coverage
dist
*.dot
*.png
src/*.dot
src/*.png
README.html
coverage
.DS_Store
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/cell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,9 @@ export class Cell<
get name(): string {
return this._sheet.name(this.id);
}
get fullName(): string {
return this._sheet.name(this.id, true);
}

/**
* get the immediate cell value. or pointed value
Expand Down Expand Up @@ -522,7 +525,7 @@ export class Cell<
// Invalidation for outdated computation
if (computationRank < this._valueRank) {
DEV &&
console.log(
console.warn(
`Cell ${this.name}: `,
`setting to ${newValue} has been invalidated`,
{
Expand Down
17 changes: 9 additions & 8 deletions src/cellify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ export type Uncellified<T> = T extends AnyCell<infer U>
: U
: T;

// @todo is type only if true
// exclude classes
export const isObject = <K extends string | number | symbol>(
v: unknown
): v is Record<K, unknown> =>
typeof v === "object" && v !== null && v.constructor?.name === "Object";

/**
* cellify converts any value to a Cellified value where each array or record
* becomes a Cell in canonical form.
Expand All @@ -34,9 +41,7 @@ export const _cellify = <T>(proxy: SheetProxy, v: T): Cellified<T> => {
return proxy.new(
Array.isArray(v)
? v.map((vv) => _cellify(proxy, vv), "cellify.[]")
: typeof v === "object" &&
v !== null &&
v.constructor.prototype === Object.prototype // exclude classes
: isObject(v)
? Object.fromEntries(
Object.entries(v).map(
([k, vv]) => [k, _cellify(proxy, vv)],
Expand All @@ -62,11 +67,7 @@ export const _uncellify = async <T>(
return Promise.all(
value.map(async (_element) => await _uncellify(_element))
) as Promise<Uncellified<T>>;
if (
typeof value === "object" &&
value !== null &&
value.constructor.prototype === Object.prototype // exclude classes
)
if (isObject(value))
return Object.fromEntries(
await Promise.all(
Object.entries(value).map(async ([k, vv]) => [k, await _uncellify(vv)])
Expand Down
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ export {
reduce,
sort
} from "./array";
export {
_cellify,
_uncellify,
isObject,
type Cellified,
type Uncellified
} from "./cellify";
export { Debugger } from "./debug";
export { jsonStringify } from "./json";
export { nextSubscriber } from "./next";
export { asyncReduce, mapObject, reduceObject } from "./object";
91 changes: 91 additions & 0 deletions src/object.test.ts
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 }
);
106 changes: 106 additions & 0 deletions src/object.ts
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
);
};
17 changes: 11 additions & 6 deletions src/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ export class Sheet {
*
*/
private _pointers: Graph<number>;
private _containers: Graph<number>;

/** equality function */
public equals: <V>(prev: V, next: V) => boolean;
readonly _marshaller: <V>(a: V) => string;
Expand All @@ -102,14 +104,12 @@ export class Sheet {
this.g = new Graph();
this._cells = {};
this._pointers = new Graph();
// this.order = [];
this._containers = new Graph();
this[count] = 0;
this[size] = 0;
this.equals = equality;
this._marshaller = marshaller;

this.working = new Working();

this.errors = new CellErrors();
this._gc = new Set();
}
Expand All @@ -118,9 +118,11 @@ export class Sheet {
this.g.bless(id, name);
}

name(id: number): string {
return this.g.name(id) || id.toString();
name(id: number, full = false): string {
const name = this.g.name(id);
return name ? (full ? `${name} (${id})` : name) : id.toString();
}

/**
* Promise that keeps pending until all computations that were running at call-time are settled.
*/
Expand Down Expand Up @@ -793,8 +795,11 @@ export class Sheet {
}

private dependentCells(id) {
return Array.from(new Set([...this.g.get(id), ...this._pointers.get(id)]));
return Array.from(
new Set([...(this.g.get(id) || []), ...this._pointers.get(id)])
);
}

/**
* Computes the cells that can be safely computed.
* Safely means that their dependencies will not change until an external modification occurs.
Expand Down

0 comments on commit 9a9f138

Please sign in to comment.