Skip to content

Commit

Permalink
Merge pull request #35 from okcontract/initial-value
Browse files Browse the repository at this point in the history
feature: initialValue
  • Loading branch information
hbbio committed May 26, 2024
2 parents c1a0eaa + d56f679 commit f58b563
Show file tree
Hide file tree
Showing 9 changed files with 92 additions and 27 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
13 changes: 9 additions & 4 deletions src/cellify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,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 @@ -72,6 +73,7 @@ export const cellify = <T>(

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

/**
Expand All @@ -81,10 +83,13 @@ export type UncellifyOptions = {
*/
export const uncellify = async <T>(
v: T | AnyCell<T>,
options: UncellifyOptions = { getter: (cell) => cell.value }
options: UncellifyOptions = { getter: (cell) => cell.consolidatedValue }
): Promise<Uncellified<T>> => {
const value = v instanceof Cell ? await options.getter(v) : v;
if (value instanceof Error) throw value;
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, options))
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);
};
};
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ 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 {
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
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");
});
6 changes: 1 addition & 5 deletions src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,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 = [];
}
}
18 changes: 11 additions & 7 deletions src/sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,12 +567,7 @@ export class Sheet {
this._internalNotify(_result.done);

// Collect garbage
if (this._gc.size) {
const l = Array.from(this._gc);
this.debug(l, "gc", { deleting: l });
this._gc = new Set();
this.delete(...l);
}
this.collection();

// End of the update
release();
Expand All @@ -592,6 +587,15 @@ export class Sheet {
);
}

collection() {
if (this._gc.size) {
const l = Array.from(this._gc);
this.debug(l, "gc", { deleting: l });
this._gc = new Set();
this.delete(...l);
}
}

private registerCancelAndDone<V>(
updatable: number[],
computations: (V | Canceled | Error)[],
Expand Down Expand Up @@ -957,7 +961,7 @@ export class Sheet {
this.debug(ids, "collect", { collecting: ids, cells: input });
for (const id of ids) {
const deps = this.g.partialTopologicalSort(id);
this.debug(undefined, "collect", { deps });
// this.debug(undefined, "collect", { deps });
for (const dep of deps) this._gc.add(dep);
}
}
Expand Down

0 comments on commit f58b563

Please sign in to comment.