state.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769
  1. /**
  2. * Copyright (c) 2018-2019 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 { StateTransform } from './transform';
  9. import { StateTransformer } from './transformer';
  10. import { RuntimeContext, Task } from '../mol-task';
  11. import { StateSelection } from './state/selection';
  12. import { RxEventHelper } from '../mol-util/rx-event-helper';
  13. import { StateBuilder } from './state/builder';
  14. import { StateAction } from './action';
  15. import { StateActionManager } from './action/manager';
  16. import { TransientTree } from './tree/transient';
  17. import { LogEntry } from '../mol-util/log-entry';
  18. import { now, formatTimespan } from '../mol-util/now';
  19. import { ParamDefinition } from '../mol-util/param-definition';
  20. import { StateTreeSpine } from './tree/spine';
  21. import { AsyncQueue } from '../mol-util/async-queue';
  22. import { isProductionMode } from '../mol-util/debug'
  23. import { arraySetAdd, arraySetRemove } from '../mol-util/array';
  24. import { UniqueArray } from '../mol-data/generic';
  25. export { State }
  26. class State {
  27. private _tree: TransientTree;
  28. protected errorFree = true;
  29. private ev = RxEventHelper.create();
  30. readonly globalContext: unknown = void 0;
  31. readonly events = {
  32. cell: {
  33. stateUpdated: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(),
  34. created: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(),
  35. removed: this.ev<State.ObjectEvent & { parent: StateTransform.Ref }>(),
  36. },
  37. object: {
  38. updated: this.ev<State.ObjectEvent & { action: 'in-place' | 'recreate', obj: StateObject, oldObj?: StateObject }>(),
  39. created: this.ev<State.ObjectEvent & { obj: StateObject }>(),
  40. removed: this.ev<State.ObjectEvent & { obj?: StateObject }>()
  41. },
  42. log: this.ev<LogEntry>(),
  43. changed: this.ev<State>(),
  44. isUpdating: this.ev<boolean>()
  45. };
  46. readonly behaviors = {
  47. currentObject: this.ev.behavior<State.ObjectEvent>({ state: this, ref: StateTransform.RootRef })
  48. };
  49. readonly actions = new StateActionManager();
  50. get tree(): StateTree { return this._tree; }
  51. get transforms() { return (this._tree as StateTree).transforms; }
  52. get current() { return this.behaviors.currentObject.value.ref; }
  53. build() { return new StateBuilder.Root(this.tree, this); }
  54. readonly cells: State.Cells = new Map();
  55. private spine = new StateTreeSpine.Impl(this.cells);
  56. getSnapshot(): State.Snapshot {
  57. return { tree: StateTree.toJSON(this._tree) };
  58. }
  59. setSnapshot(snapshot: State.Snapshot) {
  60. const tree = StateTree.fromJSON(snapshot.tree);
  61. return this.updateTree(tree);
  62. }
  63. setCurrent(ref: StateTransform.Ref) {
  64. this.behaviors.currentObject.next({ state: this, ref });
  65. }
  66. updateCellState(ref: StateTransform.Ref, stateOrProvider: ((old: StateTransform.State) => Partial<StateTransform.State>) | Partial<StateTransform.State>) {
  67. const cell = this.cells.get(ref);
  68. if (!cell) return;
  69. const update = typeof stateOrProvider === 'function' ? stateOrProvider(cell.state) : stateOrProvider;
  70. if (StateTransform.assignState(cell.state, update)) {
  71. cell.transform = this._tree.assignState(cell.transform.ref, update);
  72. this.events.cell.stateUpdated.next({ state: this, ref, cell });
  73. }
  74. }
  75. dispose() {
  76. this.ev.dispose();
  77. }
  78. /**
  79. * Select Cells using the provided selector.
  80. * @example state.query(StateSelection.Generators.byRef('test').ancestorOfType([type]))
  81. * @example state.query('test')
  82. */
  83. select<C extends StateObjectCell>(selector: StateSelection.Selector<C>) {
  84. return StateSelection.select(selector, this)
  85. }
  86. /**
  87. * Select Cells by building a query generated on the fly.
  88. * @example state.select(q => q.byRef('test').subtree())
  89. */
  90. selectQ<C extends StateObjectCell>(selector: (q: typeof StateSelection.Generators) => StateSelection.Selector<C>) {
  91. if (typeof selector === 'string') return StateSelection.select(selector, this);
  92. return StateSelection.select(selector(StateSelection.Generators), this)
  93. }
  94. /**
  95. * Creates a Task that applies the specified StateAction (i.e. must use run* on the result)
  96. * If no ref is specified, apply to root.
  97. */
  98. applyAction<A extends StateAction>(action: A, params: StateAction.Params<A>, ref: StateTransform.Ref = StateTransform.RootRef): Task<void> {
  99. return Task.create('Apply Action', ctx => {
  100. const cell = this.cells.get(ref);
  101. if (!cell) throw new Error(`'${ref}' does not exist.`);
  102. if (cell.status !== 'ok') throw new Error(`Action cannot be applied to a cell with status '${cell.status}'`);
  103. return runTask(action.definition.run({ ref, cell, a: cell.obj!, params, state: this }, this.globalContext), ctx);
  104. });
  105. }
  106. /**
  107. * Queues up a reconciliation of the existing state tree.
  108. *
  109. * If the tree is StateBuilder.To<T>, the corresponding StateObject is returned by the task.
  110. * @param tree Tree instance or a tree builder instance
  111. * @param doNotReportTiming Indicates whether to log timing of the individual transforms
  112. */
  113. updateTree<T extends StateObject>(tree: StateBuilder.To<T, any>, options?: Partial<State.UpdateOptions>): Task<T>
  114. updateTree(tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions>): Task<void>
  115. updateTree(tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions>): Task<any> {
  116. const params: UpdateParams = { tree, options };
  117. return Task.create('Update Tree', async taskCtx => {
  118. const removed = await this.updateQueue.enqueue(params);
  119. if (!removed) return;
  120. try {
  121. const ret = options && options.revertIfAborted
  122. ? await this._revertibleTreeUpdate(taskCtx, params)
  123. : await this._updateTree(taskCtx, params);
  124. return ret.cell;
  125. } finally {
  126. this.updateQueue.handled(params);
  127. }
  128. }, () => {
  129. this.updateQueue.remove(params);
  130. });
  131. }
  132. private updateQueue = new AsyncQueue<UpdateParams>();
  133. private async _revertibleTreeUpdate(taskCtx: RuntimeContext, params: UpdateParams) {
  134. const old = this.tree;
  135. const ret = await this._updateTree(taskCtx, params);
  136. if (ret.ctx.wasAborted) return await this._updateTree(taskCtx, { tree: old, options: params.options });
  137. return ret;
  138. }
  139. private async _updateTree(taskCtx: RuntimeContext, params: UpdateParams) {
  140. this.events.isUpdating.next(true);
  141. let updated = false;
  142. const ctx = this.updateTreeAndCreateCtx(params.tree, taskCtx, params.options);
  143. try {
  144. updated = await update(ctx);
  145. if (StateBuilder.isTo(params.tree)) {
  146. const cell = this.select(params.tree.ref)[0];
  147. return { ctx, cell: cell && cell.obj };
  148. }
  149. return { ctx };
  150. } finally {
  151. this.spine.current = undefined;
  152. if (updated) this.events.changed.next(this);
  153. this.events.isUpdating.next(false);
  154. }
  155. }
  156. private updateTreeAndCreateCtx(tree: StateTree | StateBuilder, taskCtx: RuntimeContext, options: Partial<State.UpdateOptions> | undefined) {
  157. const _tree = (StateBuilder.is(tree) ? tree.getTree() : tree).asTransient();
  158. const oldTree = this._tree;
  159. this._tree = _tree;
  160. const ctx: UpdateContext = {
  161. parent: this,
  162. editInfo: StateBuilder.is(tree) ? tree.editInfo : void 0,
  163. errorFree: this.errorFree,
  164. taskCtx,
  165. oldTree,
  166. tree: _tree,
  167. cells: this.cells as Map<StateTransform.Ref, StateObjectCell>,
  168. spine: this.spine,
  169. results: [],
  170. options: { ...StateUpdateDefaultOptions, ...options },
  171. changed: false,
  172. hadError: false,
  173. wasAborted: false,
  174. newCurrent: void 0
  175. };
  176. this.errorFree = true;
  177. return ctx;
  178. }
  179. constructor(rootObject: StateObject, params?: { globalContext?: unknown, rootState?: StateTransform.State }) {
  180. this._tree = StateTree.createEmpty(StateTransform.createRoot(params && params.rootState)).asTransient();
  181. const tree = this._tree;
  182. const root = tree.root;
  183. (this.cells as Map<StateTransform.Ref, StateObjectCell>).set(root.ref, {
  184. parent: this,
  185. transform: root,
  186. sourceRef: void 0,
  187. obj: rootObject,
  188. status: 'ok',
  189. state: { ...root.state },
  190. errorText: void 0,
  191. params: {
  192. definition: {},
  193. values: {}
  194. },
  195. dependencies: { dependentBy: [], dependsOn: [] },
  196. cache: { }
  197. });
  198. this.globalContext = params && params.globalContext;
  199. }
  200. }
  201. namespace State {
  202. export type Cells = ReadonlyMap<StateTransform.Ref, StateObjectCell>
  203. export type Tree = StateTree
  204. export type Builder = StateBuilder
  205. export interface ObjectEvent {
  206. state: State,
  207. ref: Ref
  208. }
  209. export interface Snapshot {
  210. readonly tree: StateTree.Serialized
  211. }
  212. export interface UpdateOptions {
  213. doNotLogTiming: boolean,
  214. doNotUpdateCurrent: boolean,
  215. revertIfAborted: boolean
  216. }
  217. export function create(rootObject: StateObject, params?: { globalContext?: unknown, rootState?: StateTransform.State }) {
  218. return new State(rootObject, params);
  219. }
  220. }
  221. const StateUpdateDefaultOptions: State.UpdateOptions = {
  222. doNotLogTiming: false,
  223. doNotUpdateCurrent: false,
  224. revertIfAborted: false
  225. };
  226. type Ref = StateTransform.Ref
  227. type UpdateParams = { tree: StateTree | StateBuilder, options?: Partial<State.UpdateOptions> }
  228. interface UpdateContext {
  229. parent: State,
  230. editInfo: StateBuilder.EditInfo | undefined
  231. errorFree: boolean,
  232. taskCtx: RuntimeContext,
  233. oldTree: StateTree,
  234. tree: TransientTree,
  235. cells: Map<StateTransform.Ref, StateObjectCell>,
  236. spine: StateTreeSpine.Impl,
  237. results: UpdateNodeResult[],
  238. // suppress timing messages
  239. options: State.UpdateOptions,
  240. changed: boolean,
  241. hadError: boolean,
  242. wasAborted: boolean,
  243. newCurrent?: Ref
  244. }
  245. async function update(ctx: UpdateContext) {
  246. // if only a single node was added/updated, we can skip potentially expensive diffing
  247. const fastTrack = !!(ctx.errorFree && ctx.editInfo && ctx.editInfo.count === 1 && ctx.editInfo.lastUpdate && ctx.editInfo.sourceTree === ctx.oldTree);
  248. let deletes: StateTransform.Ref[], deletedObjects: (StateObject | undefined)[] = [], roots: StateTransform.Ref[];
  249. if (fastTrack) {
  250. deletes = [];
  251. roots = [ctx.editInfo!.lastUpdate!];
  252. } else {
  253. // find all nodes that will definitely be deleted.
  254. // this is done in "post order", meaning that leaves will be deleted first.
  255. deletes = findDeletes(ctx);
  256. const current = ctx.parent.current;
  257. let hasCurrent = false;
  258. for (const d of deletes) {
  259. if (d === current) {
  260. hasCurrent = true;
  261. break;
  262. }
  263. }
  264. if (hasCurrent) {
  265. const newCurrent = findNewCurrent(ctx.oldTree, current, deletes, ctx.cells);
  266. ctx.parent.setCurrent(newCurrent);
  267. }
  268. for (const d of deletes) {
  269. const cell = ctx.cells.get(d);
  270. if (cell) unlinkCell(cell);
  271. const obj = cell && cell.obj;
  272. ctx.cells.delete(d);
  273. deletedObjects.push(obj);
  274. }
  275. // Find roots where transform version changed or where nodes will be added.
  276. roots = findUpdateRoots(ctx.cells, ctx.tree);
  277. }
  278. // Init empty cells where not present
  279. // this is done in "pre order", meaning that "parents" will be created 1st.
  280. const init = initCells(ctx, roots);
  281. // Notify additions of new cells.
  282. for (const cell of init.added) {
  283. ctx.parent.events.cell.created.next({ state: ctx.parent, ref: cell.transform.ref, cell });
  284. }
  285. for (let i = 0; i < deletes.length; i++) {
  286. const d = deletes[i];
  287. const parent = ctx.oldTree.transforms.get(d).parent;
  288. ctx.parent.events.object.removed.next({ state: ctx.parent, ref: d, obj: deletedObjects[i] });
  289. ctx.parent.events.cell.removed.next({ state: ctx.parent, ref: d, parent: parent });
  290. }
  291. if (deletedObjects.length) deletedObjects = [];
  292. if (init.dependent) {
  293. for (const cell of init.dependent) {
  294. roots.push(cell.transform.ref);
  295. }
  296. }
  297. // Set status of cells that will be updated to 'pending'.
  298. initCellStatus(ctx, roots);
  299. // Sequentially update all the subtrees.
  300. for (const root of roots) {
  301. await updateSubtree(ctx, root);
  302. }
  303. // Sync cell states
  304. if (!ctx.editInfo) {
  305. syncNewStates(ctx);
  306. }
  307. let newCurrent: StateTransform.Ref | undefined = ctx.newCurrent;
  308. // Raise object updated events
  309. for (const update of ctx.results) {
  310. if (update.action === 'created') {
  311. ctx.parent.events.object.created.next({ state: ctx.parent, ref: update.ref, obj: update.obj! });
  312. if (!ctx.newCurrent) {
  313. const transform = ctx.tree.transforms.get(update.ref);
  314. if (!transform.state.isGhost && update.obj !== StateObject.Null) newCurrent = update.ref;
  315. }
  316. } else if (update.action === 'updated') {
  317. ctx.parent.events.object.updated.next({ state: ctx.parent, ref: update.ref, action: 'in-place', obj: update.obj });
  318. } else if (update.action === 'replaced') {
  319. ctx.parent.events.object.updated.next({ state: ctx.parent, ref: update.ref, action: 'recreate', obj: update.obj, oldObj: update.oldObj });
  320. }
  321. }
  322. if (newCurrent) {
  323. if (!ctx.options.doNotUpdateCurrent) ctx.parent.setCurrent(newCurrent);
  324. } else {
  325. // check if old current or its parent hasn't become null
  326. const current = ctx.parent.current;
  327. const currentCell = ctx.cells.get(current);
  328. if (currentCell && (currentCell.obj === StateObject.Null
  329. || (currentCell.status === 'error' && currentCell.errorText === ParentNullErrorText))) {
  330. newCurrent = findNewCurrent(ctx.oldTree, current, [], ctx.cells);
  331. ctx.parent.setCurrent(newCurrent);
  332. }
  333. }
  334. return deletes.length > 0 || roots.length > 0 || ctx.changed;
  335. }
  336. function findUpdateRoots(cells: Map<StateTransform.Ref, StateObjectCell>, tree: StateTree) {
  337. const findState = { roots: [] as Ref[], cells };
  338. StateTree.doPreOrder(tree, tree.root, findState, findUpdateRootsVisitor);
  339. return findState.roots;
  340. }
  341. function findUpdateRootsVisitor(n: StateTransform, _: any, s: { roots: Ref[], cells: Map<Ref, StateObjectCell> }) {
  342. const cell = s.cells.get(n.ref);
  343. if (!cell || cell.transform.version !== n.version || cell.status === 'error') {
  344. s.roots.push(n.ref);
  345. return false;
  346. }
  347. // nothing below a Null object can be an update root
  348. if (cell && cell.obj === StateObject.Null) return false;
  349. return true;
  350. }
  351. type FindDeletesCtx = { newTree: StateTree, cells: State.Cells, deletes: Ref[] }
  352. function checkDeleteVisitor(n: StateTransform, _: any, ctx: FindDeletesCtx) {
  353. if (!ctx.newTree.transforms.has(n.ref) && ctx.cells.has(n.ref)) ctx.deletes.push(n.ref);
  354. }
  355. function findDeletes(ctx: UpdateContext): Ref[] {
  356. const deleteCtx: FindDeletesCtx = { newTree: ctx.tree, cells: ctx.cells, deletes: [] };
  357. StateTree.doPostOrder(ctx.oldTree, ctx.oldTree.root, deleteCtx, checkDeleteVisitor);
  358. return deleteCtx.deletes;
  359. }
  360. function syncNewStatesVisitor(n: StateTransform, tree: StateTree, ctx: UpdateContext) {
  361. const cell = ctx.cells.get(n.ref);
  362. if (!cell || !StateTransform.syncState(cell.state, n.state)) return;
  363. ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref: n.ref, cell });
  364. }
  365. function syncNewStates(ctx: UpdateContext) {
  366. StateTree.doPreOrder(ctx.tree, ctx.tree.root, ctx, syncNewStatesVisitor);
  367. }
  368. function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Status, errorText?: string) {
  369. const cell = ctx.cells.get(ref)!;
  370. const changed = cell.status !== status;
  371. cell.status = status;
  372. cell.errorText = errorText;
  373. if (changed) ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref, cell });
  374. }
  375. function initCellStatusVisitor(t: StateTransform, _: any, ctx: UpdateContext) {
  376. ctx.cells.get(t.ref)!.transform = t;
  377. setCellStatus(ctx, t.ref, 'pending');
  378. }
  379. function initCellStatus(ctx: UpdateContext, roots: Ref[]) {
  380. for (const root of roots) {
  381. StateTree.doPreOrder(ctx.tree, ctx.tree.transforms.get(root), ctx, initCellStatusVisitor);
  382. }
  383. }
  384. function unlinkCell(cell: StateObjectCell) {
  385. for (const other of cell.dependencies.dependsOn) {
  386. arraySetRemove(other.dependencies.dependentBy, cell);
  387. }
  388. }
  389. type InitCellsCtx = { ctx: UpdateContext, visited: Set<Ref>, added: StateObjectCell[] }
  390. function addCellsVisitor(transform: StateTransform, _: any, { ctx, added, visited }: InitCellsCtx) {
  391. visited.add(transform.ref);
  392. if (ctx.cells.has(transform.ref)) {
  393. return;
  394. }
  395. const cell: StateObjectCell = {
  396. parent: ctx.parent,
  397. transform,
  398. sourceRef: void 0,
  399. status: 'pending',
  400. state: { ...transform.state },
  401. errorText: void 0,
  402. params: void 0,
  403. dependencies: { dependentBy: [], dependsOn: [] },
  404. cache: void 0
  405. };
  406. ctx.cells.set(transform.ref, cell);
  407. added.push(cell);
  408. }
  409. // type LinkCellsCtx = { ctx: UpdateContext, visited: Set<Ref>, dependent: UniqueArray<Ref, StateObjectCell> }
  410. function linkCells(target: StateObjectCell, ctx: UpdateContext) {
  411. if (!target.transform.dependsOn) return;
  412. for (const ref of target.transform.dependsOn) {
  413. const t = ctx.tree.transforms.get(ref);
  414. if (!t) {
  415. throw new Error(`Cannot depend on a non-existent transform.`);
  416. }
  417. const cell = ctx.cells.get(ref)!;
  418. arraySetAdd(target.dependencies.dependsOn, cell);
  419. arraySetAdd(cell.dependencies.dependentBy, target);
  420. }
  421. }
  422. function initCells(ctx: UpdateContext, roots: Ref[]) {
  423. const initCtx: InitCellsCtx = { ctx, visited: new Set(), added: [] };
  424. // Add new cells
  425. for (const root of roots) {
  426. StateTree.doPreOrder(ctx.tree, ctx.tree.transforms.get(root), initCtx, addCellsVisitor);
  427. }
  428. // Update links for newly added cells
  429. for (const cell of initCtx.added) {
  430. linkCells(cell, ctx);
  431. }
  432. let dependent: UniqueArray<Ref, StateObjectCell>;
  433. // Find dependent cells
  434. initCtx.visited.forEach(ref => {
  435. const cell = ctx.cells.get(ref)!;
  436. for (const by of cell.dependencies.dependentBy) {
  437. if (initCtx.visited.has(by.transform.ref)) continue;
  438. if (!dependent) dependent = UniqueArray.create();
  439. UniqueArray.add(dependent, by.transform.ref, by);
  440. }
  441. });
  442. // TODO: check if dependent cells are all "proper roots"
  443. return { added: initCtx.added, dependent: dependent! ? dependent!.array : void 0 };
  444. }
  445. function findNewCurrent(tree: StateTree, start: Ref, deletes: Ref[], cells: Map<Ref, StateObjectCell>) {
  446. const deleteSet = new Set(deletes);
  447. return _findNewCurrent(tree, start, deleteSet, cells);
  448. }
  449. function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>, cells: Map<Ref, StateObjectCell>): Ref {
  450. if (ref === StateTransform.RootRef) return ref;
  451. const node = tree.transforms.get(ref)!;
  452. const siblings = tree.children.get(node.parent)!.values();
  453. let prevCandidate: Ref | undefined = void 0, seenRef = false;
  454. while (true) {
  455. const s = siblings.next();
  456. if (s.done) break;
  457. if (deletes.has(s.value)) continue;
  458. const cell = cells.get(s.value);
  459. if (!cell || cell.status === 'error' || cell.obj === StateObject.Null) {
  460. continue;
  461. }
  462. const t = tree.transforms.get(s.value);
  463. if (t.state.isGhost) continue;
  464. if (s.value === ref) {
  465. seenRef = true;
  466. if (!deletes.has(ref)) prevCandidate = ref;
  467. continue;
  468. }
  469. if (seenRef) return t.ref;
  470. prevCandidate = t.ref;
  471. }
  472. if (prevCandidate) return prevCandidate;
  473. return _findNewCurrent(tree, node.parent, deletes, cells);
  474. }
  475. /** Set status and error text of the cell. Remove all existing objects in the subtree. */
  476. function doError(ctx: UpdateContext, ref: Ref, errorObject: any | undefined, silent: boolean) {
  477. if (!silent) {
  478. ctx.hadError = true;
  479. (ctx.parent as any as { errorFree: boolean }).errorFree = false;
  480. }
  481. const cell = ctx.cells.get(ref)!;
  482. if (errorObject) {
  483. ctx.wasAborted = ctx.wasAborted || Task.isAbort(errorObject);
  484. const message = '' + errorObject;
  485. setCellStatus(ctx, ref, 'error', message);
  486. if (!silent) ctx.parent.events.log.next({ type: 'error', timestamp: new Date(), message });
  487. } else {
  488. cell.params = void 0;
  489. }
  490. if (cell.obj) {
  491. const obj = cell.obj;
  492. cell.obj = void 0;
  493. cell.cache = void 0;
  494. ctx.parent.events.object.removed.next({ state: ctx.parent, ref, obj });
  495. }
  496. // remove the objects in the child nodes if they exist
  497. const children = ctx.tree.children.get(ref).values();
  498. while (true) {
  499. const next = children.next();
  500. if (next.done) return;
  501. doError(ctx, next.value, void 0, silent);
  502. }
  503. }
  504. type UpdateNodeResult =
  505. | { ref: Ref, action: 'created', obj: StateObject }
  506. | { ref: Ref, action: 'updated', obj: StateObject }
  507. | { ref: Ref, action: 'replaced', oldObj?: StateObject, obj: StateObject }
  508. | { action: 'none' }
  509. const ParentNullErrorText = 'Parent is null';
  510. async function updateSubtree(ctx: UpdateContext, root: Ref) {
  511. setCellStatus(ctx, root, 'processing');
  512. let isNull = false;
  513. try {
  514. const start = now();
  515. const update = await updateNode(ctx, root);
  516. const time = now() - start;
  517. if (update.action !== 'none') ctx.changed = true;
  518. setCellStatus(ctx, root, 'ok');
  519. ctx.results.push(update);
  520. if (update.action === 'created') {
  521. isNull = update.obj === StateObject.Null;
  522. if (!isNull && !ctx.options.doNotLogTiming) ctx.parent.events.log.next(LogEntry.info(`Created ${update.obj.label} in ${formatTimespan(time)}.`));
  523. } else if (update.action === 'updated') {
  524. isNull = update.obj === StateObject.Null;
  525. if (!isNull && !ctx.options.doNotLogTiming) ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`));
  526. } else if (update.action === 'replaced') {
  527. isNull = update.obj === StateObject.Null;
  528. if (!isNull && !ctx.options.doNotLogTiming) ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`));
  529. }
  530. } catch (e) {
  531. ctx.changed = true;
  532. if (!ctx.hadError) ctx.newCurrent = root;
  533. doError(ctx, root, e, false);
  534. if (!isProductionMode) console.error(e);
  535. return;
  536. }
  537. const children = ctx.tree.children.get(root).values();
  538. while (true) {
  539. const next = children.next();
  540. if (next.done) return;
  541. if (isNull) doError(ctx, next.value, void 0, true);
  542. else await updateSubtree(ctx, next.value);
  543. }
  544. }
  545. function resolveParams(ctx: UpdateContext, transform: StateTransform, src: StateObject) {
  546. const prms = transform.transformer.definition.params;
  547. const definition = prms ? prms(src, ctx.parent.globalContext) : {};
  548. const values = transform.params ? transform.params : ParamDefinition.getDefaultValues(definition);
  549. return { definition, values };
  550. }
  551. async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNodeResult> {
  552. const { oldTree, tree } = ctx;
  553. const current = ctx.cells.get(currentRef)!;
  554. const transform = current.transform;
  555. // special case for Root
  556. if (current.transform.ref === StateTransform.RootRef) {
  557. return { action: 'none' };
  558. }
  559. let parentCell = transform.transformer.definition.from.length === 0
  560. ? ctx.cells.get(current.transform.parent)
  561. : StateSelection.findAncestorOfType(tree, ctx.cells, currentRef, transform.transformer.definition.from);
  562. if (!parentCell) {
  563. throw new Error(`No suitable parent found for '${currentRef}'`);
  564. }
  565. ctx.spine.current = current;
  566. const parent = parentCell.obj!;
  567. current.sourceRef = parentCell.transform.ref;
  568. const params = resolveParams(ctx, transform, parent);
  569. if (!oldTree.transforms.has(currentRef) || !current.params) {
  570. current.params = params;
  571. const obj = await createObject(ctx, current, transform.transformer, parent, params.values);
  572. updateTag(obj, transform);
  573. current.obj = obj;
  574. return { ref: currentRef, action: 'created', obj };
  575. } else {
  576. const oldParams = current.params.values;
  577. const newParams = params.values;
  578. current.params = params;
  579. const updateKind = !!current.obj && current.obj !== StateObject.Null
  580. ? await updateObject(ctx, current, transform.transformer, parent, current.obj!, oldParams, newParams)
  581. : StateTransformer.UpdateResult.Recreate;
  582. switch (updateKind) {
  583. case StateTransformer.UpdateResult.Recreate: {
  584. const oldObj = current.obj;
  585. const newObj = await createObject(ctx, current, transform.transformer, parent, newParams);
  586. updateTag(newObj, transform);
  587. current.obj = newObj;
  588. return { ref: currentRef, action: 'replaced', oldObj, obj: newObj };
  589. }
  590. case StateTransformer.UpdateResult.Updated:
  591. updateTag(current.obj, transform);
  592. return { ref: currentRef, action: 'updated', obj: current.obj! };
  593. case StateTransformer.UpdateResult.Null: {
  594. current.obj = StateObject.Null;
  595. return { ref: currentRef, action: 'updated', obj: current.obj! };
  596. }
  597. default:
  598. return { action: 'none' };
  599. }
  600. }
  601. }
  602. function updateTag(obj: StateObject | undefined, transform: StateTransform) {
  603. if (!obj || obj === StateObject.Null) return;
  604. (obj.tags as string[] | undefined) = transform.tags;
  605. }
  606. function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {
  607. if (typeof (t as any).runInContext === 'function') return (t as Task<T>).runInContext(ctx);
  608. return t as T;
  609. }
  610. function createObject(ctx: UpdateContext, cell: StateObjectCell, transformer: StateTransformer, a: StateObject, params: any) {
  611. if (!cell.cache) cell.cache = Object.create(null);
  612. return runTask(transformer.definition.apply({ a, params, cache: cell.cache, spine: ctx.spine, dependencies: resolveDependencies(ctx, cell) }, ctx.parent.globalContext), ctx.taskCtx);
  613. }
  614. function resolveDependencies(ctx: UpdateContext, cell: StateObjectCell) {
  615. if (cell.dependencies.dependsOn.length === 0) return void 0;
  616. const deps = Object.create(null);
  617. for (const dep of cell.dependencies.dependsOn) {
  618. if (!dep.obj) {
  619. throw new Error('Unresolved dependency.');
  620. }
  621. deps[dep.transform.ref] = dep.obj;
  622. }
  623. return deps;
  624. }
  625. async function updateObject(ctx: UpdateContext, cell: StateObjectCell, transformer: StateTransformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
  626. if (!transformer.definition.update) {
  627. return StateTransformer.UpdateResult.Recreate;
  628. }
  629. if (!cell.cache) cell.cache = Object.create(null);
  630. return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache: cell.cache, spine: ctx.spine, dependencies: resolveDependencies(ctx, cell) }, ctx.parent.globalContext), ctx.taskCtx);
  631. }