Skip to content

Commit

Permalink
feat(bettermath): Add ref support
Browse files Browse the repository at this point in the history
  • Loading branch information
imnotteixeira committed Feb 24, 2024
1 parent 1e38b91 commit 1e6aa22
Show file tree
Hide file tree
Showing 15 changed files with 14,998 additions and 1,436 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
dist
dist
.vscode
2 changes: 1 addition & 1 deletion commitlint.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module.exports = {
export default {
extends: ['@commitlint/config-conventional'],
rules: {
'subject-case': [2, 'always', 'sentence-case'],
Expand Down
16,135 changes: 14,831 additions & 1,304 deletions package-lock.json

Large diffs are not rendered by default.

22 changes: 16 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,32 @@
"scripts": {
"build": "tsc",
"bundle": "rollup -c",
"run:bundle": "node ./dist/index.js",
"start": "ts-node ./src/index.ts",
"dev": "ts-node-dev --respawn ./src/index.ts",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"prepublish": "npm run build"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@commitlint/cli": "^17.0.3",
"@commitlint/config-conventional": "^17.0.3",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.3",
"@rollup/plugin-typescript": "^11.1.2",
"@rollup/plugin-typescript": "^11.1.5",
"@semantic-release/git": "^10.0.1",
"@types/node": "^17.0.5",
"rollup": "^3.26.2",
"rollup-plugin-typescript2": "^0.35.0",
"ts-node": "^10.4.0",
"semantic-release": "^21.0.7",
"ts-node": "^10.9.1",
"ts-node-dev": "^1.1.8",
"tslib": "^2.6.0",
"typescript": "^4.9.5"
"typescript": "^5.2.2"
},
"dependencies": {
"@tecido/bettermath": "^0.5.1",
"immutable": "^5.0.0-beta.4"
}
}
File renamed without changes.
4 changes: 3 additions & 1 deletion rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import typescript from '@rollup/plugin-typescript';
import terser from '@rollup/plugin-terser';
import commonjs from '@rollup/plugin-commonjs';
import { nodeResolve } from '@rollup/plugin-node-resolve';

export default {
input: 'src/index.ts',
Expand All @@ -8,7 +10,7 @@ export default {
format: 'es',
plugins: [terser()]
},
plugins: [typescript()],
plugins: [typescript(), nodeResolve(), commonjs()],
treeshake: {
moduleSideEffects: false
}
Expand Down
35 changes: 0 additions & 35 deletions src/functions/Addition.ts

This file was deleted.

9 changes: 5 additions & 4 deletions src/functions/DataFunction.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { INode } from "../model/Node";
import { ValueResolvingResult } from "./adapter/bettermath";

type ComputeFunction<T> = (dependencies: Map<string, INode<T> | undefined>) => T | undefined;
type ComputeFunction<T> = (dependencies: Map<string, INode<T> | undefined>) => ValueResolvingResult<T | undefined>;

export type DefinitionParsingResults<T> = {
compute: ComputeFunction<T>,
dependencies: Set<string>
dependencyIds: Set<string>
}
export interface IDefinitionParser<T> {
parseDefinition: (def: string) => DefinitionParsingResults<T>
Expand All @@ -28,10 +29,10 @@ export class DataFunction<T> implements IDataFunction<T> {
try{
return parsingResults.compute(...args);
} catch(e) {
return undefined
return ValueResolvingResult.error(e as Error)
}
}
this.dependencyIds = parsingResults.dependencies;
this.dependencyIds = parsingResults.dependencyIds;
}

}
61 changes: 50 additions & 11 deletions src/functions/adapter/bettermath.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,65 @@
import { FunctionRegistry, default as buildGrammar} from "@tissue/bettermath"
import { IValueType, IExpressionType, BettermathGrammarParser, ValueResolvingResult as BettermathValueResolvingResult, IBaseType} from "@tecido/bettermath"
import { IDataFunction } from "../DataFunction";
import { INode } from "../../model/Node";
import { getRefIds } from "../grammar/bettermath";

class BettermathDataFunction implements IDataFunction<IValueType<any>> {
export class ValueResolvingResult<T> {
readonly isError: boolean;
readonly value?: T;
readonly error?: Error;

private constructor(isError: boolean, result?: {value? : T, error?: Error}) {
this.isError = isError;
this.value = result?.value;
this.error = result?.error;
}

static success = <T>(value: T) => new ValueResolvingResult(false, {value});

static error = <T>(error: Error) => new ValueResolvingResult<T>(true, {error});

static fromBettermath = <T>(valueResolvingResult: BettermathValueResolvingResult<T>) =>
new ValueResolvingResult(valueResolvingResult.isError, {
value: valueResolvingResult.value,
error: valueResolvingResult.error,
})

get = () => {
if (this.isError) {
throw new Error(
"The ValueResolvingResult is an error. Cannot get value. Inner error is: "
+ this.error
);
}

return this.value as T;
}

getOrElse = (elseValue: any) => this.isError ? elseValue : this.value;

getError = () => this.error;

toString = () => `ValueResolvingResult(${this.isError? "[ERROR]" : this.value})`
}

class BettermathDataFunction implements IDataFunction<any> {
parsedExpression: IExpressionType<any>;
dependencyIds: Set<string>;

// TODO: Inject grammar
constructor(grammar: P.Parser<IExpressionType<any>>, fn: string) {
constructor(grammar: BettermathGrammarParser, fn: string) {
// TODO: Handle parsing error
this.parsedExpression = grammar.tryParse(fn)
// TODO: Add REF() support to extract ids from expressions
this.dependencyIds = this.parsedExpression.getRefs()
this.dependencyIds = new Set(getRefIds(this.parsedExpression).toJS().map(({column, line}) => `${column}${line}`))
}

compute: (dependencies: Map<string, INode<IValueType<any>> | undefined>) => {
// TODO: Add support for REF() value injection.
// Might be better to pass <refId, node.data> pairs directly, to avoid extra recomputation
return parsedExpression.getValue(refMap)
compute = (dependencies: Map<string, INode<any> | undefined>) => {
// TODO: Might be worth it introducing the concept of async compute for functions
// (as it can take some time to resolve REF values to compute this function value)
const dependencyValues: Map<string, any> = new Map(Array.from(dependencies)
.map(([refId, refNode]) => ([refId, refNode?.data.get()])))
return ValueResolvingResult.fromBettermath(this.parsedExpression.getValue(dependencyValues))
};


}

export default BettermathDataFunction;
17 changes: 17 additions & 0 deletions src/functions/grammar/bettermath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { BettermathGrammarParser, FunctionRegistry, IBaseType, IExpressionType, Types, buildGrammar } from '@tecido/bettermath';
import { IRef, parseRefId } from '../../model/Ref';
import { Set, MapOf } from "immutable";
import { IRefType } from '@tecido/bettermath';

export default (): BettermathGrammarParser => {
const functionRegistry: FunctionRegistry = new FunctionRegistry();
return buildGrammar(functionRegistry)
}

export const getRefIds = (expression: IExpressionType<any>): Set<MapOf<IRef>> => {
const refIds = expression
.find((elem: IBaseType<any>) => elem.type === Types.REF)
.map((refNode: IBaseType<any>) => parseRefId((refNode as IRefType).value))

return Set(refIds);
}
56 changes: 2 additions & 54 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,7 @@
import { DumbNumberSumDefinitionParser } from "./functions/Addition";
import { DataFunction } from "./functions/DataFunction";
import { Node } from "./model/Node";
import { NodeMesh } from "./model/NodeMesh";
import BettermathDataFunction from "./functions/adapter/bettermath";

const NodeA = new Node<number>("A", new DataFunction<number>("1+1", new DumbNumberSumDefinitionParser()))
const NodeB = new Node<number>("B", new DataFunction<number>("1+A", new DumbNumberSumDefinitionParser()))
const NodeC = new Node<number>("C", new DataFunction<number>("A+B", new DumbNumberSumDefinitionParser()))

const grid = new NodeMesh([
NodeA,
NodeB,
NodeC,
])

console.info("Iteration #0")
grid.printNodes()

console.info("Iteration #1 :: Change Add Node D")
const NodeD = new Node<number>("D", new DataFunction<number>("C+E", new DumbNumberSumDefinitionParser()))
grid.addNode(NodeD)
grid.printNodes()

console.info("Iteration #2 :: Change Add Node E")
const NodeE = new Node<number>("E", new DataFunction<number>("1+A", new DumbNumberSumDefinitionParser()))
grid.addNode(NodeE)
grid.printNodes()

console.info("Iteration #3 :: Change A to 0")
NodeA.setDataFunction(new DataFunction<number>("0+0", new DumbNumberSumDefinitionParser()))
grid.printNodes()

console.info("Iteration #4 :: Add Node F")
const NodeF = new Node<number>("F", new DataFunction<number>("1+0", new DumbNumberSumDefinitionParser()))
grid.addNode(NodeF)
grid.printNodes()

console.info("Iteration #5 :: Change A to undefined (Simulate Error)")
NodeA.setDataFunction(new DataFunction<number>("", new DumbNumberSumDefinitionParser()))
grid.printNodes()

console.info("Iteration #5 :: Change A to good value, but C to undefined (Simulate Error)")
NodeA.setDataFunction(new DataFunction<number>("1+1", new DumbNumberSumDefinitionParser()))
NodeC.setDataFunction(new DataFunction<number>("", new DumbNumberSumDefinitionParser()))
grid.printNodes()

console.info("Iteration #6 :: Revert C to A+B; Put C to sleep and change A (dependency)")
NodeC.setDataFunction(new DataFunction<number>("A+B", new DumbNumberSumDefinitionParser()))
grid.printNodes()

NodeC.sleep()
NodeA.setDataFunction(new DataFunction<number>("1+2", new DumbNumberSumDefinitionParser()))
grid.printNodes()

console.info("Iteration #7 :: Wake up C (should reconcile its data with the new values from A and B)")
NodeC.wakeUp()
grid.printNodes()

export {NodeMesh, Node, DataFunction, DumbNumberSumDefinitionParser}
export { NodeMesh, Node, DataFunction, BettermathDataFunction }
38 changes: 24 additions & 14 deletions src/model/Node.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { IDataFunction } from "../functions/DataFunction";
import { ValueResolvingResult } from "../functions/adapter/bettermath";

export type UpdatePropagator<T> = (notifierId: string, deps: Set<string>, data: T | undefined) => void;
export type UpdatePropagator<T> = (notifierId: string, deps: Set<string>, data: ValueResolvingResult<T | undefined>) => void;

export interface INode<T> {
id: string,
hasOutdatedValue: boolean,
data: T | undefined,
data: ValueResolvingResult<T | undefined>,
dataFn: IDataFunction<T> | undefined,
setDataFunction: (_: IDataFunction<T>) => T | undefined,
setDataFunction: (_: IDataFunction<T>) => ValueResolvingResult<T | undefined>,
setUpdatePropagator: (updatePropagator: UpdatePropagator<T>) => INode<T>,
setNodeFetcher: (fn: (id: string) => INode<T> | undefined) => INode<T>,
dependents: Set<string>,
dependencies: Map<string, INode<T> | undefined>,
notify: (id: string, data: T | undefined) => void,
notify: (id: string, data: ValueResolvingResult<T | undefined>) => void,
propagateUpdate: UpdatePropagator<T>,
subscribe: (dependentId: string) => void,
unsubscribe: (dependentId: string) => void,
triggerDataRecomputation: () => T | undefined,
triggerDataRecomputation: () => ValueResolvingResult<T | undefined>,
triggerDataReconciliation: () => void,
requestSubscription: (requesterId: string, targetId: string) => void
setSubscriptionRequester: (fn: (requesterId: string, targetId: string) => void) => INode<T>,
Expand All @@ -35,7 +36,7 @@ enum PropagatorState {

export class Node<T> implements INode<T> {
id: string;
data: T | undefined;
data: ValueResolvingResult<T | undefined>;
dataFn: IDataFunction<T> | undefined;
hasOutdatedValue: boolean = false
dependents: Set<string>;
Expand All @@ -58,7 +59,11 @@ export class Node<T> implements INode<T> {
this.id = id;
this.dependents = new Set();
this.dataFn = dataFn;
this.data = dataFn.compute(this.dependencies);

// Nodes are lazy. Data starts as undefined, setting this.hasOutdatedValue
// Once they are used in a NodeMesh, their values will be calculated as neeeded.
this.data = ValueResolvingResult.success(undefined)
this.hasOutdatedValue = true;
}

/** Updates this node with the propagator (should come from NodeMesh) */
Expand Down Expand Up @@ -86,18 +91,23 @@ export class Node<T> implements INode<T> {
}

/** Modify self data and notify dependents */
setDataFunction = (fn?: IDataFunction<T>): T | undefined => {
setDataFunction = (fn?: IDataFunction<T>): ValueResolvingResult<T | undefined> => {

// TODO: Prevent cycle dependencies (need to check if this node is anywhere above in the dependency graph)
this.dataFn = fn;
return this.triggerDataRecomputation()
}

computeData = (): T | undefined => {
getData = (): ValueResolvingResult<T | undefined> => this.data;

computeData = (): ValueResolvingResult<T | undefined> => {

if(this.propagationState === PropagatorState.SLEEPING) return this.data;

if(this.propagationState === PropagatorState.SLEEPING) return;
this.data = this.dataFn
? this.dataFn.compute(this.dependencies)
: ValueResolvingResult.success<T | undefined>(undefined);

this.data = this.dataFn?.compute(this.dependencies);
this.hasOutdatedValue = false
// TODO optimization candidate: batching updates
// - Do not notify every time this changes right away, wait some time
Expand Down Expand Up @@ -157,20 +167,20 @@ export class Node<T> implements INode<T> {
if(this.hasOutdatedValue) this.computeData()
}

triggerDataRecomputation = (): T | undefined => {
triggerDataRecomputation = (): ValueResolvingResult<T | undefined> => {
console.info(`[Node ${this.id}] Recomputing data...`)
this.dependencies = this.reconcileDependencies(this.dataFn?.dependencyIds);
console.info(`[Node ${this.id}] Dependency values:`)

for (const [id, dep] of this.dependencies) {
console.info(`[Node ${this.id}]\t${id} => ${dep?.data}`)
console.info(`[Node ${this.id}]\t${id} => ${dep?.data.getOrElse("<NO VALUE>")}`)
}

return this.computeData();
}

/** Receive a notificaton when a dependency changes */
notify = (dependencyId: string, newValue: T | undefined): void => {
notify = (dependencyId: string, newValue: ValueResolvingResult<T | undefined>): void => {
console.info(`[Node ${this.id}] Dependency [Node ${dependencyId}] changed to ${newValue}. Updating value...`)
this.computeData()
}
Expand Down
Loading

0 comments on commit 1e6aa22

Please sign in to comment.