state.ts 34 KB

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