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 @@
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
}
}
}
......
......@@ -46,8 +46,9 @@ export namespace StateObject {
}
}
export interface Wrapped {
export interface Node {
state: StateType,
props: unknown,
errorText?: string,
obj?: StateObject,
version: string
......
......@@ -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);
}
}
......@@ -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,
......
/**
* 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> {
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));
}
......
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