joist-core 2.0.3-next.2 → 2.0.3-next.21

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 (105) hide show
  1. package/build/AliasAssigner.d.ts +3 -0
  2. package/build/AliasAssigner.d.ts.map +1 -1
  3. package/build/AliasAssigner.js +12 -3
  4. package/build/AliasAssigner.js.map +1 -1
  5. package/build/EntityManager.d.ts +20 -9
  6. package/build/EntityManager.d.ts.map +1 -1
  7. package/build/EntityManager.js +94 -58
  8. package/build/EntityManager.js.map +1 -1
  9. package/build/ReactionsManager.d.ts +1 -1
  10. package/build/ReactionsManager.d.ts.map +1 -1
  11. package/build/ReactionsManager.js +7 -5
  12. package/build/ReactionsManager.js.map +1 -1
  13. package/build/batchloaders/BatchLoader.d.ts +14 -0
  14. package/build/batchloaders/BatchLoader.d.ts.map +1 -0
  15. package/build/batchloaders/BatchLoader.js +49 -0
  16. package/build/batchloaders/BatchLoader.js.map +1 -0
  17. package/build/batchloaders/loadBatchLoader.d.ts +17 -0
  18. package/build/batchloaders/loadBatchLoader.d.ts.map +1 -0
  19. package/build/batchloaders/loadBatchLoader.js +55 -0
  20. package/build/batchloaders/loadBatchLoader.js.map +1 -0
  21. package/build/batchloaders/manyToManyBatchLoader.d.ts +7 -0
  22. package/build/batchloaders/manyToManyBatchLoader.d.ts.map +1 -0
  23. package/build/batchloaders/manyToManyBatchLoader.js +57 -0
  24. package/build/batchloaders/manyToManyBatchLoader.js.map +1 -0
  25. package/build/batchloaders/oneToManyBatchLoader.d.ts +7 -0
  26. package/build/batchloaders/oneToManyBatchLoader.d.ts.map +1 -0
  27. package/build/{dataloaders/oneToManyDataLoader.js → batchloaders/oneToManyBatchLoader.js} +9 -18
  28. package/build/batchloaders/oneToManyBatchLoader.js.map +1 -0
  29. package/build/batchloaders/oneToOneBatchLoader.d.ts +7 -0
  30. package/build/batchloaders/oneToOneBatchLoader.d.ts.map +1 -0
  31. package/build/{dataloaders/oneToOneDataLoader.js → batchloaders/oneToOneBatchLoader.js} +10 -16
  32. package/build/batchloaders/oneToOneBatchLoader.js.map +1 -0
  33. package/build/{dataloaders/populateDataLoader.d.ts → batchloaders/populateBatchLoader.d.ts} +5 -5
  34. package/build/batchloaders/populateBatchLoader.d.ts.map +1 -0
  35. package/build/{dataloaders/populateDataLoader.js → batchloaders/populateBatchLoader.js} +97 -54
  36. package/build/batchloaders/populateBatchLoader.js.map +1 -0
  37. package/build/batchloaders/recursiveChildrenBatchLoader.d.ts +7 -0
  38. package/build/batchloaders/recursiveChildrenBatchLoader.d.ts.map +1 -0
  39. package/build/{dataloaders/recursiveChildrenDataLoader.js → batchloaders/recursiveChildrenBatchLoader.js} +4 -6
  40. package/build/batchloaders/recursiveChildrenBatchLoader.js.map +1 -0
  41. package/build/batchloaders/recursiveParentsBatchLoader.d.ts +7 -0
  42. package/build/batchloaders/recursiveParentsBatchLoader.d.ts.map +1 -0
  43. package/build/{dataloaders/recursiveParentsDataLoader.js → batchloaders/recursiveParentsBatchLoader.js} +4 -5
  44. package/build/batchloaders/recursiveParentsBatchLoader.js.map +1 -0
  45. package/build/changes.js +2 -2
  46. package/build/changes.js.map +1 -1
  47. package/build/config.js +1 -1
  48. package/build/config.js.map +1 -1
  49. package/build/dataloaders/findIdsDataLoader.js +2 -2
  50. package/build/dataloaders/findIdsDataLoader.js.map +1 -1
  51. package/build/dataloaders/findOrCreateDataLoader.js +1 -1
  52. package/build/logging/ReactionLogger.d.ts +8 -4
  53. package/build/logging/ReactionLogger.d.ts.map +1 -1
  54. package/build/logging/ReactionLogger.js +11 -4
  55. package/build/logging/ReactionLogger.js.map +1 -1
  56. package/build/preloading/JsonAggregatePreloader.js +29 -24
  57. package/build/preloading/JsonAggregatePreloader.js.map +1 -1
  58. package/build/relations/Collection.d.ts +6 -1
  59. package/build/relations/Collection.d.ts.map +1 -1
  60. package/build/relations/Collection.js.map +1 -1
  61. package/build/relations/ManyToManyCollection.d.ts +8 -1
  62. package/build/relations/ManyToManyCollection.d.ts.map +1 -1
  63. package/build/relations/ManyToManyCollection.js +338 -130
  64. package/build/relations/ManyToManyCollection.js.map +1 -1
  65. package/build/relations/ManyToOneReference.d.ts +2 -6
  66. package/build/relations/ManyToOneReference.d.ts.map +1 -1
  67. package/build/relations/ManyToOneReference.js +115 -78
  68. package/build/relations/ManyToOneReference.js.map +1 -1
  69. package/build/relations/OneToManyCollection.d.ts +8 -4
  70. package/build/relations/OneToManyCollection.d.ts.map +1 -1
  71. package/build/relations/OneToManyCollection.js +415 -173
  72. package/build/relations/OneToManyCollection.js.map +1 -1
  73. package/build/relations/OneToOneReference.d.ts.map +1 -1
  74. package/build/relations/OneToOneReference.js +12 -7
  75. package/build/relations/OneToOneReference.js.map +1 -1
  76. package/build/relations/ReactiveManyToMany.js +4 -4
  77. package/build/relations/ReactiveManyToMany.js.map +1 -1
  78. package/build/relations/ReactiveManyToManyOtherSide.js +3 -3
  79. package/build/relations/ReactiveManyToManyOtherSide.js.map +1 -1
  80. package/build/relations/RecursiveCollection.d.ts.map +1 -1
  81. package/build/relations/RecursiveCollection.js +22 -8
  82. package/build/relations/RecursiveCollection.js.map +1 -1
  83. package/package.json +3 -4
  84. package/build/dataloaders/loadDataLoader.d.ts +0 -11
  85. package/build/dataloaders/loadDataLoader.d.ts.map +0 -1
  86. package/build/dataloaders/loadDataLoader.js +0 -54
  87. package/build/dataloaders/loadDataLoader.js.map +0 -1
  88. package/build/dataloaders/manyToManyDataLoader.d.ts +0 -8
  89. package/build/dataloaders/manyToManyDataLoader.d.ts.map +0 -1
  90. package/build/dataloaders/manyToManyDataLoader.js +0 -74
  91. package/build/dataloaders/manyToManyDataLoader.js.map +0 -1
  92. package/build/dataloaders/oneToManyDataLoader.d.ts +0 -7
  93. package/build/dataloaders/oneToManyDataLoader.d.ts.map +0 -1
  94. package/build/dataloaders/oneToManyDataLoader.js.map +0 -1
  95. package/build/dataloaders/oneToOneDataLoader.d.ts +0 -7
  96. package/build/dataloaders/oneToOneDataLoader.d.ts.map +0 -1
  97. package/build/dataloaders/oneToOneDataLoader.js.map +0 -1
  98. package/build/dataloaders/populateDataLoader.d.ts.map +0 -1
  99. package/build/dataloaders/populateDataLoader.js.map +0 -1
  100. package/build/dataloaders/recursiveChildrenDataLoader.d.ts +0 -7
  101. package/build/dataloaders/recursiveChildrenDataLoader.d.ts.map +0 -1
  102. package/build/dataloaders/recursiveChildrenDataLoader.js.map +0 -1
  103. package/build/dataloaders/recursiveParentsDataLoader.d.ts +0 -7
  104. package/build/dataloaders/recursiveParentsDataLoader.d.ts.map +0 -1
  105. package/build/dataloaders/recursiveParentsDataLoader.js.map +0 -1
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OneToManyCollection = void 0;
4
4
  exports.hasMany = hasMany;
