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

Object utilities + follow #29

Merged
merged 27 commits into from
Apr 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
a2cc59a
object: mapObject, reduceObject
hbbio Mar 14, 2024
3d48eab
object: cellifyObject
hbbio Mar 15, 2024
bba417b
object: extend cellifyObject test to subscribers
hbbio Mar 15, 2024
59825c4
object: fix cellifyObject type
hbbio Mar 15, 2024
bc84141
gitignore: update
hbbio Mar 16, 2024
86157cb
index: export object utilities
hbbio Mar 16, 2024
9c00fbb
object: improve cellifyObject test
hbbio Mar 16, 2024
afff40f
cell: fullName
hbbio Mar 16, 2024
18f1312
object: add CellObject<T> type variable
hbbio Apr 4, 2024
2c61252
sheet: fix dependentCells
hbbio Apr 4, 2024
eaea179
object: remove cellifyObject
hbbio Apr 4, 2024
70748af
object: use mapNoPrevious in reduceObject
hbbio Apr 4, 2024
b3ada91
object: clean imports
hbbio Apr 4, 2024
8eac607
cellify: isObject
hbbio Apr 4, 2024
340a051
cellify: follow, type Path
hbbio Apr 4, 2024
818b8b1
cellify: fix follow
hbbio Apr 8, 2024
8aa2cbd
json: jsonStringify failOnCell
hbbio Apr 10, 2024
8a3c035
json: use toString for classes
hbbio Apr 10, 2024
0efb631
array: update mapArray return type
hbbio Apr 10, 2024
a4f59f4
types: fix import/export
hbbio Apr 10, 2024
70e780d
cellify: use consolidatedValue in _uncellify
hbbio Apr 10, 2024
d96ff86
cellify: optional name in _cellify
hbbio Apr 10, 2024
fe6ac26
array: reduce function has length as 4th argument
hbbio Apr 11, 2024
7314aba
array: mapArray provides cell as 3rd argument
hbbio Apr 12, 2024
0f418c2
cellify: type Key
hbbio Apr 14, 2024
c49f25f
fix(build): tsc linting
xikimay Mar 19, 2024
f23016d
package: bump version
hbbio Apr 14, 2024
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
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
Loading