joist-core 2.2.0-next.4 → 2.2.0-next.40

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 (169) hide show
  1. package/build/Aliases.d.ts.map +1 -1
  2. package/build/Aliases.js.map +1 -1
  3. package/build/BaseEntity.d.ts +2 -0
  4. package/build/BaseEntity.d.ts.map +1 -1
  5. package/build/BaseEntity.js +1 -1
  6. package/build/BaseEntity.js.map +1 -1
  7. package/build/Entity.d.ts +1 -0
  8. package/build/Entity.d.ts.map +1 -1
  9. package/build/EntityFilter.d.ts +27 -8
  10. package/build/EntityFilter.d.ts.map +1 -1
  11. package/build/EntityFilter.js +18 -0
  12. package/build/EntityFilter.js.map +1 -1
  13. package/build/EntityManager.d.ts +14 -10
  14. package/build/EntityManager.d.ts.map +1 -1
  15. package/build/EntityManager.js +184 -90
  16. package/build/EntityManager.js.map +1 -1
  17. package/build/EntityMetadata.d.ts +20 -1
  18. package/build/EntityMetadata.d.ts.map +1 -1
  19. package/build/EntityMetadata.js +13 -0
  20. package/build/EntityMetadata.js.map +1 -1
  21. package/build/IndexManager.d.ts +5 -5
  22. package/build/IndexManager.d.ts.map +1 -1
  23. package/build/IndexManager.js +48 -32
  24. package/build/IndexManager.js.map +1 -1
  25. package/build/InstanceData.d.ts +16 -2
  26. package/build/InstanceData.d.ts.map +1 -1
  27. package/build/InstanceData.js +36 -3
  28. package/build/InstanceData.js.map +1 -1
  29. package/build/IsLoadedCache.d.ts.map +1 -1
  30. package/build/IsLoadedCache.js +31 -25
  31. package/build/IsLoadedCache.js.map +1 -1
  32. package/build/JoinRows.js +1 -1
  33. package/build/JoinRows.js.map +1 -1
  34. package/build/QueryParser.d.ts +6 -1
  35. package/build/QueryParser.d.ts.map +1 -1
  36. package/build/QueryParser.js +265 -190
  37. package/build/QueryParser.js.map +1 -1
  38. package/build/QueryParser.test.d.ts +2 -0
  39. package/build/QueryParser.test.d.ts.map +1 -0
  40. package/build/QueryParser.test.js +16 -0
  41. package/build/QueryParser.test.js.map +1 -0
  42. package/build/ReactionsManager.d.ts +2 -1
  43. package/build/ReactionsManager.d.ts.map +1 -1
  44. package/build/ReactionsManager.js +53 -49
  45. package/build/ReactionsManager.js.map +1 -1
  46. package/build/batchloaders/BatchLoader.d.ts.map +1 -1
  47. package/build/batchloaders/BatchLoader.js +6 -1
  48. package/build/batchloaders/BatchLoader.js.map +1 -1
  49. package/build/batchloaders/manyToManyBatchLoader.d.ts.map +1 -1
  50. package/build/batchloaders/manyToManyBatchLoader.js +1 -1
  51. package/build/batchloaders/manyToManyBatchLoader.js.map +1 -1
  52. package/build/batchloaders/populateBatchLoader.d.ts.map +1 -1
  53. package/build/batchloaders/populateBatchLoader.js +26 -8
  54. package/build/batchloaders/populateBatchLoader.js.map +1 -1
  55. package/build/batchloaders/recursiveChildrenBatchLoader.d.ts.map +1 -1
  56. package/build/batchloaders/recursiveChildrenBatchLoader.js +2 -0
  57. package/build/batchloaders/recursiveChildrenBatchLoader.js.map +1 -1
  58. package/build/batchloaders/recursiveM2mBatchLoader.d.ts.map +1 -1
  59. package/build/batchloaders/recursiveM2mBatchLoader.js +1 -3
  60. package/build/batchloaders/recursiveM2mBatchLoader.js.map +1 -1
  61. package/build/batchloaders/recursiveParentsBatchLoader.d.ts.map +1 -1
  62. package/build/batchloaders/recursiveParentsBatchLoader.js +2 -0
  63. package/build/batchloaders/recursiveParentsBatchLoader.js.map +1 -1
  64. package/build/configure.d.ts +5 -3
  65. package/build/configure.d.ts.map +1 -1
  66. package/build/configure.js +70 -0
  67. package/build/configure.js.map +1 -1
  68. package/build/dataloaders/fastWhereFilterHash.d.ts +15 -0
  69. package/build/dataloaders/fastWhereFilterHash.d.ts.map +1 -0
  70. package/build/dataloaders/fastWhereFilterHash.js +164 -0
  71. package/build/dataloaders/fastWhereFilterHash.js.map +1 -0
  72. package/build/dataloaders/fastWhereFilterHash.test.d.ts +2 -0
  73. package/build/dataloaders/fastWhereFilterHash.test.d.ts.map +1 -0
  74. package/build/dataloaders/fastWhereFilterHash.test.js +59 -0
  75. package/build/dataloaders/fastWhereFilterHash.test.js.map +1 -0
  76. package/build/dataloaders/findCountDataLoader.d.ts +12 -2
  77. package/build/dataloaders/findCountDataLoader.d.ts.map +1 -1
  78. package/build/dataloaders/findCountDataLoader.js +46 -4
  79. package/build/dataloaders/findCountDataLoader.js.map +1 -1
  80. package/build/dataloaders/findDataLoader.d.ts +14 -1
  81. package/build/dataloaders/findDataLoader.d.ts.map +1 -1
  82. package/build/dataloaders/findDataLoader.js +53 -44
  83. package/build/dataloaders/findDataLoader.js.map +1 -1
  84. package/build/dataloaders/findIdsDataLoader.d.ts.map +1 -1
  85. package/build/dataloaders/findIdsDataLoader.js +5 -3
  86. package/build/dataloaders/findIdsDataLoader.js.map +1 -1
  87. package/build/dataloaders/findOrCreateDataLoader.d.ts.map +1 -1
  88. package/build/dataloaders/findOrCreateDataLoader.js +3 -3
  89. package/build/dataloaders/findOrCreateDataLoader.js.map +1 -1
  90. package/build/dataloaders/findPaginatedDataLoader.d.ts.map +1 -1
  91. package/build/dataloaders/findPaginatedDataLoader.js +11 -5
  92. package/build/dataloaders/findPaginatedDataLoader.js.map +1 -1
  93. package/build/drivers/EntityWriter.js +13 -7
  94. package/build/drivers/EntityWriter.js.map +1 -1
  95. package/build/fields.d.ts.map +1 -1
  96. package/build/fields.js +16 -6
  97. package/build/fields.js.map +1 -1
  98. package/build/index.d.ts +2 -0
  99. package/build/index.d.ts.map +1 -1
  100. package/build/index.js +4 -1
  101. package/build/index.js.map +1 -1
  102. package/build/keys.d.ts.map +1 -1
  103. package/build/keys.js +8 -6
  104. package/build/keys.js.map +1 -1
  105. package/build/loadHints.d.ts +9 -0
  106. package/build/loadHints.d.ts.map +1 -1
  107. package/build/loadHints.js +63 -9
  108. package/build/loadHints.js.map +1 -1
  109. package/build/loadLens.d.ts +11 -0
  110. package/build/loadLens.d.ts.map +1 -1
  111. package/build/loadLens.js +43 -10
  112. package/build/loadLens.js.map +1 -1
  113. package/build/newEntity.d.ts.map +1 -1
  114. package/build/newEntity.js +16 -14
  115. package/build/newEntity.js.map +1 -1
  116. package/build/normalizeHints.d.ts +7 -0
  117. package/build/normalizeHints.d.ts.map +1 -1
  118. package/build/normalizeHints.js +40 -2
  119. package/build/normalizeHints.js.map +1 -1
  120. package/build/relations/ManyToManyCollection.d.ts.map +1 -1
  121. package/build/relations/ManyToManyCollection.js +2 -2
  122. package/build/relations/ManyToManyCollection.js.map +1 -1
  123. package/build/relations/ManyToManyLargeCollection.d.ts.map +1 -1
  124. package/build/relations/ManyToManyLargeCollection.js +2 -1
  125. package/build/relations/ManyToManyLargeCollection.js.map +1 -1
  126. package/build/relations/OneToManyCollection.d.ts.map +1 -1
  127. package/build/relations/OneToManyCollection.js +2 -2
  128. package/build/relations/OneToManyCollection.js.map +1 -1
  129. package/build/relations/OneToManyLargeCollection.d.ts.map +1 -1
  130. package/build/relations/OneToManyLargeCollection.js +2 -1
  131. package/build/relations/OneToManyLargeCollection.js.map +1 -1
  132. package/build/relations/ReactiveManyToMany.d.ts.map +1 -1
  133. package/build/relations/ReactiveManyToMany.js +1 -1
  134. package/build/relations/ReactiveManyToMany.js.map +1 -1
  135. package/build/relations/ReactiveManyToManyOtherSide.d.ts.map +1 -1
  136. package/build/relations/ReactiveManyToManyOtherSide.js +1 -1
  137. package/build/relations/ReactiveManyToManyOtherSide.js.map +1 -1
  138. package/build/relations/RecursiveCollection.d.ts +2 -6
  139. package/build/relations/RecursiveCollection.d.ts.map +1 -1
  140. package/build/relations/RecursiveCollection.js +9 -17
  141. package/build/relations/RecursiveCollection.js.map +1 -1
  142. package/build/relations/RecursiveCycleError.d.ts +13 -0
  143. package/build/relations/RecursiveCycleError.d.ts.map +1 -0
  144. package/build/relations/RecursiveCycleError.js +15 -0
  145. package/build/relations/RecursiveCycleError.js.map +1 -0
  146. package/build/relations/hasOneThrough.d.ts.map +1 -1
  147. package/build/relations/hasOneThrough.js +6 -4
  148. package/build/relations/hasOneThrough.js.map +1 -1
  149. package/build/relations/hasProperty.d.ts +2 -1
  150. package/build/relations/hasProperty.d.ts.map +1 -1
  151. package/build/relations/hasProperty.js +15 -5
  152. package/build/relations/hasProperty.js.map +1 -1
  153. package/build/scopes.d.ts +185 -0
  154. package/build/scopes.d.ts.map +1 -0
  155. package/build/scopes.js +302 -0
  156. package/build/scopes.js.map +1 -0
  157. package/build/temporal.d.ts +32 -2
  158. package/build/temporal.d.ts.map +1 -1
  159. package/build/temporal.js +18 -0
  160. package/build/temporal.js.map +1 -1
  161. package/build/utils.d.ts +2 -0
  162. package/build/utils.d.ts.map +1 -1
  163. package/build/utils.js +12 -0
  164. package/build/utils.js.map +1 -1
  165. package/package.json +3 -5
  166. package/build/caches.d.ts +0 -6
  167. package/build/caches.d.ts.map +0 -1
  168. package/build/caches.js +0 -42
  169. package/build/caches.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");
