diff --git a/src/mol-state/context.ts b/src/mol-state/context.ts index 16e06198eeabaf284b8d0cb524a5592f781c72dc..bb04a1de101777951c6c67e0cdb8b5f88908c3fe 100644 --- a/src/mol-state/context.ts +++ b/src/mol-state/context.ts @@ -6,13 +6,13 @@ import { Subject } from 'rxjs' import { StateObject } from './object'; -import { Task } from 'mol-task'; import { Transform } from './tree/transform'; interface StateContext { events: { object: { stateChanged: Subject<{ ref: Transform.Ref }>, + propsChanged: Subject<{ ref: Transform.Ref, newProps: unknown }>, updated: Subject<{ ref: Transform.Ref }>, replaced: Subject<{ ref: Transform.Ref, old?: StateObject }>, created: Subject<{ ref: Transform.Ref }>, @@ -21,15 +21,16 @@ interface StateContext { warn: Subject<string> }, globalContext: unknown, - runTask<T>(task: T | Task<T>): T | Promise<T> + defaultObjectProps: unknown } namespace StateContext { - export function create(globalContext?: unknown/* task?: { observer?: Progress.Observer, updateRateMs?: number } */): StateContext { + export function create(params: { globalContext: unknown, defaultObjectProps: unknown }): StateContext { return { events: { object: { stateChanged: new Subject(), + propsChanged: new Subject(), updated: new Subject(), replaced: new Subject(), created: new Subject(), @@ -37,11 +38,8 @@ namespace StateContext { }, warn: new Subject() }, - globalContext, - runTask<T>(t: T | Task<T>) { - if (typeof (t as any).run === 'function') return (t as Task<T>).run(); - return t as T; - } + globalContext: params.globalContext, + defaultObjectProps: params.defaultObjectProps } } } diff --git a/src/mol-state/object.ts b/src/mol-state/object.ts index 7f9019fc9bc5e03c776c5cc242e6aeb7d254787d..61f9142cf8edb433173664ec844742edc2f5b744 100644 --- a/src/mol-state/object.ts +++ b/src/mol-state/object.ts @@ -46,8 +46,9 @@ export namespace StateObject { } } - export interface Wrapped { + export interface Node { state: StateType, + props: unknown, errorText?: string, obj?: StateObject, version: string diff --git a/src/mol-state/state.ts b/src/mol-state/state.ts index dfba3eba5b84f9ff8ca355a901e04ea2aff19356..b586e5c2cc4de70b4115bf2bfe505e398f451160 100644 --- a/src/mol-state/state.ts +++ b/src/mol-state/state.ts @@ -7,60 +7,59 @@ import { StateObject } from './object'; import { StateTree } from './tree'; import { Transform } from './tree/transform'; -import { Map as ImmutableMap } from 'immutable'; -// import { StateContext } from './context/context'; import { ImmutableTree } from './util/immutable-tree'; import { Transformer } from './transformer'; import { StateContext } from './context'; import { UUID } from 'mol-util'; +import { RuntimeContext, Task } from 'mol-task'; -export interface State<ObjectProps = unknown> { - definition: State.Definition<ObjectProps>, +export interface State { + tree: StateTree, objects: State.Objects, context: StateContext } export namespace State { export type Ref = Transform.Ref - export type ObjectProps<P = unknown> = ImmutableMap<Ref, P> - export type Objects = Map<Ref, StateObject.Wrapped> + export type Objects = Map<Ref, StateObject.Node> - export interface Definition<P = unknown> { - tree: StateTree, - // things like object visibility - props: ObjectProps<P> - } - - export function create(params?: { globalContext?: unknown }): State { + export function create(params?: { globalContext?: unknown, defaultObjectProps: unknown }) { const tree = StateTree.create(); const objects: Objects = new Map(); const root = tree.getValue(tree.rootRef)!; + const defaultObjectProps = (params && params.defaultObjectProps) || { } - objects.set(tree.rootRef, { obj: void 0 as any, state: StateObject.StateType.Ok, version: root.version }); + objects.set(tree.rootRef, { obj: void 0 as any, state: StateObject.StateType.Ok, version: root.version, props: { ...defaultObjectProps } }); return { - definition: { - tree, - props: ImmutableMap() - }, + tree, objects, - context: StateContext.create(params && params.globalContext) + context: StateContext.create({ + globalContext: params && params.globalContext, + defaultObjectProps + }) }; } - export async function update<P>(state: State<P>, tree: StateTree): Promise<State<P>> { - const ctx: UpdateContext = { - stateCtx: state.context, - old: state.definition, - tree: tree, - props: state.definition.props.asMutable(), - objects: state.objects - }; + export function update(state: State, tree: StateTree): Task<State> { + return Task.create('Update Tree', taskCtx => { + const ctx: UpdateContext = { + stateCtx: state.context, + taskCtx, + oldTree: state.tree, + tree: tree, + objects: state.objects + }; + return _update(ctx); + }) + } - const roots = findUpdateRoots(state.objects, tree); + async function _update(ctx: UpdateContext): Promise<State> { + const roots = findUpdateRoots(ctx.objects, ctx.tree); const deletes = findDeletes(ctx); for (const d of deletes) { - state.objects.delete(d); + ctx.objects.delete(d); + ctx.stateCtx.events.object.removed.next({ ref: d }); } initObjectState(ctx, roots); @@ -70,17 +69,17 @@ export namespace State { } return { - definition: { tree, props: ctx.props.asImmutable() as ObjectProps<P> }, - objects: state.objects, - context: state.context + tree: ctx.tree, + objects: ctx.objects, + context: ctx.stateCtx }; } interface UpdateContext { stateCtx: StateContext, - old: Definition, + taskCtx: RuntimeContext, + oldTree: StateTree, tree: StateTree, - props: ObjectProps, objects: Objects } @@ -120,15 +119,18 @@ export namespace State { } function setObjectState(ctx: UpdateContext, ref: Ref, state: StateObject.StateType, errorText?: string) { + let changed = false; if (ctx.objects.has(ref)) { const obj = ctx.objects.get(ref)!; + changed = obj.state !== state; obj.state = state; obj.errorText = errorText; } else { - const obj = { state, version: UUID.create(), errorText }; + const obj = { state, version: UUID.create(), errorText, props: { ...ctx.stateCtx.defaultObjectProps } }; ctx.objects.set(ref, obj); + changed = true; } - ctx.stateCtx.events.object.stateChanged.next({ ref }); + if (changed) ctx.stateCtx.events.object.stateChanged.next({ ref }); } function _initVisitor(t: ImmutableTree.Node<Transform>, _: any, ctx: UpdateContext) { @@ -170,15 +172,17 @@ export namespace State { } async function updateSubtree(ctx: UpdateContext, root: Ref) { - setObjectState(ctx, root, StateObject.StateType.Pending); + setObjectState(ctx, root, StateObject.StateType.Processing); try { const update = await updateNode(ctx, root); setObjectState(ctx, root, StateObject.StateType.Ok); - if (update === 'created') { + if (update.action === 'created') { ctx.stateCtx.events.object.created.next({ ref: root }); - } else if (update === 'updated') { + } else if (update.action === 'updated') { ctx.stateCtx.events.object.updated.next({ ref: root }); + } else if (update.action === 'replaced') { + ctx.stateCtx.events.object.replaced.next({ ref: root, old: update.old }); } } catch (e) { doError(ctx, root, '' + e); @@ -194,43 +198,61 @@ export namespace State { } async function updateNode(ctx: UpdateContext, currentRef: Ref) { - const { old: { tree: oldTree }, tree, objects } = ctx; + const { oldTree, tree, objects } = ctx; const transform = tree.getValue(currentRef)!; const parent = findParent(tree, objects, currentRef, transform.transformer.definition.from); - //console.log('parent', transform.transformer.id, transform.transformer.definition.from[0].type, parent ? parent.ref : 'undefined') + // console.log('parent', transform.transformer.id, transform.transformer.definition.from[0].type, parent ? parent.ref : 'undefined') if (!oldTree.nodes.has(currentRef) || !objects.has(currentRef)) { - console.log('creating...', transform.transformer.id, oldTree.nodes.has(currentRef), objects.has(currentRef)); + // console.log('creating...', transform.transformer.id, oldTree.nodes.has(currentRef), objects.has(currentRef)); const obj = await createObject(ctx, transform.transformer, parent, transform.params); obj.ref = currentRef; - objects.set(currentRef, { obj, state: StateObject.StateType.Ok, version: transform.version }); - return 'created'; + objects.set(currentRef, { + obj, + state: StateObject.StateType.Ok, + version: transform.version, + props: { ...ctx.stateCtx.defaultObjectProps, ...transform.defaultProps } + }); + return { action: 'created' }; } else { - console.log('updating...', transform.transformer.id); + // console.log('updating...', transform.transformer.id); const current = objects.get(currentRef)!; const oldParams = oldTree.getValue(currentRef)!.params; switch (await updateObject(ctx, transform.transformer, parent, current.obj!, oldParams, transform.params)) { case Transformer.UpdateResult.Recreate: { const obj = await createObject(ctx, transform.transformer, parent, transform.params); obj.ref = currentRef; - objects.set(currentRef, { obj, state: StateObject.StateType.Ok, version: transform.version }); - ctx.stateCtx.events.object.created.next({ ref: currentRef }); - return 'created'; + objects.set(currentRef, { + obj, + state: StateObject.StateType.Ok, + version: transform.version, + props: { ...ctx.stateCtx.defaultObjectProps, ...current.props, ...transform.defaultProps } + }); + return { action: 'replaced', old: current.obj! }; } case Transformer.UpdateResult.Updated: current.version = transform.version; - return 'updated'; + current.props = { ...ctx.stateCtx.defaultObjectProps, ...current.props, ...transform.defaultProps }; + return { action: 'updated' }; + default: + // TODO check if props need to be updated + return { action: 'none' }; } } } + function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) { + if (typeof (t as any).run === 'function') return (t as Task<T>).runInContext(ctx); + return t as T; + } + function createObject(ctx: UpdateContext, transformer: Transformer, a: StateObject, params: any) { - return ctx.stateCtx.runTask(transformer.definition.apply({ a, params, globalCtx: ctx.stateCtx.globalContext })); + return runTask(transformer.definition.apply({ a, params }, ctx.stateCtx.globalContext), ctx.taskCtx); } async function updateObject(ctx: UpdateContext, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) { if (!transformer.definition.update) { return Transformer.UpdateResult.Recreate; } - return ctx.stateCtx.runTask(transformer.definition.update({ a, oldParams, b, newParams, globalCtx: ctx.stateCtx.globalContext })); + return runTask(transformer.definition.update({ a, oldParams, b, newParams }, ctx.stateCtx.globalContext), ctx.taskCtx); } } diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts index 139a1d6107bc58589f5e528018860e653bcb1c7c..701085fff0adf0b6ca0d63357ed78e2fd9f354b0 100644 --- a/src/mol-state/transformer.ts +++ b/src/mol-state/transformer.ts @@ -23,16 +23,14 @@ export namespace Transformer { export interface ApplyParams<A extends StateObject = StateObject, P = unknown> { a: A, - params: P, - globalCtx: unknown + params: P } export interface UpdateParams<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> { a: A, b: B, oldParams: P, - newParams: P, - globalCtx: unknown + newParams: P } export enum UpdateResult { Unchanged, Updated, Recreate } @@ -46,14 +44,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(params: ApplyParams<A, P>): Task<B> | B, + apply(params: ApplyParams<A, P>, globalCtx: unknown): 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. */ - update?(params: UpdateParams<A, B, P>): Task<UpdateResult> | UpdateResult, + update?(params: UpdateParams<A, B, P>, globalCtx: unknown): Task<UpdateResult> | UpdateResult, /** Check the parameters and return a list of errors if the are not valid. */ defaultParams?(a: A, globalCtx: unknown): P, diff --git a/src/mol-util/object.ts b/src/mol-util/object.ts new file mode 100644 index 0000000000000000000000000000000000000000..2ee047d59ee226390d30cf68beb09f6f21b7df22 --- /dev/null +++ b/src/mol-util/object.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +const hasOwnProperty = Object.prototype.hasOwnProperty; + +/** Create new object if any property in "update" changes in "source". */ +export function shallowMerge2<T>(source: T, update: Partial<T>): T { + // Adapted from LiteMol (https://github.com/dsehnal/LiteMol) + let changed = false; + for (let k of Object.keys(update)) { + if (!hasOwnProperty.call(update, k)) continue; + + if ((update as any)[k] !== (source as any)[k]) { + changed = true; + break; + } + } + + if (!changed) return source; + return Object.assign({}, source, update); +} + +export function shallowEqual<T>(a: T, b: T) { + if (!a) { + if (!b) return true; + return false; + } + if (!b) return false; + + let keys = Object.keys(a); + if (Object.keys(b).length !== keys.length) return false; + for (let k of keys) { + if (!hasOwnProperty.call(a, k) || (a as any)[k] !== (b as any)[k]) return false; + } + + return true; +} + +export function shallowMerge<T>(source: T, ...rest: (Partial<T> | undefined)[]): T { + // Adapted from LiteMol (https://github.com/dsehnal/LiteMol) + let ret: any = source; + + for (let s = 0; s < rest.length; s++) { + if (!rest[s]) continue; + ret = shallowMerge2(source, rest[s] as T); + if (ret !== source) { + for (let i = s + 1; i < rest.length; i++) { + ret = Object.assign(ret, rest[i]); + } + break; + } + } + return ret; +} \ No newline at end of file diff --git a/src/perf-tests/state.ts b/src/perf-tests/state.ts index 976c329f6db349f9f3cc99e11345f964e7562776..0344b8dc4ff318ab8c32d9cba4d381f06407f31f 100644 --- a/src/perf-tests/state.ts +++ b/src/perf-tests/state.ts @@ -64,10 +64,20 @@ export async function runTask<A>(t: A | Task<A>): Promise<A> { return t as A; } +function hookEvents(state: State) { + state.context.events.object.created.subscribe(e => console.log('created:', e.ref)); + state.context.events.object.removed.subscribe(e => console.log('removed:', e.ref)); + state.context.events.object.replaced.subscribe(e => console.log('replaced:', e.ref)); + state.context.events.object.stateChanged.subscribe(e => console.log('stateChanged:', e.ref, + StateObject.StateType[state.objects.get(e.ref)!.state])); + state.context.events.object.updated.subscribe(e => console.log('updated:', e.ref)); +} + export async function testState() { const state = State.create(); + hookEvents(state); - const tree = state.definition.tree; + const tree = state.tree; const builder = StateTree.build(tree); builder.toRoot<Root>() .apply(CreateSquare, { a: 10 }, { ref: 'square' }) @@ -80,7 +90,7 @@ export async function testState() { printTTree(tree1); printTTree(tree2); - const state1 = await State.update(state, tree1); + const state1 = await State.update(state, tree1).run(); console.log('----------------'); console.log(util.inspect(state1.objects, true, 3, true)); @@ -93,7 +103,7 @@ export async function testState() { printTTree(treeFromJson); console.log('----------------'); - const state2 = await State.update(state1, treeFromJson); + const state2 = await State.update(state1, treeFromJson).run(); console.log(util.inspect(state2.objects, true, 3, true)); }