state.ts 35 KB

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