state.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. /**
  2. * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. */
  6. import { StateObject, StateObjectCell } from './object';
  7. import { StateTree } from './tree';
  8. import { Transform } from './transform';
  9. import { Transformer } from './transformer';
  10. import { UUID } from 'mol-util';
  11. import { RuntimeContext, Task } from 'mol-task';
  12. import { StateSelection } from './state/selection';
  13. import { RxEventHelper } from 'mol-util/rx-event-helper';
  14. import { StateTreeBuilder } from './tree/builder';
  15. import { StateAction } from './action';
  16. import { StateActionManager } from './action/manager';
  17. import { TransientTree } from './tree/transient';
  18. import { LogEntry } from 'mol-util/log-entry';
  19. import { now, formatTimespan } from 'mol-util/now';
  20. import { ParamDefinition } from 'mol-util/param-definition';
  21. export { State }
  22. class State {
  23. private _tree: TransientTree = StateTree.createEmpty().asTransient();
  24. protected errorFree = true;
  25. private transformCache = new Map<Transform.Ref, unknown>();
  26. private ev = RxEventHelper.create();
  27. readonly globalContext: unknown = void 0;
  28. readonly events = {
  29. cell: {
  30. stateUpdated: this.ev<State.ObjectEvent & { cellState: StateObjectCell.State}>(),
  31. created: this.ev<State.ObjectEvent & { cell: StateObjectCell }>(),
  32. removed: this.ev<State.ObjectEvent & { parent: Transform.Ref }>(),
  33. },
  34. object: {
  35. updated: this.ev<State.ObjectEvent & { action: 'in-place' | 'recreate', obj: StateObject, oldObj?: StateObject }>(),
  36. created: this.ev<State.ObjectEvent & { obj: StateObject }>(),
  37. removed: this.ev<State.ObjectEvent & { obj?: StateObject }>()
  38. },
  39. log: this.ev<LogEntry>(),
  40. changed: this.ev<void>()
  41. };
  42. readonly behaviors = {
  43. currentObject: this.ev.behavior<State.ObjectEvent>({ state: this, ref: Transform.RootRef })
  44. };
  45. readonly actions = new StateActionManager();
  46. get tree(): StateTree { return this._tree; }
  47. get transforms() { return (this._tree as StateTree).transforms; }
  48. get cellStates() { return (this._tree as StateTree).cellStates; }
  49. get current() { return this.behaviors.currentObject.value.ref; }
  50. build() { return this._tree.build(); }
  51. readonly cells: State.Cells = new Map();
  52. getSnapshot(): State.Snapshot {
  53. return { tree: StateTree.toJSON(this._tree) };
  54. }
  55. setSnapshot(snapshot: State.Snapshot) {
  56. const tree = StateTree.fromJSON(snapshot.tree);
  57. return this.update(tree);
  58. }
  59. setCurrent(ref: Transform.Ref) {
  60. this.behaviors.currentObject.next({ state: this, ref });
  61. }
  62. updateCellState(ref: Transform.Ref, stateOrProvider: ((old: StateObjectCell.State) => Partial<StateObjectCell.State>) | Partial<StateObjectCell.State>) {
  63. const update = typeof stateOrProvider === 'function'
  64. ? stateOrProvider(this.tree.cellStates.get(ref))
  65. : stateOrProvider;
  66. if (this._tree.updateCellState(ref, update)) {
  67. this.events.cell.stateUpdated.next({ state: this, ref, cellState: this.tree.cellStates.get(ref) });
  68. }
  69. }
  70. dispose() {
  71. this.ev.dispose();
  72. }
  73. /**
  74. * Select Cells by ref or a query generated on the fly.
  75. * @example state.select('test')
  76. * @example state.select(q => q.byRef('test').subtree())
  77. */
  78. select(selector: Transform.Ref | ((q: typeof StateSelection.Generators) => StateSelection.Selector)) {
  79. if (typeof selector === 'string') return StateSelection.select(selector, this);
  80. return StateSelection.select(selector(StateSelection.Generators), this)
  81. }
  82. /**
  83. * Select Cells using the provided selector.
  84. * @example state.select('test')
  85. * @example state.select(q => q.byRef('test').subtree())
  86. */
  87. query(selector: StateSelection.Selector) {
  88. return StateSelection.select(selector, this)
  89. }
  90. /** If no ref is specified, apply to root */
  91. apply<A extends StateAction>(action: A, params: StateAction.Params<A>, ref: Transform.Ref = Transform.RootRef): Task<void> {
  92. return Task.create('Apply Action', ctx => {
  93. const cell = this.cells.get(ref);
  94. if (!cell) throw new Error(`'${ref}' does not exist.`);
  95. if (cell.status !== 'ok') throw new Error(`Action cannot be applied to a cell with status '${cell.status}'`);
  96. return runTask(action.definition.run({ ref, cell, a: cell.obj!, params, state: this }, this.globalContext), ctx);
  97. });
  98. }
  99. update(tree: StateTree | StateTreeBuilder, silent: boolean = false): Task<void> {
  100. const _tree = (StateTreeBuilder.is(tree) ? tree.getTree() : tree).asTransient();
  101. return Task.create('Update Tree', async taskCtx => {
  102. let updated = false;
  103. try {
  104. const oldTree = this._tree;
  105. this._tree = _tree;
  106. const ctx: UpdateContext = {
  107. parent: this,
  108. editInfo: StateTreeBuilder.is(tree) ? tree.editInfo : void 0,
  109. errorFree: this.errorFree,
  110. taskCtx,
  111. oldTree,
  112. tree: _tree,
  113. cells: this.cells as Map<Transform.Ref, StateObjectCell>,
  114. transformCache: this.transformCache,
  115. results: [],
  116. silent,
  117. changed: false,
  118. hadError: false,
  119. newCurrent: void 0
  120. };
  121. this.errorFree = true;
  122. // TODO: handle "cancelled" error? Or would this be handled automatically?
  123. updated = await update(ctx);
  124. } finally {
  125. if (updated) this.events.changed.next();
  126. }
  127. });
  128. }
  129. constructor(rootObject: StateObject, params?: { globalContext?: unknown }) {
  130. const tree = this._tree;
  131. const root = tree.root;
  132. (this.cells as Map<Transform.Ref, StateObjectCell>).set(root.ref, {
  133. transform: root,
  134. sourceRef: void 0,
  135. obj: rootObject,
  136. status: 'ok',
  137. version: root.version,
  138. errorText: void 0,
  139. params: {
  140. definition: { },
  141. values: { }
  142. }
  143. });
  144. this.globalContext = params && params.globalContext;
  145. }
  146. }
  147. namespace State {
  148. export type Cells = ReadonlyMap<Transform.Ref, StateObjectCell>
  149. export type Tree = StateTree
  150. export type Builder = StateTreeBuilder
  151. export interface ObjectEvent {
  152. state: State,
  153. ref: Ref
  154. }
  155. export interface Snapshot {
  156. readonly tree: StateTree.Serialized
  157. }
  158. export function create(rootObject: StateObject, params?: { globalContext?: unknown, defaultObjectProps?: unknown }) {
  159. return new State(rootObject, params);
  160. }
  161. }
  162. type Ref = Transform.Ref
  163. interface UpdateContext {
  164. parent: State,
  165. editInfo: StateTreeBuilder.EditInfo | undefined
  166. errorFree: boolean,
  167. taskCtx: RuntimeContext,
  168. oldTree: StateTree,
  169. tree: TransientTree,
  170. cells: Map<Transform.Ref, StateObjectCell>,
  171. transformCache: Map<Ref, unknown>,
  172. results: UpdateNodeResult[],
  173. // suppress timing messages
  174. silent: boolean,
  175. changed: boolean,
  176. hadError: boolean,
  177. newCurrent?: Ref
  178. }
  179. async function update(ctx: UpdateContext) {
  180. // if only a single node was added/updated, we can skip potentially expensive diffing
  181. const fastTrack = !!(ctx.errorFree && ctx.editInfo && ctx.editInfo.count === 1 && ctx.editInfo.lastUpdate && ctx.editInfo.sourceTree === ctx.oldTree);
  182. let deletes: Transform.Ref[], deletedObjects: (StateObject | undefined)[] = [], roots: Transform.Ref[];
  183. if (fastTrack) {
  184. deletes = [];
  185. roots = [ctx.editInfo!.lastUpdate!];
  186. } else {
  187. // find all nodes that will definitely be deleted.
  188. // this is done in "post order", meaning that leaves will be deleted first.
  189. deletes = findDeletes(ctx);
  190. const current = ctx.parent.current;
  191. let hasCurrent = false;
  192. for (const d of deletes) {
  193. if (d === current) {
  194. hasCurrent = true;
  195. break;
  196. }
  197. }
  198. if (hasCurrent) {
  199. const newCurrent = findNewCurrent(ctx.oldTree, current, deletes, ctx.cells);
  200. ctx.parent.setCurrent(newCurrent);
  201. }
  202. for (const d of deletes) {
  203. const obj = ctx.cells.has(d) ? ctx.cells.get(d)!.obj : void 0;
  204. ctx.cells.delete(d);
  205. ctx.transformCache.delete(d);
  206. deletedObjects.push(obj);
  207. }
  208. // Find roots where transform version changed or where nodes will be added.
  209. roots = findUpdateRoots(ctx.cells, ctx.tree);
  210. }
  211. // Init empty cells where not present
  212. // this is done in "pre order", meaning that "parents" will be created 1st.
  213. const addedCells = initCells(ctx, roots);
  214. // Ensure cell states stay consistent
  215. if (!ctx.editInfo) {
  216. syncStates(ctx);
  217. }
  218. // Notify additions of new cells.
  219. for (const cell of addedCells) {
  220. ctx.parent.events.cell.created.next({ state: ctx.parent, ref: cell.transform.ref, cell });
  221. }
  222. for (let i = 0; i < deletes.length; i++) {
  223. const d = deletes[i];
  224. const parent = ctx.oldTree.transforms.get(d).parent;
  225. ctx.parent.events.object.removed.next({ state: ctx.parent, ref: d, obj: deletedObjects[i] });
  226. ctx.parent.events.cell.removed.next({ state: ctx.parent, ref: d, parent: parent });
  227. }
  228. if (deletedObjects.length) deletedObjects = [];
  229. // Set status of cells that will be updated to 'pending'.
  230. initCellStatus(ctx, roots);
  231. // Sequentially update all the subtrees.
  232. for (const root of roots) {
  233. await updateSubtree(ctx, root);
  234. }
  235. let newCurrent: Transform.Ref | undefined = ctx.newCurrent;
  236. // Raise object updated events
  237. for (const update of ctx.results) {
  238. if (update.action === 'created') {
  239. ctx.parent.events.object.created.next({ state: ctx.parent, ref: update.ref, obj: update.obj! });
  240. if (!ctx.newCurrent) {
  241. const transform = ctx.tree.transforms.get(update.ref);
  242. if (!(transform.props && transform.props.isGhost) && update.obj !== StateObject.Null) newCurrent = update.ref;
  243. }
  244. } else if (update.action === 'updated') {
  245. ctx.parent.events.object.updated.next({ state: ctx.parent, ref: update.ref, action: 'in-place', obj: update.obj });
  246. } else if (update.action === 'replaced') {
  247. ctx.parent.events.object.updated.next({ state: ctx.parent, ref: update.ref, action: 'recreate', obj: update.obj, oldObj: update.oldObj });
  248. }
  249. }
  250. if (newCurrent) ctx.parent.setCurrent(newCurrent);
  251. else {
  252. // check if old current or its parent hasn't become null
  253. const current = ctx.parent.current;
  254. const currentCell = ctx.cells.get(current);
  255. if (currentCell && (
  256. currentCell.obj === StateObject.Null
  257. || (currentCell.status === 'error' && currentCell.errorText === ParentNullErrorText))) {
  258. newCurrent = findNewCurrent(ctx.oldTree, current, [], ctx.cells);
  259. ctx.parent.setCurrent(newCurrent);
  260. }
  261. }
  262. return deletes.length > 0 || roots.length > 0 || ctx.changed;
  263. }
  264. function findUpdateRoots(cells: Map<Transform.Ref, StateObjectCell>, tree: StateTree) {
  265. const findState = { roots: [] as Ref[], cells };
  266. StateTree.doPreOrder(tree, tree.root, findState, findUpdateRootsVisitor);
  267. return findState.roots;
  268. }
  269. function findUpdateRootsVisitor(n: Transform, _: any, s: { roots: Ref[], cells: Map<Ref, StateObjectCell> }) {
  270. const cell = s.cells.get(n.ref);
  271. if (!cell || cell.version !== n.version || cell.status === 'error') {
  272. s.roots.push(n.ref);
  273. return false;
  274. }
  275. // nothing below a Null object can be an update root
  276. if (cell && cell.obj === StateObject.Null) return false;
  277. return true;
  278. }
  279. type FindDeletesCtx = { newTree: StateTree, cells: State.Cells, deletes: Ref[] }
  280. function checkDeleteVisitor(n: Transform, _: any, ctx: FindDeletesCtx) {
  281. if (!ctx.newTree.transforms.has(n.ref) && ctx.cells.has(n.ref)) ctx.deletes.push(n.ref);
  282. }
  283. function findDeletes(ctx: UpdateContext): Ref[] {
  284. const deleteCtx: FindDeletesCtx = { newTree: ctx.tree, cells: ctx.cells, deletes: [] };
  285. StateTree.doPostOrder(ctx.oldTree, ctx.oldTree.root, deleteCtx, checkDeleteVisitor);
  286. return deleteCtx.deletes;
  287. }
  288. function syncStatesVisitor(n: Transform, tree: StateTree, oldState: StateTree.CellStates) {
  289. if (!oldState.has(n.ref)) return;
  290. (tree as TransientTree).updateCellState(n.ref, oldState.get(n.ref));
  291. }
  292. function syncStates(ctx: UpdateContext) {
  293. StateTree.doPreOrder(ctx.tree, ctx.tree.root, ctx.oldTree.cellStates, syncStatesVisitor);
  294. }
  295. function setCellStatus(ctx: UpdateContext, ref: Ref, status: StateObjectCell.Status, errorText?: string) {
  296. const cell = ctx.cells.get(ref)!;
  297. const changed = cell.status !== status;
  298. cell.status = status;
  299. cell.errorText = errorText;
  300. if (changed) ctx.parent.events.cell.stateUpdated.next({ state: ctx.parent, ref, cellState: ctx.tree.cellStates.get(ref) });
  301. }
  302. function initCellStatusVisitor(t: Transform, _: any, ctx: UpdateContext) {
  303. ctx.cells.get(t.ref)!.transform = t;
  304. setCellStatus(ctx, t.ref, 'pending');
  305. }
  306. function initCellStatus(ctx: UpdateContext, roots: Ref[]) {
  307. for (const root of roots) {
  308. StateTree.doPreOrder(ctx.tree, ctx.tree.transforms.get(root), ctx, initCellStatusVisitor);
  309. }
  310. }
  311. type InitCellsCtx = { ctx: UpdateContext, added: StateObjectCell[] }
  312. function initCellsVisitor(transform: Transform, _: any, { ctx, added }: InitCellsCtx) {
  313. if (ctx.cells.has(transform.ref)) {
  314. return;
  315. }
  316. const cell: StateObjectCell = {
  317. transform,
  318. sourceRef: void 0,
  319. status: 'pending',
  320. version: UUID.create22(),
  321. errorText: void 0,
  322. params: void 0
  323. };
  324. ctx.cells.set(transform.ref, cell);
  325. added.push(cell);
  326. }
  327. function initCells(ctx: UpdateContext, roots: Ref[]) {
  328. const initCtx: InitCellsCtx = { ctx, added: [] };
  329. for (const root of roots) {
  330. StateTree.doPreOrder(ctx.tree, ctx.tree.transforms.get(root), initCtx, initCellsVisitor);
  331. }
  332. return initCtx.added;
  333. }
  334. function findNewCurrent(tree: StateTree, start: Ref, deletes: Ref[], cells: Map<Ref, StateObjectCell>) {
  335. const deleteSet = new Set(deletes);
  336. return _findNewCurrent(tree, start, deleteSet, cells);
  337. }
  338. function _findNewCurrent(tree: StateTree, ref: Ref, deletes: Set<Ref>, cells: Map<Ref, StateObjectCell>): Ref {
  339. if (ref === Transform.RootRef) return ref;
  340. const node = tree.transforms.get(ref)!;
  341. const siblings = tree.children.get(node.parent)!.values();
  342. let prevCandidate: Ref | undefined = void 0, seenRef = false;
  343. while (true) {
  344. const s = siblings.next();
  345. if (s.done) break;
  346. if (deletes.has(s.value)) continue;
  347. const cell = cells.get(s.value);
  348. if (!cell || cell.status === 'error' || cell.obj === StateObject.Null) {
  349. continue;
  350. }
  351. const t = tree.transforms.get(s.value);
  352. if (t.props && t.props.isGhost) continue;
  353. if (s.value === ref) {
  354. seenRef = true;
  355. if (!deletes.has(ref)) prevCandidate = ref;
  356. continue;
  357. }
  358. if (seenRef) return t.ref;
  359. prevCandidate = t.ref;
  360. }
  361. if (prevCandidate) return prevCandidate;
  362. return _findNewCurrent(tree, node.parent, deletes, cells);
  363. }
  364. /** Set status and error text of the cell. Remove all existing objects in the subtree. */
  365. function doError(ctx: UpdateContext, ref: Ref, errorText: string | undefined, silent: boolean) {
  366. if (!silent) {
  367. ctx.hadError = true;
  368. (ctx.parent as any as { errorFree: boolean }).errorFree = false;
  369. }
  370. const cell = ctx.cells.get(ref)!;
  371. if (errorText) {
  372. setCellStatus(ctx, ref, 'error', errorText);
  373. if (!silent) ctx.parent.events.log.next({ type: 'error', timestamp: new Date(), message: errorText });
  374. } else {
  375. cell.params = void 0;
  376. }
  377. if (cell.obj) {
  378. const obj = cell.obj;
  379. cell.obj = void 0;
  380. ctx.parent.events.object.removed.next({ state: ctx.parent, ref, obj });
  381. ctx.transformCache.delete(ref);
  382. }
  383. // remove the objects in the child nodes if they exist
  384. const children = ctx.tree.children.get(ref).values();
  385. while (true) {
  386. const next = children.next();
  387. if (next.done) return;
  388. doError(ctx, next.value, void 0, silent);
  389. }
  390. }
  391. type UpdateNodeResult =
  392. | { ref: Ref, action: 'created', obj: StateObject }
  393. | { ref: Ref, action: 'updated', obj: StateObject }
  394. | { ref: Ref, action: 'replaced', oldObj?: StateObject, obj: StateObject }
  395. | { action: 'none' }
  396. const ParentNullErrorText = 'Parent is null';
  397. async function updateSubtree(ctx: UpdateContext, root: Ref) {
  398. setCellStatus(ctx, root, 'processing');
  399. let isNull = false;
  400. try {
  401. const start = now();
  402. const update = await updateNode(ctx, root);
  403. const time = now() - start;
  404. if (update.action !== 'none') ctx.changed = true;
  405. setCellStatus(ctx, root, 'ok');
  406. ctx.results.push(update);
  407. if (update.action === 'created') {
  408. isNull = update.obj === StateObject.Null;
  409. if (!isNull && !ctx.silent) ctx.parent.events.log.next(LogEntry.info(`Created ${update.obj.label} in ${formatTimespan(time)}.`));
  410. } else if (update.action === 'updated') {
  411. isNull = update.obj === StateObject.Null;
  412. if (!isNull && !ctx.silent) ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`));
  413. } else if (update.action === 'replaced') {
  414. isNull = update.obj === StateObject.Null;
  415. if (!isNull && !ctx.silent) ctx.parent.events.log.next(LogEntry.info(`Updated ${update.obj.label} in ${formatTimespan(time)}.`));
  416. }
  417. } catch (e) {
  418. ctx.changed = true;
  419. if (!ctx.hadError) ctx.newCurrent = root;
  420. doError(ctx, root, '' + e, false);
  421. return;
  422. }
  423. const children = ctx.tree.children.get(root).values();
  424. while (true) {
  425. const next = children.next();
  426. if (next.done) return;
  427. if (isNull) doError(ctx, next.value, void 0, true);
  428. else await updateSubtree(ctx, next.value);
  429. }
  430. }
  431. function resolveParams(ctx: UpdateContext, transform: Transform, src: StateObject) {
  432. const prms = transform.transformer.definition.params;
  433. const definition = prms ? prms(src, ctx.parent.globalContext) : { };
  434. const values = transform.params ? transform.params : ParamDefinition.getDefaultValues(definition);
  435. return { definition, values };
  436. }
  437. async function updateNode(ctx: UpdateContext, currentRef: Ref): Promise<UpdateNodeResult> {
  438. const { oldTree, tree } = ctx;
  439. const current = ctx.cells.get(currentRef)!;
  440. const transform = current.transform;
  441. // special case for Root
  442. if (current.transform.ref === Transform.RootRef) {
  443. current.version = transform.version;
  444. return { action: 'none' };
  445. }
  446. const parentCell = StateSelection.findAncestorOfType(tree, ctx.cells, currentRef, transform.transformer.definition.from);
  447. if (!parentCell) {
  448. throw new Error(`No suitable parent found for '${currentRef}'`);
  449. }
  450. const parent = parentCell.obj!;
  451. current.sourceRef = parentCell.transform.ref;
  452. const params = resolveParams(ctx, transform, parent);
  453. if (!oldTree.transforms.has(currentRef) || !current.params) {
  454. current.params = params;
  455. const obj = await createObject(ctx, currentRef, transform.transformer, parent, params.values);
  456. current.obj = obj;
  457. current.version = transform.version;
  458. return { ref: currentRef, action: 'created', obj };
  459. } else {
  460. const oldParams = current.params.values;
  461. const newParams = params.values;
  462. current.params = params;
  463. const updateKind = !!current.obj && current.obj !== StateObject.Null
  464. ? await updateObject(ctx, currentRef, transform.transformer, parent, current.obj!, oldParams, newParams)
  465. : Transformer.UpdateResult.Recreate;
  466. switch (updateKind) {
  467. case Transformer.UpdateResult.Recreate: {
  468. const oldObj = current.obj;
  469. const newObj = await createObject(ctx, currentRef, transform.transformer, parent, newParams);
  470. current.obj = newObj;
  471. current.version = transform.version;
  472. return { ref: currentRef, action: 'replaced', oldObj, obj: newObj };
  473. }
  474. case Transformer.UpdateResult.Updated:
  475. current.version = transform.version;
  476. return { ref: currentRef, action: 'updated', obj: current.obj! };
  477. default:
  478. current.version = transform.version;
  479. return { action: 'none' };
  480. }
  481. }
  482. }
  483. function runTask<T>(t: T | Task<T>, ctx: RuntimeContext) {
  484. if (typeof (t as any).runInContext === 'function') return (t as Task<T>).runInContext(ctx);
  485. return t as T;
  486. }
  487. function createObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, params: any) {
  488. const cache = Object.create(null);
  489. ctx.transformCache.set(ref, cache);
  490. return runTask(transformer.definition.apply({ a, params, cache }, ctx.parent.globalContext), ctx.taskCtx);
  491. }
  492. async function updateObject(ctx: UpdateContext, ref: Ref, transformer: Transformer, a: StateObject, b: StateObject, oldParams: any, newParams: any) {
  493. if (!transformer.definition.update) {
  494. return Transformer.UpdateResult.Recreate;
  495. }
  496. let cache = ctx.transformCache.get(ref);
  497. if (!cache) {
  498. cache = Object.create(null);
  499. ctx.transformCache.set(ref, cache);
  500. }
  501. return runTask(transformer.definition.update({ a, oldParams, b, newParams, cache }, ctx.parent.globalContext), ctx.taskCtx);
  502. }