joist-core 2.1.0 → 2.2.0-next.10

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 (217) hide show
  1. package/build/BaseEntity.js +1 -1
  2. package/build/BaseEntity.js.map +1 -1
  3. package/build/ConditionBuilder.d.ts +1 -22
  4. package/build/ConditionBuilder.d.ts.map +1 -1
  5. package/build/ConditionBuilder.js +6 -72
  6. package/build/ConditionBuilder.js.map +1 -1
  7. package/build/EntityFilter.d.ts +4 -2
  8. package/build/EntityFilter.d.ts.map +1 -1
  9. package/build/EntityGraphQLFilter.d.ts +2 -0
  10. package/build/EntityGraphQLFilter.d.ts.map +1 -1
  11. package/build/EntityGraphQLFilter.js.map +1 -1
  12. package/build/EntityManager.d.ts +27 -39
  13. package/build/EntityManager.d.ts.map +1 -1
  14. package/build/EntityManager.js +243 -122
  15. package/build/EntityManager.js.map +1 -1
  16. package/build/EntityMetadata.d.ts +19 -1
  17. package/build/EntityMetadata.d.ts.map +1 -1
  18. package/build/EntityMetadata.js.map +1 -1
  19. package/build/IndexManager.d.ts +2 -2
  20. package/build/IndexManager.d.ts.map +1 -1
  21. package/build/IndexManager.js +22 -16
  22. package/build/IndexManager.js.map +1 -1
  23. package/build/InstanceData.d.ts +21 -3
  24. package/build/InstanceData.d.ts.map +1 -1
  25. package/build/InstanceData.js +70 -10
  26. package/build/InstanceData.js.map +1 -1
  27. package/build/IsLoadedCache.d.ts +1 -1
  28. package/build/IsLoadedCache.d.ts.map +1 -1
  29. package/build/IsLoadedCache.js +37 -26
  30. package/build/IsLoadedCache.js.map +1 -1
  31. package/build/JoinRows.d.ts +1 -1
  32. package/build/JoinRows.js +6 -1
  33. package/build/JoinRows.js.map +1 -1
  34. package/build/PluginManager.d.ts +12 -0
  35. package/build/PluginManager.d.ts.map +1 -1
  36. package/build/PluginManager.js +18 -2
  37. package/build/PluginManager.js.map +1 -1
  38. package/build/QueryParser.collectionJoins.d.ts +27 -0
  39. package/build/QueryParser.collectionJoins.d.ts.map +1 -0
  40. package/build/QueryParser.collectionJoins.js +466 -0
  41. package/build/QueryParser.collectionJoins.js.map +1 -0
  42. package/build/QueryParser.collectionJoins.test.d.ts +2 -0
  43. package/build/QueryParser.collectionJoins.test.d.ts.map +1 -0
  44. package/build/QueryParser.collectionJoins.test.js +772 -0
  45. package/build/QueryParser.collectionJoins.test.js.map +1 -0
  46. package/build/QueryParser.d.ts +71 -11
  47. package/build/QueryParser.d.ts.map +1 -1
  48. package/build/QueryParser.js +39 -33
  49. package/build/QueryParser.js.map +1 -1
  50. package/build/QueryParser.pruning.d.ts +4 -2
  51. package/build/QueryParser.pruning.d.ts.map +1 -1
  52. package/build/QueryParser.pruning.js +87 -10
  53. package/build/QueryParser.pruning.js.map +1 -1
  54. package/build/QueryParser.pruning.test.d.ts +2 -0
  55. package/build/QueryParser.pruning.test.d.ts.map +1 -0
  56. package/build/QueryParser.pruning.test.js +106 -0
  57. package/build/QueryParser.pruning.test.js.map +1 -0
  58. package/build/QueryVisitor.d.ts.map +1 -1
  59. package/build/QueryVisitor.js +22 -0
  60. package/build/QueryVisitor.js.map +1 -1
  61. package/build/ReactionsManager.d.ts +2 -1
  62. package/build/ReactionsManager.d.ts.map +1 -1
  63. package/build/ReactionsManager.js +55 -51
  64. package/build/ReactionsManager.js.map +1 -1
  65. package/build/batchloaders/BatchLoader.d.ts.map +1 -1
  66. package/build/batchloaders/BatchLoader.js +9 -1
  67. package/build/batchloaders/BatchLoader.js.map +1 -1
  68. package/build/batchloaders/manyToManyBatchLoader.js +3 -1
  69. package/build/batchloaders/manyToManyBatchLoader.js.map +1 -1
  70. package/build/batchloaders/populateBatchLoader.d.ts.map +1 -1
  71. package/build/batchloaders/populateBatchLoader.js +2 -1
  72. package/build/batchloaders/populateBatchLoader.js.map +1 -1
  73. package/build/batchloaders/recursiveM2mBatchLoader.d.ts.map +1 -1
  74. package/build/batchloaders/recursiveM2mBatchLoader.js +3 -1
  75. package/build/batchloaders/recursiveM2mBatchLoader.js.map +1 -1
  76. package/build/changes.d.ts.map +1 -1
  77. package/build/changes.js +1 -4
  78. package/build/changes.js.map +1 -1
  79. package/build/config.d.ts.map +1 -1
  80. package/build/config.js +18 -10
  81. package/build/config.js.map +1 -1
  82. package/build/configure.d.ts +3 -3
  83. package/build/configure.d.ts.map +1 -1
  84. package/build/configure.js +66 -2
  85. package/build/configure.js.map +1 -1
  86. package/build/dataloaders/fastWhereFilterHash.d.ts +15 -0
  87. package/build/dataloaders/fastWhereFilterHash.d.ts.map +1 -0
  88. package/build/dataloaders/fastWhereFilterHash.js +164 -0
  89. package/build/dataloaders/fastWhereFilterHash.js.map +1 -0
  90. package/build/dataloaders/fastWhereFilterHash.test.d.ts +2 -0
  91. package/build/dataloaders/fastWhereFilterHash.test.d.ts.map +1 -0
  92. package/build/dataloaders/fastWhereFilterHash.test.js +59 -0
  93. package/build/dataloaders/fastWhereFilterHash.test.js.map +1 -0
  94. package/build/dataloaders/findCountDataLoader.d.ts +1 -2
  95. package/build/dataloaders/findCountDataLoader.d.ts.map +1 -1
  96. package/build/dataloaders/findCountDataLoader.js +16 -13
  97. package/build/dataloaders/findCountDataLoader.js.map +1 -1
  98. package/build/dataloaders/findDataLoader.d.ts +7 -3
  99. package/build/dataloaders/findDataLoader.d.ts.map +1 -1
  100. package/build/dataloaders/findDataLoader.js +105 -91
  101. package/build/dataloaders/findDataLoader.js.map +1 -1
  102. package/build/dataloaders/findIdsDataLoader.d.ts +1 -2
  103. package/build/dataloaders/findIdsDataLoader.d.ts.map +1 -1
  104. package/build/dataloaders/findIdsDataLoader.js +16 -15
  105. package/build/dataloaders/findIdsDataLoader.js.map +1 -1
  106. package/build/dataloaders/findOrCreateDataLoader.d.ts.map +1 -1
  107. package/build/dataloaders/findOrCreateDataLoader.js +7 -2
  108. package/build/dataloaders/findOrCreateDataLoader.js.map +1 -1
  109. package/build/dataloaders/findPaginatedDataLoader.d.ts +7 -0
  110. package/build/dataloaders/findPaginatedDataLoader.d.ts.map +1 -0
  111. package/build/dataloaders/findPaginatedDataLoader.js +79 -0
  112. package/build/dataloaders/findPaginatedDataLoader.js.map +1 -0
  113. package/build/defaults.d.ts.map +1 -1
  114. package/build/defaults.js +49 -42
  115. package/build/defaults.js.map +1 -1
  116. package/build/drivers/EntityWriter.js +13 -7
  117. package/build/drivers/EntityWriter.js.map +1 -1
  118. package/build/drivers/buildRawQuery.d.ts +6 -4
  119. package/build/drivers/buildRawQuery.d.ts.map +1 -1
  120. package/build/drivers/buildRawQuery.js +11 -6
  121. package/build/drivers/buildRawQuery.js.map +1 -1
  122. package/build/drivers/buildUtils.d.ts +7 -2
  123. package/build/drivers/buildUtils.d.ts.map +1 -1
  124. package/build/drivers/buildUtils.js +14 -5
  125. package/build/drivers/buildUtils.js.map +1 -1
  126. package/build/fields.d.ts.map +1 -1
  127. package/build/fields.js +31 -12
  128. package/build/fields.js.map +1 -1
  129. package/build/index.d.ts +2 -1
  130. package/build/index.d.ts.map +1 -1
  131. package/build/index.js +7 -5
  132. package/build/index.js.map +1 -1
  133. package/build/json.d.ts +3 -3
  134. package/build/json.d.ts.map +1 -1
  135. package/build/json.js +4 -4
  136. package/build/json.js.map +1 -1
  137. package/build/keys.d.ts.map +1 -1
  138. package/build/keys.js +8 -6
  139. package/build/keys.js.map +1 -1
  140. package/build/loadHints.d.ts +13 -4
  141. package/build/loadHints.d.ts.map +1 -1
  142. package/build/loadHints.js +63 -9
  143. package/build/loadHints.js.map +1 -1
  144. package/build/loadLens.d.ts +11 -0
  145. package/build/loadLens.d.ts.map +1 -1
  146. package/build/loadLens.js +25 -8
  147. package/build/loadLens.js.map +1 -1
  148. package/build/normalizeHints.d.ts +7 -0
  149. package/build/normalizeHints.d.ts.map +1 -1
  150. package/build/normalizeHints.js +40 -2
  151. package/build/normalizeHints.js.map +1 -1
  152. package/build/preloading/JsonAggregatePreloader.js +6 -2
  153. package/build/preloading/JsonAggregatePreloader.js.map +1 -1
  154. package/build/reactiveHints.d.ts +24 -7
  155. package/build/reactiveHints.d.ts.map +1 -1
  156. package/build/reactiveHints.js +45 -28
  157. package/build/reactiveHints.js.map +1 -1
  158. package/build/relations/AbstractRelationImpl.d.ts +7 -2
  159. package/build/relations/AbstractRelationImpl.d.ts.map +1 -1
  160. package/build/relations/AbstractRelationImpl.js.map +1 -1
  161. package/build/relations/AsyncProperty.d.ts +36 -0
  162. package/build/relations/AsyncProperty.d.ts.map +1 -0
  163. package/build/relations/AsyncProperty.js +80 -0
  164. package/build/relations/AsyncProperty.js.map +1 -0
  165. package/build/relations/{ReactiveQueryField.d.ts → AsyncReactiveField.d.ts} +10 -10
  166. package/build/relations/AsyncReactiveField.d.ts.map +1 -0
  167. package/build/relations/{ReactiveQueryField.js → AsyncReactiveField.js} +19 -19
  168. package/build/relations/{ReactiveQueryField.js.map → AsyncReactiveField.js.map} +1 -1
  169. package/build/relations/ReactiveField.d.ts +7 -9
  170. package/build/relations/ReactiveField.d.ts.map +1 -1
  171. package/build/relations/ReactiveField.js +5 -10
  172. package/build/relations/ReactiveField.js.map +1 -1
  173. package/build/relations/ReactiveGetter.d.ts +5 -5
  174. package/build/relations/ReactiveGetter.d.ts.map +1 -1
  175. package/build/relations/ReactiveGetter.js +3 -3
  176. package/build/relations/ReactiveGetter.js.map +1 -1
  177. package/build/relations/ReactiveReference.d.ts +2 -2
  178. package/build/relations/ReactiveReference.d.ts.map +1 -1
  179. package/build/relations/ReactiveReference.js +100 -36
  180. package/build/relations/ReactiveReference.js.map +1 -1
  181. package/build/relations/hasOneThrough.d.ts.map +1 -1
  182. package/build/relations/hasOneThrough.js +6 -4
  183. package/build/relations/hasOneThrough.js.map +1 -1
  184. package/build/relations/{hasAsyncProperty.d.ts → hasProperty.d.ts} +12 -12
  185. package/build/relations/hasProperty.d.ts.map +1 -0
  186. package/build/relations/{hasAsyncProperty.js → hasProperty.js} +20 -20
  187. package/build/relations/hasProperty.js.map +1 -0
  188. package/build/relations/index.d.ts +3 -2
  189. package/build/relations/index.d.ts.map +1 -1
  190. package/build/relations/index.js +16 -11
  191. package/build/relations/index.js.map +1 -1
  192. package/build/resurrection.d.ts +10 -0
  193. package/build/resurrection.d.ts.map +1 -0
  194. package/build/resurrection.js +93 -0
  195. package/build/resurrection.js.map +1 -0
  196. package/build/rules.js +3 -3
  197. package/build/trusted.d.ts +1 -1
  198. package/build/trusted.d.ts.map +1 -1
  199. package/build/trusted.js +1 -1
  200. package/build/trusted.js.map +1 -1
  201. package/build/upsert.d.ts.map +1 -1
  202. package/build/upsert.js +26 -10
  203. package/build/upsert.js.map +1 -1
  204. package/build/utils.d.ts +2 -0
  205. package/build/utils.d.ts.map +1 -1
  206. package/build/utils.js +12 -0
  207. package/build/utils.js.map +1 -1
  208. package/build/withLoaded.js +5 -5
  209. package/build/withLoaded.js.map +1 -1
  210. package/package.json +10 -12
  211. package/build/caches.d.ts +0 -6
  212. package/build/caches.d.ts.map +0 -1
  213. package/build/caches.js +0 -42
  214. package/build/caches.js.map +0 -1
  215. package/build/relations/ReactiveQueryField.d.ts.map +0 -1
  216. package/build/relations/hasAsyncProperty.d.ts.map +0 -1
  217. package/build/relations/hasAsyncProperty.js.map +0 -1