5
- const oneToManyDataLoader_1 = require("../dataloaders/oneToManyDataLoader");
5
+ const oneToManyBatchLoader_1 = require("../batchloaders/oneToManyBatchLoader");
6
6
  const oneToManyFindDataLoader_1 = require("../dataloaders/oneToManyFindDataLoader");
7
7
  const index_1 = require("../index");
8
8
  const newEntity_1 = require("../newEntity");
@@ -18,83 +18,76 @@ function hasMany() {
18
18
  }
19
19
  class OneToManyCollection extends AbstractRelationImpl_1.AbstractRelationImpl {
20
20
  #field;
21
- // We can track both value-and-isLoaded with a single `#loaded` b/c `[]` is always our empty value
22
- #loaded;
21
+ #state;
22
+ #loadPromise;
23
23
  // Constantly filtering+sorting our `.get` values can be surprisingly expensive if called
24
24
  // when processing many entities/writing code that calls it repeatedly, so we cache it
25
25
  // both the "without deleted" default (`getSorted`) and "all deleted" (`allSorted`).
26
26
  #getSorted;
27
27
  #allSorted;
28
- // We _used_ to not track `#removed`, because if a child is removed in our unloaded state,
29
- // when we load and get back the `child X has parent_id = our id` rows from the db, `loaderForCollection`
30
- // groups the hydrated rows by their _current parent m2o field value_, which for a removed child will no
31
- // longer be us, so it will effectively not show up in our post-load `loaded` array.
32
- // However, now with join preloading, the getPreloadedRelation might still have pre-load removed children.
33
- #added;
34
- #removed;
35
- #hasBeenSet = false;
36
28
  constructor(entity, field) {
37
29
  super(entity);
38
30
  this.#field = field;
39
31
  if ((0, index_1.getInstanceData)(entity).isOrWasNew) {
40
- this.#loaded = [];
32
+ this.#state = new O2MLoadedState(this, [], false);
33
+ }
34
+ else {
35
+ const { em } = entity;
36
+ // If any m2o.set(ourId) were made before our entity was loaded, pull in those changes
37
+ const pending = (0, index_1.getEmInternalApi)(em).pendingPercolate.get(entity.idTagged)?.get(this.fieldName);
38
+ if (pending) {
39
+ this.#state = new O2MUnloadedAddedRemovedState(this, pending.adds, pending.removes);
40
+ (0, index_1.getEmInternalApi)(em).pendingPercolate.get(entity.idTagged)?.delete(this.fieldName);
41
+ }
42
+ else {
43
+ this.#state = new O2MUnloadedPristineState(this);
44
+ }
41
45
  }
42
46
  }
43
47
  // opts is an internal parameter
44
48
  async load(opts = {}) {
45
49
  (0, index_1.ensureNotDeleted)(this.entity, "pending");
46
- if (this.#loaded === undefined || (opts.forceReload && !this.entity.isNewEntity)) {
47
- // If forceReload=true, the `.load` might return a cached array, which one would think is stale
48
- // (i.e. it doesn't have our WIP adds & removes applied to it), _but_ because we've been mutating
49
- // our `this.loaded`, really `.load` is a noop, and just gives us back the same list we had before.
50
- //
51
- // ...although if we'd:
52
- // a) created an array in the DL cache
53
- // b) em.flushed & reset the dataloaders
54
- // c) make WIP changes to our existing array
55
- // d) called `forceReload: true`
56
- // e) we'll ask the dataloader for a new array, and will be missing our WIP changes
57
- const dl = (0, oneToManyDataLoader_1.oneToManyDataLoader)(this.entity.em, this);
58
- this.#loaded =
59
- this.getPreloaded() ??
60
- (await dl.load(this.entity.idTagged).catch(function load(err) {
61
- throw (0, index_1.appendStack)(err, new Error());
62
- }));
63
- // If we're reloading (i.e. `forceReload: true`), then we need to clear our caches
50
+ if (!this.#state.isLoaded || (opts.forceReload && !this.entity.isNewEntity)) {
51
+ const maybePreloaded = this.getPreloaded();
52
+ if (maybePreloaded) {
53
+ this.#state = this.#state.applyLoad(maybePreloaded);
54
+ }
55
+ else {
56
+ await (this.#loadPromise ??= (0, oneToManyBatchLoader_1.oneToManyBatchLoader)(this.entity.em, this).load(this.entity.idTagged))
57
+ .then(() => this.preload())
58
+ .catch(function load(err) {
59
+ throw (0, index_1.appendStack)(err, new Error());
60
+ })
61
+ .finally(() => {
62
+ this.#loadPromise = undefined;
63
+ });
64
+ }
64
65
  this.#getSorted = undefined;
65
66
  this.#allSorted = undefined;
66
- this.maybeAppendAddedBeforeLoaded();
67
67
  }
68
68
  return opts?.withDeleted ? this.getWithDeleted : this.get;
69
69
  }
70
70
  async find(id) {
71
71
  (0, index_1.ensureNotDeleted)(this.entity, "pending");
72
- if (this.#loaded !== undefined) {
73
- return this.#loaded.find((other) => !other.isNewEntity && other.id === id);
74
- }
75
- else {
76
- const added = this.#added?.find((u) => !u.isNewEntity && u.id === id);
77
- if (added)
78
- return added;
79
- // Make a cacheable tuple to look up this specific o2m row
80
- const key = `id=${id},${this.#field.otherColumnName}=${this.entity.id}`;
81
- return (0, oneToManyFindDataLoader_1.oneToManyFindDataLoader)(this.entity.em, this)
82
- .load(key)
83
- .catch(function find(err) {
84
- throw (0, index_1.appendStack)(err, new Error());
85
- });
86
- }
72
+ const inMemory = this.#state.find(id);
73
+ if (inMemory)
74
+ return inMemory;
75
+ if (this.#state.isLoaded)
76
+ return undefined;
77
+ const key = `id=${id},${this.#field.otherColumnName}=${this.entity.id}`;
78
+ return (0, oneToManyFindDataLoader_1.oneToManyFindDataLoader)(this.entity.em, this)
79
+ .load(key)
80
+ .catch(function find(err) {
81
+ throw (0, index_1.appendStack)(err, new Error());
82
+ });
87
83
  }
88
84
  async includes(other) {
89
85
  return (0, index_1.sameEntity)(this.entity, this.getOtherRelation(other).current());
90
86
  }
91
87
  get isLoaded() {
92
- return this.#loaded !== undefined;
88
+ return this.#state.isLoaded;
93
89
  }
94
90
  resetIsLoaded() {
95
- // Invalidate our .get cache on any mutation; in theory we could do this only if this
96
- // mutation was from an `other`, i.e. when entities are deleted or something in our
97
- // `orderBy` changes (although we some RF `orderBy`s, which might be a pain to track).
98
91
  this.#getSorted = undefined;
99
92
  this.#allSorted = undefined;
100
93
  }
@@ -102,21 +95,16 @@ class OneToManyCollection extends AbstractRelationImpl_1.AbstractRelationImpl {
102
95
  return !!this.getPreloaded();
103
96
  }
104
97
  preload() {
105
- this.#loaded = this.getPreloaded();
106
- this.maybeAppendAddedBeforeLoaded();
107
- }
108
- import(other, findEntity) {
109
- function map(v) {
110
- if (v === undefined)
111
- return undefined;
112
- const result = new Array(v.length);
113
- for (let i = 0; i < v.length; i++)
114
- result[i] = findEntity(v[i]);
115
- return result;
98
+ const preloaded = this.getPreloaded();
99
+ if (preloaded) {
100
+ this.#state = this.#state.applyLoad(preloaded);
101
+ this.#getSorted = undefined;
102
+ this.#allSorted = undefined;
116
103
  }
117
- this.#loaded = map(other.#loaded);
118
- this.#added = map(other.#added);
119
- this.#removed = map(other.#removed);
104
+ }
105
+ /** Copy another em's `source` collection into our state. */
106
+ import(source, findEntity) {
107
+ this.#state = source.#state.import(this, findEntity);
120
108
  this.#getSorted = undefined;
121
109
  this.#allSorted = undefined;
122
110
  }
@@ -125,7 +113,7 @@ class OneToManyCollection extends AbstractRelationImpl_1.AbstractRelationImpl {
125
113
  (0, index_1.ensureNotDeleted)(this.entity, "pending");
126
114
  if (this.#getSorted !== undefined)
127
115
  return this.#getSorted;
128
- this.#getSorted = Object.freeze(this.filterDeleted(this.doGet(), { withDeleted: false }));
116
+ this.#getSorted = Object.freeze(this.filterDeleted(this.#state.doGet(), { withDeleted: false }));
129
117
  (0, index_1.getEmInternalApi)(this.entity.em).isLoadedCache.addNaive(this);
130
118
  return this.#getSorted;
131
119
  }
@@ -133,101 +121,64 @@ class OneToManyCollection extends AbstractRelationImpl_1.AbstractRelationImpl {
133
121
  (0, index_1.ensureNotDeleted)(this.entity, "pending");
134
122
  if (this.#allSorted !== undefined)
135
123
  return this.#allSorted;
136
- this.#allSorted = Object.freeze(this.filterDeleted(this.doGet(), { withDeleted: true }));
124
+ this.#allSorted = Object.freeze(this.filterDeleted(this.#state.doGet(), { withDeleted: true }));
137
125
  (0, index_1.getEmInternalApi)(this.entity.em).isLoadedCache.addNaive(this);
138
126
  return this.#allSorted;
139
127
  }
140
- doGet() {
141
- // This should only be callable in the type system if we've already resolved this to an instance
142
- if (this.#loaded === undefined) {
143
- throw new Error("get was called when not loaded");
144
- }
145
- return this.#loaded;
146
- }
147
128
  set(values) {
148
129
  (0, index_1.ensureNotDeleted)(this.entity);
149
- if (this.#loaded === undefined) {
150
- throw new Error("set was called when not loaded");
151
- }
152
- this.#hasBeenSet = true;
153
- // If we're changing `a1.books = [b1, b2]` to `a1.books = [b2]`, then implicitly delete the old book
154
- const otherCannotChange = this.otherMeta.allFields[this.otherFieldName].immutable;
155
- if (this.isCascadeDelete && otherCannotChange) {
156
- const implicitlyDeleted = this.#loaded.filter((e) => !values.includes(e));
157
- // The `em.delete` will internally invalidate our `#getSorted` / `#allSorted` caches, which will be dirty now
158
- implicitlyDeleted.forEach((e) => this.entity.em.delete(e));
159
- // Keep the implicitlyDeleted values for `getWithDeleted` to return
160
- values = [...values, ...implicitlyDeleted];
161
- }
162
- // Make a copy for safe iteration
163
- const loaded = [...this.#loaded];
164
- // Remove old values
165
- for (const other of loaded) {
166
- if (!values.includes(other)) {
167
- this.remove(other);
168
- }
169
- }
170
- for (const other of values) {
171
- if (!loaded.includes(other)) {
172
- this.add(other);
173
- }
174
- }
130
+ this.#state = this.#state.set(values);
131
+ this.registerAsMutated();
132
+ this.#getSorted = undefined;
133
+ this.#allSorted = undefined;
175
134
  }
176
135
  add(other) {
177
136
  (0, index_1.ensureNotDeleted)(this.entity);
178
- this.#added ??= [];
179
- (0, utils_1.maybeAdd)(this.#added, other);
180
- (0, utils_1.maybeRemove)(this.#removed, other);
181
- if (this.#loaded !== undefined) {
182
- (0, utils_1.maybeAdd)(this.#loaded, other);
183
- this.#getSorted = undefined;
184
- this.#allSorted = undefined;
185
- }
137
+ this.#state = this.#state.add(other);
138
+ this.percolateAdd(other);
186
139
  this.registerAsMutated();
187
- // This will no-op and mark other dirty if necessary
188
- this.getOtherRelation(other).set(this.entity);
140
+ this.#getSorted = undefined;
141
+ this.#allSorted = undefined;
189
142
  }
190
- // We're not supported remove(other) because that might leave other.otherFieldName as undefined,
191
- // which we don't know if that's valid or not, i.e. depending on whether the field is nullable.
192
143
  remove(other, opts = { requireLoaded: true }) {
193
144
  (0, index_1.ensureNotDeleted)(this.entity, "pending");
194
- if (this.#loaded === undefined && opts.requireLoaded) {
195
- throw new Error("remove was called when not loaded");
196
- }
197
- this.#removed ??= [];
198
- (0, utils_1.maybeAdd)(this.#removed, other);
199
- (0, utils_1.maybeRemove)(this.#added, other);
200
- if (this.#loaded !== undefined) {
201
- (0, utils_1.remove)(this.#loaded, other);
202
- this.#getSorted = undefined;
203
- this.#allSorted = undefined;
204
- }
145
+ this.#state = this.#state.remove(other, opts);
146
+ this.percolateRemove(other);
205
147
  this.registerAsMutated();
206
- // This will no-op and mark other dirty if necessary
207
- this.getOtherRelation(other).set(undefined);
148
+ this.#getSorted = undefined;
149
+ this.#allSorted = undefined;
208
150
  }
209
151
  removeAll() {
210
152
  (0, index_1.ensureNotDeleted)(this.entity);
211
- if (this.#loaded === undefined) {
153
+ if (!this.#state.isLoaded) {
212
154
  throw new Error("removeAll was called when not loaded");
213
155
  }
214
- for (const other of [...this.#loaded]) {
156
+ for (const other of [...this.#state.doGet()]) {
215
157
  this.remove(other);
216
158
  }
217
159
  }
218
160
  // internal impl
161
+ /** @internal */
162
+ percolateAdd(other) {
163
+ this.getOtherRelation(other).set(this.entity);
164
+ }
165
+ /** @internal */
166
+ percolateRemove(other) {
167
+ this.getOtherRelation(other).set(undefined);
168
+ }
169
+ /** @internal */
170
+ registerAsMutated() {
171
+ (0, index_1.getEmInternalApi)(this.entity.em).mutatedCollections.add(this);
172
+ }
219
173
  setFromOpts(others) {
220
- this.#loaded = [];
174
+ this.#state = new O2MLoadedState(this, [], false);
221
175
  others.forEach((o) => this.add(o));
222
176
  }
223
177
  removeIfLoaded(other) {
224
- this.#removed ??= [];
225
- (0, utils_1.maybeRemove)(this.#added, other);
226
- (0, utils_1.maybeAdd)(this.#removed, other);
227
- if (this.#loaded !== undefined) {
228
- (0, utils_1.remove)(this.#loaded, other);
229
- }
178
+ this.#state = this.#state.removeIfLoaded(other);
230
179
  this.registerAsMutated();
180
+ this.#getSorted = undefined;
181
+ this.#allSorted = undefined;
231
182
  }
232
183
  maybeCascadeDelete() {
233
184
  if (this.isCascadeDelete) {
@@ -243,43 +194,13 @@ class OneToManyCollection extends AbstractRelationImpl_1.AbstractRelationImpl {
243
194
  current.forEach((other) => {
244
195
  const m2o = this.getOtherRelation(other);
245
196
  if ((0, index_1.maybeResolveReferenceToId)(m2o.current({ withDeleted: true })) === this.entity.idMaybe) {
246
- // TODO What if other.otherFieldName is required/not-null?
247
197
  m2o.set(undefined);
248
198
  }
249
199
  });
250
- this.#loaded = [];
251
- this.#added = [];
252
- this.#removed = [];
200
+ this.#state = new O2MLoadedState(this, [], false);
253
201
  this.#getSorted = undefined;
254
202
  this.#allSorted = undefined;
255
203
  }
256
- maybeAppendAddedBeforeLoaded() {
257
- // If our entity is not new, then entities in the EM might have been mutated to point
258
- // to our foreign key (instead of our loaded instance), which means they should be in
259
- // `addedBeforeLoaded` but are not.
260
- //
261
- // (Note that we don't have to handle the case for "removed before loaded" here because
262
- // the oneToManyDataLoader already handles that; although maybe arguably that logic should
263
- // be handled here?)
264
- if (!this.entity.isNewEntity) {
265
- const { em } = this.entity;
266
- const pending = (0, index_1.getEmInternalApi)(em).pendingChildren.get(this.entity.idTagged)?.get(this.fieldName);
267
- if (pending) {
268
- (this.#added ??= []).push(...pending.adds);
269
- (this.#removed ??= []).push(...pending.removes);
270
- (0, utils_1.clear)(pending.adds);
271
- (0, utils_1.clear)(pending.removes);
272
- }
273
- }
274
- if (this.#added) {
275
- const newEntities = this.#added.filter((e) => !this.#loaded?.includes(e));
276
- // Push on the end to better match the db order of "newer things come last"
277
- this.#loaded.push(...newEntities);
278
- }
279
- if (this.#removed) {
280
- this.#removed.forEach((e) => (0, utils_1.remove)(this.#loaded, e));
281
- }
282
- }
283
204
  // These are public to our internal implementation but not exposed in the Collection API
284
205
  get fieldName() {
285
206
  return this.#field.fieldName;
@@ -288,7 +209,7 @@ class OneToManyCollection extends AbstractRelationImpl_1.AbstractRelationImpl {
288
209
  return this.#field.otherFieldName;
289
210
  }
290
211
  current(opts) {
291
- return this.filterDeleted(this.#loaded ?? this.#added ?? [], opts);
212
+ return this.filterDeleted(this.#state.current(), opts);
292
213
  }
293
214
  get meta() {
294
215
  return (0, index_1.getMetadata)(this.entity);
@@ -297,15 +218,14 @@ class OneToManyCollection extends AbstractRelationImpl_1.AbstractRelationImpl {
297
218
  return (0, index_1.getMetadata)(this.entity).allFields[this.fieldName].otherMetadata();
298
219
  }
299
220
  get hasBeenSet() {
300
- return this.#hasBeenSet;
221
+ return this.#state.hasBeenSet;
301
222
  }
302
223
  toString() {
303
224
  return `OneToManyCollection(entity: ${this.entity}, fieldName: ${this.fieldName}, otherType: ${this.otherMeta.type}, otherFieldName: ${this.otherFieldName})`;
304
225
  }
305
226
  /** Called after `em.flush` to reset our dirty tracking. */
306
227
  resetAddedRemoved() {
307
- this.#added = undefined;
308
- this.#removed = undefined;
228
+ this.#state.resetAddedRemoved();
309
229
  }
310
230
  /** Removes pending-hard-delete or soft-deleted entities, unless explicitly asked for. */
311
231
  filterDeleted(entities, opts) {
@@ -330,18 +250,340 @@ class OneToManyCollection extends AbstractRelationImpl_1.AbstractRelationImpl {
330
250
  return undefined;
331
251
  return (0, index_1.getEmInternalApi)(this.entity.em).getPreloadedRelation(this.entity.idTagged, this.fieldName);
332
252
  }
333
- registerAsMutated() {
334
- (0, index_1.getEmInternalApi)(this.entity.em).mutatedCollections.add(this);
335
- }
336
253
  // Exposed for changes
337
254
  added() {
338
- return this.#added ?? [];
255
+ return this.#state.added();
339
256
  }
340
257
  removed() {
341
- return this.#removed ?? [];
258
+ return this.#state.removed();
342
259
  }
343
260
  [Relation_1.RelationT] = null;
344
261
  [Relation_1.RelationU] = null;
345
262
  }
346
263
  exports.OneToManyCollection = OneToManyCollection;
264
+ /** Initial state for existing entities - no data loaded yet. */
265
+ class O2MUnloadedPristineState {
266
+ isLoaded = false;
267
+ hasBeenSet = false;
268
+ #o2m;
269
+ constructor(o2m) {
270
+ this.#o2m = o2m;
271
+ }
272
+ add(other) {
273
+ return new O2MUnloadedAddedRemovedState(this.#o2m, [other], []);
274
+ }
275
+ remove(_other, opts) {
276
+ if (opts.requireLoaded) {
277
+ throw new Error("remove was called when not loaded");
278
+ }
279
+ return new O2MUnloadedAddedRemovedState(this.#o2m, [], [_other]);
280
+ }
281
+ removeIfLoaded(other) {
282
+ return new O2MUnloadedAddedRemovedState(this.#o2m, [], [other]);
283
+ }
284
+ set(values) {
285
+ return new O2MPendingSetState(this.#o2m, values);
286
+ }
287
+ doGet() {
288
+ throw new Error("get was called when not loaded");
289
+ }
290
+ find(_id) {
291
+ return undefined;
292
+ }
293
+ applyLoad(dbEntities) {
294
+ return new O2MLoadedState(this.#o2m, dbEntities, false);
295
+ }
296
+ current() {
297
+ return [];
298
+ }
299
+ import(target, _findEntity) {
300
+ return new O2MUnloadedPristineState(target);
301
+ }
302
+ added() {
303
+ return [];
304
+ }
305
+ removed() {
306
+ return [];
307
+ }
308
+ resetAddedRemoved() { }
309
+ }
310
+ /** State when add/remove called before load - tracks changes to merge later. */
311
+ class O2MUnloadedAddedRemovedState {
312
+ isLoaded = false;
313
+ hasBeenSet = false;
314
+ #o2m;
315
+ #added;
316
+ #removed;
317
+ constructor(o2m, added, removed) {
318
+ this.#o2m = o2m;
319
+ this.#added = new Set(added);
320
+ this.#removed = new Set(removed);
321
+ }
322
+ add(other) {
323
+ this.#added.add(other);
324
+ this.#removed.delete(other);
325
+ return this;
326
+ }
327
+ remove(other, opts) {
328
+ if (opts.requireLoaded) {
329
+ throw new Error("remove was called when not loaded");
330
+ }
331
+ this.#removed.add(other);
332
+ this.#added.delete(other);
333
+ return this;
334
+ }
335
+ removeIfLoaded(other) {
336
+ this.#added.delete(other);
337
+ this.#removed.add(other);
338
+ return this;
339
+ }
340
+ set(values) {
341
+ // We should do the implicit deletion check?
342
+ // Should we keep #added/#removed?
343
+ return new O2MPendingSetState(this.#o2m, values);
344
+ }
345
+ doGet() {
346
+ throw new Error("get was called when not loaded");
347
+ }
348
+ find(id) {
349
+ for (const u of this.#added) {
350
+ if (!u.isNewEntity && u.id === id)
351
+ return u;
352
+ }
353
+ return undefined;
354
+ }
355
+ applyLoad(dbEntities) {
356
+ // Push added entities on the end to better match the db order of "newer things come last"
357
+ const loaded = new Set([...dbEntities]);
358
+ for (const e of this.#added)
359
+ loaded.add(e);
360
+ for (const e of this.#removed)
361
+ loaded.delete(e);
362
+ return new O2MLoadedState(this.#o2m, [...loaded], false, [...this.#added], [...this.#removed]);
363
+ }
364
+ current() {
365
+ return [...this.#added];
366
+ }
367
+ import(target, findEntity) {
368
+ return new O2MUnloadedAddedRemovedState(target, mapEntities([...this.#added], findEntity), mapEntities([...this.#removed], findEntity));
369
+ }
370
+ added() {
371
+ return [...this.#added];
372
+ }
373
+ removed() {
374
+ return [...this.#removed];
375
+ }
376
+ resetAddedRemoved() {
377
+ // This is called after em.flush, so these changes have been pushed into the db; we don't
378
+ // need to track them anymore b/c any future `o2m.load` will see up.
379
+ this.#added = new Set();
380
+ this.#removed = new Set();
381
+ }
382
+ }
383
+ /** State when set() called before load - holds pending values to diff on load. */
384
+ class O2MPendingSetState {
385
+ isLoaded = false;
386
+ hasBeenSet = true;
387
+ #o2m;
388
+ #pendingValues;
389
+ constructor(o2m, pendingValues, skipPercolate = false) {
390
+ this.#o2m = o2m;
391
+ this.#pendingValues = new Set(pendingValues);
392
+ // We percolate on normal set() calls to immediately update the other side's m2o references.
393
+ // We skip percolation on import() because those entities already have correct references
394
+ // in the cloned EntityManager.
395
+ if (!skipPercolate)
396
+ this.#percolate();
397
+ (0, index_1.getEmInternalApi)(o2m.entity.em).pendingLoads.add(o2m);
398
+ }
399
+ add(other) {
400
+ this.#pendingValues.add(other);
401
+ return this;
402
+ }
403
+ remove(other, _opts) {
404
+ this.#pendingValues.delete(other);
405
+ return this;
406
+ }
407
+ removeIfLoaded(other) {
408
+ this.#pendingValues.delete(other);
409
+ return this;
410
+ }
411
+ set(values) {
412
+ this.#pendingValues = new Set(values);
413
+ this.#percolate();
414
+ return this;
415
+ }
416
+ doGet() {
417
+ return [...this.#pendingValues];
418
+ }
419
+ find(id) {
420
+ for (const u of this.#pendingValues) {
421
+ if (!u.isNewEntity && u.id === id)
422
+ return u;
423
+ }
424
+ return undefined;
425
+ }
426
+ applyLoad(dbEntities) {
427
+ const o2m = this.#o2m;
428
+ const entity = o2m.entity;
429
+ // Handle cascade delete + immutable: entities removed from the collection that
430
+ // cannot have their FK changed should be deleted instead
431
+ const otherCannotChange = o2m.otherMeta.allFields[o2m.otherFieldName].immutable;
432
+ const isCascade = (0, AbstractRelationImpl_1.isCascadeDelete)(o2m, o2m.fieldName);
433
+ // Calc our added/removed so that `.changes` in the new loaded state are correct
434
+ const dbSet = new Set(dbEntities);
435
+ const added = [];
436
+ for (const other of this.#pendingValues) {
437
+ if (!dbSet.has(other))
438
+ added.push(other);
439
+ }
440
+ const removed = [];
441
+ for (const other of dbEntities) {
442
+ if (!this.#pendingValues.has(other)) {
443
+ removed.push(other);
444
+ if (isCascade && otherCannotChange) {
445
+ entity.em.delete(other);
446
+ }
447
+ else {
448
+ o2m.percolateRemove(other);
449
+ }
450
+ }
451
+ }
452
+ return new O2MLoadedState(o2m, [...this.#pendingValues], true, added, removed);
453
+ }
454
+ current() {
455
+ return [...this.#pendingValues];
456
+ }
457
+ import(target, findEntity) {
458
+ return new O2MPendingSetState(target, mapEntities([...this.#pendingValues], findEntity), true);
459
+ }
460
+ added() {
461
+ return [...this.#pendingValues];
462
+ }
463
+ removed() {
464
+ return [];
465
+ }
466
+ resetAddedRemoved() { }
467
+ #percolate() {
468
+ const o2m = this.#o2m;
469
+ // All new values, we immediately tell them we're their new parent
470
+ for (const other of this.#pendingValues) {
471
+ o2m.percolateAdd(other);
472
+ }
473
+ // If we're an existing entity, we can scan for any in-memory entities that were previously
474
+ // pointing to us, but now should not
475
+ if (!o2m.entity.isNewEntity) {
476
+ for (const other of o2m.entity.em.getEntities(o2m.otherMeta.cstr)) {
477
+ if (this.#pendingValues.has(other))
478
+ continue;
479
+ if ((0, index_1.sameEntity)(other[o2m.otherFieldName].current(), o2m.entity)) {
480
+ o2m.percolateRemove(other);
481
+ }
482
+ }
483
+ }
484
+ }
485
+ }
486
+ /** State when collection is loaded - all operations work directly on loaded array. */
487
+ class O2MLoadedState {
488
+ isLoaded = true;
489
+ #o2m;
490
+ #loaded;
491
+ #added;
492
+ #removed;
493
+ #hasBeenSet;
494
+ constructor(o2m, loaded, hasBeenSet, added = [], removed = []) {
495
+ this.#o2m = o2m;
496
+ this.#loaded = new Set(loaded);
497
+ this.#added = new Set(added);
498
+ this.#removed = new Set(removed);
499
+ this.#hasBeenSet = hasBeenSet;
500
+ }
501
+ get hasBeenSet() {
502
+ return this.#hasBeenSet;
503
+ }
504
+ add(other) {
505
+ this.#added.add(other);
506
+ this.#removed.delete(other);
507
+ this.#loaded.add(other);
508
+ return this;
509
+ }
510
+ remove(other, _opts) {
511
+ this.#removed.add(other);
512
+ this.#added.delete(other);
513
+ this.#loaded.delete(other);
514
+ return this;
515
+ }
516
+ removeIfLoaded(other) {
517
+ this.#added.delete(other);
518
+ this.#removed.add(other);
519
+ this.#loaded.delete(other);
520
+ return this;
521
+ }
522
+ set(values) {
523
+ this.#hasBeenSet = true;
524
+ let valuesSet = new Set(values);
525
+ const o2m = this.#o2m;
526
+ const otherCannotChange = o2m.otherMeta.allFields[o2m.otherFieldName].immutable;
527
+ const isCascade = (0, AbstractRelationImpl_1.isCascadeDelete)(o2m, o2m.fieldName);
528
+ if (isCascade && otherCannotChange) {
529
+ const implicitlyDeleted = [...this.#loaded].filter((e) => !valuesSet.has(e));
530
+ implicitlyDeleted.forEach((e) => o2m.entity.em.delete(e));
531
+ // ...we restore the implicitlyDeleted for getWithDeleted
532
+ values = [...values, ...implicitlyDeleted];
533
+ valuesSet = new Set(values);
534
+ }
535
+ const loaded = new Set(this.#loaded);
536
+ for (const other of loaded) {
537
+ if (!valuesSet.has(other))
538
+ o2m.remove(other);
539
+ }
540
+ for (const other of values) {
541
+ if (!loaded.has(other))
542
+ o2m.add(other);
543
+ }
544
+ return this;
545
+ }
546
+ doGet() {
547
+ return [...this.#loaded];
548
+ }
549
+ find(id) {
550
+ for (const other of this.#loaded) {
551
+ if (!other.isNewEntity && other.id === id)
552
+ return other;
553
+ }
554
+ return undefined;
555
+ }
556
+ applyLoad(dbEntities) {
557
+ // On forceReload, replace loaded but preserve pending adds/removes
558
+ const loaded = new Set(dbEntities);
559
+ for (const e of this.#added)
560
+ loaded.add(e);
561
+ for (const e of this.#removed)
562
+ loaded.delete(e);
563
+ this.#loaded = loaded;
564
+ return this;
565
+ }
566
+ current() {
567
+ return [...this.#loaded];
568
+ }
569
+ import(target, findEntity) {
570
+ return new O2MLoadedState(target, mapEntities([...this.#loaded], findEntity), this.#hasBeenSet, mapEntities([...this.#added], findEntity), mapEntities([...this.#removed], findEntity));
571
+ }
572
+ added() {
573
+ return [...this.#added];
574
+ }
575
+ removed() {
576
+ return [...this.#removed];
577
+ }
578
+ resetAddedRemoved() {
579
+ this.#added = new Set();
580
+ this.#removed = new Set();
581
+ }
582
+ }
583
+ function mapEntities(entities, findEntity) {
584
+ const result = new Array(entities.length);
585
+ for (let i = 0; i < entities.length; i++)
586
+ result[i] = findEntity(entities[i]);
587
+ return result;
588
+ }
347
589
  //# sourceMappingURL=OneToManyCollection.js.map