Skip to content

Commit

Permalink
Object utilities + follow (#29)
Browse files Browse the repository at this point in the history
* object: mapObject, reduceObject

* object: cellifyObject

* object: extend cellifyObject test to subscribers

* object: fix cellifyObject type

* gitignore: update

* index: export object utilities

* object: improve cellifyObject test

* cell: fullName

* object: add CellObject<T> type variable

* sheet: fix dependentCells

* object: remove cellifyObject

* object: use mapNoPrevious in reduceObject

* object: clean imports

* cellify: isObject

* cellify: follow, type Path

* cellify: fix follow

* json: jsonStringify failOnCell

* json: use toString for classes

* array: update mapArray return type

* types: fix import/export

* cellify: use consolidatedValue in _uncellify

* cellify: optional name in _cellify

* array: reduce function has length as 4th argument

* array: mapArray provides cell as 3rd argument

* cellify: type Key

* fix(build): tsc linting

* package: bump version

---------

Co-authored-by: Cheun Marec <cheun@okcontract.io>
  • Loading branch information
hbbio and xikimay committed Apr 14, 2024
1 parent e41cee7 commit c4b1f58
Show file tree
Hide file tree
Showing 22 changed files with 370 additions and 59 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.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@okcontract/cells",
"version": "0.2.3",
"version": "0.2.4",
"description": "Simplified reactive functional programming for the web",
"private": false,
"main": "dist/cells.umd.cjs",
Expand Down
23 changes: 18 additions & 5 deletions src/array.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type AnyCell, MapCell, ValueCell } from "./cell";
import { type AnyCell, type MapCell, ValueCell } from "./cell";
import { collector, reuseOrCreate } from "./gc";
import type { SheetProxy } from "./proxy";

Expand All @@ -18,12 +18,17 @@ export type CellArray<T> = AnyCell<AnyCell<T>[]>;
export const mapArray = <T, U>(
proxy: SheetProxy,
arr: CellArray<T>,
fn: (v: T, index?: number) => U | Promise<U>,
fn: (
v: T,
index?: number,
cell?: AnyCell<T>
) => U | Promise<U | AnyCell<U>> | AnyCell<U>,
name = "map"
): MapCell<MapCell<U, false>[], false> =>
proxy.map(
[arr],
(cells, prev) => {
if (!cells) return [];
const set = new Set((prev || []).map((cell) => cell.id));
const res = cells.map((cell, index) => {
// reuse previously mapped cell
Expand All @@ -32,7 +37,11 @@ export const mapArray = <T, U>(
return (
reuse ||
// create new map
proxy.map([cell], (_cell) => fn(_cell, index), `[${index}]`)
proxy.map(
[cell],
(_cell) => fn(_cell, index, cell),
`${cell.id}:[${index}]`
)
);
});
// collect unused previously mapped cells
Expand Down Expand Up @@ -166,7 +175,7 @@ export const reduce = <
>(
proxy: SheetProxy,
arr: CellArray<T>,
fn: (acc: R, elt: T, index?: number) => R,
fn: (acc: R, elt: T, index?: number, length?: number) => R,
init: R,
name = "reduce",
nf?: NF
Expand All @@ -178,7 +187,11 @@ export const reduce = <
coll(
proxy.mapNoPrevious(
cells,
(..._cells) => _cells.reduce(fn, init),
(..._cells) =>
_cells.reduce(
(acc, elt, i) => fn(acc, elt, i, _cells.length),
init
),
"_reduce"
)
),
Expand Down
9 changes: 6 additions & 3 deletions src/cell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ const DEBUG_RANK = false;
import { CellError } from "./errors";
import { dispatch, dispatchPromiseOrValueArray } from "./promise";
import { SheetProxy } from "./proxy";
import { Sheet } from "./sheet";
import { type Unsubscriber } from "./types";
import type { Sheet } from "./sheet";
import type { Unsubscriber } from "./types";

let idCounter = 0;

Expand Down 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
4 changes: 2 additions & 2 deletions src/cellWithClass.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { expect, test } from "vitest";

import { type AnyCell } from "./cell";
import type { AnyCell } from "./cell";
import { delayed } from "./promise";
import { SheetProxy } from "./proxy";
import { Sheet } from "./sheet";
import { type Unsubscriber } from "./types";
import type { Unsubscriber } from "./types";
import { WrappedCell } from "./wrapped";

const unwrappedCell = (
Expand Down
33 changes: 32 additions & 1 deletion src/cellify.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { expect, test } from "vitest";

import { Cellified, Uncellified, _cellify, _uncellify } from "./cellify";
import {
type Cellified,
type Uncellified,
_cellify,
_uncellify,
follow
} from "./cellify";
import { SheetProxy } from "./proxy";
import { Sheet } from "./sheet";

Expand Down Expand Up @@ -45,3 +51,28 @@ test("_cellify one", async () => {
const cell = await res.get();
await expect(cell.a.get()).resolves.toBe(1);
});

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 f = follow(proxy, cv, ["a", 1]);
await expect(f.get()).resolves.toBe(2);
expect(sheet.stats).toEqual({ size: 12, count: 12 });

// update the cell directly
(await (await cv.get()).a.get())[1].set(4);
await expect(f.get()).resolves.toBe(4);
expect(sheet.stats).toEqual({ size: 12, count: 12 }); // unchanged

// prepend a new cell in array
(await cv.get()).a.update((l) => [proxy.new(0), ...l]);
await expect(f.get()).resolves.toBe(1);
expect(sheet.stats).toEqual({ size: 13, count: 14 }); // one new cell, update one pointer

// delete path, cell is error
(await cv.get()).a.set([]);
await expect(f.get()).resolves.toBeInstanceOf(Error);
expect(sheet.stats).toEqual({ size: 13, count: 14 }); // unchanged
});
68 changes: 51 additions & 17 deletions src/cellify.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AnyCell, Cell, ValueCell } from "./cell";
import { SheetProxy } from "./proxy";
import { type AnyCell, Cell, type MapCell, type ValueCell } from "./cell";
import { collector } from "./gc";
import type { SheetProxy } from "./proxy";

// Cellified computes a cellified type.
export type Cellified<T> = T extends object
Expand All @@ -21,6 +22,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 @@ -29,22 +37,21 @@ export type Uncellified<T> = T extends AnyCell<infer U>
* @returns
* @todo cell reuses
*/
export const _cellify = <T>(proxy: SheetProxy, v: T): Cellified<T> => {
export const _cellify = <T>(
proxy: SheetProxy,
v: T,
name = "cellify"
): Cellified<T> => {
if (v instanceof Cell) throw new Error("cell");
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)],
"cellify.{}"
)
Object.entries(v).map(([k, vv]) => [k, _cellify(proxy, vv)], "ç{}")
)
: v,
"cellify"
name
) as Cellified<T>;
};