@@ -29,7 +29,6 @@ const IndexManager_1 = require("./IndexManager");
29
29
  // We alias `Entity => EntityW` to denote "Entity wide" i.e. the non-narrowed Entity
30
30
  const loadBatchLoader_1 = require("./batchloaders/loadBatchLoader");
31
31
  const populateBatchLoader_1 = require("./batchloaders/populateBatchLoader");
32
- const caches_1 = require("./caches");
33
32
  const config_1 = require("./config");
34
33
  const configure_1 = require("./configure");
35
34
  const findByUniqueDataLoader_1 = require("./dataloaders/findByUniqueDataLoader");
@@ -37,17 +36,20 @@ const findCountDataLoader_1 = require("./dataloaders/findCountDataLoader");
37
36
  const findDataLoader_1 = require("./dataloaders/findDataLoader");
38
37
  const findIdsDataLoader_1 = require("./dataloaders/findIdsDataLoader");
39
38
  const findOrCreateDataLoader_1 = require("./dataloaders/findOrCreateDataLoader");
39
+ const findPaginatedDataLoader_1 = require("./dataloaders/findPaginatedDataLoader");
40
40
  const Entity_1 = require("./Entity");
41
41
  const FlushLock_1 = require("./FlushLock");
42
42
  const index_1 = require("./index");