@@ -43,6 +42,7 @@ const FlushLock_1 = require("./FlushLock");
43
42
  const index_1 = require("./index");
44
43
  const IsLoadedCache_1 = require("./IsLoadedCache");
45
44
  const JoinRows_1 = require("./JoinRows");
45
+ const loadHints_1 = require("./loadHints");
46
46
  const newEntity_1 = require("./newEntity");
47
47
  const newTestInstance_1 = require("./newTestInstance");
48
48
  const PluginManager_1 = require("./PluginManager");
@@ -52,6 +52,7 @@ const relations_1 = require("./relations");
52
52
  const AsyncProperty_1 = require("./relations/AsyncProperty");
53
53
  const hasAsyncMethod_1 = require("./relations/hasAsyncMethod");
54
54
  const RecursiveCollection_1 = require("./relations/RecursiveCollection");
55
+ const scopes_1 = require("./scopes");
55
56
  const Todo_1 = require("./Todo");
56
57
  const trusted_1 = require("./trusted");
57
58
  const upsert_1 = require("./upsert");
@@ -88,6 +89,8 @@ class EntityManager {
88
89
  txn;
89
90
  entityLimit = defaultEntityLimit;
90
91
  #entitiesArray = [];
92
+ // Incrementally track dirty entities so we don't have to scan `#entityArray` during flush
93
+ #maybePendingFlushEntities = new Set();
91
94
  // Indexes the currently loaded entities by their tagged ids and `toTaggedString` ids (i.e. `a#`). This fixes
92
95
  // real-world performance issues where `findExistingInstance` scanning `#entities` was an `O(n^2)`.
93
96
  #entitiesById = new Map();
@@ -106,6 +109,7 @@ class EntityManager {
106
109
  * so that both see the most accurate state.
107
110
  */
108
111
  #pendingDeletes = [];
112
+ #hasAnyDeletes = false;
109
113
  #dataloaders = {};
110
114
  #batchLoaders = {};
111
115
  #joinRows = {};
@@ -155,6 +159,20 @@ class EntityManager {
155
159
  indexManager: this.#indexManager,
156
160
  isLoadedCache: this.#isLoadedCache,
157
161
  pluginManager,
162
+ markMaybePending(entity) {
163
+ em.#maybePendingFlushEntities.add(entity);
164
+ },
165
+ unmarkMaybePending(entity) {
166
+ em.#maybePendingFlushEntities.delete(entity);
167
+ },
168
+ hasAnyDeletes() {
169
+ return em.#hasAnyDeletes;
170
+ },
171
+ pendingDeleteIds(type) {
172
+ return em.#pendingDeletes
173
+ .filter((entity) => entity instanceof type && entity.idMaybe !== undefined)
174
+ .map((entity) => (0, index_1.keyToNumber)((0, index_1.getMetadata)(entity), entity.id));
175
+ },
158
176
  isMerging(entity) {
159
177
  return em.#merging?.has(entity) ?? false;
160
178
  },
@@ -247,11 +265,11 @@ class EntityManager {
247
265
  }
248
266
  async find(type, where, options) {
249
267
  const { populate, ...rest } = options || {};
250
- const settings = { where, ...rest };
251
- const result = await (hasPaginationSettings(rest)
268
+ const normalized = (0, index_1.mergeFindOptions)(where, rest);
269
+ const settings = { where: normalized.where, ...normalized.options };
270
+ const result = await (hasPaginationSettings(normalized.options)
252
271
  ? (0, findPaginatedDataLoader_1.findPaginatedDataLoader)(this, type, settings, populate)
253
- : (0, findDataLoader_1.findDataLoader)(this, type, settings, populate))
254
- .catch(function find(err) {
272
+ : (0, findDataLoader_1.findDataLoader)(this, type, settings, populate)).catch(function find(err) {
255
273
  throw appendStack(err, new Error());
256
274
  });
257
275
  if (populate) {
@@ -346,6 +364,9 @@ class EntityManager {
346
364
  }
347
365
  else {
348
366
  const [entity] = this.hydrate(type, [row]);
367
+ if (this.#hasAnyDeletes && entity.isDeletedEntity) {
368
+ return undefined;
369
+ }
349
370
  if (populate) {
350
371
  await this.populate(entity, populate);
351
372
  }
@@ -361,16 +382,16 @@ class EntityManager {
361
382
  * Note: this method is not currently auto-batched, so it will cause N+1s if called in a loop.
362
383
  */
363
384
  async findCount(type, where, options = {}) {
364
- const settings = { where, ...options };
365
- let count = await (0, findCountDataLoader_1.findCountDataLoader)(this, type, settings)
366
- .catch(function findCount(err) {
385
+ const normalized = (0, findCountDataLoader_1.mergeCountOptions)(where, options);
386
+ const settings = { where: normalized.where, ...normalized.options };
387
+ let count = await (0, findCountDataLoader_1.findCountDataLoader)(this, type, settings).catch(function findCount(err) {
367
388
  throw appendStack(err, new Error());
368
389
  });
369
390
  // If the user is do "count all", we can adjust the number up/down based on
370
391
  // WIP creates/deletes. We can't do this if the WHERE clause is populated b/c
371
392
  // then we'd also have to eval each created/deleted entity against the WHERE
372
393
  // clause before knowing if it should adjust teh amount.
373
- const isSelectAll = Object.keys(where).length === 0;
394
+ const isSelectAll = (0, scopes_1.isSelectAllFilter)(where, options.conditions);
374
395
  if (isSelectAll) {
375
396
  const tagged = this.#entitiesByTag.get((0, index_1.getMetadata)(type).tagName) ?? [];
376
397
  for (const entity of tagged) {
@@ -394,9 +415,9 @@ class EntityManager {
394
415
  * as `em.find`.
395
416
  */
396
417
  async findIds(type, where, options = {}) {
397
- const settings = { where, ...options };
398
- return (0, findIdsDataLoader_1.findIdsDataLoader)(this, type, settings)
399
- .catch(function findIds(err) {
418
+ const normalized = (0, findCountDataLoader_1.mergeCountOptions)(where, options);
419
+ const settings = { where: normalized.where, ...normalized.options };
420
+ return (0, findIdsDataLoader_1.findIdsDataLoader)(this, type, settings).catch(function findIds(err) {
400
421
  throw appendStack(err, new Error());
401
422
  });
402
423
  }
@@ -685,48 +706,86 @@ class EntityManager {
685
706
  }
686
707
  async loadAll(type, _ids, hint) {
687
708
  const meta = (0, index_1.getMetadata)(type);
688
- const ids = _ids.map((id) => (0, index_1.tagId)(meta, id));
689
- const idsToLoad = ids.filter((id) => !this.findExistingInstance(id));
690
- if (idsToLoad.length > 0) {
709
+ // Use pre-allocated arrays/for loops instead of `.filter`s since this can be a hot spot
710
+ const ids = new Array(_ids.length);
711
+ const entities = new Array(_ids.length);
712
+ let idsToLoad;
713
+ let positionsToLoad;
714
+ for (let i = 0; i < _ids.length; i++) {
715
+ const id = (0, index_1.tagId)(meta, _ids[i]);
716
+ ids[i] = id;
717
+ const entity = this.findExistingInstance(id);
718
+ if (entity) {
719
+ entities[i] = entity;
720
+ }
721
+ else {
722
+ (idsToLoad ??= []).push(id);
723
+ (positionsToLoad ??= []).push(i);
724
+ }
725
+ }
726
+ if (idsToLoad && idsToLoad.length > 0) {
691
727
  await (0, loadBatchLoader_1.loadBatchLoader)(this, meta)
692
728
  .loadAll(idsToLoad.map((id) => ({ taggedId: id, hint })))
693
729
  .catch(function loadAll(err) {
694
730
  throw appendStack(err, new Error());
695
731
  });
732
+ for (const i of positionsToLoad) {
733
+ entities[i] = this.findExistingInstance(ids[i]);
734
+ }
696
735
  }
697
- const entities = [];
698
- for (const id of ids) {
699
- const entity = this.findExistingInstance(id);
700
- if (entity)
701
- entities.push(entity);
736
+ let idsNotFound;
737
+ for (let i = 0; i < entities.length; i++) {
738
+ if (entities[i] === undefined) {
739
+ (idsNotFound ??= []).push(ids[i]);
740
+ }
702
741
  }
703
- if (entities.length !== ids.length) {
704
- const idsNotFound = ids.filter((_, i) => entities[i] === undefined);
742
+ if (idsNotFound) {
705
743
  throw new NotFoundError(`${idsNotFound.join(",")} were not found`);
706
744
  }
745
+ const loadedEntities = entities;
707
746
  if (hint) {
708
- await this.populate(entities, hint);
747
+ await this.populate(loadedEntities, hint);
709
748
  }
710
749
  if (meta.inheritanceType === "sti" && meta.baseType) {
711
- const wrongType = entities.filter((e) => !(e instanceof meta.cstr));
750
+ const wrongType = loadedEntities.filter((e) => !(e instanceof meta.cstr));
712
751
  if (wrongType.length > 0) {
713
752
  throw new Error(`${wrongType.join(", ")} were not of type ${meta.cstr.name}`);
714
753
  }
715
754
  }
716
- return entities;
755
+ return loadedEntities;
717
756
  }
718
757
  async loadAllIfExists(type, _ids, hint) {
719
758
  const meta = (0, index_1.getMetadata)(type);
720
- const ids = _ids.map((id) => (0, index_1.tagId)(meta, id));
721
- const idsToLoad = ids.filter((id) => !this.findExistingInstance(id));
722
- if (idsToLoad.length > 0) {
759
+ // Use pre-allocated arrays/for loops instead of `.filter`s since this can be a hot spot
760
+ const ids = new Array(_ids.length);
761
+ const entities = [];
762
+ let idsToLoad;
763
+ // Ensure the ids are tagged, and find any not-yet-loaded
764
+ for (let i = 0; i < _ids.length; i++) {
765
+ const id = (0, index_1.tagId)(meta, _ids[i]);
766
+ ids[i] = id;
767
+ const entity = this.findExistingInstance(id);
768
+ if (entity) {
769
+ entities.push(entity);
770
+ }
771
+ else {
772
+ (idsToLoad ??= []).push(id);
773
+ }
774
+ }
775
+ if (idsToLoad && idsToLoad.length > 0) {
723
776
  await (0, loadBatchLoader_1.loadBatchLoader)(this, meta)
724
777
  .loadAll(idsToLoad.map((id) => ({ taggedId: id, hint })))
725
778
  .catch(function loadAllIfExists(err) {
726
779
  throw appendStack(err, new Error());
727
780
  });
781
+ // Now that everything is loaded, recalc `entities`
782
+ entities.length = 0;
783
+ for (const id of ids) {
784
+ const entity = this.findExistingInstance(id);
785
+ if (entity)
786
+ entities.push(entity);
787
+ }
728
788
  }
729
- const entities = ids.map((id) => this.findExistingInstance(id)).filter(Boolean);
730
789
  if (hint) {
731
790
  await this.populate(entities, hint);
732
791
  }
@@ -777,6 +836,12 @@ class EntityManager {
777
836
  if (list.length === 0) {
778
837
  return !fn ? entityOrList : fn(entityOrList);
779
838
  }
839
+ // Avoid building a HintTree/batchloader for persisted entities when everything is already loaded; in the phase 9
840
+ // benchmark this moved already-loaded populates from ~1.10ms to ~0.89ms, and nested already-loaded populates from
841
+ // ~2.39ms to ~1.94ms.
842
+ if (!opts.forceReload && list.every((entity) => !entity.isNewEntity && (0, loadHints_1.isLoadedForPopulate)(entity, hintOpt))) {
843
+ return fn ? fn(entityOrList) : entityOrList;
844
+ }
780
845
  const meta = (0, index_1.getMetadata)(list[0]);
781
846
  if (this.#preloader) {
782
847
  // If we can preload, prevent promise deadlocking by one large-batch preload populate (which can't have
@@ -825,9 +890,11 @@ class EntityManager {
825
890
  delete(entityOrArray) {
826
891
  for (const entity of (0, utils_1.toArray)(entityOrArray)) {
827
892
  // Early return if already deleted.
828
- const alreadyMarked = (0, BaseEntity_1.getInstanceData)(entity).markDeleted(entity);
893
+ const alreadyMarked = (0, BaseEntity_1.getInstanceData)(entity).markDeleted();
829
894
  if (!alreadyMarked)
830
895
  continue;
896
+ // This monotonic flag lets find hot paths skip scanning results until a delete has ever happened in this EM.
897
+ this.#hasAnyDeletes = true;
831
898
  // Any derived fields that read this entity will need recalc-d
832
899
  this.#rm.queueAllDownstreamFields(entity, "deleted");
833
900
  // Synchronously unhook the entity if the relations are loaded
@@ -921,7 +988,7 @@ class EntityManager {
921
988
  const pendingHooks = new Set();
922
989
  // Subset of pendingFlush entities that had hooks invoked in a prior `runHooksOnPendingEntities`
923
990
  const alreadyRanHooks = new Set();
924
- findPendingFlushEntities(this.entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
991
+ findPendingFlushEntities(this.#maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
925
992
  // If we're re-looping for AsyncReactiveField, make sure to bump updatedAt
926
993
  // each time, so that for an INSERT-then-UPDATE the triggers don't think the
927
994
  // UPDATE forgot to self-bump updatedAt, and then "helpfully" bump it for us.
@@ -945,9 +1012,13 @@ class EntityManager {
945
1012
  // fields, they'd be via the synchronous getter and would not be stale.
946
1013
  recalcSynchronousDerivedFields(todos);
947
1014
  // The hooks could have deleted this-loop or prior-loop entities, so re-cascade again.
948
- await this.flushDeletes();
949
- // The hooks could have changed fields, so recalc again.
950
- await this.#rm.recalcPendingReactables("reactables");
1015
+ // Reactions (the recalc below) can themselves `em.delete` entities, so keep draining
1016
+ // until both deletes and recalcs have settled.
1017
+ do {
1018
+ await this.flushDeletes();
1019
+ // The hooks could have changed fields, so recalc again.
1020
+ await this.#rm.recalcPendingReactables("reactables");
1021
+ } while (this.#pendingDeletes.length > 0);
951
1022
  // We may have reactables that failed earlier, but will succeed now that hooks have been run and cascade
952
1023
  // deletes have been processed
953
1024
  if (this.#rm.hasPendingTypeErrors) {
@@ -960,7 +1031,7 @@ class EntityManager {
960
1031
  hooksInvoked.add(e);
961
1032
  pendingHooks.clear();
962
1033
  // See if the hooks mutated any new, not-yet-hooksInvoked entities
963
- findPendingFlushEntities(this.entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
1034
+ findPendingFlushEntities(this.#maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks);
964
1035
  // The final run of recalcPendingReactables could have left us with pending type errors and no entities in
965
1036
  // pendingHooks. If so, we need to re-run recalcPendingTypeErrors to get those errors to transition into
966
1037
  // suppressed errors so that we will fail after simpleValidation.
@@ -1095,7 +1166,7 @@ class EntityManager {
1095
1166
  for (const e of createdThenDeleted)
1096
1167
  (0, BaseEntity_1.getInstanceData)(e).fixupCreatedThenDeleted();
1097
1168
  this.#merging?.clear();
1098
- return [...allFlushedEntities];
1169
+ return [...allFlushedEntities].sort((a, b) => (0, BaseEntity_1.getInstanceData)(a).entityIndex - (0, BaseEntity_1.getInstanceData)(b).entityIndex);
1099
1170
  }
1100
1171
  catch (e) {
1101
1172
  if (e instanceof RecursiveCollection_1.RecursiveCycleError) {
@@ -1118,8 +1189,9 @@ class EntityManager {
1118
1189
  throw new index_1.ValidationErrors(message);
1119
1190
  }
1120
1191
  }
1121
- if (e instanceof InMemoryRollbackError)
1122
- return [...allFlushedEntities];
1192
+ if (e instanceof InMemoryRollbackError) {
1193
+ return [...allFlushedEntities].sort((a, b) => (0, BaseEntity_1.getInstanceData)(a).entityIndex - (0, BaseEntity_1.getInstanceData)(b).entityIndex);
1194
+ }
1123
1195
  throw e;
1124
1196
  }
1125
1197
  finally {
@@ -1227,10 +1299,13 @@ class EntityManager {
1227
1299
  */
1228
1300
  hydrate(type, rows, options) {
1229
1301
  const maybeBaseMeta = (0, index_1.getMetadata)(type);
1302
+ const taggedIdPrefix = `${maybeBaseMeta.tagName}:`;
1303
+ const overwriteExisting = options?.overwriteExisting === true;
1230
1304
  let i = 0;
1231
1305
  const entities = new Array(rows.length);
1232
1306
  for (const row of rows) {
1233
- const taggedId = (0, index_1.keyToTaggedId)(maybeBaseMeta, row["id"]) || (0, utils_1.fail)("No id column was available");
1307
+ const id = row["id"];
1308
+ const taggedId = id === undefined || id === null ? (0, utils_1.fail)("No id column was available") : `${taggedIdPrefix}${id}`;
1234
1309
  // See if this is already in our UoW
1235
1310
  let entity = this.findExistingInstance(taggedId);
1236
1311
  if (!entity) {
@@ -1239,23 +1314,36 @@ class EntityManager {
1239
1314
  // Pass id as a hint that we're in hydrate mode
1240
1315
  entity = (0, newEntity_1.newEntity)(this, (0, index_1.asConcreteCstr)(meta.cstr), false);
1241
1316
  (0, BaseEntity_1.getInstanceData)(entity).row = row;
1242
- this.#doRegister(entity, taggedId);
1317
+ this.#doRegister(entity, taggedId, meta, true);
1243
1318
  }
1244
- else if (options?.overwriteExisting === true) {
1319
+ else if (overwriteExisting) {
1245
1320
  // Usually if the entity already exists, we don't write over it, but in this case we assume that
1246
1321
  // `EntityManager.refresh` is telling us to explicitly load the latest data.
1247
1322
  // First swap out the old row with the new row
1248
- (0, BaseEntity_1.getInstanceData)(entity).row = row;
1323
+ const instanceData = (0, BaseEntity_1.getInstanceData)(entity);
1324
+ instanceData.row = row;
1249
1325
  // And then only refresh the data keys that have already been serde-d from rows
1250
1326
  // (this keeps us from deserializing data out of rows that we don't need).
1251
- const { data, originalData } = (0, BaseEntity_1.getInstanceData)(entity);
1252
- const changedFields = entity.changes.fieldsWithoutRelations;
1253
- for (const fieldName of Object.keys(data)) {
1254
- const serde = (0, index_1.getMetadata)(entity).allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1255
- serde.setOnEntity(data, row);
1256
- // Make the field look not-dirty
1257
- if (changedFields.includes(fieldName)) {
1258
- delete originalData[fieldName];
1327
+ const { data } = instanceData;
1328
+ const dataKeys = Object.keys(data);
1329
+ if (dataKeys.length > 0) {
1330
+ const allFields = (0, index_1.getMetadata)(entity).allFields;
1331
+ const changedFields = entity.changes.fieldsWithoutRelations;
1332
+ if (changedFields.length === 0) {
1333
+ for (const fieldName of dataKeys) {
1334
+ const serde = allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1335
+ serde.setOnEntity(data, row);
1336
+ }
1337
+ }
1338
+ else {
1339
+ for (const fieldName of dataKeys) {
1340
+ const serde = allFields[fieldName].serde ?? (0, utils_1.fail)(`Missing serde for ${fieldName}`);
1341
+ serde.setOnEntity(data, row);
1342
+ // Make the field look not-dirty
1343
+ if (changedFields.includes(fieldName)) {
1344
+ instanceData.markFieldClean(fieldName);
1345
+ }
1346
+ }
1259
1347
  }
1260
1348
  }
1261
1349
  }
@@ -1264,8 +1352,9 @@ class EntityManager {
1264
1352
  return entities;
1265
1353
  }
1266
1354
  touch(entityOrEntities) {
1267
- for (const entity of (0, utils_1.toArray)(entityOrEntities))
1268
- (0, BaseEntity_1.getInstanceData)(entity).isTouched = true;
1355
+ for (const entity of (0, utils_1.toArray)(entityOrEntities)) {
1356
+ (0, BaseEntity_1.getInstanceData)(entity).markTouched();
1357
+ }
1269
1358
  }
1270
1359
  async recalc(entityOrEntities) {
1271
1360
  // Look for async reactive fields
@@ -1342,9 +1431,16 @@ class EntityManager {
1342
1431
  // Run the beforeDelete hook before we unhook the entity
1343
1432
  const todos = (0, Todo_1.createTodos)(entities);
1344
1433
  await beforeDelete(this.ctx, todos);
1345
- // For all relations, unhook the entity from the other side
1346
- // (...we're using `concat` because `.push(...reallyBigArray)` with ~100k relations can blow the stack size
1347
- relationsToCleanup = relationsToCleanup.concat(entities.flatMap(index_1.getRelations));
1434
+ // For all relations, unhook the entity from the other side; this append path is optimized for
1435
+ // large deletes with ~100k relations by avoiding `flatMap` intermediates and `concat` copies.
1436
+ for (const entity of entities) {
1437
+ const relations = (0, index_1.getRelations)(entity);
1438
+ const start = relationsToCleanup.length;
1439
+ relationsToCleanup.length += relations.length;
1440
+ for (let i = 0; i < relations.length; i++) {
1441
+ relationsToCleanup[start + i] = relations[i];
1442
+ }
1443
+ }
1348
1444
  entities = this.#pendingDeletes;
1349
1445
  this.#pendingDeletes = [];
1350
1446
  }
@@ -1400,9 +1496,9 @@ class EntityManager {
1400
1496
  return entities.filter((e) => e instanceof cstr && !e.isDeletedEntity);
1401
1497
  }
1402
1498
  if (this.#indexManager.shouldIndexType(entities.length)) {
1403
- this.#indexManager.enableIndexingForType(meta, entities);
1499
+ this.#indexManager.enableIndexingForType(meta, entities, where);
1404
1500
  return (this.#indexManager
1405
- .findMatching(meta, where)
1501
+ .findMatching(meta, entities, where)
1406
1502
  // Still filter by `instanceof cstr` to handle subtyping
1407
1503
  .filter((e) => e instanceof cstr && !e.isDeletedEntity));
1408
1504
  }
@@ -1502,7 +1598,8 @@ class EntityManager {
1502
1598
  continue;
1503
1599
  const { originalData: oldOriginalData, data: oldData } = oldInstanceData;
1504
1600
  const newEntity = mapEntity(oldEntity);
1505
- const { originalData: newOriginalData, data: newData } = newEntity.__data;
1601
+ const newInstanceData = newEntity.__data;
1602
+ const { data: newData } = newInstanceData;
1506
1603
  // for new entities, anything in `data` is changed and should be copied across. for existing entities, we
1507
1604
  // only care about changed fields, which are enumerated by originalData
1508
1605
  const maybeEntity = (value) => ((0, Entity_1.isEntity)(value) ? mapEntity(value) : value);
@@ -1510,7 +1607,7 @@ class EntityManager {
1510
1607
  for (const field of fields) {
1511
1608
  // copy over originalData so .changes is consistent across ems
1512
1609
  if (field in oldOriginalData)
1513
- newOriginalData[field] = maybeEntity(oldOriginalData[field]);
1610
+ newInstanceData.markFieldDirty(field, maybeEntity(oldOriginalData[field]));
1514
1611
  newData[field] = maybeEntity(oldData[field]);
1515
1612
  }
1516
1613
  }
@@ -1520,7 +1617,7 @@ class EntityManager {
1520
1617
  const newEntity = mapEntity(oldEntity);
1521
1618
  if (oldEntity.isDeletedEntity) {
1522
1619
  // If the old entity was deleted, that should be persisted in the new em
1523
- newEntity.__data.markDeleted(newEntity);
1620
+ newEntity.__data.markDeleted();
1524
1621
  // deleted entities will fail if you try to `get` their relations, so skip them since they should be cleared
1525
1622
  // out regardless
1526
1623
  continue;
@@ -1665,11 +1762,11 @@ class EntityManager {
1665
1762
  }
1666
1763
  }
1667
1764
  /** Registers a newly-instantiated entity with our EntityManager; only called by #doCreate and hydrate. */
1668
- #doRegister(entity, id) {
1765
+ #doRegister(entity, id, meta, skipDuplicateCheck = false) {
1669
1766
  // Keep our indexes up to date...
1670
1767
  const maybeId = id ?? entity.idTaggedMaybe;
1671
1768
  if (maybeId) {
1672
- if (this.findExistingInstance(maybeId) !== undefined) {
1769
+ if (!skipDuplicateCheck && this.findExistingInstance(maybeId) !== undefined) {
1673
1770
  throw new Error(`Entity ${entity} has a duplicate instance already loaded`);
1674
1771
  }
1675
1772
  this.#entitiesById.set(maybeId, entity);
@@ -1678,8 +1775,9 @@ class EntityManager {
1678
1775
  // Also register by the `a#1` style tagged string for new entities
1679
1776
  this.#entitiesById.set(entity.toTaggedString(), entity);
1680
1777
  }
1778
+ (0, BaseEntity_1.getInstanceData)(entity).entityIndex = this.#entitiesArray.length;
1681
1779
  this.#entitiesArray.push(entity);
1682
- const meta = (0, index_1.getMetadata)(entity);
1780
+ meta ??= (0, index_1.getMetadata)(entity);
1683
1781
  const set = this.#entitiesByTag.get(meta.tagName) ?? [];
1684
1782
  if (set.length === 0)
1685
1783
  this.#entitiesByTag.set(meta.tagName, set);
@@ -1693,8 +1791,6 @@ class EntityManager {
1693
1791
  .join(", ");
1694
1792
  throw new Error(`More than ${this.entityLimit} entities have been instantiated (top entities: ${topTypes})`);
1695
1793
  }
1696
- // If indexing is enabled for this type, add it...
1697
- this.#indexManager.maybeIndexEntity(entity);
1698
1794
  }
1699
1795
  }
1700
1796
  exports.EntityManager = EntityManager;
@@ -1778,14 +1874,14 @@ async function validateReactiveRules(em, logger, todos, joinRowTodos) {
1778
1874
  const p1 = Object.values(todos).flatMap((todo) => {
1779
1875
  const entities = [...todo.inserts, ...todo.updates, ...todo.deletes];
1780
1876
  // Find each statically-declared reactive rule for the given entity type
1781
- const rules = (0, caches_1.getReactiveRules)(todo.metadata);
1877
+ const rules = todo.metadata.reactiveRules;
1782
1878
  return rules.map((rule) => {
1783
1879
  // Of all changed entities of this type, how many specifically trigger this rule?
1784
1880
  const triggered = entities.filter((e) => {
1785
1881
  // If the rule is for a different subtype, skip it
1786
1882
  if (!(e instanceof rule.source))
1787
1883
  return false;
1788
- // Any new-or-deleted entity fires every rule (getReactiveRules has already filtered out read-only)
1884
+ // Any new-or-deleted entity fires every rule (reactiveRules has already filtered out read-only)
1789
1885
  if (e.isNewEntity || e.isDeletedEntity)
1790
1886
  return true;
1791
1887
  // Otherwise see if the changed fields overlaps with the rule's fields
@@ -1803,15 +1899,15 @@ async function validateReactiveRules(em, logger, todos, joinRowTodos) {
1803
1899
  const p2 = Object.values(joinRowTodos).flatMap((todo) => {
1804
1900
  const entities = [...todo.newRows, ...todo.deletedRows].flatMap((jr) => Object.values(jr.columns));
1805
1901
  // Do the first side
1806
- const p1 = (0, caches_1.getReactiveRules)(todo.m2m.meta)
1807
- .filter((rule) => rule.fields.includes(todo.m2m.fieldName))
1902
+ const p1 = todo.m2m.meta
1903
+ .reactiveRules.filter((rule) => rule.fields.includes(todo.m2m.fieldName))
1808
1904
  .map((rule) => {
1809
1905
  const triggered = entities.filter((e) => e instanceof todo.m2m.meta.cstr);
1810
1906
  return followAndQueue(triggered, rule);
1811
1907
  });
1812
1908
  // And the second side
1813
- const p2 = (0, caches_1.getReactiveRules)(todo.m2m.otherMeta)
1814
- .filter((rule) => rule.fields.includes(todo.m2m.otherFieldName))
1909
+ const p2 = todo.m2m.otherMeta
1910
+ .reactiveRules.filter((rule) => rule.fields.includes(todo.m2m.otherFieldName))
1815
1911
  .map((rule) => {
1816
1912
  const triggered = entities.filter((e) => e instanceof todo.m2m.otherMeta.cstr);
1817
1913
  return followAndQueue(triggered, rule);
@@ -2040,7 +2136,7 @@ function maybeBumpUpdatedAt(rm, todos, now) {
2040
2136
  // it has changed. This is technically true, but this will break the oplock SQL generation,
2041
2137
  // so force the field to be dirty.
2042
2138
  const orm = (0, BaseEntity_1.getInstanceData)(e);
2043
- orm.originalData[updatedAt] = (0, fields_1.getField)(e, updatedAt);
2139
+ orm.markFieldDirty(updatedAt, (0, fields_1.getField)(e, updatedAt));
2044
2140
  const serde = todo.metadata.fields[updatedAt].serde;
2045
2141
  orm.data[updatedAt] = serde.mapFromNow(now);
2046
2142
  rm.queueDownstreamReactables(e, updatedAt);
@@ -2077,17 +2173,13 @@ function findConcreteMeta(maybeBaseMeta, row) {
2077
2173
  throw new Error(`${maybeBaseMeta.type} ${(0, index_1.tagId)(maybeBaseMeta, row.id)} must be instantiated via a subtype`);
2078
2174
  }
2079
2175
  // Look for the CTI __class from the driver telling us which subtype to instantiate
2080
- return maybeBaseMeta.subTypes.find((st) => st.type === row.__class) ?? maybeBaseMeta;
2176
+ return maybeBaseMeta.subTypesByType.get(row.__class) ?? maybeBaseMeta;
2081
2177
  }
2082
2178
  else if (maybeBaseMeta.inheritanceType === "sti") {
2083
2179
  // Look for the STI discriminator value
2084
2180
  const baseMeta = (0, index_1.getBaseMeta)(maybeBaseMeta);
2085
- const field = baseMeta.fields[baseMeta.stiDiscriminatorField];
2086
- if (field.kind !== "enum")
2087
- throw new Error("Discriminator field must be an enum");
2088
- const columnName = field.serde.columns[0].columnName;
2089
- const value = row[columnName];
2090
- return baseMeta.subTypes.find((st) => st.stiDiscriminatorValue === value) ?? baseMeta;
2181
+ const value = row[baseMeta.stiDiscriminatorColumnName];
2182
+ return baseMeta.subTypesByStiValue.get(value) ?? baseMeta;
2091
2183
  }
2092
2184
  else {
2093
2185
  throw new Error("Unknown inheritance type");
@@ -2096,7 +2188,7 @@ function findConcreteMeta(maybeBaseMeta, row) {
2096
2188
  /** Sets the `Animal.type` enum to the right subtype value. */
2097
2189
  function setStiDiscriminatorValue(baseMeta, entity) {
2098
2190
  const typeName = entity.constructor.name;
2099
- const st = baseMeta.subTypes.find((st) => st.type === typeName);
2191
+ const st = baseMeta.subTypesByType.get(typeName);
2100
2192
  if (st) {
2101
2193
  const field = baseMeta.fields[baseMeta.stiDiscriminatorField];
2102
2194
  const code = field.enumDetailType.findById(st.stiDiscriminatorValue).code;
@@ -2106,17 +2198,19 @@ function setStiDiscriminatorValue(baseMeta, entity) {
2106
2198
  entity[baseMeta.stiDiscriminatorField] = undefined;
2107
2199
  }
2108
2200
  }
2109
- function findPendingFlushEntities(entities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks) {
2110
- for (const e of entities) {
2111
- if ((0, BaseEntity_1.getInstanceData)(e).pendingOperation !== "none") {
2112
- if (!hooksInvoked.has(e)) {
2113
- pendingHooks.add(e);
2114
- }
2115
- else {
2116
- alreadyRanHooks.add(e);
2117
- }
2118
- pendingFlush.add(e);
2201
+ function findPendingFlushEntities(maybePendingFlushEntities, hooksInvoked, pendingFlush, pendingHooks, alreadyRanHooks) {
2202
+ for (const e of maybePendingFlushEntities) {
2203
+ if ((0, BaseEntity_1.getInstanceData)(e).pendingOperation === "none") {
2204
+ maybePendingFlushEntities.delete(e);
2205
+ continue;
2206
+ }
2207
+ if (!hooksInvoked.has(e)) {
2208
+ pendingHooks.add(e);
2209
+ }
2210
+ else {
2211
+ alreadyRanHooks.add(e);
2119
2212
  }
2213
+ pendingFlush.add(e);
2120
2214
  }
2121
2215
  }
2122
2216
  /** Returns true if the caller explicitly asked `find` to use SQL pagination. */