Expand All @@ -56,17 +63,13 @@ export const _cellify = <T>(proxy: SheetProxy, v: T): Cellified<T> => {
export const _uncellify = async <T>(
v: T | AnyCell<T>
): Promise<Uncellified<T>> => {
const value = v instanceof Cell ? await v.get() : v;
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>>;
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 All @@ -75,3 +78,34 @@ export const _uncellify = async <T>(
// Classes, null or base types (string, number, ...)
return value as Uncellified<T>;
};

export type Key = string | number;
export type Path = Key[];

/**
* follow a static path for a Cellified value.
*/
export const follow = (
proxy: SheetProxy,
v: Cellified<unknown>,
path: Path,
name = "follow"
) => {
const aux = (v: Cellified<unknown>, path: Path, name: string) => {
// @todo multi collector?
const coll = collector<MapCell<unknown, false>>(proxy);
return proxy.map(
[v],
(_v) => {
const key = path[0];
const isContainer = Array.isArray(_v) || isObject(_v);
if (isContainer && _v[key])
return coll(aux(_v[key], path.slice(1), `${name}.${key}`));
if (isContainer) throw new Error(`path not found: ${key}`);
return v; // pointer
},
name
);
};
return aux(v, path, name);
};
2 changes: 1 addition & 1 deletion src/deadlock.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test } from "vitest";

import { type AnyCell } from "./cell";
import type { AnyCell } from "./cell";
import { delayed } from "./promise";
import { SheetProxy } from "./proxy";
import { Sheet } from "./sheet";
Expand Down
19 changes: 18 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,25 @@ export {
mapArray,
mapArrayCell,
reduce,
sort
sort,
type CellArray
} from "./array";
export {
_cellify,
_uncellify,
follow,
isObject,
type Cellified,
type Key,
type Path,
type Uncellified
} from "./cellify";
export { Debugger } from "./debug";
export { jsonStringify } from "./json";
export { nextSubscriber } from "./next";
export {
asyncReduce,
mapObject,
reduceObject,
type CellObject
} from "./object";
10 changes: 8 additions & 2 deletions src/json.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Cell } from "./cell";
import { isObject } from "./cellify";

const errorCell = new Error("cell");

Expand All @@ -9,9 +10,12 @@ const errorCell = new Error("cell");
*/
export const jsonStringify = <T>(
obj: T,
options: { skipNull?: boolean } = {}
options: { skipNull?: boolean; failOnCell?: boolean } = {}
) => {
if (obj instanceof Cell) throw errorCell;
if (obj instanceof Cell) {
if (options?.failOnCell) throw errorCell;
return jsonStringify(obj.value, options);
}
let out = "";
const aux = <T>(v: T) => {
if (Array.isArray(v)) {
Expand All @@ -31,6 +35,8 @@ export const jsonStringify = <T>(
out += "null";
break;
}
// use toString for classes
if (!isObject(v)) out += JSON.stringify(v.toString());
out += "{";
let first = true;
// sort objects alphabetically
Expand Down
2 changes: 1 addition & 1 deletion src/load.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test } from "vitest";

import { type AnyCell, Cell, ValueCell } from "./cell";
import type { AnyCell, Cell, ValueCell } from "./cell";
import { SheetProxy } from "./proxy";
import { Sheet } from "./sheet";

Expand Down
4 changes: 2 additions & 2 deletions src/next.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AnyCell } from "./cell";
import { Unsubscriber } from "./types";
import type { AnyCell } from "./cell";
import type { Unsubscriber } from "./types";

/**
* nextSubscriber subscribes to get the next value of a cell. This is useful
Expand Down
Loading

0 comments on commit c4b1f58

Please sign in to comment.