Skip to content
Snippets Groups Projects
Commit 408bf48a authored by David Sehnal's avatar David Sehnal
Browse files

mol-state: updated transformer model

parent 5ad983e9
No related branches found
No related tags found
No related merge requests found
...@@ -115,22 +115,32 @@ export namespace State { ...@@ -115,22 +115,32 @@ export namespace State {
} }
} }
async function updateNode(oldTree: TransformTree, tree: TransformTree, objects: Objects, root: Transform.Ref) { async function updateNode(oldTree: TransformTree, tree: TransformTree, objects: Objects, currentRef: Transform.Ref) {
const transform = tree.getValue(root)!; const transform = tree.getValue(currentRef)!;
const parent = findParent(tree, objects, root, transform.transformer.definition.from); const parent = findParent(tree, objects, currentRef, transform.transformer.definition.from);
console.log('parent', parent ? parent.ref : 'undefined') console.log('parent', parent ? parent.ref : 'undefined')
if (!oldTree.nodes.has(transform.ref) || !objects.has(transform.ref)) { if (!oldTree.nodes.has(transform.ref) || !objects.has(transform.ref)) {
console.log('creating...', transform.transformer.id, oldTree.nodes.has(transform.ref), objects.has(transform.ref)); console.log('creating...', transform.transformer.id, oldTree.nodes.has(transform.ref), objects.has(transform.ref));
const obj = await createObject(transform.transformer, parent, transform.params); const obj = await createObject(transform.transformer, parent, transform.params);
obj.ref = transform.ref; obj.ref = transform.ref;
objects.set(root, { obj, state: StateObject.StateType.Ok, version: transform.version }); objects.set(currentRef, { obj, state: StateObject.StateType.Ok, version: transform.version });
} else { } else {
console.log('updating...', transform.transformer.id); console.log('updating...', transform.transformer.id);
const current = objects.get(transform.ref)!.obj; const current = objects.get(transform.ref)!.obj;
const oldParams = oldTree.getValue(transform.ref)!.params; const oldParams = oldTree.getValue(transform.ref)!.params;
await updateObject(transform.transformer, parent, current, oldParams, transform.params); switch (await updateObject(transform.transformer, parent, current, oldParams, transform.params)) {
const obj = objects.get(root)!; case Transformer.UpdateResult.Recreate: {
obj.version = transform.version; const obj = await createObject(transform.transformer, parent, transform.params);
obj.ref = transform.ref;
objects.set(currentRef, { obj, state: StateObject.StateType.Ok, version: transform.version });
break;
}
case Transformer.UpdateResult.Updated: {
const obj = objects.get(currentRef)!;
obj.version = transform.version;
break;
}
}
} }
} }
...@@ -139,15 +149,14 @@ export namespace State { ...@@ -139,15 +149,14 @@ export namespace State {
return t as A; return t as A;
} }
function createObject(transformer: Transformer, parent: StateObject, params: any) { function createObject(transformer: Transformer, a: StateObject, params: any) {
return runTask(transformer.definition.apply(parent, params, 0 as any)); return runTask(transformer.definition.apply({ a, params }));
} }
async function updateObject(transformer: Transformer, parent: StateObject, obj: StateObject, oldParams: any, params: any) { async function updateObject(transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
if (!transformer.definition.update) { if (!transformer.definition.update) {
// TODO return Transformer.UpdateResult.Recreate;
throw 'nyi';
} }
return transformer.definition.update!(parent, oldParams, obj, params, 0 as any); return runTask(transformer.definition.update({ a, oldParams, b, newParams }));
} }
} }
...@@ -11,6 +11,7 @@ import { Transform } from './tree/transform'; ...@@ -11,6 +11,7 @@ import { Transform } from './tree/transform';
export interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> { export interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> {
apply(params?: P, props?: Partial<Transform.Props>): Transform<A, B, P>, apply(params?: P, props?: Partial<Transform.Props>): Transform<A, B, P>,
readonly namespace: string,
readonly id: Transformer.Id, readonly id: Transformer.Id,
readonly definition: Transformer.Definition<A, B, P> readonly definition: Transformer.Definition<A, B, P>
} }
...@@ -21,9 +22,22 @@ export namespace Transformer { ...@@ -21,9 +22,22 @@ export namespace Transformer {
export type To<T extends Transformer<any, any, any>> = T extends Transformer<any, infer B, any> ? B : unknown; export type To<T extends Transformer<any, any, any>> = T extends Transformer<any, infer B, any> ? B : unknown;
export type ControlsFor<Props> = { [P in keyof Props]?: any } export type ControlsFor<Props> = { [P in keyof Props]?: any }
export interface ApplyParams<A extends StateObject = StateObject, P = unknown> {
a: A,
params: P
}
export interface UpdateParams<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> {
a: A,
b: B,
oldParams: P,
newParams: P
}
export enum UpdateResult { Unchanged, Updated, Recreate }
export interface Definition<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> { export interface Definition<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> {
readonly name: string, readonly name: string,
readonly namespace?: string,
readonly from: { type: StateObject.Type }[], readonly from: { type: StateObject.Type }[],
readonly to: { type: StateObject.Type }[], readonly to: { type: StateObject.Type }[],
...@@ -31,16 +45,14 @@ export namespace Transformer { ...@@ -31,16 +45,14 @@ export namespace Transformer {
* Apply the actual transformation. It must be pure (i.e. with no side effects). * Apply the actual transformation. It must be pure (i.e. with no side effects).
* Returns a task that produces the result of the result directly. * Returns a task that produces the result of the result directly.
*/ */
apply(a: A, params: P, context: TransformContext): Task<B> | B, apply(params: ApplyParams<A, P>): Task<B> | B,
/** /**
* Attempts to update the entity in a non-destructive way. * Attempts to update the entity in a non-destructive way.
* For example changing a color scheme of a visual does not require computing new geometry. * For example changing a color scheme of a visual does not require computing new geometry.
* Return/resolve to undefined if the update is not possible. * Return/resolve to undefined if the update is not possible.
*
* The ability to resolve the task to undefined is present for "async updates" (i.e. containing an ajax call).
*/ */
update?(a: A, oldParams: P, b: B, newParams: P, context: TransformContext): Task<B | undefined> | B | undefined, update?(params: UpdateParams<A, B, P>): Task<UpdateResult> | UpdateResult,
/** Check the parameters and return a list of errors if the are not valid. */ /** Check the parameters and return a list of errors if the are not valid. */
defaultParams?(a: A, context: TransformContext): P, defaultParams?(a: A, context: TransformContext): P,
...@@ -66,12 +78,6 @@ export namespace Transformer { ...@@ -66,12 +78,6 @@ export namespace Transformer {
const registry = new Map<Id, Transformer>(); const registry = new Map<Id, Transformer>();
function typeToString(a: { type: StateObject.Type }[]) {
if (!a.length) return '()';
if (a.length === 1) return a[0].type.kind;
return `(${a.map(t => t.type.kind).join(' | ')})`;
}
export function get(id: string): Transformer { export function get(id: string): Transformer {
const t = registry.get(id as Id); const t = registry.get(id as Id);
if (!t) { if (!t) {
...@@ -81,8 +87,8 @@ export namespace Transformer { ...@@ -81,8 +87,8 @@ export namespace Transformer {
} }
export function create<A extends StateObject, B extends StateObject, P>(namespace: string, definition: Definition<A, B, P>) { export function create<A extends StateObject, B extends StateObject, P>(namespace: string, definition: Definition<A, B, P>) {
const { from, to, name } = definition; const { name } = definition;
const id = `${namespace}.${name} :: ${typeToString(from)} -> ${typeToString(to)}` as Id; const id = `${namespace}.${name}` as Id;
if (registry.has(id)) { if (registry.has(id)) {
throw new Error(`A transform with id '${name}' is already registered. Please pick a unique identifier for your transforms and/or register them only once. This is to ensure that transforms can be serialized and replayed.`); throw new Error(`A transform with id '${name}' is already registered. Please pick a unique identifier for your transforms and/or register them only once. This is to ensure that transforms can be serialized and replayed.`);
...@@ -90,6 +96,7 @@ export namespace Transformer { ...@@ -90,6 +96,7 @@ export namespace Transformer {
const t: Transformer<A, B, P> = { const t: Transformer<A, B, P> = {
apply(params, props) { return Transform.create<A, B, P>(t as any, params, props); }, apply(params, props) { return Transform.create<A, B, P>(t as any, params, props); },
namespace,
id, id,
definition definition
}; };
......
...@@ -18,19 +18,17 @@ export class Square extends _obj<{ a: number }>('square', { name: 'Square', clas ...@@ -18,19 +18,17 @@ export class Square extends _obj<{ a: number }>('square', { name: 'Square', clas
export class Circle extends _obj<{ r: number }>('circle', { name: 'Circle', class: 'shape' }) { } export class Circle extends _obj<{ r: number }>('circle', { name: 'Circle', class: 'shape' }) { }
export class Area extends _obj<{ volume: number }>('volume', { name: 'Volume', class: 'prop' }) { } export class Area extends _obj<{ volume: number }>('volume', { name: 'Volume', class: 'prop' }) { }
const root = new Root({ label: 'Root' }, {});
export const CreateSquare = _transform<Root, Square, { a: number }>({ export const CreateSquare = _transform<Root, Square, { a: number }>({
name: 'create-square', name: 'create-square',
from: [Root], from: [Root],
to: [Square], to: [Square],
apply(a, p) { apply({ params: p }) {
return new Square({ label: `Square a=${p.a}` }, p); return new Square({ label: `Square a=${p.a}` }, p);
}, },
update(a, _, b, p) { update({ b, newParams: p }) {
b.props.label = `Square a=${p.a}` b.props.label = `Square a=${p.a}`
b.data.a = p.a; b.data.a = p.a;
return b; return Transformer.UpdateResult.Updated;
} }
}); });
...@@ -38,13 +36,13 @@ export const CreateCircle = _transform<Root, Circle, { r: number }>({ ...@@ -38,13 +36,13 @@ export const CreateCircle = _transform<Root, Circle, { r: number }>({
name: 'create-circle', name: 'create-circle',
from: [Root], from: [Root],
to: [Square], to: [Square],
apply(a, p) { apply({ params: p }) {
return new Circle({ label: `Circle r=${p.r}` }, p); return new Circle({ label: `Circle r=${p.r}` }, p);
}, },
update(a, _, b, p) { update({ b, newParams: p }) {
b.props.label = `Circle r=${p.r}` b.props.label = `Circle r=${p.r}`
b.data.r = p.r; b.data.r = p.r;
return b; return Transformer.UpdateResult.Updated;
} }
}); });
...@@ -52,30 +50,24 @@ export const CaclArea = _transform<Square | Circle, Area, {}>({ ...@@ -52,30 +50,24 @@ export const CaclArea = _transform<Square | Circle, Area, {}>({
name: 'calc-area', name: 'calc-area',
from: [Square, Circle], from: [Square, Circle],
to: [Area], to: [Area],
apply(a) { apply({ a }) {
if (a instanceof Square) return new Area({ label: 'Area' }, { volume: a.data.a * a.data.a }); if (a instanceof Square) return new Area({ label: 'Area' }, { volume: a.data.a * a.data.a });
else if (a instanceof Circle) return new Area({ label: 'Area' }, { volume: a.data.r * a.data.r * Math.PI }); else if (a instanceof Circle) return new Area({ label: 'Area' }, { volume: a.data.r * a.data.r * Math.PI });
throw new Error('Unknown object type.'); throw new Error('Unknown object type.');
}, },
update(a, _, b) { update({ a, b }) {
if (a instanceof Square) b.data.volume = a.data.a * a.data.a; if (a instanceof Square) b.data.volume = a.data.a * a.data.a;
else if (a instanceof Circle) b.data.volume = a.data.r * a.data.r * Math.PI; else if (a instanceof Circle) b.data.volume = a.data.r * a.data.r * Math.PI;
else throw new Error('Unknown object type.'); else throw new Error('Unknown object type.');
return b; return Transformer.UpdateResult.Updated;
} }
}); });
async function runTask<A>(t: A | Task<A>): Promise<A> { export async function runTask<A>(t: A | Task<A>): Promise<A> {
if ((t as any).run) return await (t as Task<A>).run(); if ((t as any).run) return await (t as Task<A>).run();
return t as A; return t as A;
} }
export async function test() {
const sq = await runTask(CreateSquare.definition.apply(root, { a: 10 }, 0 as any));
const area = await runTask(CaclArea.definition.apply(sq, {}, 0 as any));
console.log(area);
}
export async function testState() { export async function testState() {
const state = State.create(); const state = State.create();
...@@ -117,8 +109,8 @@ testState(); ...@@ -117,8 +109,8 @@ testState();
export function printTTree(tree: TransformTree) { export function printTTree(tree: TransformTree) {
let lines: string[] = []; let lines: string[] = [];
function print(offset: string, ref: any) { function print(offset: string, ref: any) {
let t = tree.nodes.get(ref)!; const t = tree.nodes.get(ref)!;
let tr = t.value; const tr = t.value;
const name = tr.transformer.id; const name = tr.transformer.id;
lines.push(`${offset}|_ (${ref}) ${name} ${tr.params ? JSON.stringify(tr.params) : ''}, v${t.value.version}`); lines.push(`${offset}|_ (${ref}) ${name} ${tr.params ? JSON.stringify(tr.params) : ''}, v${t.value.version}`);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment