Skip to content

Commit

Permalink
Merge f58b563 into 1403428
Browse files Browse the repository at this point in the history
  • Loading branch information
hbbio committed May 26, 2024
2 parents 1403428 + f58b563 commit 928b403
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 39 deletions.
2 changes: 1 addition & 1 deletion src/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ export const mapFlat = <T, NF extends boolean = false>(
nf?: NF
) => {
const coll = collector<MapCell<T[], NF>>(proxy);
return proxy.map(
return proxy.mapNoPrevious(
[arr],
(cells) => coll(proxy.mapNoPrevious(cells, (..._cells) => _cells)),
name,
Expand Down
40 changes: 30 additions & 10 deletions src/cellify.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { type AnyCell, Cell, type MapCell, type ValueCell } from "./cell";
import {
type AnyCell,
Cell,
type CellResult,
type MapCell,
type Pending,
type ValueCell
} from "./cell";
import { collector } from "./gc";
import type { SheetProxy } from "./proxy";

Expand All @@ -22,14 +29,15 @@ export type Uncellified<T> = T extends AnyCell<infer U>
: U
: T;

// @todo is type only if true
// exclude classes
// isObject returns true if the value is a regular JavaScript Object,
// but not null neither a custom Class instance.
export const isObject = <K extends string | number | symbol>(
v: unknown
): 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 Down Expand Up @@ -63,24 +71,36 @@ export const cellify = <T>(
) as Cellified<T>;
};

export type UncellifyOptions = {
getter: <T>(c: AnyCell<T>) => Pending<T, boolean> | CellResult<T, boolean>;
errorsAsValues?: boolean;
};

/**
* 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>(
v: T | AnyCell<T>
v: T | AnyCell<T>,
options: UncellifyOptions = { getter: (cell) => cell.consolidatedValue }
): Promise<Uncellified<T>> => {
const value = v instanceof Cell ? await v.consolidatedValue : v;
if (value instanceof Error) throw value;
const value = v instanceof Cell ? await options.getter(v) : v;
if (value instanceof Error) {
if (options?.errorsAsValues) return value as Uncellified<T>;
throw value;
}
if (Array.isArray(value))
return Promise.all(value.map((_element) => uncellify(_element))) as Promise<
Uncellified<T>
>;
return Promise.all(
value.map((_element) => uncellify(_element, options))
) 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, options)
])
)
);
// Classes, null or base types (string, number, ...)
Expand Down
30 changes: 30 additions & 0 deletions src/debouncer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { ValueCell } from "./cell";

export type Debouncer = <T>(cb: (v: T) => void | Promise<void>, v: T) => void;

/**
* debouncer creates a debounce function that will execute a callback after a _delay_.
*
* Create with `const debounce = debouncer()`
* and use as `debounce(cb, v, delay)`.
* @param cb callback
* @param v value passed to callback
* @param delay optional delay in ms, default: 750
*/
export const debouncer = (
delay = 750,
working: ValueCell<boolean> | undefined = undefined
): Debouncer => {
// console.log({ setting: delay });
let timer: ReturnType<typeof setTimeout>;
return <T>(cb: (v: T) => void | Promise<void>, v: T) => {
// console.log({ called: delay });
if (working !== undefined) working.set(true);
clearTimeout(timer);
timer = setTimeout(async () => {
// console.log({ deb: delay });
await cb(v);
if (working !== undefined) working.set(false);
}, delay);
};
};
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,14 @@ export {
} from "./cellify";
export { clock, clockWork, type Clock } from "./clock";
export { copy } from "./copy";
export { debouncer, type Debouncer } from "./debouncer";
export { Debugger } from "./debug";
export { initialValue } from "./initial";
export { jsonStringify } from "./json";
export { nextSubscriber } from "./next";
export {
asyncReduce,
flattenObject,
mapObject,
reduceObject,
type CellObject
Expand Down
19 changes: 19 additions & 0 deletions src/initial.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { AnyCell, MapCell } from "./cell";
import type { SheetProxy } from "./proxy";

export const initialValue = <T>(
proxy: SheetProxy,
v0: T | AnyCell<T>,
v: AnyCell<T>,
name = "initial"
): MapCell<T, true> => {
const cell = proxy.new(v0, name);
v.subscribe((v) => {
// We do not propagate errors yet.
if (v instanceof Error) return;
cell.set(v);
});
// We fake being a MapCell to prevent setting the cell
// outside of this function.
return cell as unknown as MapCell<T, true>;
};
2 changes: 1 addition & 1 deletion src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const jsonStringify = <T>(
case "symbol":
break;
case "bigint":
out += v < 1_000_000n ? Number(v) : `"${v.toString()}"`;
out += v < 1_000_000_000n ? Number(v) : `"${v.toString()}"`;
break;
default:
out += JSON.stringify(v);
Expand Down
21 changes: 18 additions & 3 deletions src/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ 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 { asyncReduce, flattenObject, mapObject, reduceObject } from "./object";
import { delayed } from "./promise";
import { SheetProxy } from "./proxy";
import { Sheet } from "./sheet";

test("mapObject", async () => {
const sheet = new Sheet();
const sheet = new Sheet(isEqual);
const proxy = new SheetProxy(sheet);

const obj = cellify(proxy, { a: 1, b: "foo", c: "bar" });
Expand Down Expand Up @@ -60,7 +60,7 @@ test("asyncReduce", async () => {
test(
"reduceObject",
async () => {
const sheet = new Sheet();
const sheet = new Sheet(isEqual);
const debug = new Debugger(sheet);
const proxy = new SheetProxy(sheet);

Expand Down Expand Up @@ -89,3 +89,18 @@ test(
},
{ timeout: 1000 }
);

test("flattenObject", async () => {
const sheet = new Sheet(isEqual);
const debug = new Debugger(sheet);
const proxy = new SheetProxy(sheet);

const obj = cellify(proxy, { a: 1, b: 2, c: 3 });
const f = flattenObject(proxy, obj);
await expect(f.consolidatedValue).resolves.toEqual({ a: 1, b: 2, c: 3 });
expect(sheet.stats).toEqual({ count: 6, size: 6 }); // 3+1 obj +1 flatten +1 pointer

await (await obj.consolidatedValue).a.set(4);
await expect(f.get()).resolves.toEqual({ a: 4, b: 2, c: 3 });
expect(sheet.stats).toEqual({ count: 6, size: 6 }); // unchanged
});
28 changes: 25 additions & 3 deletions src/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export type CellObject<T> = AnyCell<Record<string, AnyCell<T>>>;
export const mapObject = <T, U, NF extends boolean = false>(
proxy: SheetProxy,
obj: CellObject<T>,
// @todo return type
fn: (
key: string,
value: T,
Expand All @@ -22,7 +21,7 @@ export const mapObject = <T, U, NF extends boolean = false>(
): MapCell<Record<string, AnyCell<U>>, NF> =>
proxy.map(
[obj],
(cells, prev) => {
async (cells, prev) => {
const set = new Set(Object.keys(prev || {}));
const res = Object.fromEntries(
Object.entries(cells).map(([k, v]) => {
Expand Down Expand Up @@ -83,7 +82,7 @@ export const reduceObject = <T, R, NF extends boolean = false>(
const coll = collector<MapCell<R, NF>>(proxy);
return proxy.mapNoPrevious(
[obj],
(cells) => {
async (cells) => {
const keys = Object.keys(cells);
const values = Object.values(cells);
// console.log({ reduce: keys, name, count: proxy._sheet.stats.count });
Expand All @@ -104,3 +103,26 @@ export const reduceObject = <T, R, NF extends boolean = false>(
nf
);
};

export const flattenObject = <T, NF extends boolean = false>(
proxy: SheetProxy,
obj: CellObject<T>,
name = "flatten",
nf?: NF
) => {
const coll = collector<MapCell<Record<string, T>, NF>>(proxy);
return proxy.mapNoPrevious(
[obj],
async (cells) => {
const keys = Object.keys(cells);
const values = Object.values(cells);
return coll(
proxy.mapNoPrevious(values, (..._cells) =>
Object.fromEntries(_cells.map((v, i) => [keys[i], v]))
)
);
},
name,
nf
);
};
27 changes: 18 additions & 9 deletions src/proxy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,26 @@ test("native proxy", () => {
expect(trigger).toBeTruthy();
});

test("SheetProxy", () => {
const store = new Sheet();
const value = store.new(2);
const proxy = new SheetProxy(store);
test("SheetProxy", async () => {
const sheet = new Sheet();
const value = sheet.new(2);
const proxy = new SheetProxy(sheet);
const double = proxy.map([value], (x) => 2 * x);
const add = proxy.map([double], (x) => x + 1);
proxy.destroy();
// Force GC collection
sheet.collection();
await proxy.working.wait();
value.set(3); // will not update detached/deleted cells
expect(double.value).toBe(4);
expect(add.value).toBe(5);
});

test("Sheet multiple async updates", async () => {
const store = new Sheet();
const value = store.new(2);
const sheet = new Sheet();
const value = sheet.new(2);
const double = value.map(async (x) => delayed(2 * x, 50));
const add = store.map([value, double], (value, double) =>
const add = sheet.map([value, double], (value, double) =>
delayed(value + double + 1, 30)
);
// console.log("value", value.id, "double", double.id, "add", add.id);
Expand All @@ -54,7 +57,7 @@ test("Sheet multiple async updates", async () => {
value.set(3);
expect(await add.consolidatedValue).toBe(10);
value.set(4);
await store.wait();
await sheet.wait();
expect(await add.consolidatedValue).toBe(13);
});

Expand Down Expand Up @@ -98,9 +101,13 @@ test("proxy deletion", async () => {
const c = sub.map([a, b], async (a, b) => a + b);
expect(sheet.stats).toEqual({ count: 3, size: 3 });
sub.destroy();
await proxy.working.wait();
expect(sheet.stats).toEqual({ count: 3, size: 2 });
await expect(b.get()).resolves.toBe(2);
proxy.destroy();
// Force collection.
sheet.collection();
// Collection is not happening yet.
expect(sheet.stats).toEqual({ count: 3, size: 0 });
});

Expand All @@ -113,5 +120,7 @@ test("proxy deletion with loop", async () => {
const c = sub.map([a, b], async (a, b) => delayed(a + b, 5));
const d = proxy.map([c], async (v) => delayed(v * 2, 15));
expect(sheet.stats).toEqual({ count: 4, size: 4 });
expect(() => sub.destroy()).toThrow("Cell has references");
// Since we now collect the whole subgraph, there is no error.
sub.destroy();
// expect(() => sub.destroy()).toThrow("Cell has references");
});
31 changes: 26 additions & 5 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,31 @@ export class SheetProxy {
name?: string,
noFail?: NF
): MapCell<V, NF>;
map<D1, D2, D3, D4, D5, D6, D7, D8, V, NF extends boolean = false>(
dependencies: [
AnyCell<D1>,
AnyCell<D2>,
AnyCell<D3>,
AnyCell<D4>,
AnyCell<D5>,
AnyCell<D6>,
AnyCell<D7>,
AnyCell<D8>
],
computeFn: (
arg1: D1,
arg2: D2,
arg3: D3,
arg4: D4,
arg5: D5,
arg6: D6,
arg7: D7,
arg8: D8,
prev?: V
) => V | Promise<V | AnyCell<V>> | AnyCell<V>,
name?: string,
noFail?: NF
): MapCell<V, NF>;

/**
* map a list to cells to a new cells using the compute function.
Expand Down Expand Up @@ -250,13 +275,9 @@ export class SheetProxy {

/**
* destroy the Proxy and free memory.
* @todo check for memory leaks
*/
destroy() {
// if (!this?._sheet) {
// throw new Error(`missing: ${this?._name}`);
// }
this._sheet.delete(...this._list);
this._sheet.collect(...this._list);
this._list = [];
}
}
Loading

0 comments on commit 928b403

Please sign in to comment.