state.ts 12 KB

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