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

mol-state: wip

parent 958bbb79
No related branches found
No related tags found
No related merge requests found
...@@ -6,13 +6,13 @@ ...@@ -6,13 +6,13 @@
import { Subject } from 'rxjs' import { Subject } from 'rxjs'
import { StateObject } from './object'; import { StateObject } from './object';
import { Task } from 'mol-task';
import { Transform } from './tree/transform'; import { Transform } from './tree/transform';
interface StateContext { interface StateContext {
events: { events: {
object: { object: {
stateChanged: Subject<{ ref: Transform.Ref }>, stateChanged: Subject<{ ref: Transform.Ref }>,
propsChanged: Subject<{ ref: Transform.Ref, newProps: unknown }>,
updated: Subject<{ ref: Transform.Ref }>, updated: Subject<{ ref: Transform.Ref }>,
replaced: Subject<{ ref: Transform.Ref, old?: StateObject }>, replaced: Subject<{ ref: Transform.Ref, old?: StateObject }>,
created: Subject<{ ref: Transform.Ref }>, created: Subject<{ ref: Transform.Ref }>,
...@@ -21,15 +21,16 @@ interface StateContext { ...@@ -21,15 +21,16 @@ interface StateContext {
warn: Subject<string> warn: Subject<string>
}, },
globalContext: unknown, globalContext: unknown,
runTask<T>(task: T | Task<T>): T | Promise<T> defaultObjectProps: unknown
} }
namespace StateContext { namespace StateContext {
export function create(globalContext?: unknown/* task?: { observer?: Progress.Observer, updateRateMs?: number } */): StateContext { export function create(params: { globalContext: unknown, defaultObjectProps: unknown }): StateContext {
return { return {
events: { events: {
object: { object: {
stateChanged: new Subject(), stateChanged: new Subject(),
propsChanged: new Subject(),
updated: new Subject(), updated: new Subject(),
replaced: new Subject(), replaced: new Subject(),
created: new Subject(), created: new Subject(),
...@@ -37,11 +38,8 @@ namespace StateContext { ...@@ -37,11 +38,8 @@ namespace StateContext {
}, },
warn: new Subject() warn: new Subject()
}, },
globalContext, globalContext: params.globalContext,
runTask<T>(t: T | Task<T>) { defaultObjectProps: params.defaultObjectProps
if (typeof (t as any).run === 'function') return (t as Task<T>).run();
return t as T;
}
} }
} }
} }
......
...@@ -46,8 +46,9 @@ export namespace StateObject { ...@@ -46,8 +46,9 @@ export namespace StateObject {
} }
} }
export interface Wrapped { export interface Node {
state: StateType, state: StateType,
props: unknown,
errorText?: string, errorText?: string,
obj?: StateObject, obj?: StateObject,
version: string version: string
......
...@@ -7,60 +7,59 @@ ...@@ -7,60 +7,59 @@
import { StateObject } from './object'; import { StateObject } from './object';
import { StateTree } from './tree'; import { StateTree } from './tree';
import { Transform } from './tree/transform'; import { Transform } from './tree/transform';
import { Map as ImmutableMap } from 'immutable';
// import { StateContext } from './context/context';
import { ImmutableTree } from './util/immutable-tree'; import { ImmutableTree } from './util/immutable-tree';
import { Transformer } from './transformer'; import { Transformer } from './transformer';
import { StateContext } from './context'; import { StateContext } from './context';
import { UUID } from 'mol-util'; import { UUID } from 'mol-util';
import { RuntimeContext, Task } from 'mol-task';
export interface State<ObjectProps = unknown> { export interface State {
definition: State.Definition<ObjectProps>, tree: StateTree,
objects: State.Objects, objects: State.Objects,
context: StateContext context: StateContext
} }
export namespace State { export namespace State {
export type Ref = Transform.Ref export type Ref = Transform.Ref
export type ObjectProps<P = unknown> = ImmutableMap<Ref, P> export type Objects = Map<Ref, StateObject.Node>
export type Objects = Map<Ref, StateObject.Wrapped>
export interface Definition<P = unknown> { export function create(params?: { globalContext?: unknown, defaultObjectProps: unknown }) {
tree: StateTree,
// things like object visibility
props: ObjectProps<P>
}
export function create(params?: { globalContext?: unknown }): State {
const tree = StateTree.create(); const tree = StateTree.create();
const objects: Objects = new Map(); const objects: Objects = new Map();
const root = tree.getValue(tree.rootRef)!; 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 { return {
definition: {
tree, tree,
props: ImmutableMap()
},
objects, 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>> { export function update(state: State, tree: StateTree): Task<State> {
return Task.create('Update Tree', taskCtx => {
const ctx: UpdateContext = { const ctx: UpdateContext = {
stateCtx: state.context, stateCtx: state.context,
old: state.definition, taskCtx,
oldTree: state.tree,
tree: tree, tree: tree,
props: state.definition.props.asMutable(),
objects: state.objects 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); const deletes = findDeletes(ctx);
for (const d of deletes) { for (const d of deletes) {
state.objects.delete(d); ctx.objects.delete(d);
ctx.stateCtx.events.object.removed.next({ ref: d });
} }
initObjectState(ctx, roots); initObjectState(ctx, roots);
...@@ -70,17 +69,17 @@ export namespace State { ...@@ -70,17 +69,17 @@ export namespace State {
} }
return { return {
definition: { tree, props: ctx.props.asImmutable() as ObjectProps<P> }, tree: ctx.tree,
objects: state.objects, objects: ctx.objects,
context: state.context context: ctx.stateCtx
}; };
} }
interface UpdateContext { interface UpdateContext {
stateCtx: StateContext, stateCtx: StateContext,
old: Definition, taskCtx: RuntimeContext,
oldTree: StateTree,
tree: StateTree, tree: StateTree,
props: ObjectProps,
objects: Objects objects: Objects
} }
...@@ -120,15 +119,18 @@ export namespace State { ...@@ -120,15 +119,18 @@ export namespace State {
} }
function setObjectState(ctx: UpdateContext, ref: Ref, state: StateObject.StateType, errorText?: string) { function setObjectState(ctx: UpdateContext, ref: Ref, state: StateObject.StateType, errorText?: string) {
let changed = false;
if (ctx.objects.has(ref)) { if (ctx.objects.has(ref)) {
const obj = ctx.objects.get(ref)!; const obj = ctx.objects.get(ref)!;
changed = obj.state !== state;
obj.state = state; obj.state = state;
obj.errorText = errorText; obj.errorText = errorText;
} else { } else {
const obj = { state, version: UUID.create(), errorText }; const obj = { state, version: UUID.create(), errorText, props: { ...ctx.stateCtx.defaultObjectProps } };
ctx.objects.set(ref, obj); 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) { function _initVisitor(t: ImmutableTree.Node<Transform>, _: any, ctx: UpdateContext) {
...@@ -170,15 +172,17 @@ export namespace State { ...@@ -170,15 +172,17 @@ export namespace State {
} }
async function updateSubtree(ctx: UpdateContext, root: Ref) { async function updateSubtree(ctx: UpdateContext, root: Ref) {
setObjectState(ctx, root, StateObject.StateType.Pending); setObjectState(ctx, root, StateObject.StateType.Processing);
try { try {
const update = await updateNode(ctx, root); const update = await updateNode(ctx, root);
setObjectState(ctx, root, StateObject.StateType.Ok); setObjectState(ctx, root, StateObject.StateType.Ok);
if (update === 'created') { if (update.action === 'created') {
ctx.stateCtx.events.object.created.next({ ref: root }); 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 }); 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) { } catch (e) {
doError(ctx, root, '' + e); doError(ctx, root, '' + e);
...@@ -194,43 +198,61 @@ export namespace State { ...@@ -194,43 +198,61 @@ export namespace State {
} }
async function updateNode(ctx: UpdateContext, currentRef: Ref) { 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 transform = tree.getValue(currentRef)!;
const parent = findParent(tree, objects, currentRef, transform.transformer.definition.from); 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)) { 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); const obj = await createObject(ctx, transform.transformer, parent, transform.params);
obj.ref = currentRef; obj.ref = currentRef;
objects.set(currentRef, { obj, state: StateObject.StateType.Ok, version: transform.version }); objects.set(currentRef, {
return 'created'; obj,
state: StateObject.StateType.Ok,
version: transform.version,
props: { ...ctx.stateCtx.defaultObjectProps, ...transform.defaultProps }
});
return { action: 'created' };
} else { } else {
console.log('updating...', transform.transformer.id); // console.log('updating...', transform.transformer.id);
const current = objects.get(currentRef)!; const current = objects.get(currentRef)!;
const oldParams = oldTree.getValue(currentRef)!.params; const oldParams = oldTree.getValue(currentRef)!.params;
switch (await updateObject(ctx, transform.transformer, parent, current.obj!, oldParams, transform.params)) { switch (await updateObject(ctx, transform.transformer, parent, current.obj!, oldParams, transform.params)) {
case Transformer.UpdateResult.Recreate: { case Transformer.UpdateResult.Recreate: {
const obj = await createObject(ctx, transform.transformer, parent, transform.params); const obj = await createObject(ctx, transform.transformer, parent, transform.params);
obj.ref = currentRef; obj.ref = currentRef;
objects.set(currentRef, { obj, state: StateObject.StateType.Ok, version: transform.version }); objects.set(currentRef, {
ctx.stateCtx.events.object.created.next({ ref: currentRef }); obj,
return 'created'; 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: case Transformer.UpdateResult.Updated:
current.version = transform.version; 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) { 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) { async function updateObject(ctx: UpdateContext, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
if (!transformer.definition.update) { if (!transformer.definition.update) {
return Transformer.UpdateResult.Recreate; 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);
} }
} }
...@@ -23,16 +23,14 @@ export namespace Transformer { ...@@ -23,16 +23,14 @@ export namespace Transformer {
export interface ApplyParams<A extends StateObject = StateObject, P = unknown> { export interface ApplyParams<A extends StateObject = StateObject, P = unknown> {
a: A, a: A,
params: P, params: P
globalCtx: unknown
} }
export interface UpdateParams<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> { export interface UpdateParams<A extends StateObject = StateObject, B extends StateObject = StateObject, P = unknown> {
a: A, a: A,
b: B, b: B,
oldParams: P, oldParams: P,
newParams: P, newParams: P
globalCtx: unknown
} }
export enum UpdateResult { Unchanged, Updated, Recreate } export enum UpdateResult { Unchanged, Updated, Recreate }
...@@ -46,14 +44,14 @@ export namespace Transformer { ...@@ -46,14 +44,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(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. * 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.
*/ */
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. */ /** Check the parameters and return a list of errors if the are not valid. */
defaultParams?(a: A, globalCtx: unknown): P, defaultParams?(a: A, globalCtx: unknown): P,
......
/**
* 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
...@@ -64,10 +64,20 @@ export async function runTask<A>(t: A | Task<A>): Promise<A> { ...@@ -64,10 +64,20 @@ export async function runTask<A>(t: A | Task<A>): Promise<A> {
return t as 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() { export async function testState() {
const state = State.create(); const state = State.create();
hookEvents(state);
const tree = state.definition.tree; const tree = state.tree;
const builder = StateTree.build(tree); const builder = StateTree.build(tree);
builder.toRoot<Root>() builder.toRoot<Root>()
.apply(CreateSquare, { a: 10 }, { ref: 'square' }) .apply(CreateSquare, { a: 10 }, { ref: 'square' })
...@@ -80,7 +90,7 @@ export async function testState() { ...@@ -80,7 +90,7 @@ export async function testState() {
printTTree(tree1); printTTree(tree1);
printTTree(tree2); printTTree(tree2);
const state1 = await State.update(state, tree1); const state1 = await State.update(state, tree1).run();
console.log('----------------'); console.log('----------------');
console.log(util.inspect(state1.objects, true, 3, true)); console.log(util.inspect(state1.objects, true, 3, true));
...@@ -93,7 +103,7 @@ export async function testState() { ...@@ -93,7 +103,7 @@ export async function testState() {
printTTree(treeFromJson); printTTree(treeFromJson);
console.log('----------------'); 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)); console.log(util.inspect(state2.objects, true, 3, true));
} }
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment