state.ts 36 KB

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