43
43
  const IsLoadedCache_1 = require("./IsLoadedCache");
44
44
  const JoinRows_1 = require("./JoinRows");
45
+ const loadHints_1 = require("./loadHints");
45
46
  const newEntity_1 = require("./newEntity");
46
47
  const newTestInstance_1 = require("./newTestInstance");
47
48
  const PluginManager_1 = require("./PluginManager");
48
49
  const ReactionsManager_1 = require("./ReactionsManager");
49
50
  const reactiveHints_1 = require("./reactiveHints");
50
51
  const relations_1 = require("./relations");
52
+ const AsyncProperty_1 = require("./relations/AsyncProperty");
51
53
  const hasAsyncMethod_1 = require("./relations/hasAsyncMethod");
52
54
  const RecursiveCollection_1 = require("./relations/RecursiveCollection");
53
55
  const Todo_1 = require("./Todo");
@@ -86,6 +88,8 @@ class EntityManager {
86
88
  txn;
87
89
  entityLimit = defaultEntityLimit;
88
90
  #entitiesArray = [];
91
+ // Incrementally track dirty entities so we don't have to scan `#entityArray` during flush
92
+ #maybePendingFlushEntities = new Set();
89
93
  // Indexes the currently loaded entities by their tagged ids and `toTaggedString` ids (i.e. `a#`). This fixes
90
94
  // real-world performance issues where `findExistingInstance` scanning `#entities` was an `O(n^2)`.
91
95
  #entitiesById = new Map();
@@ -153,6 +157,12 @@ class EntityManager {
153
157
  indexManager: this.#indexManager,
154
158
  isLoadedCache: this.#isLoadedCache,
155
159
  pluginManager,
160
+ markMaybePending(entity) {
161
+ em.#maybePendingFlushEntities.add(entity);
162
+ },
163
+ unmarkMaybePending(entity) {
164
+ em.#maybePendingFlushEntities.delete(entity);
165
+ },
156
166
  isMerging(entity) {
157
167
  return em.#merging?.has(entity) ?? false;
158
168
  },
@@ -246,9 +256,9 @@ class EntityManager {
246
256
  async find(type, where, options) {
247
257
  const { populate, ...rest } = options || {};
248
258
  const settings = { where, ...rest };
249
- const result = await (0, findDataLoader_1.findDataLoader)(this, type, settings, populate)
250
- .load(settings)
251
- .catch(function find(err) {
259
+ const result = await (hasPaginationSettings(rest)
260
+ ? (0, findPaginatedDataLoader_1.findPaginatedDataLoader)(this, type, settings, populate)
261
+ : (0, findDataLoader_1.findDataLoader)(this, type, settings, populate)).catch(function find(err) {
252
262
  throw appendStack(err, new Error());
253
263
  });
254
264
  if (populate) {
@@ -256,30 +266,52 @@ class EntityManager {
256
266
  }
257
267
  return result;
258
268
  }
259
- async findPaginated(type, where, options) {
260
- const { populate, limit, offset, ...rest } = options || {};
261
- const meta = (0, index_1.getMetadata)(type);
262
- const query = (0, index_1.parseFindQuery)(meta, where, rest);
263
- const rows = await this.executeFind(meta, "findPaginated", query, { limit, offset });
264
- // check row limit
265
- const result = this.hydrate(type, rows);
266
- if (populate) {
267
- await this.populate(result, populate);
268
- }
269
- return result;
270
- }
269
+ /** Runs the post-parse find pipeline: plugins mutate the logical AST, then Joist optimizes/prunes before SQL. */
271
270
  async executeFind(meta, operation, parsed, settings) {
271
+ const { checkLimit, findSettings } = this.prepareFind(meta, operation, parsed, settings);
272
+ return this.executePreparedFind(meta, operation, parsed, findSettings, checkLimit);
273
+ }
274
+ /** Executes a query that has already had find hooks and optimizations applied. */
275
+ async executePreparedFind(meta, operation, parsed, findSettings, checkLimit) {
272
276
  const { pluginManager } = getEmInternalApi(this);
273
- pluginManager.beforeFind(meta, operation, parsed, settings);
274
- const rows = await this.driver.executeFind(this, parsed, settings);
277
+ const rows = await this.driver.executeFind(this, parsed, findSettings);
278
+ // Check by default unless explicitly disabled or the caller removed the LIMIT via `limit: undefined`
279
+ const shouldCheck = checkLimit ?? !("limit" in findSettings && findSettings.limit === undefined);
280
+ if (shouldCheck && rows.length >= this.entityLimit) {
281
+ throw new Error(`Query returned more than ${this.entityLimit} entityLimit rows`);
282
+ }
275
283
  pluginManager.afterFind(meta, operation, rows);
276
284
  return rows;
277
285
  }
278
- async findGql(type, where, options) {
279
- return this.find(type, where, options);
286
+ /**
287
+ * Runs pre-SQL find hooks and optimizations against a parsed query.
288
+ *
289
+ * This allows plugins to see "pre-batched" / "logical" query ASTs, instead of our
290
+ * more complicated `_find` batched queries. The flow would be:
291
+ *
292
+ * - A loader calls `prepareFind(originalQuery)`
293
+ * - Plugins inspect/modify the query as/if needed
294
+ * - The loader crafts a new, more complicated query that embeds the originalQuery
295
+ * - The loader calls `executePreparedFind` with the 2nd query
296
+ */
297
+ prepareFind(meta, operation, parsed, settings) {
298
+ const { checkLimit, ...findSettings } = settings;
299
+ const { pluginManager } = getEmInternalApi(this);
300
+ // Plugins may mutate the settings object, so return the post-hook version that loaders must reuse.
301
+ pluginManager.beforeFind(meta, operation, parsed, findSettings);
302
+ (0, index_1.optimizeCollectionJoins)(parsed, settings);
303
+ return { checkLimit, findSettings };
280
304
  }
281
- async findGqlPaginated(type, where, options) {
282
- return this.findPaginated(type, where, options);
305
+ async findGql(type, where, options) {
306
+ if (!options) {
307
+ return this.find(type, where);
308
+ }
309
+ const normalized = { ...options };
310
+ if ("limit" in normalized)
311
+ normalized.limit = normalized.limit ?? undefined;
312
+ if ("offset" in normalized)
313
+ normalized.offset = normalized.offset ?? undefined;
314
+ return this.find(type, where, normalized);
283
315
  }
284
316
  async findOne(type, where, options) {
285
317
  const list = await this.find(type, where, options);
@@ -337,9 +369,7 @@ class EntityManager {
337
369
  */
338
370
  async findCount(type, where, options = {}) {
339
371
  const settings = { where, ...options };
340
- let count = await (0, findCountDataLoader_1.findCountDataLoader)(this, type, settings)
341
- .load(settings)
342
- .catch(function findCount(err) {
372
+ let count = await (0, findCountDataLoader_1.findCountDataLoader)(this, type, settings).catch(function findCount(err) {
343
373
  throw appendStack(err, new Error());
344
374
  });
345
375
  // If the user is do "count all", we can adjust the number up/down based on
@@ -371,9 +401,7 @@ class EntityManager {
371
401
  */
372
402
  async findIds(type, where, options = {}) {
373
403
  const settings = { where, ...options };
374
- return (0, findIdsDataLoader_1.findIdsDataLoader)(this, type, settings)
375
- .load(settings)
376
- .catch(function findIds(err) {
404
+ return (0, findIdsDataLoader_1.findIdsDataLoader)(this, type, settings).catch(function findIds(err) {
377
405
  throw appendStack(err, new Error());
378
406
  });
379
407
  }
@@ -662,48 +690,86 @@ class EntityManager {
662
690
  }
663
691
  async loadAll(type, _ids, hint) {
664
692
  const meta = (0, index_1.getMetadata)(type);
665
- const ids = _ids.map((id) => (0, index_1.tagId)(meta, id));
666
- const idsToLoad = ids.filter((id) => !this.findExistingInstance(id));
667
- if (idsToLoad.length > 0) {
693
+ // Use pre-allocated arrays/for loops instead of `.filter`s since this can be a hot spot
694
+ const ids = new Array(_ids.length);
695
+ const entities = new Array(_ids.length);
696
+ let idsToLoad;
697
+ let positionsToLoad;
698
+ for (let i = 0; i < _ids.length; i++) {
699
+ const id = (0, index_1.tagId)(meta, _ids[i]);
700
+ ids[i] = id;
701
+ const entity = this.findExistingInstance(id);
702
+ if (entity) {
703
+ entities[i] = entity;
704
+ }
705
+ else {
706
+ (idsToLoad ??= []).push(id);
707
+ (positionsToLoad ??= []).push(i);
708
+ }
709
+ }
710
+ if (idsToLoad && idsToLoad.length > 0) {
668
711
  await (0, loadBatchLoader_1.loadBatchLoader)(this, meta)
669
712
  .loadAll(idsToLoad.map((id) => ({ taggedId: id, hint })))
670
713
  .catch(function loadAll(err) {
671
714
  throw appendStack(err, new Error());
672
715
  });
716
+ for (const i of positionsToLoad) {
717
+ entities[i] = this.findExistingInstance(ids[i]);
718
+ }
673
719
  }
674
- const entities = [];
675
- for (const id of ids) {
676
- const entity = this.findExistingInstance(id);
677
- if (entity)
678
- entities.push(entity);
720
+ let idsNotFound;
721
+ for (let i = 0; i < entities.length; i++) {
722
+ if (entities[i] === undefined) {
723
+ (idsNotFound ??= []).push(ids[i]);
724
+ }
679
725
  }
680
- if (entities.length !== ids.length) {
681
- const idsNotFound = ids.filter((_, i) => entities[i] === undefined);
726
+ if (idsNotFound) {
682
727
  throw new NotFoundError(`${idsNotFound.join(",")} were not found`);
683
728
  }
729
+ const loadedEntities = entities;
684
730
  if (hint) {
685
- await this.populate(entities, hint);
731
+ await this.populate(loadedEntities, hint);
686
732
  }
687
733
  if (meta.inheritanceType === "sti" && meta.baseType) {
688
- const wrongType = entities.filter((e) => !(e instanceof meta.cstr));
734
+ const wrongType = loadedEntities.filter((e) => !(e instanceof meta.cstr));
689
735
  if (wrongType.length > 0) {
690
736
  throw new Error(`${wrongType.join(", ")} were not of type ${meta.cstr.name}`);
691
737
  }
692
738
  }
693
- return entities;
739
+ return loadedEntities;
694
740
  }
695
741
  async loadAllIfExists(type, _ids, hint) {
696
742
  const meta = (0, index_1.getMetadata)(type);
697
- const ids = _ids.map((id) => (0, index_1.tagId)(meta, id));
698
- const idsToLoad = ids.filter((id) => !this.findExistingInstance(id));
699
- if (idsToLoad.length > 0) {
743
+ // Use pre-allocated arrays/for loops instead of `.filter`s since this can be a hot spot
744
+ const ids = new Array(_ids.length);
745
+ const entities = [];
746
+ let idsToLoad;
747
+ // Ensure the ids are tagged, and find any not-yet-loaded
748
+ for (let i = 0; i < _ids.length; i++) {
749
+ const id = (0, index_1.tagId)(meta, _ids[i]);
750
+ ids[i] = id;
751
+ const entity = this.findExistingInstance(id);
752
+ if (entity) {
753
+ entities.push(entity);
754
+ }
755
+ else {
756
+ (idsToLoad ??= []).push(id);
757
+ }
758
+ }
759
+ if (idsToLoad && idsToLoad.length > 0) {
700
760
  await (0, loadBatchLoader_1.loadBatchLoader)(this, meta)
701
761
  .loadAll(idsToLoad.map((id) => ({ taggedId: id, hint })))
702
762
  .catch(function loadAllIfExists(err) {
703
763
  throw appendStack(err, new Error());
704
764
  });
765
+ // Now that everything is loaded, recalc `entities`
766
+ entities.length = 0;
767
+ for (const id of ids) {
768
+ const entity = this.findExistingInstance(id);
769
+ if (entity)
770
+ entities.push(entity);
771
+ }
705
772
  }
706
- const entities = ids.map((id) => this.findExistingInstance(id)).filter(Boolean);
707
773
  if (hint) {
708
774
  await this.populate(entities, hint);
709
775
  }
@@ -754,6 +820,12 @@ class EntityManager {
754
820
  if (list.length === 0) {
755
821
  return !fn ? entityOrList : fn(entityOrList);
756
822
  }
823
+ // Avoid building a HintTree/batchloader for persisted entities when everything is already loaded; in the phase 9
824
+ // benchmark this moved already-loaded populates from ~1.10ms to ~0.89ms, and nested already-loaded populates from
825
+ // ~2.39ms to ~1.94ms.
826
+ if (!opts.forceReload && list.every((entity) => !entity.isNewEntity && (0, loadHints_1.isLoadedForPopulate)(entity, hintOpt))) {
827
+ return fn ? fn(entityOrList) : entityOrList;
828
+ }
757
829
  const meta = (0, index_1.getMetadata)(list[0]);
758
830
  if (this.#preloader) {
759
831
  // If we can preload, prevent promise deadlocking by one large-batch preload populate (which can't have
@@ -802,7 +874,7 @@ class EntityManager {
802
874
  delete(entityOrArray) {
803
875
  for (const entity of (0, utils_1.toArray)(entityOrArray)) {
804
876
  // Early return if already deleted.
805
- const alreadyMarked = (0, BaseEntity_1.getInstanceData)(entity).markDeleted(entity);
877
+ const alreadyMarked = (0, BaseEntity_1.getInstanceData)(entity).markDeleted();
806
878
  if (!alreadyMarked)
807
879
  continue;
808
880
  // Any derived fields that read this entity will need recalc-d
@@ -878,11 +950,11 @@ class EntityManager {
878
950
  const createdThenDeleted = new Set();
879
951
  // We'll only invoke hooks once/entity (the 1st time that entity goes through runHooksOnPendingEntities)
880
952
  const hooksInvoked = new Set();
881
- // Make sure two ReactiveQueryFields don't ping-pong each other forever
953
+ // Make sure two AsyncReactiveFields don't ping-pong each other forever
882
954
  let hookLoops = 0;
883
955
  let now = getNow();
884
956
  const suppressedDefaultTypeErrors = [];
885
- // Make a lambda that we can invoke multiple times, if we loop for ReactiveQueryFields
957
+ // Make a lambda that we can invoke multiple times, if we loop for AsyncReactiveFields
886
958
  const runHooksOnPendingEntities = async () => {
887
959
  if (hookLoops++ >= 10)
888
960
  throw new Error("runHooksOnPendingEntities has ran 10 iterations, aborting");
@@ -898,19 +970,19 @@ class EntityManager {
898
970
  const pendingHooks = new Set();
899
971
  // Subset of pendingFlush entities that had hooks invoked in a prior `runHooksOnPendingEntities`
900
972
  const alreadyRanHooks = new Set();
901
- findPendingFlushEntities(this.entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
902
- // If we're re-looping for ReactiveQueryField, make sure to bump updatedAt
973
+ findPendingFlushEntities(this.#maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
974
+ // If we're re-looping for AsyncReactiveField, make sure to bump updatedAt
903
975
  // each time, so that for an INSERT-then-UPDATE the triggers don't think the
904
976
  // UPDATE forgot to self-bump updatedAt, and then "helpfully" bump it for us.
905
977
  if (alreadyRanHooks.size > 0) {
906
- maybeBumpUpdatedAt((0, Todo_1.createTodos)([...alreadyRanHooks]), now);
978
+ maybeBumpUpdatedAt(this.#rm, (0, Todo_1.createTodos)([...alreadyRanHooks]), now);
907
979
  }
908
980
  // Run hooks in a series of loops until things "settle down"
909
981
  while (pendingHooks.size > 0) {
910
982
  await this.#fl.allowWrites(async () => {
911
983
  let todos = (0, Todo_1.createTodos)([...pendingHooks]);
912
984
  await (0, defaults_1.setAsyncDefaults)(suppressedDefaultTypeErrors, this.ctx, Todo_1.Todo.groupInsertsByTypeAndSubType(todos));
913
- maybeBumpUpdatedAt(todos, now);
985
+ maybeBumpUpdatedAt(this.#rm, todos, now);
914
986
  // Run our hooks
915
987
  for (const group of maybeSetupHookOrdering(todos)) {
916
988
  await beforeCreate(this.ctx, group);
@@ -937,7 +1009,7 @@ class EntityManager {
937
1009
  hooksInvoked.add(e);
938
1010
  pendingHooks.clear();
939
1011
  // See if the hooks mutated any new, not-yet-hooksInvoked entities
940
- findPendingFlushEntities(this.entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
1012
+ findPendingFlushEntities(this.#maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
941
1013
  // The final run of recalcPendingReactables could have left us with pending type errors and no entities in
942
1014
  // pendingHooks. If so, we need to re-run recalcPendingTypeErrors to get those errors to transition into
943
1015
  // suppressed errors so that we will fail after simpleValidation.
@@ -955,24 +1027,30 @@ class EntityManager {
955
1027
  return !createThenDelete;
956
1028
  });
957
1029
  };
958
- const runValidation = async (entityTodos, joinRowTodos) => {
1030
+ const { pluginManager } = getEmInternalApi(this);
1031
+ const runValidation = async (entityTodos, joinRowTodos, validate) => {
1032
+ const changedEntities = entitiesFromTodos(entityTodos, joinRowTodos);
959
1033
  try {
960
1034
  this.#isValidating = true;
961
- // Run simple rules first b/c it includes not-null/required rules, so that then when we run
962
- // `validateReactiveRules` next, the app's lambdas won't see fundamentally invalid entities & NPE.
963
- await validateSimpleRules(entityTodos);
964
- // After we've let any "author is not set" simple rules fail before prematurely throwing
965
- // the "of course that caused an NPE" `TypeError`s, if all the authors *were* valid/set,
966
- // and we still have TypeErrors (from derived valeus), they were real, unrelated errors
967
- // that the user should see.
968
- if (suppressedDefaultTypeErrors.length > 0)
969
- throw suppressedDefaultTypeErrors[0];
970
- await validateReactiveRules(this, this.#rm.logger, entityTodos, joinRowTodos);
1035
+ await pluginManager.beforeValidate(changedEntities);
1036
+ if (validate) {
1037
+ // Run simple rules first b/c it includes not-null/required rules, so that then when we run
1038
+ // `validateReactiveRules` next, the app's lambdas won't see fundamentally invalid entities & NPE.
1039
+ await validateSimpleRules(entityTodos);
1040
+ // After we've let any "author is not set" simple rules fail before prematurely throwing
1041
+ // the "of course that caused an NPE" `TypeError`s, if all the authors *were* valid/set,
1042
+ // and we still have TypeErrors (from derived valeus), they were real, unrelated errors
1043
+ // that the user should see.
1044
+ if (suppressedDefaultTypeErrors.length > 0)
1045
+ throw suppressedDefaultTypeErrors[0];
1046
+ await validateReactiveRules(this, this.#rm.logger, entityTodos, joinRowTodos);
1047
+ await afterValidation(this.ctx, entityTodos);
1048
+ }
1049
+ await pluginManager.afterValidate(changedEntities);
971
1050
  }
972
1051
  finally {
973
1052
  this.#isValidating = false;
974
1053
  }
975
- await afterValidation(this.ctx, entityTodos);
976
1054
  };
977
1055
  // Run hooks (in iterative loops if hooks mutate new entities) on pending entities
978
1056
  let entitiesToFlush = await runHooksOnPendingEntities();
@@ -982,13 +1060,10 @@ class EntityManager {
982
1060
  // the full set of entities that will be INSERT/UPDATE/DELETE-d in the database.
983
1061
  let entityTodos = (0, Todo_1.createTodos)(entitiesToFlush);
984
1062
  let joinRowTodos = (0, Todo_1.combineJoinRows)(this.#joinRows);
985
- if (!skipValidation) {
986
- await runValidation(entityTodos, joinRowTodos);
987
- }
1063
+ await runValidation(entityTodos, joinRowTodos, !skipValidation);
988
1064
  this.#rm.throwIfAnySuppressedTypeErrors();
989
1065
  if (suppressedDefaultTypeErrors.length > 0)
990
1066
  throw suppressedDefaultTypeErrors[0];
991
- const { pluginManager } = getEmInternalApi(this);
992
1067
  if (Object.keys(entityTodos).length > 0 || Object.keys(joinRowTodos).length > 0) {
993
1068
  // The driver will handle the right thing if we're already in an existing transaction.
994
1069
  await this.driver.transaction(this, async () => {
@@ -1009,7 +1084,7 @@ class EntityManager {
1009
1084
  // Actually do the recalc
1010
1085
  await this.#fl.allowWrites(async () => {
1011
1086
  await this.#rm.recalcPendingReactables("reactiveQueries");
1012
- // If any ReactiveFields depended on ReactiveQueryFields, go ahead and calc those now
1087
+ // If any ReactiveFields depended on AsyncReactiveFields, go ahead and calc those now
1013
1088
  await this.#rm.recalcPendingReactables("reactables");
1014
1089
  });
1015
1090
  // Advance `now` so that our triggers don't think our UPDATEs are forgetting to self-bump
@@ -1022,7 +1097,7 @@ class EntityManager {
1022
1097
  // Recreate `entityTodos` against the only-the-just-changed entities
1023
1098
  entityTodos = (0, Todo_1.createTodos)(entitiesToFlush);
1024
1099
  joinRowTodos = (0, Todo_1.combineJoinRows)(this.#joinRows);
1025
- await runValidation(entityTodos, joinRowTodos);
1100
+ await runValidation(entityTodos, joinRowTodos, true);
1026
1101
  this.#rm.throwIfAnySuppressedTypeErrors();
1027
1102
  }
1028
1103
  else {
@@ -1044,6 +1119,11 @@ class EntityManager {
1044
1119
  if (e.isNewEntity && !e.isDeletedEntity)
1045
1120
  this.#entitiesById.set(e.idTagged, e);
1046
1121
  (0, BaseEntity_1.getInstanceData)(e).resetAfterFlushed();
1122
+ // Reset AsyncQueryProperties since DB state may have changed
1123
+ for (const rel of Object.values((0, BaseEntity_1.getInstanceData)(e).relations)) {
1124
+ if (rel instanceof AsyncProperty_1.AsyncPropertyImpl)
1125
+ rel.resetAfterFlush();
1126
+ }
1047
1127
  }
1048
1128
  // Update the joinRows refs to reflect the new state
1049
1129
  for (const joinRow of Object.values(joinRowTodos)) {
@@ -1196,10 +1276,13 @@ class EntityManager {
1196
1276
  */
1197
1277
  hydrate(type, rows, options) {
1198
1278
  const maybeBaseMeta = (0, index_1.getMetadata)(type);
1279
+ const taggedIdPrefix = `${maybeBaseMeta.tagName}:`;
1280
+ const overwriteExisting = options?.overwriteExisting === true;
1199
1281
  let i = 0;
1200
1282
  const entities = new Array(rows.length);
1201
1283
  for (const row of rows) {
1202
- const taggedId = (0, index_1.keyToTaggedId)(maybeBaseMeta, row["id"]) || (0, utils_1.fail)("No id column was available");
1284
+ const id = row["id"];
1285
+ const taggedId = id === undefined || id === null ? (0, utils_1.fail)("No id column was available") : `${taggedIdPrefix}${id}`;
1203
1286
  // See if this is already in our UoW
1204
1287
  let entity = this.findExistingInstance(taggedId);
1205
1288
  if (!entity) {
@@ -1208,23 +1291,36 @@ class EntityManager {
1208
1291
  // Pass id as a hint that we're in hydrate mode
1209
1292
  entity = (0, newEntity_1.newEntity)(this, (0, index_1.asConcreteCstr)(meta.cstr), false);
1210
1293
  (0, BaseEntity_1.getInstanceData)(entity).row = row;
1211
- this.#doRegister(entity, taggedId);
1294
+ this.#doRegister(entity, taggedId, meta, true);
1212
1295
  }
1213
- else if (options?.overwriteExisting === true) {
1296
+ else if (overwriteExisting) {
1214
1297
  // Usually if the entity already exists, we don't write over it, but in this case we assume that
1215
1298
  // `EntityManager.refresh` is telling us to explicitly load the latest data.
1216
1299
  // First swap out the old row with the new row
1217
- (0, BaseEntity_1.getInstanceData)(entity).row = row;
1300
+ const instanceData = (0, BaseEntity_1.getInstanceData)(entity);
1301
+ instanceData.row = row;
1218
1302
  // And then only refresh the data keys that have already been serde-d from rows
1219
1303
  // (this keeps us from deserializing data out of rows that we don't need).
1220
- const { data, originalData } = (0, BaseEntity_1.getInstanceData)(entity);
1221
- const changedFields = entity.changes.fieldsWithoutRelations;
1222
- for (const fieldName of Object.keys(data)) {
1223
- const serde = (0, index_1.getMetadata)(entity).allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1224
- serde.setOnEntity(data, row);
1225
- // Make the field look not-dirty
1226
- if (changedFields.includes(fieldName)) {
1227
- delete originalData[fieldName];
1304
+ const { data } = instanceData;
1305
+ const dataKeys = Object.keys(data);
1306
+ if (dataKeys.length > 0) {
1307
+ const allFields = (0, index_1.getMetadata)(entity).allFields;
1308
+ const changedFields = entity.changes.fieldsWithoutRelations;
1309
+ if (changedFields.length === 0) {
1310
+ for (const fieldName of dataKeys) {
1311
+ const serde = allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1312
+ serde.setOnEntity(data, row);
1313
+ }
1314
+ }
1315
+ else {
1316
+ for (const fieldName of dataKeys) {
1317
+ const serde = allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1318
+ serde.setOnEntity(data, row);
1319
+ // Make the field look not-dirty
1320
+ if (changedFields.includes(fieldName)) {
1321
+ instanceData.markFieldClean(fieldName);
1322
+ }
1323
+ }
1228
1324
  }
1229
1325
  }
1230
1326
  }
@@ -1233,8 +1329,9 @@ class EntityManager {
1233
1329
  return entities;
1234
1330
  }
1235
1331
  touch(entityOrEntities) {
1236
- for (const entity of (0, utils_1.toArray)(entityOrEntities))
1237
- (0, BaseEntity_1.getInstanceData)(entity).isTouched = true;
1332
+ for (const entity of (0, utils_1.toArray)(entityOrEntities)) {
1333
+ (0, BaseEntity_1.getInstanceData)(entity).markTouched();
1334
+ }
1238
1335
  }
1239
1336
  async recalc(entityOrEntities) {
1240
1337
  // Look for async reactive fields
@@ -1251,7 +1348,7 @@ class EntityManager {
1251
1348
  (0, fields_1.setField)(entity, field.fieldName, entity[field.fieldName]);
1252
1349
  }));
1253
1350
  // `.load()` recalculated the immediate relations, go ahead and recalc any downstream reactables.
1254
- // We'll still defer ReactiveQueryFields to the em.flush loop.
1351
+ // We'll still defer AsyncReactiveFields to the em.flush loop.
1255
1352
  await this.#rm.recalcPendingReactables("reactables");
1256
1353
  }
1257
1354
  beforeBegin(fn) {
@@ -1311,9 +1408,16 @@ class EntityManager {
1311
1408
  // Run the beforeDelete hook before we unhook the entity
1312
1409
  const todos = (0, Todo_1.createTodos)(entities);
1313
1410
  await beforeDelete(this.ctx, todos);
1314
- // For all relations, unhook the entity from the other side
1315
- // (...we're using `concat` because `.push(...reallyBigArray)` with ~100k relations can blow the stack size
1316
- relationsToCleanup = relationsToCleanup.concat(entities.flatMap(index_1.getRelations));
1411
+ // For all relations, unhook the entity from the other side; this append path is optimized for
1412
+ // large deletes with ~100k relations by avoiding `flatMap` intermediates and `concat` copies.
1413
+ for (const entity of entities) {
1414
+ const relations = (0, index_1.getRelations)(entity);
1415
+ const start = relationsToCleanup.length;
1416
+ relationsToCleanup.length += relations.length;
1417
+ for (let i = 0; i < relations.length; i++) {
1418
+ relationsToCleanup[start + i] = relations[i];
1419
+ }
1420
+ }
1317
1421
  entities = this.#pendingDeletes;
1318
1422
  this.#pendingDeletes = [];
1319
1423
  }
@@ -1369,7 +1473,7 @@ class EntityManager {
1369
1473
  return entities.filter((e) => e instanceof cstr && !e.isDeletedEntity);
1370
1474
  }
1371
1475
  if (this.#indexManager.shouldIndexType(entities.length)) {
1372
- this.#indexManager.enableIndexingForType(meta, entities);
1476
+ this.#indexManager.enableIndexingForType(meta, entities, where);
1373
1477
  return (this.#indexManager
1374
1478
  .findMatching(meta, where)
1375
1479
  // Still filter by `instanceof cstr` to handle subtyping
@@ -1471,7 +1575,8 @@ class EntityManager {
1471
1575
  continue;
1472
1576
  const { originalData: oldOriginalData, data: oldData } = oldInstanceData;
1473
1577
  const newEntity = mapEntity(oldEntity);
1474
- const { originalData: newOriginalData, data: newData } = newEntity.__data;
1578
+ const newInstanceData = newEntity.__data;
1579
+ const { data: newData } = newInstanceData;
1475
1580
  // for new entities, anything in `data` is changed and should be copied across. for existing entities, we
1476
1581
  // only care about changed fields, which are enumerated by originalData
1477
1582
  const maybeEntity = (value) => ((0, Entity_1.isEntity)(value) ? mapEntity(value) : value);
@@ -1479,7 +1584,7 @@ class EntityManager {
1479
1584
  for (const field of fields) {
1480
1585
  // copy over originalData so .changes is consistent across ems
1481
1586
  if (field in oldOriginalData)
1482
- newOriginalData[field] = maybeEntity(oldOriginalData[field]);
1587
+ newInstanceData.markFieldDirty(field, maybeEntity(oldOriginalData[field]));
1483
1588
  newData[field] = maybeEntity(oldData[field]);
1484
1589
  }
1485
1590
  }
@@ -1489,7 +1594,7 @@ class EntityManager {
1489
1594
  const newEntity = mapEntity(oldEntity);
1490
1595
  if (oldEntity.isDeletedEntity) {
1491
1596
  // If the old entity was deleted, that should be persisted in the new em
1492
- newEntity.__data.markDeleted(newEntity);
1597
+ newEntity.__data.markDeleted();
1493
1598
  // deleted entities will fail if you try to `get` their relations, so skip them since they should be cleared
1494
1599
  // out regardless
1495
1600
  continue;
@@ -1634,11 +1739,11 @@ class EntityManager {
1634
1739
  }
1635
1740
  }
1636
1741
  /** Registers a newly-instantiated entity with our EntityManager; only called by #doCreate and hydrate. */
1637
- #doRegister(entity, id) {
1742
+ #doRegister(entity, id, meta, skipDuplicateCheck = false) {
1638
1743
  // Keep our indexes up to date...
1639
1744
  const maybeId = id ?? entity.idTaggedMaybe;
1640
1745
  if (maybeId) {
1641
- if (this.findExistingInstance(maybeId) !== undefined) {
1746
+ if (!skipDuplicateCheck && this.findExistingInstance(maybeId) !== undefined) {
1642
1747
  throw new Error(`Entity ${entity} has a duplicate instance already loaded`);
1643
1748
  }
1644
1749
  this.#entitiesById.set(maybeId, entity);
@@ -1648,7 +1753,7 @@ class EntityManager {
1648
1753
  this.#entitiesById.set(entity.toTaggedString(), entity);
1649
1754
  }
1650
1755
  this.#entitiesArray.push(entity);
1651
- const meta = (0, index_1.getMetadata)(entity);
1756
+ meta ??= (0, index_1.getMetadata)(entity);
1652
1757
  const set = this.#entitiesByTag.get(meta.tagName) ?? [];
1653
1758
  if (set.length === 0)
1654
1759
  this.#entitiesByTag.set(meta.tagName, set);
@@ -1747,14 +1852,14 @@ async function validateReactiveRules(em, logger, todos, joinRowTodos) {
1747
1852
  const p1 = Object.values(todos).flatMap((todo) => {
1748
1853
  const entities = [...todo.inserts, ...todo.updates, ...todo.deletes];
1749
1854
  // Find each statically-declared reactive rule for the given entity type
1750
- const rules = (0, caches_1.getReactiveRules)(todo.metadata);
1855
+ const rules = todo.metadata.reactiveRules;
1751
1856
  return rules.map((rule) => {
1752
1857
  // Of all changed entities of this type, how many specifically trigger this rule?
1753
1858
  const triggered = entities.filter((e) => {
1754
1859
  // If the rule is for a different subtype, skip it
1755
1860
  if (!(e instanceof rule.source))
1756
1861
  return false;
1757
- // Any new-or-deleted entity fires every rule (getReactiveRules has already filtered out read-only)
1862
+ // Any new-or-deleted entity fires every rule (reactiveRules has already filtered out read-only)
1758
1863
  if (e.isNewEntity || e.isDeletedEntity)
1759
1864
  return true;
1760
1865
  // Otherwise see if the changed fields overlaps with the rule's fields
@@ -1772,15 +1877,15 @@ async function validateReactiveRules(em, logger, todos, joinRowTodos) {
1772
1877
  const p2 = Object.values(joinRowTodos).flatMap((todo) => {
1773
1878
  const entities = [...todo.newRows, ...todo.deletedRows].flatMap((jr) => Object.values(jr.columns));
1774
1879
  // Do the first side
1775
- const p1 = (0, caches_1.getReactiveRules)(todo.m2m.meta)
1776
- .filter((rule) => rule.fields.includes(todo.m2m.fieldName))
1880
+ const p1 = todo.m2m.meta
1881
+ .reactiveRules.filter((rule) => rule.fields.includes(todo.m2m.fieldName))
1777
1882
  .map((rule) => {
1778
1883
  const triggered = entities.filter((e) => e instanceof todo.m2m.meta.cstr);
1779
1884
  return followAndQueue(triggered, rule);
1780
1885
  });
1781
1886
  // And the second side
1782
- const p2 = (0, caches_1.getReactiveRules)(todo.m2m.otherMeta)
1783
- .filter((rule) => rule.fields.includes(todo.m2m.otherFieldName))
1887
+ const p2 = todo.m2m.otherMeta
1888
+ .reactiveRules.filter((rule) => rule.fields.includes(todo.m2m.otherFieldName))
1784
1889
  .map((rule) => {
1785
1890
  const triggered = entities.filter((e) => e instanceof todo.m2m.otherMeta.cstr);
1786
1891
  return followAndQueue(triggered, rule);
@@ -1859,6 +1964,19 @@ function beforeUpdate(ctx, todos) {
1859
1964
  function afterValidation(ctx, todos) {
1860
1965
  return runHookOnTodos(ctx, "afterValidation", todos, ["inserts", "updates"]);
1861
1966
  }
1967
+ /** Collects changed entities from flush todos, i.e. m2m endpoint entities. */
1968
+ function entitiesFromTodos(entityTodos, joinRowTodos) {
1969
+ const entities = new Set();
1970
+ for (const todo of Object.values(entityTodos)) {
1971
+ [...todo.inserts, ...todo.updates, ...todo.deletes].forEach((entity) => entities.add(entity));
1972
+ }
1973
+ for (const todo of Object.values(joinRowTodos)) {
1974
+ [...todo.newRows, ...todo.deletedRows].forEach((row) => {
1975
+ Object.values(row.columns).forEach((entity) => entities.add(entity));
1976
+ });
1977
+ }
1978
+ return [...entities];
1979
+ }
1862
1980
  function beforeCommit(ctx, entities) {
1863
1981
  return runHook(ctx, "beforeCommit", [...entities]);
1864
1982
  }
@@ -1986,7 +2104,7 @@ function getCascadeDeleteRelations(entity) {
1986
2104
  function isCustomRelation(r) {
1987
2105
  return r instanceof index_1.CustomCollection || r instanceof index_1.CustomReference || r instanceof relations_1.ReactiveReferenceImpl;
1988
2106
  }
1989
- function maybeBumpUpdatedAt(todos, now) {
2107
+ function maybeBumpUpdatedAt(rm, todos, now) {
1990
2108
  for (const todo of Object.values(todos)) {
1991
2109
  const { updatedAt } = todo.metadata.timestampFields ?? {};
1992
2110
  if (updatedAt) {
@@ -1996,9 +2114,10 @@ function maybeBumpUpdatedAt(todos, now) {
1996
2114
  // it has changed. This is technically true, but this will break the oplock SQL generation,
1997
2115
  // so force the field to be dirty.
1998
2116
  const orm = (0, BaseEntity_1.getInstanceData)(e);
1999
- orm.originalData[updatedAt] = (0, fields_1.getField)(e, updatedAt);
2117
+ orm.markFieldDirty(updatedAt, (0, fields_1.getField)(e, updatedAt));
2000
2118
  const serde = todo.metadata.fields[updatedAt].serde;
2001
2119
  orm.data[updatedAt] = serde.mapFromNow(now);
2120
+ rm.queueDownstreamReactables(e, updatedAt);
2002
2121
  }
2003
2122
  }
2004
2123
  }
@@ -2032,17 +2151,13 @@ function findConcreteMeta(maybeBaseMeta, row) {
2032
2151
  throw new Error(`${maybeBaseMeta.type} ${(0, index_1.tagId)(maybeBaseMeta, row.id)} must be instantiated via a subtype`);
2033
2152
  }
2034
2153
  // Look for the CTI __class from the driver telling us which subtype to instantiate
2035
- return maybeBaseMeta.subTypes.find((st) => st.type === row.__class) ?? maybeBaseMeta;
2154
+ return maybeBaseMeta.subTypesByType.get(row.__class) ?? maybeBaseMeta;
2036
2155
  }
2037
2156
  else if (maybeBaseMeta.inheritanceType === "sti") {
2038
2157
  // Look for the STI discriminator value
2039
2158
  const baseMeta = (0, index_1.getBaseMeta)(maybeBaseMeta);
2040
- const field = baseMeta.fields[baseMeta.stiDiscriminatorField];
2041
- if (field.kind !== "enum")
2042
- throw new Error("Discriminator field must be an enum");
2043
- const columnName = field.serde.columns[0].columnName;
2044
- const value = row[columnName];
2045
- return baseMeta.subTypes.find((st) => st.stiDiscriminatorValue === value) ?? baseMeta;
2159
+ const value = row[baseMeta.stiDiscriminatorColumnName];
2160
+ return baseMeta.subTypesByStiValue.get(value) ?? baseMeta;
2046
2161
  }
2047
2162
  else {
2048
2163
  throw new Error("Unknown inheritance type");
@@ -2051,7 +2166,7 @@ function findConcreteMeta(maybeBaseMeta, row) {
2051
2166
  /** Sets the `Animal.type` enum to the right subtype value. */
2052
2167
  function setStiDiscriminatorValue(baseMeta, entity) {
2053
2168
  const typeName = entity.constructor.name;
2054
- const st = baseMeta.subTypes.find((st) => st.type === typeName);
2169
+ const st = baseMeta.subTypesByType.get(typeName);
2055
2170
  if (st) {
2056
2171
  const field = baseMeta.fields[baseMeta.stiDiscriminatorField];
2057
2172
  const code = field.enumDetailType.findById(st.stiDiscriminatorValue).code;
@@ -2061,19 +2176,25 @@ function setStiDiscriminatorValue(baseMeta, entity) {
2061
2176
  entity[baseMeta.stiDiscriminatorField] = undefined;
2062
2177
  }
2063
2178
  }
2064
- function findPendingFlushEntities(entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks) {
2065
- for (const e of entities) {
2066
- if ((0, BaseEntity_1.getInstanceData)(e).pendingOperation !== "none") {
2067
- if (!hooksInvoked.has(e)) {
2068
- pendingHooks.add(e);
2069
- }
2070
- else {
2071
- alreadyRanHooks.add(e);
2072
- }
2073
- pendingFlush.add(e);
2179
+ function findPendingFlushEntities(maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks) {
2180
+ for (const e of maybePendingFlushEntities) {
2181
+ if ((0, BaseEntity_1.getInstanceData)(e).pendingOperation === "none") {
2182
+ maybePendingFlushEntities.delete(e);
2183
+ continue;
2074
2184
  }
2185
+ if (!hooksInvoked.has(e)) {
2186
+ pendingHooks.add(e);
2187
+ }
2188
+ else {
2189
+ alreadyRanHooks.add(e);
2190
+ }
2191
+ pendingFlush.add(e);
2075
2192
  }
2076
2193
  }
2194
+ /** Returns true if the caller explicitly asked `find` to use SQL pagination. */
2195
+ function hasPaginationSettings(options) {
2196
+ return "limit" in options || "offset" in options;
2197
+ }
2077
2198
  /** An error we throw to get knex to `ROLLBACK`, but then catch. */
2078
2199
  class InMemoryRollbackError extends Error {
2079
2200
  }