mobx-keystone 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/mobx-keystone.esm.js +2341 -780
  3. package/dist/mobx-keystone.esm.mjs +2341 -780
  4. package/dist/mobx-keystone.umd.js +2346 -782
  5. package/dist/types/actionMiddlewares/transactionMiddleware.d.ts +1 -1
  6. package/dist/types/actionMiddlewares/undoMiddleware.d.ts +17 -10
  7. package/dist/types/parent/arrayPaths.d.ts +1 -0
  8. package/dist/types/parent/treeOrder.d.ts +1 -0
  9. package/dist/types/patch/applyPatches.d.ts +2 -1
  10. package/dist/types/ref/modelRefIdIndex.d.ts +1 -0
  11. package/dist/types/ref/nodesById.d.ts +1 -0
  12. package/dist/types/ref/refIdIndex.d.ts +1 -0
  13. package/dist/types/snapshot/applySnapshot.d.ts +1 -0
  14. package/dist/types/snapshot/fromSnapshot.d.ts +0 -3
  15. package/dist/types/tweaker/changeRollback.d.ts +1 -0
  16. package/dist/types/tweaker/tweakedChangeListeners.d.ts +1 -0
  17. package/dist/types/utils/ModelPool.d.ts +7 -1
  18. package/dist/types/utils/chunks.d.ts +1 -0
  19. package/package.json +2 -2
  20. package/src/actionMiddlewares/transactionMiddleware.ts +40 -41
  21. package/src/actionMiddlewares/undoMiddleware.ts +47 -13
  22. package/src/model/BaseModel.ts +6 -4
  23. package/src/modelShared/modelDecorator.ts +5 -25
  24. package/src/parent/arrayPaths.ts +242 -0
  25. package/src/parent/coreObjectChildren.ts +300 -48
  26. package/src/parent/onChildAttachedTo.ts +130 -44
  27. package/src/parent/path.ts +10 -2
  28. package/src/parent/setParent.ts +27 -15
  29. package/src/parent/treeOrder.ts +53 -0
  30. package/src/parent/walkTree.ts +0 -73
  31. package/src/patch/applyPatches.ts +31 -26
  32. package/src/ref/core.ts +63 -27
  33. package/src/ref/modelRefIdIndex.ts +131 -0
  34. package/src/ref/nodesById.ts +60 -0
  35. package/src/ref/refIdIndex.ts +209 -0
  36. package/src/snapshot/applySnapshot.ts +26 -18
  37. package/src/snapshot/fromPlainObjectSnapshot.ts +3 -8
  38. package/src/snapshot/fromSnapshot.ts +2 -6
  39. package/src/snapshot/internal.ts +67 -10
  40. package/src/tweaker/changeRollback.ts +218 -0
  41. package/src/tweaker/core.ts +33 -20
  42. package/src/tweaker/treeNodeMetadata.ts +12 -2
  43. package/src/tweaker/tweak.ts +181 -18
  44. package/src/tweaker/tweakArray.ts +107 -27
  45. package/src/tweaker/tweakPlainObject.ts +56 -31
  46. package/src/tweaker/tweakedChangeListeners.ts +61 -0
  47. package/src/tweaker/typeChecking.ts +5 -104
  48. package/src/types/arrayBased/typesArray.ts +10 -11
  49. package/src/types/createCachedTypeCheck.ts +522 -94
  50. package/src/types/objectBased/typesRecord.ts +2 -5
  51. package/src/utils/ModelPool.ts +75 -19
  52. package/src/utils/chunks.ts +271 -0
  53. package/src/utils/index.ts +37 -0
@@ -1,7 +1,13 @@
1
- import { action, reaction, runInAction } from "mobx"
1
+ import { action, createAtom, reaction, runInAction } from "mobx"
2
2
  import { assertTweakedObject } from "../tweaker/core"
3
3
  import { assertIsFunction } from "../utils"
4
+ import {
5
+ addObjectChildrenListener,
6
+ type ObjectChildrenListener,
7
+ removeObjectChildrenListener,
8
+ } from "./coreObjectChildren"
4
9
  import { getChildrenObjects } from "./getChildrenObjects"
10
+ import { compareTreePaths, getPathFromAncestor } from "./treeOrder"
5
11
 
