diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index 6f3fa1c456c776f6a7aa9324e802f0967e876994..f2db317f28a476c8e7dbaa27f573fdb4b479a6be 100644 --- a/src/mol-state/state.ts +++ b/src/mol-state/state.ts @@ -115,22 +115,32 @@ export namespace State { } } - async function updateNode(oldTree: TransformTree, tree: TransformTree, objects: Objects, root: Transform.Ref) { - const transform = tree.getValue(root)!; - const parent = findParent(tree, objects, root, transform.transformer.definition.from); + async function updateNode(oldTree: TransformTree, tree: TransformTree, objects: Objects, currentRef: Transform.Ref) { + const transform = tree.getValue(currentRef)!; + const parent = findParent(tree, objects, currentRef, transform.transformer.definition.from); console.log('parent', parent ? parent.ref : 'undefined') 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)); const obj = await createObject(transform.transformer, parent, transform.params); 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 { console.log('updating...', transform.transformer.id); const current = objects.get(transform.ref)!.obj; const oldParams = oldTree.getValue(transform.ref)!.params; - await updateObject(transform.transformer, parent, current, oldParams, transform.params); - const obj = objects.get(root)!; - obj.version = transform.version; + switch (await updateObject(transform.transformer, parent, current, oldParams, transform.params)) { + case Transformer.UpdateResult.Recreate: { + 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 { return t as A; } - function createObject(transformer: Transformer, parent: StateObject, params: any) { - return runTask(transformer.definition.apply(parent, params, 0 as any)); + function createObject(transformer: Transformer, a: StateObject, params: 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) { - // TODO - throw 'nyi'; + return Transformer.UpdateResult.Recreate; } - return transformer.definition.update!(parent, oldParams, obj, params, 0 as any); + return runTask(transformer.definition.update({ a, oldParams, b, newParams })); } } diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts index 9f94340d18785227fefab4cdfbd9c1000f8792bb..7b6cfaa4e56d1647f5c76eb98f3841b0c7836f2c 100644 --- a/src/mol-state/transformer.ts +++ b/src/mol-state/transformer.ts @@ -11,6 +11,7 @@ import { Transform } from './tree/transform'; export interface Transformer<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> { apply(params?: P, props?: Partial<Transform.Props>): Transform<A, B, P>, + readonly namespace: string, readonly id: Transformer.Id, readonly definition: Transformer.Definition<A, B, P> } @@ -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 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> { readonly name: string, - readonly namespace?: string, readonly from: { type: StateObject.Type }[], readonly to: { type: StateObject.Type }[], @@ -31,16 +45,14 @@ export namespace Transformer { * 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. */ - 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. * 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. - * - * 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. */ defaultParams?(a: A, context: TransformContext): P, @@ -66,12 +78,6 @@ export namespace 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 { const t = registry.get(id as Id); if (!t) { @@ -81,8 +87,8 @@ export namespace Transformer { } export function create<A extends StateObject, B extends StateObject, P>(namespace: string, definition: Definition<A, B, P>) { - const { from, to, name } = definition; - const id = `${namespace}.${name} :: ${typeToString(from)} -> ${typeToString(to)}` as Id; + const { name } = definition; + const id = `${namespace}.${name}` as 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.`); @@ -90,6 +96,7 @@ export namespace Transformer { const t: Transformer<A, B, P> = { apply(params, props) { return Transform.create<A, B, P>(t as any, params, props); }, + namespace, id, definition }; diff --git a/src/perf-tests/state.ts b/src/perf-tests/state.ts index af121c96d5a6e48b508872bf141e8e7495be1188..e46ffcd5beab495833ffa9c59d97843634a0d138 100644 --- a/src/perf-tests/state.ts +++ b/src/perf-tests/state.ts @@ -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 Area extends _obj<{ volume: number }>('volume', { name: 'Volume', class: 'prop' }) { } -const root = new Root({ label: 'Root' }, {}); - export const CreateSquare = _transform<Root, Square, { a: number }>({ name: 'create-square', from: [Root], to: [Square], - apply(a, p) { + apply({ params: 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.data.a = p.a; - return b; + return Transformer.UpdateResult.Updated; } }); @@ -38,13 +36,13 @@ export const CreateCircle = _transform<Root, Circle, { r: number }>({ name: 'create-circle', from: [Root], to: [Square], - apply(a, p) { + apply({ params: 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.data.r = p.r; - return b; + return Transformer.UpdateResult.Updated; } }); @@ -52,30 +50,24 @@ export const CaclArea = _transform<Square | Circle, Area, {}>({ name: 'calc-area', from: [Square, Circle], to: [Area], - apply(a) { + apply({ 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 }); 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; else if (a instanceof Circle) b.data.volume = a.data.r * a.data.r * Math.PI; 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(); 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() { const state = State.create(); @@ -117,8 +109,8 @@ testState(); export function printTTree(tree: TransformTree) { let lines: string[] = []; function print(offset: string, ref: any) { - let t = tree.nodes.get(ref)!; - let tr = t.value; + const t = tree.nodes.get(ref)!; + const tr = t.value; const name = tr.transformer.id; lines.push(`${offset}|_ (${ref}) ${name} ${tr.params ? JSON.stringify(tr.params) : ''}, v${t.value.version}`);