state.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. /**
  2. * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. */
  6. import { StateObject, StateObjectCell } from './object';
  7. import { StateTree } from './tree';
  8. import { Transform } from './transform';
  9. import { ImmutableTree } from './immutable-tree';
  10. import { Transformer } from './transformer';
  11. import { StateContext } from './context';
  12. import { UUID } from 'mol-util';
  13. import { RuntimeContext, Task } from 'mol-task';
  14. export { State }
  15. class State {
  16. private _tree: StateTree = StateTree.create();
  17. private _current: Transform.Ref = this._tree.rootRef;
  18. private transformCache = new Map<Transform.Ref, unknown>();
  19. get tree() { return this._tree; }
  20. get current() { return this._current; }
  21. readonly cells: State.Cells = new Map();
  22. readonly context: StateContext;
  23. getSnapshot(): State.Snapshot {
  24. const props = Object.create(null);
  25. const keys = this.cells.keys();
  26. while (true) {
  27. const key = keys.next();
  28. if (key.done) break;
  29. const o = this.cells.get(key.value)!;
  30. props[key.value] = { ...o.props };
  31. }
  32. return {
  33. tree: StateTree.toJSON(this._tree),
  34. props
  35. };
  36. }
  37. setSnapshot(snapshot: State.Snapshot) {
  38. const tree = StateTree.fromJSON(snapshot.tree);
  39. // TODO: support props and async
  40. return this.update(tree).run();
  41. }
  42. setCurrent(ref: Transform.Ref) {
  43. this._current = ref;
  44. this.context.behaviors.currentObject.next({ ref });
  45. }
  46. dispose() {
  47. this.context.dispose();
  48. }
  49. update(tree: StateTree): Task<void> {
  50. // TODO: support props
  51. return Task.create('Update Tree', async taskCtx => {
  52. try {
  53. const oldTree = this._tree;
  54. this._tree = tree;
  55. const ctx: UpdateContext = {
  56. stateCtx: this.context,
  57. taskCtx,
  58. oldTree,
  59. tree: tree,
  60. objects: this.cells,
  61. transformCache: this.transformCache
  62. };
  63. // TODO: have "cancelled" error? Or would this be handled automatically?
  64. await update(ctx);
  65. } finally {
  66. this.context.events.updated.next();
  67. }
  68. });
  69. }
  70. constructor(rootObject: StateObject, params?: { globalContext?: unknown, defaultObjectProps?: unknown }) {
  71. const tree = this._tree;
  72. const root = tree.getValue(tree.rootRef)!;
  73. const defaultObjectProps = (params && params.defaultObjectProps) || { }
  74. this.cells.set(tree.rootRef, {
  75. ref: tree.rootRef,
  76. obj: rootObject,
  77. status: 'ok',
  78. version: root.version,
  79. props: { ...defaultObjectProps }
  80. });
  81. this.context = new StateContext({
  82. globalContext: params && params.globalContext,
  83. defaultObjectProps,
  84. rootRef: tree.rootRef
  85. });
  86. }
  87. }
  88. namespace State {
  89. export type Cells = Map<Transform.Ref, StateObjectCell>
  90. export interface Snapshot {
  91. readonly tree: StateTree.Serialized,
  92. readonly props: { [key: string]: unknown }
  93. }
  94. export function create(rootObject: StateObject, params?: { globalContext?: unknown, defaultObjectProps?: unknown }) {
  95. return new State(rootObject, params);
  96. }
  97. }
  98. type Ref = Transform.Ref
  99. interface UpdateContext {
  100. stateCtx: StateContext,
  101. taskCtx: RuntimeContext,
  102. oldTree: StateTree,
  103. tree: StateTree,
  104. objects: State.Cells,
  105. transformCache: Map<Ref, unknown>
  106. }
  107. async function update(ctx: UpdateContext) {
  108. const roots = findUpdateRoots(ctx.objects, ctx.tree);
  109. const deletes = findDeletes(ctx);
  110. for (const d of deletes) {
  111. const obj = ctx.objects.has(d) ? ctx.objects.get(d)!.obj : void 0;
  112. ctx.objects.delete(d);
  113. ctx.transformCache.delete(d);
  114. ctx.stateCtx.events.object.removed.next({ ref: d, obj });
  115. // TODO: handle current object change
  116. }
  117. initObjectState(ctx, roots);
  118. for (const root of roots) {
  119. await updateSubtree(ctx, root);
  120. }
  121. }
  122. function findUpdateRoots(objects: State.Cells, tree: StateTree) {
  123. const findState = {
  124. roots: [] as Ref[],
  125. objects
  126. };
  127. ImmutableTree.doPreOrder(tree, tree.nodes.get(tree.rootRef)!, findState, (n, _, s) => {
  128. if (!s.objects.has(n.ref)) {
  129. s.roots.push(n.ref);
  130. return false;
  131. }
  132. const o = s.objects.get(n.ref)!;
  133. if (o.version !== n.value.version) {
  134. s.roots.push(n.ref);
  135. return false;
  136. }
  137. return true;
  138. });
  139. return findState.roots;
  140. }
  141. function findDeletes(ctx: UpdateContext): Ref[] {
  142. // TODO: do this in some sort of "tree order"?
  143. const deletes: Ref[] = [];
  144. const keys = ctx.objects.keys();
  145. while (true) {
  146. const key = keys.next();
  147. if (key.done) break;
  148. if (!ctx.tree.nodes.has(key.value)) deletes.push(key.value);
  149. }
  150. return deletes;
  151. }
  152. function setObjectState(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Status, errorText?: string) {
  153. let changed = false;
  154. if (ctx.objects.has(ref)) {
  155. const obj = ctx.objects.get(ref)!;
  156. changed = obj.status !== status;
  157. obj.status = status;
  158. obj.errorText = errorText;
  159. } else {
  160. const obj: StateObjectCell = { ref, status, version: UUID.create(), errorText, props: { ...ctx.stateCtx.defaultObjectProps } };
  161. ctx.objects.set(ref, obj);
  162. changed = true;
  163. }
  164. if (changed) ctx.stateCtx.events.object.stateChanged.next({ ref });
  165. }
  166. function _initVisitor(t: ImmutableTree.Node<Transform>, _: any, ctx: UpdateContext) {
  167. setObjectState(ctx, t.ref, 'pending');
  168. }
  169. /** Return "resolve set" */
  170. function initObjectState(ctx: UpdateContext, roots: Ref[]) {
  171. for (const root of roots) {
  172. ImmutableTree.doPreOrder(ctx.tree, ctx.tree.nodes.get(root), ctx, _initVisitor);
  173. }
  174. }
  175. function doError(ctx: UpdateContext, ref: Ref, errorText: string) {
  176. setObjectState(ctx, ref, 'error', errorText);
  177. const wrap = ctx.objects.get(ref)!;
  178. if (wrap.obj) {
  179. ctx.stateCtx.events.object.removed.next({ ref });
  180. ctx.transformCache.delete(ref);
  181. wrap.obj = void 0;
  182. }
  183. const children = ctx.tree.nodes.get(ref)!.children.values();
  184. while (true) {
  185. const next = children.next();
  186. if (next.done) return;
  187. doError(ctx, next.value, 'Parent node contains error.');
  188. }
  189. }
  190. function findAncestor(tree: StateTree, objects: State.Cells, root: Ref, types: { type: StateObject.Type }[]): StateObject {
  191. let current = tree.nodes.get(root)!;
  192. while (true) {
  193. current = tree.nodes.get(current.parent)!;
  194. if (current.ref === tree.rootRef) {
  195. return objects.get(tree.rootRef)!.obj!;
  196. }
  197. const obj = objects.get(current.ref)!.obj!;
  198. for (const t of types) if (obj.type === t.type) return objects.get(current.ref)!.obj!;
  199. }
  200. }
  201. async function updateSubtree(ctx: UpdateContext, root: Ref) {
  202. setObjectState(ctx, root, 'processing');
  203. try {
  204. const update = await updateNode(ctx, root);
  205. setObjectState(ctx, root, 'ok');
  206. if (update.action === 'created') {
  207. ctx.stateCtx.events.object.created.next({ ref: root, obj: update.obj! });
  208. } else if (update.action === 'updated') {
  209. ctx.stateCtx.events.object.updated.next({ ref: root, obj: update.obj });
  210. } else if (update.action === 'replaced') {
  211. ctx.stateCtx.events.object.replaced.next({ ref: root, oldObj: update.oldObj, newObj: update.newObj });
  212. }
  213. } catch (e) {
  214. doError(ctx, root, '' + e);
  215. return;
  216. }
  217. const children = ctx.tree.nodes.get(root)!.children.values();
  218. while (true) {
  219. const next = children.next();
  220. if (next.done) return;
  221. await updateSubtree(ctx, next.value);
  222. }
  223. }
  224. async function updateNode(ctx: UpdateContext, currentRef: Ref) {
  225. const { oldTree, tree, objects } = ctx;
  226. const transform = tree.getValue(currentRef)!;
  227. const parent = findAncestor(tree, objects, currentRef, transform.transformer.definition.from);
  228. // console.log('parent', transform.transformer.id, transform.transformer.definition.from[0].type, parent ? parent.ref : 'undefined')
  229. if (!oldTree.nodes.has(currentRef) || !objects.has(currentRef)) {
  230. // console.log('creating...', transform.transformer.id, oldTree.nodes.has(currentRef), objects.has(currentRef));
  231. const obj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params);
  232. objects.set(currentRef, {
  233. ref: currentRef,
  234. obj,
  235. status: 'ok',
  236. version: transform.version,
  237. props: { ...ctx.stateCtx.defaultObjectProps, ...transform.defaultProps }
  238. });
  239. return { action: 'created', obj };
  240. } else {
  241. // console.log('updating...', transform.transformer.id);
  242. const current = objects.get(currentRef)!;
  243. const oldParams = oldTree.getValue(currentRef)!.params;
  244. switch (await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, transform.params)) {
  245. case Transformer.UpdateResult.Recreate: {
  246. const obj = await createObject(ctx, currentRef, transform.transformer, parent, transform.params);
  247. objects.set(currentRef, {
  248. ref: currentRef,
  249. obj,
  250. status: 'ok',
  251. version: transform.version,
  252. props: { ...ctx.stateCtx.defaultObjectProps, ...current.props, ...transform.defaultProps }
  253. });
  254. return { action: 'replaced', oldObj: current.obj!, newObj: obj };
  255. }
  256. case Transformer.UpdateResult.Updated:
  257. current.version = transform.version;
  258. current.props = { ...ctx.stateCtx.defaultObjectProps, ...current.props, ...transform.defaultProps };
  259. return { action: 'updated', obj: current.obj };
  260. default:
  261. // TODO check if props need to be updated
  262. return { action: 'none' };
  263. }
  264. }
  265. }
  266. function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {
  267. if (typeof (t as any).run === 'function') return (t as Task<T>).runInContext(ctx);
  268. return t as T;
  269. }
  270. function createObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, params: any) {
  271. const cache = { };
  272. ctx.transformCache.set(ref, cache);
  273. return runTask(transformer.definition.apply({ a, params, cache }, ctx.stateCtx.globalContext), ctx.taskCtx);
  274. }
  275. async function updateObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
  276. if (!transformer.definition.update) {
  277. return Transformer.UpdateResult.Recreate;
  278. }
  279. let cache = ctx.transformCache.get(ref);
  280. if (!cache) {
  281. cache = { };
  282. ctx.transformCache.set(ref, cache);
  283. }
  284. return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache }, ctx.stateCtx.globalContext), ctx.taskCtx);
  285. }