6
12
  /**
7
13
  * Runs a callback everytime a new object is attached to a given node.
@@ -47,7 +53,7 @@ export function onChildAttachedTo(
47
53
  }
48
54
  }
49
55
 
50
- const runDetachDisposers = (nodes: object[]) => {
56
+ const runDetachDisposers = (nodes: readonly object[]) => {
51
57
  let firstError: { value: unknown } | undefined
52
58
  for (let i = nodes.length - 1; i >= 0; i--) {
53
59
  try {
@@ -71,58 +77,137 @@ export function onChildAttachedTo(
71
77
  }
72
78
 
73
79
  const getChildrenObjectOpts = { deep }
74
- const getCurrentChildren = () => {
80
+ const getTarget = () => {
75
81
  const t = target()
76
82
  assertTweakedObject(t, "target()")
77
-
78
- return new Set(getChildrenObjects(t, getChildrenObjectOpts))
83
+ return t
79
84
  }
80
85
 
81
- const currentChildren = fireForCurrentChildren ? new Set<object>() : getCurrentChildren()
86
+ /** Children the callback ran for, with increasing numbers in the order it did. */
87
+ const currentChildren = new Map<object, number>()
88
+ let lastChildOrder = 0
82
89
 
83
- const disposer = reaction(
84
- () => getCurrentChildren(),
85
- (newChildren) => {
86
- const disposersToRun: object[] = []
87
-
88
- // find dead
89
- const currentChildrenIter = currentChildren.values()
90
- let currentChildrenCur = currentChildrenIter.next()
91
- while (!currentChildrenCur.done) {
92
- const n = currentChildrenCur.value
93
- if (!newChildren.has(n)) {
94
- currentChildren.delete(n)
95
-
96
- // we should run it in inverse order
97
- disposersToRun.push(n)
98
- }
90
+ const updateChildren = (dead: readonly object[], born: Iterable<object>) => {
91
+ for (const n of dead) {
92
+ currentChildren.delete(n)
93
+ }
94
+ // we should run them in inverse order
95
+ let firstError = runDetachDisposers(dead)
99
96
 
100
- currentChildrenCur = currentChildrenIter.next()
97
+ for (const n of born) {
98
+ if (disposed) {
99
+ break
101
100
  }
101
+ currentChildren.set(n, ++lastChildOrder)
102
102
 
103
- let firstError = runDetachDisposers(disposersToRun)
104
-
105
- // find new
106
- const newChildrenIter = newChildren.values()
107
- let newChildrenCur = newChildrenIter.next()
108
- while (!disposed && !newChildrenCur.done) {
109
- const n = newChildrenCur.value
110
- if (!currentChildren.has(n)) {
111
- currentChildren.add(n)
112
-
113
- try {
114
- const detachAction = runInAction(() => fn(n))
115
- addDetachDisposer(n, detachAction)
116
- } catch (value) {
117
- firstError ??= { value }
118
- }
119
- }
103
+ try {
104
+ const detachAction = runInAction(() => fn(n))
105
+ addDetachDisposer(n, detachAction)
106
+ } catch (value) {
107
+ firstError ??= { value }
108
+ }
109
+ }
110
+ if (firstError) {
111
+ throw firstError.value
112
+ }
113
+ }
120
114
 
121
- newChildrenCur = newChildrenIter.next()
115
+ const updateChildrenByDiff = (newChildren: ReadonlySet<object>) => {
116
+ const dead: object[] = []
117
+ for (const n of currentChildren.keys()) {
118
+ if (!newChildren.has(n)) {
119
+ dead.push(n)
120
+ }
121
+ }
122
+ const born: object[] = []
123
+ for (const n of newChildren) {
124
+ if (!currentChildren.has(n)) {
125
+ born.push(n)
122
126
  }
123
- if (firstError) {
124
- throw firstError.value
127
+ }
128
+ updateChildren(dead, born)
129
+ }
130
+
131
+ /** Updates the children given the only nodes that may have been attached or detached. */
132
+ const updateChildrenIncrementally = (t: object, nodes: ReadonlySet<object>) => {
133
+ const dead: object[] = []
134
+ const born: { node: object; path: object[] }[] = []
135
+ for (const node of nodes) {
136
+ const path = getPathFromAncestor(t, node, false)
137
+ const isChild = path !== undefined && (deep || path.length === 1)
138
+ if (currentChildren.has(node)) {
139
+ if (!isChild) {
140
+ dead.push(node)
141
+ }
142
+ } else if (isChild) {
143
+ born.push({ node, path })
125
144
  }
145
+ }
146
+ // same orders as a diff: the old children in the order they were added,
147
+ // and the new ones in the order of the children set
148
+ dead.sort((a, b) => currentChildren.get(a)! - currentChildren.get(b)!)
149
+ born.sort((a, b) => compareTreePaths(a.path, b.path, true))
150
+ updateChildren(
151
+ dead,
152
+ born.map((b) => b.node)
153
+ )
154
+ }
155
+
156
+ // nodes that may have been attached to or detached from the target since
157
+ // the last update
158
+ let touched = new Set<object>()
159
+ const touchedAtom = createAtom("onChildAttachedTo")
160
+ const listener: ObjectChildrenListener = {
161
+ deep,
162
+ onChildrenChanged(nodes) {
163
+ for (const n of nodes) {
164
+ touched.add(n)
165
+ }
166
+ touchedAtom.reportChanged()
167
+ },
168
+ }
169
+ let listenedTarget: object | undefined
170
+
171
+ const stopListening = () => {
172
+ if (listenedTarget) {
173
+ removeObjectChildrenListener(listenedTarget, listener)
174
+ listenedTarget = undefined
175
+ }
176
+ touched = new Set()
177
+ }
178
+
179
+ const update = (t: object) => {
180
+ if (t !== listenedTarget) {
181
+ stopListening()
182
+ addObjectChildrenListener(t, listener)
183
+ listenedTarget = t
184
+ updateChildrenByDiff(getChildrenObjects(t, getChildrenObjectOpts))
185
+ return
186
+ }
187
+
188
+ const nodes = touched
189
+ touched = new Set()
190
+ // (even when most children changed, this is about as fast as a diff)
191
+ if (nodes.size > 0) {
192
+ updateChildrenIncrementally(t, nodes)
193
+ }
194
+ }
195
+
196
+ if (!fireForCurrentChildren) {
197
+ for (const n of getChildrenObjects(getTarget(), getChildrenObjectOpts)) {
198
+ currentChildren.set(n, ++lastChildOrder)
199
+ }
200
+ }
201
+
202
+ const disposer = reaction(
203
+ () => {
204
+ const t = getTarget()
205
+ touchedAtom.reportObserved()
206
+ // a new object, so every change runs the effect
207
+ return { t }
208
+ },
209
+ ({ t }) => {
210
+ update(t)
126
211
  },
127
212
  {
128
213
  fireImmediately: true,
@@ -134,7 +219,8 @@ export function onChildAttachedTo(
134
219
  disposed = true
135
220
  runCleanupAfterDispose = runPendingDetachDisposers
136
221
  disposer()
137
- const pendingChildren = runPendingDetachDisposers ? Array.from(currentChildren) : []
222
+ stopListening()
223
+ const pendingChildren = runPendingDetachDisposers ? Array.from(currentChildren.keys()) : []
138
224
  currentChildren.clear()
139
225
  const firstError = runDetachDisposers(pendingChildren)
140
226
  if (firstError) {
@@ -3,6 +3,7 @@ import { isModel } from "../model/utils"
3
3
  import { assertTweakedObject } from "../tweaker/core"
4
4
  import { treeNodeMetadata } from "../tweaker/treeNodeMetadata"
5
5
  import { hasOwnProp, isArray, isObject } from "../utils"
6
+ import { LazyArrayParentPath } from "./arrayPaths"
6
7
  import {
7
8
  dataToModelNode,
8
9
  getDataObjectParent,
@@ -61,7 +62,10 @@ export interface RootPath<T extends object> {
61
62
  export function getParentPath<T extends object = any>(value: object): ParentPath<T> | undefined {
62
63
  assertTweakedObject(value, "value")
63
64
 
64
- return fastGetParentPath(value, true)
65
+ const parentPath = fastGetParentPath<T>(value, true)
66
+ return parentPath instanceof LazyArrayParentPath
67
+ ? (parentPath.toParentPath() as ParentPath<T>)
68
+ : parentPath
65
69
  }
66
70
 
67
71
  /**
@@ -71,10 +75,14 @@ export function fastGetParentPath<T extends object = any>(
71
75
  value: object,
72
76
  useAtom: boolean
73
77
  ): ParentPath<T> | undefined {
78
+ const parentPath = treeNodeMetadata.get(value)?.parentPath
74
79
  if (useAtom) {
75
80
  reportParentPathObserved(value)
81
+ if (parentPath instanceof LazyArrayParentPath) {
82
+ parentPath.reportObserved()
83
+ }
76
84
  }
77
- return treeNodeMetadata.get(value)?.parentPath as ParentPath<T> | undefined
85
+ return parentPath as ParentPath<T> | undefined
78
86
  }
79
87
 
80
88
  /**
@@ -17,9 +17,11 @@ import {
17
17
  reportParentPathChanged,
18
18
  setDataObjectParent,
19
19
  } from "./core"
20
- import { addObjectChild, removeObjectChild } from "./coreObjectChildren"
20
+ import { addObjectChild, hasObjectChildren, removeObjectChild } from "./coreObjectChildren"
21
21
  import { fastGetParentPath, fastGetRoot, type ParentPath } from "./path"
22
22
 
23
+ const reindexChunkSize = 4096
24
+
23
25
  /**
24
26
  * Updates the unchanged tail of an array before a splice is applied. These
25
27
  * children keep their parent, so neither attachment nor child-index work is
@@ -29,18 +31,24 @@ import { fastGetParentPath, fastGetRoot, type ParentPath } from "./path"
29
31
  export const reindexArrayChildren = action(
30
32
  "reindexArrayChildren",
31
33
  (array: readonly unknown[], oldStart: number, newStart: number): void => {
32
- for (let i = oldStart, j = newStart; i < array.length; i++, j++) {
33
- const value = array[i]
34
- if (isPrimitive(value)) {
35
- continue
36
- }
37
- const metadata = treeNodeMetadata.get(value as object)
38
- if (inDevMode && metadata?.parentPath?.parent !== array) {
39
- throw failure("assertion failed: reindexed child must already belong to the array")
34
+ // Copy the tail in chunks: indexed reads through an observable array are
35
+ // much slower than a slice, and chunking bounds the temporary copy size.
36
+ const length = array.length
37
+ for (let chunkStart = oldStart; chunkStart < length; chunkStart += reindexChunkSize) {
38
+ const chunk = array.slice(chunkStart, chunkStart + reindexChunkSize)
39
+ for (let i = 0, j = newStart + chunkStart - oldStart; i < chunk.length; i++, j++) {
40
+ const value = chunk[i]
41
+ if (isPrimitive(value)) {
42
+ continue
43
+ }
44
+ const metadata = treeNodeMetadata.get(value as object)
45
+ if (inDevMode && metadata?.parentPath?.parent !== array) {
46
+ throw failure("assertion failed: reindexed child must already belong to the array")
47
+ }
48
+ // Replace the path object: callers may have retained the previous path.
49
+ metadata!.parentPath = { parent: array, path: j }
50
+ reportParentPathChanged(value as object)
40
51
  }
41
- // Replace the path object: callers may have retained the previous path.
42
- metadata!.parentPath = { parent: array, path: j }
43
- reportParentPathChanged(value as object)
44
52
  }
45
53
  }
46
54
  )
@@ -123,11 +131,15 @@ export const setParent = action(
123
131
 
124
132
  const postUntweaker = parentPath ? undefined : tryUntweak(value)
125
133
 
126
- const valueIsModel = value instanceof BaseModel
134
+ // A model's root store hooks run when its own root changes. A plain node
135
+ // has none of its own, but one attached with children may bring models
136
+ // into a root store. (When detached, its children get detached one by one.)
137
+ const checkRootStore =
138
+ value instanceof BaseModel || (!oldParentPath && !!parentPath && hasObjectChildren(value))
127
139
 
128
140
  let oldRoot: any
129
141
  let oldRootStore: any
130
- if (valueIsModel) {
142
+ if (checkRootStore) {
131
143
  oldRoot = fastGetRoot(value, false)
132
144
  oldRootStore = fastIsRootStoreNoAtom(oldRoot) ? oldRoot : undefined
133
145
  }
@@ -144,7 +156,7 @@ export const setParent = action(
144
156
  }
145
157
  reportParentPathChanged(value)
146
158
 
147
- if (valueIsModel) {
159
+ if (checkRootStore) {
148
160
  const newRoot = fastGetRoot(value, false)
149
161
  const newRootStore = fastIsRootStoreNoAtom(newRoot) ? newRoot : undefined
150
162
 
@@ -0,0 +1,53 @@
1
+ import { treeNodeMetadata } from "../tweaker/treeNodeMetadata"
2
+ import { reportParentPathObserved } from "./core"
3
+
4
+ /**
5
+ * Nodes from right below the ancestor down to the node, or `undefined` when the
6
+ * node is not below it. Optionally observes their parents.
7
+ *
8
+ * @internal
9
+ */
10
+ export function getPathFromAncestor(
11
+ ancestor: object,
12
+ node: object,
13
+ observeParents: boolean
14
+ ): object[] | undefined {
15
+ const path: object[] = []
16
+ let current: object | undefined = node
17
+ while (current !== ancestor) {
18
+ if (!current) {
19
+ return undefined
20
+ }
21
+ path.push(current)
22
+ if (observeParents) {
23
+ reportParentPathObserved(current)
24
+ }
25
+ current = treeNodeMetadata.get(current)?.parentPath?.parent
26
+ }
27
+ return path.reverse()
28
+ }
29
+
30
+ /**
31
+ * Compares two paths from the same ancestor (see `getPathFromAncestor`) by the
32
+ * order a walk of the tree visits their nodes. The walk visits the children of
33
+ * a node in the order they were attached, and visits parents before their
34
+ * children when `parentsFirst`, or after them otherwise.
35
+ *
36
+ * @internal
37
+ */
38
+ export function compareTreePaths(
39
+ a: readonly object[],
40
+ b: readonly object[],
41
+ parentsFirst: boolean
42
+ ): number {
43
+ const length = Math.min(a.length, b.length)
44
+ for (let i = 0; i < length; i++) {
45
+ if (a[i] !== b[i]) {
46
+ // siblings
47
+ return treeNodeMetadata.get(a[i])!.attachOrder - treeNodeMetadata.get(b[i])!.attachOrder
48
+ }
49
+ }
50
+ // the shorter path ends at an ancestor of the other node
51
+ const depthDifference = a.length - b.length
52
+ return parentsFirst ? depthDifference : -depthDifference
53
+ }
@@ -1,4 +1,3 @@
1
- import { computed, type IComputedValue } from "mobx"
2
1
  import { assertTweakedObject } from "../tweaker/core"
3
2
  import { getObjectChildren } from "./coreObjectChildren"
4
3
 
@@ -92,75 +91,3 @@ function walkTreeChildrenFirst<T = void>(
92
91
 
93
92
  return undefined
94
93
  }
95
-
96
- /**
97
- * @internal
98
- */
99
- export interface ComputedWalkTreeAggregate<R> {
100
- walk(target: object): Map<R, object> | undefined
101
- }
102
-
103
- function getComputedTreeResult<R>(
104
- computedFns: WeakMap<object, IComputedValue<Map<R, object> | undefined>>,
105
- visit: (node: object) => R | undefined,
106
- tree: object
107
- ): Map<R, object> | undefined {
108
- let cmpted = computedFns.get(tree)
109
- if (!cmpted) {
110
- cmpted = computed(() => {
111
- return walkTreeAggregate(tree, visit, (ch) => getComputedTreeResult(computedFns, visit, ch))
112
- })
113
- computedFns.set(tree, cmpted)
114
- }
115
- return cmpted.get()
116
- }
117
-
118
- /**
119
- * @internal
120
- */
121
- export function computedWalkTreeAggregate<R>(
122
- visit: (node: object) => R | undefined
123
- ): ComputedWalkTreeAggregate<R> {
124
- const computedFns = new WeakMap<object, IComputedValue<Map<R, object> | undefined>>()
125
-
126
- return {
127
- walk: (n) => getComputedTreeResult(computedFns, visit, n),
128
- }
129
- }
130
-
131
- function walkTreeAggregate<R>(
132
- target: object,
133
- visit: (node: object) => R | undefined,
134
- recurse: (node: object) => Map<R, object> | undefined
135
- ): Map<R, object> | undefined {
136
- let map: Map<R, object> | undefined
137
- const rootVal = visit(target)
138
-
139
- const children = getObjectChildren(target)
140
-
141
- // With one child and no local value, the child's map can be reused.
142
- if (rootVal === undefined && children.size === 1) {
143
- return recurse(children.values().next().value!)
144
- }
145
-
146
- for (const child of children) {
147
- const childMap = recurse(child)
148
- if (childMap) {
149
- map ??= new Map()
150
- for (const [key, value] of childMap) {
151
- map.set(key, value)
152
- }
153
- }
154
- }
155
-
156
- // add it at the end so parent resolutions have higher
157
- // priority than child ones
158
- if (rootVal !== undefined) {
159
- if (!map) {
160
- map = new Map()
161
- }
162
- map.set(rootVal, target)
163
- }
164
-
165
- return map
166
- }
@@ -6,8 +6,8 @@ import { modelToDataNode } from "../parent/core"
6
6
  import type { Patch } from "../patch/Patch"
7
7
  import { internalApplySnapshot } from "../snapshot/applySnapshot"
8
8
  import { reconcileSnapshot } from "../snapshot/reconcileSnapshot"
9
+ import { withChangeBatch } from "../tweaker/changeRollback"
9
10
  import { assertTweakedObject } from "../tweaker/core"
10
- import { withTypeCheckingBatch } from "../tweaker/typeChecking"
11
11
  import { failure, inDevMode, isArray, lazy } from "../utils"
12
12
  import { runWithErrorDiagnosticsContext, withErrorPathSegments } from "../utils/errorDiagnostics"
13
13
  import { ModelPool } from "../utils/ModelPool"
@@ -16,7 +16,8 @@ import { setIfDifferent } from "../utils/setIfDifferent"
16
16
  /**
17
17
  * Applies the given patches to the given target object.
18
18
  * Automatic type checking validates the completed batch, including nested lists.
19
- * A rejected batch is rolled back with compensating patches.
19
+ * A batch that throws or is rejected is rolled back with compensating patches.
20
+ * Errors thrown by listeners do not roll it back.
20
21
  *
21
22
  * @param node Target object.
22
23
  * @param patches List of patches to apply.
@@ -48,35 +49,39 @@ export function internalApplyPatches(
48
49
  const obj = this
49
50
  const modelPool = new ModelPool(obj)
50
51
 
51
- withTypeCheckingBatch(() => {
52
- if (reverse) {
53
- let i = patches.length
54
- while (i--) {
55
- const p = patches[i]
56
- if (isArray(p)) {
57
- let j = p.length
58
- while (j--) {
59
- applySinglePatchWithPath(obj, p[j], modelPool)
52
+ try {
53
+ withChangeBatch(() => {
54
+ if (reverse) {
55
+ let i = patches.length
56
+ while (i--) {
57
+ const p = patches[i]
58
+ if (isArray(p)) {
59
+ let j = p.length
60
+ while (j--) {
61
+ applySinglePatchWithPath(obj, p[j], modelPool)
62
+ }
63
+ } else {
64
+ applySinglePatchWithPath(obj, p as Patch, modelPool)
60
65
  }
61
- } else {
62
- applySinglePatchWithPath(obj, p as Patch, modelPool)
63
66
  }
64
- }
65
- } else {
66
- const len = patches.length
67
- for (let i = 0; i < len; i++) {
68
- const p = patches[i]
69
- if (isArray(p)) {
70
- const len2 = p.length
71
- for (let j = 0; j < len2; j++) {
72
- applySinglePatchWithPath(obj, p[j], modelPool)
67
+ } else {
68
+ const len = patches.length
69
+ for (let i = 0; i < len; i++) {
70
+ const p = patches[i]
71
+ if (isArray(p)) {
72
+ const len2 = p.length
73
+ for (let j = 0; j < len2; j++) {
74
+ applySinglePatchWithPath(obj, p[j], modelPool)
75
+ }
76
+ } else {
77
+ applySinglePatchWithPath(obj, p as Patch, modelPool)
73
78
  }
74
- } else {
75
- applySinglePatchWithPath(obj, p as Patch, modelPool)
76
79
  }
77
80
  }
78
- }
79
- })
81
+ })
82
+ } finally {
83
+ modelPool.release()
84
+ }
80
85
  })
81
86
  }
82
87