@ember-data/store 4.8.0-alpha.4 → 4.8.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,28 +1,73 @@
1
1
  /**
2
2
  @module @ember-data/store
3
3
  */
4
-
5
- import { A } from '@ember/array';
6
- import { assert } from '@ember/debug';
7
- import { _backburner as emberBackburner } from '@ember/runloop';
8
- import { DEBUG } from '@glimmer/env';
9
-
10
- import type { CollectionResourceDocument, Meta } from '@ember-data/types/q/ember-data-json-api';
4
+ import type { CollectionResourceDocument } from '@ember-data/types/q/ember-data-json-api';
11
5
  import type { StableRecordIdentifier } from '@ember-data/types/q/identifier';
12
6
  import type { Dict } from '@ember-data/types/q/utils';
13
7
 
14
- import AdapterPopulatedRecordArray from '../record-arrays/adapter-populated-record-array';
15
- import RecordArray from '../record-arrays/record-array';
8
+ import IdentifierArray, {
9
+ Collection,
10
+ CollectionCreateOptions,
11
+ IDENTIFIER_ARRAY_TAG,
12
+ SOURCE,
13
+ } from '../record-arrays/identifier-array';
16
14
  import type Store from '../store-service';
17
- import WeakCache from '../utils/weak-cache';
18
15
 
19
- const RecordArraysCache = new WeakCache<StableRecordIdentifier, Set<RecordArray>>(DEBUG ? 'record-arrays' : '');
20
- RecordArraysCache._generator = () => new Set();
21
- export function recordArraysForIdentifier(identifier: StableRecordIdentifier): Set<RecordArray> {
22
- return RecordArraysCache.lookup(identifier);
16
+ const RecordArraysCache = new Map<StableRecordIdentifier, Set<Collection>>();
17
+ const FAKE_ARR = {};
18
+
19
+ const SLICE_BATCH_SIZE = 1200;
20
+ /**
21
+ * This is a clever optimization.
22
+ *
23
+ * clever optimizations rarely stand the test of time, so if you're
24
+ * ever curious or think something better is possible please benchmark
25
+ * and discuss. The benchmark for this at the time of writing is in
26
+ * `scripts/benchmark-push.js`
27
+ *
28
+ * This approach turns out to be 150x faster in Chrome and node than
29
+ * simply using push or concat. It's highly susceptible to the specifics
30
+ * of the batch size, and may require tuning.
31
+ *
32
+ * Clever optimizations should always come with a `why`. This optimization
33
+ * exists for two reasons.
34
+ *
35
+ * 1) array.push(...objects) and Array.prototype.push.apply(arr, objects)
36
+ * are susceptible to stack overflows. The size of objects at which this
37
+ * occurs varies by environment, browser, and current stack depth and memory
38
+ * pressure; however, it occurs in all browsers in fairly pristine conditions
39
+ * somewhere around 125k to 200k elements. Since EmberData regularly encounters
40
+ * arrays larger than this in size, we cannot use push.
41
+ *
42
+ * 2) `array.concat` or simply setting the array to a new reference is often an
43
+ * easier approach; however, native Proxy to an array cannot swap it's target array
44
+ * and attempts at juggling multiple array sources have proven to be victim to a number
45
+ * of browser implementation bugs. Should these bugs be addressed then we could
46
+ * simplify to using `concat`, however, do note this is currently 150x faster
47
+ * than concat, and due to the overloaded signature of concat will likely always
48
+ * be faster.
49
+ *
50
+ * Sincerely,
51
+ * - runspired (Chris Thoburn) 08/21/2022
52
+ *
53
+ * @function fastPush
54
+ * @internal
55
+ * @param target the array to push into
56
+ * @param source the items to push into target
57
+ */
58
+ export function fastPush<T>(target: T[], source: T[]) {
59
+ let startLength = 0;
60
+ let newLength = source.length;
61
+ while (newLength - startLength > SLICE_BATCH_SIZE) {
62
+ // eslint-disable-next-line prefer-spread
63
+ target.push.apply(target, source.slice(startLength, SLICE_BATCH_SIZE));
64
+ startLength += SLICE_BATCH_SIZE;
65
+ }
66
+ // eslint-disable-next-line prefer-spread
67
+ target.push.apply(target, source.slice(startLength));
23
68
  }
24
69
 
25
- const pendingForIdentifier: Set<StableRecordIdentifier> = new Set([]);
70
+ type ChangeSet = Map<StableRecordIdentifier, 'add' | 'del'>;
26
71
 
27
72
  /**
28
73
  @class RecordArrayManager
@@ -32,390 +77,301 @@ class RecordArrayManager {
32
77
  declare store: Store;
33
78
  declare isDestroying: boolean;
34
79
  declare isDestroyed: boolean;
35
- declare _liveRecordArrays: Dict<RecordArray>;
36
- declare _pendingIdentifiers: Dict<StableRecordIdentifier[]>;
37
- declare _adapterPopulatedRecordArrays: RecordArray[];
80
+ declare _live: Map<string, IdentifierArray>;
81
+ declare _managed: Set<IdentifierArray>;
82
+ declare _pending: Map<IdentifierArray, ChangeSet>;
83
+ declare _identifiers: Map<StableRecordIdentifier, Set<Collection>>;
84
+ declare _staged: Map<string, ChangeSet>;
38
85
 
39
86
  constructor(options: { store: Store }) {
40
87
  this.store = options.store;
41
88
  this.isDestroying = false;
42
89
  this.isDestroyed = false;
43
- this._liveRecordArrays = Object.create(null) as Dict<RecordArray>;
44
- this._pendingIdentifiers = Object.create(null) as Dict<StableRecordIdentifier[]>;
45
- this._adapterPopulatedRecordArrays = [];
90
+ this._live = new Map();
91
+ this._managed = new Set();
92
+ this._pending = new Map();
93
+ this._staged = new Map();
94
+ this._identifiers = RecordArraysCache;
46
95
  }
47
96
 
48
- /**
49
- * @method getRecordArraysForIdentifier
50
- * @internal
51
- * @param {StableIdentifier} param
52
- * @return {RecordArray} array
53
- */
54
- getRecordArraysForIdentifier(identifier: StableRecordIdentifier): Set<RecordArray> {
55
- return recordArraysForIdentifier(identifier);
56
- }
97
+ _syncArray(array: IdentifierArray) {
98
+ const pending = this._pending.get(array);
57
99
 
58
- _flushPendingIdentifiersForModelName(modelName: string, identifiers: StableRecordIdentifier[]): void {
59
- if (this.isDestroying || this.isDestroyed) {
100
+ if (!pending || this.isDestroying || this.isDestroyed) {
60
101
  return;
61
102
  }
62
- let identifiersToRemove: StableRecordIdentifier[] = [];
63
- let cache = this.store._instanceCache;
64
-
65
- for (let j = 0; j < identifiers.length; j++) {
66
- let i = identifiers[j];
67
- // mark identifiers, so they can once again be processed by the
68
- // recordArrayManager
69
- pendingForIdentifier.delete(i);
70
- // build up a set of models to ensure we have purged correctly;
71
- if (!cache.recordIsLoaded(i, true)) {
72
- identifiersToRemove.push(i);
73
- }
74
- }
75
-
76
- let array = this._liveRecordArrays[modelName];
77
- if (array) {
78
- // TODO: skip if it only changed
79
- // process liveRecordArrays
80
- updateLiveRecordArray(this.store, array, identifiers);
81
- }
82
-
83
- // process adapterPopulatedRecordArrays
84
- if (identifiersToRemove.length > 0) {
85
- removeFromAdapterPopulatedRecordArrays(this.store, identifiersToRemove);
86
- }
87
- }
88
103
 
89
- _flush() {
90
- let pending = this._pendingIdentifiers;
91
- this._pendingIdentifiers = Object.create(null) as Dict<StableRecordIdentifier[]>;
92
-
93
- for (let modelName in pending) {
94
- this._flushPendingIdentifiersForModelName(modelName, pending[modelName]!);
95
- }
96
- }
97
-
98
- _syncLiveRecordArray(array: RecordArray, modelName: string) {
99
- assert(
100
- `recordArrayManger.syncLiveRecordArray expects modelName not modelClass as the second param`,
101
- typeof modelName === 'string'
102
- );
103
- let pending = this._pendingIdentifiers[modelName];
104
-
105
- if (!Array.isArray(pending)) {
106
- return;
107
- }
108
- let hasNoPotentialDeletions = pending.length === 0;
109
- let listSize = this.store._instanceCache.peekList[modelName]?.size;
110
- let hasNoInsertionsOrRemovals = listSize === array.length;
111
-
112
- /*
113
- Ideally the recordArrayManager has knowledge of the changes to be applied to
114
- liveRecordArrays, and is capable of strategically flushing those changes and applying
115
- small diffs if desired. However, until we've refactored recordArrayManager, this dirty
116
- check prevents us from unnecessarily wiping out live record arrays returned by peekAll.
117
- */
118
- if (hasNoPotentialDeletions && hasNoInsertionsOrRemovals) {
119
- return;
120
- }
121
-
122
- this._flushPendingIdentifiersForModelName(modelName, pending);
123
- // TODO make this a map
124
- // TODO we probably do way too much work here, probably can just take
125
- // current all-known state and filter visible.
126
- // We can then provide this directly to the RecordArray instance.
127
- // If we deprecate RecordArray being an ArrayProxy this will be stellar.
128
- delete this._pendingIdentifiers[modelName];
129
-
130
- let identifiers = this._visibleIdentifiersByType(modelName);
131
- let identifiersToAdd: StableRecordIdentifier[] = [];
132
- for (let i = 0; i < identifiers.length; i++) {
133
- let identifier = identifiers[i];
134
- let recordArrays = recordArraysForIdentifier(identifier);
135
- if (recordArrays.has(array) === false) {
136
- recordArrays.add(array);
137
- identifiersToAdd.push(identifier);
138
- }
139
- }
140
-
141
- if (identifiersToAdd.length) {
142
- array._pushIdentifiers(identifiersToAdd);
143
- }
144
- }
145
-
146
- _didUpdateAll(modelName: string): void {
147
- let recordArray = this._liveRecordArrays[modelName];
148
- if (recordArray) {
149
- recordArray.isUpdating = false;
150
- // TODO potentially we should sync here, currently
151
- // this occurs as a side-effect of individual records updating
152
- // this._syncLiveRecordArray(recordArray, modelName);
153
- }
104
+ sync(array, pending);
105
+ this._pending.delete(array);
154
106
  }
155
107
 
156
108
  /**
157
109
  Get the `RecordArray` for a modelName, which contains all loaded records of
158
110
  given modelName.
159
111
 
160
- @method liveRecordArrayFor
112
+ @method liveArrayFor
161
113
  @internal
162
114
  @param {String} modelName
163
115
  @return {RecordArray}
164
116
  */
165
- liveRecordArrayFor(modelName: string): RecordArray {
166
- assert(
167
- `recordArrayManger.liveRecordArrayFor expects modelName not modelClass as the param`,
168
- typeof modelName === 'string'
169
- );
170
-
171
- let array = this._liveRecordArrays[modelName];
117
+ liveArrayFor(type: string): IdentifierArray {
118
+ let array = this._live.get(type);
119
+ let identifiers: StableRecordIdentifier[] = [];
120
+ let staged = this._staged.get(type);
121
+ if (staged) {
122
+ staged.forEach((value, key) => {
123
+ if (value === 'add') {
124
+ identifiers.push(key);
125
+ }
126
+ });
127
+ this._staged.delete(type);
128
+ }
172
129
 
173
- if (array) {
174
- // if the array already exists, synchronize
175
- this._syncLiveRecordArray(array, modelName);
176
- } else {
177
- // if the array is being newly created merely create it with its initial
178
- // content already set. This prevents unneeded change events.
179
- let identifiers = this._visibleIdentifiersByType(modelName);
180
- array = this.createRecordArray(modelName, identifiers);
181
- this._liveRecordArrays[modelName] = array;
130
+ if (!array) {
131
+ array = new IdentifierArray({
132
+ type,
133
+ identifiers,
134
+ store: this.store,
135
+ allowMutation: false,
136
+ manager: this,
137
+ });
138
+ this._live.set(type, array);
182
139
  }
183
140
 
184
141
  return array;
185
142
  }
186
143
 
187
- _visibleIdentifiersByType(modelName: string) {
188
- const cache = this.store._instanceCache;
189
- const list = cache.peekList[modelName];
190
- let all = list ? [...list.values()] : [];
191
- let visible: StableRecordIdentifier[] = [];
192
- for (let i = 0; i < all.length; i++) {
193
- let identifier = all[i];
194
-
195
- if (cache.recordIsLoaded(identifier, true)) {
196
- visible.push(identifier);
197
- }
198
- }
199
- return visible;
200
- }
201
-
202
- /**
203
- Create a `RecordArray` for a modelName.
204
-
205
- @method createRecordArray
206
- @internal
207
- @param {String} modelName
208
- @param {Array} [identifiers]
209
- @return {RecordArray}
210
- */
211
- createRecordArray(modelName: string, identifiers: StableRecordIdentifier[] = []): RecordArray {
212
- assert(
213
- `recordArrayManger.createRecordArray expects modelName not modelClass as the param`,
214
- typeof modelName === 'string'
215
- );
216
-
217
- let array = RecordArray.create({
218
- modelName,
219
- content: A(identifiers || []),
144
+ createArray(config: {
145
+ type: string;
146
+ query?: Dict<unknown>;
147
+ identifiers?: StableRecordIdentifier[];
148
+ doc?: CollectionResourceDocument;
149
+ }): Collection {
150
+ let options: CollectionCreateOptions = {
151
+ type: config.type,
152
+ links: config.doc?.links || null,
153
+ meta: config.doc?.meta || null,
154
+ query: config.query || null,
155
+ identifiers: config.identifiers || [],
156
+ isLoaded: !!config.identifiers?.length,
157
+ allowMutation: false,
220
158
  store: this.store,
221
- isLoaded: true,
222
159
  manager: this,
223
- });
224
-
225
- if (Array.isArray(identifiers)) {
226
- this._associateWithRecordArray(identifiers, array);
160
+ };
161
+ let array = new Collection(options);
162
+ this._managed.add(array);
163
+ if (config.identifiers) {
164
+ associate(array, config.identifiers);
227
165
  }
228
166
 
229
167
  return array;
230
168
  }
231
169
 
232
- /**
233
- Create a `AdapterPopulatedRecordArray` for a modelName with given query.
234
-
235
- @method createAdapterPopulatedRecordArray
236
- @internal
237
- @param {String} modelName
238
- @param {Object} query
239
- @return {AdapterPopulatedRecordArray}
240
- */
241
- createAdapterPopulatedRecordArray(
242
- modelName: string,
243
- query: Dict<unknown> | undefined,
244
- identifiers: StableRecordIdentifier[],
245
- payload?: CollectionResourceDocument
246
- ): AdapterPopulatedRecordArray {
247
- assert(
248
- `recordArrayManger.createAdapterPopulatedRecordArray expects modelName not modelClass as the first param, received ${modelName}`,
249
- typeof modelName === 'string'
250
- );
251
-
252
- let array: AdapterPopulatedRecordArray;
253
- if (Array.isArray(identifiers)) {
254
- array = AdapterPopulatedRecordArray.create({
255
- modelName,
256
- query: query,
257
- content: A(identifiers),
258
- store: this.store,
259
- manager: this,
260
- isLoaded: true,
261
- isUpdating: false,
262
- // TODO this assign kills the root reference but a deep-copy would be required
263
- // for both meta and links to actually not be by-ref. We whould likely change
264
- // this to a dev-only deep-freeze.
265
- meta: Object.assign({} as Meta, payload?.meta),
266
- links: Object.assign({}, payload?.links),
267
- });
268
-
269
- this._associateWithRecordArray(identifiers, array);
270
- } else {
271
- array = AdapterPopulatedRecordArray.create({
272
- modelName,
273
- query: query,
274
- content: A<StableRecordIdentifier>(),
275
- isLoaded: false,
276
- store: this.store,
277
- manager: this,
278
- });
170
+ dirtyArray(array: IdentifierArray): void {
171
+ if (array === FAKE_ARR) {
172
+ return;
173
+ }
174
+ let tag = array[IDENTIFIER_ARRAY_TAG];
175
+ if (!tag.shouldReset) {
176
+ tag.shouldReset = true;
177
+ tag.ref = null;
279
178
  }
179
+ }
280
180
 
281
- this._adapterPopulatedRecordArrays.push(array);
181
+ _getPendingFor(
182
+ identifier: StableRecordIdentifier,
183
+ includeManaged: boolean,
184
+ isRemove?: boolean
185
+ ): Map<IdentifierArray, ChangeSet> | void {
186
+ if (this.isDestroying || this.isDestroyed) {
187
+ return;
188
+ }
282
189
 
283
- return array;
284
- }
190
+ let liveArray = this._live.get(identifier.type);
191
+ const allPending = this._pending;
192
+ let pending: Map<IdentifierArray, ChangeSet> = new Map();
193
+
194
+ if (includeManaged) {
195
+ let managed = RecordArraysCache.get(identifier);
196
+ if (managed) {
197
+ managed.forEach((arr) => {
198
+ let changes = allPending.get(arr);
199
+ if (!changes) {
200
+ changes = new Map();
201
+ allPending.set(arr, changes);
202
+ }
203
+ pending.set(arr, changes);
204
+ });
205
+ }
206
+ }
285
207
 
286
- /**
287
- Unregister a RecordArray.
288
- So manager will not update this array.
208
+ // during unloadAll we can ignore removes since we've already
209
+ // cleared the array.
210
+ if (liveArray && liveArray[SOURCE].length === 0 && isRemove) {
211
+ return pending;
212
+ }
289
213
 
290
- @method unregisterRecordArray
291
- @internal
292
- @param {RecordArray} array
293
- */
294
- unregisterRecordArray(array: RecordArray): void {
295
- let modelName = array.modelName;
296
-
297
- // remove from adapter populated record array
298
- let removedFromAdapterPopulated = removeFromArray(this._adapterPopulatedRecordArrays, array);
299
-
300
- if (!removedFromAdapterPopulated) {
301
- let liveRecordArrayForType = this._liveRecordArrays[modelName];
302
- // unregister live record array
303
- if (liveRecordArrayForType) {
304
- if (array === liveRecordArrayForType) {
305
- delete this._liveRecordArrays[modelName];
306
- }
214
+ if (!liveArray) {
215
+ // start building a changeset for when we eventually
216
+ // do have a live array
217
+ let changes = this._staged.get(identifier.type);
218
+ if (!changes) {
219
+ changes = new Map();
220
+ this._staged.set(identifier.type, changes);
221
+ }
222
+ pending.set(FAKE_ARR as IdentifierArray, changes);
223
+ } else {
224
+ let changes = allPending.get(liveArray);
225
+ if (!changes) {
226
+ changes = new Map();
227
+ allPending.set(liveArray, changes);
307
228
  }
229
+ pending.set(liveArray, changes);
308
230
  }
231
+
232
+ return pending;
309
233
  }
310
234
 
311
- /**
312
- * @method _associateWithRecordArray
313
- * @internal
314
- * @param {StableIdentifier} identifiers
315
- * @param {RecordArray} array
316
- */
317
- _associateWithRecordArray(identifiers: StableRecordIdentifier[], array: RecordArray): void {
318
- for (let i = 0, l = identifiers.length; i < l; i++) {
319
- let identifier = identifiers[i];
320
- let recordArrays = this.getRecordArraysForIdentifier(identifier);
321
- recordArrays.add(array);
322
- }
235
+ populateManagedArray(array: Collection, identifiers: StableRecordIdentifier[], payload: CollectionResourceDocument) {
236
+ this._pending.delete(array);
237
+ const source = array[SOURCE];
238
+ const old = source.slice();
239
+ source.length = 0;
240
+ fastPush(source, identifiers);
241
+ array[IDENTIFIER_ARRAY_TAG].ref = null;
242
+ array.meta = payload.meta || null;
243
+ array.links = payload.links || null;
244
+ array.isLoaded = true;
245
+
246
+ disassociate(array, old);
247
+ associate(array, identifiers);
323
248
  }
324
249
 
325
- /**
326
- @method recordDidChange
327
- @internal
328
- */
329
- recordDidChange(identifier: StableRecordIdentifier): void {
330
- if (this.isDestroying || this.isDestroyed) {
331
- return;
250
+ identifierAdded(identifier: StableRecordIdentifier): void {
251
+ let changeSets = this._getPendingFor(identifier, false);
252
+ if (changeSets) {
253
+ changeSets.forEach((changes, array) => {
254
+ let existing = changes.get(identifier);
255
+ if (existing === 'del') {
256
+ changes.delete(identifier);
257
+ } else {
258
+ changes.set(identifier, 'add');
259
+
260
+ if (changes.size === 1) {
261
+ this.dirtyArray(array);
262
+ }
263
+ }
264
+ });
332
265
  }
333
- let modelName = identifier.type;
266
+ }
334
267
 
335
- if (pendingForIdentifier.has(identifier)) {
336
- return;
268
+ identifierRemoved(identifier: StableRecordIdentifier): void {
269
+ let changeSets = this._getPendingFor(identifier, true, true);
270
+ if (changeSets) {
271
+ changeSets.forEach((changes, array) => {
272
+ let existing = changes.get(identifier);
273
+ if (existing === 'add') {
274
+ changes.delete(identifier);
275
+ } else {
276
+ changes.set(identifier, 'del');
277
+
278
+ if (changes.size === 1) {
279
+ this.dirtyArray(array);
280
+ }
281
+ }
282
+ });
337
283
  }
284
+ }
338
285
 
339
- pendingForIdentifier.add(identifier);
286
+ identifierChanged(identifier: StableRecordIdentifier): void {
287
+ let newState = this.store._instanceCache.recordIsLoaded(identifier, true);
340
288
 
341
- let pending = this._pendingIdentifiers;
342
- let models = (pending[modelName] = pending[modelName] || []);
343
- if (models.push(identifier) !== 1) {
344
- return;
289
+ if (newState) {
290
+ this.identifierAdded(identifier);
291
+ } else {
292
+ this.identifierRemoved(identifier);
345
293
  }
346
-
347
- // TODO do we still need this schedule?
348
- // eslint-disable-next-line @typescript-eslint/unbound-method
349
- emberBackburner.schedule('actions', this, this._flush);
350
294
  }
351
295
 
352
- willDestroy() {
353
- Object.keys(this._liveRecordArrays).forEach((modelName) => this._liveRecordArrays[modelName]!.destroy());
354
- this._adapterPopulatedRecordArrays.forEach((entry) => entry.destroy());
355
- this.isDestroyed = true;
296
+ clear() {
297
+ this._live.forEach((array) => array.destroy());
298
+ this._managed.forEach((array) => array.destroy());
299
+ this._managed.clear();
300
+ RecordArraysCache.clear();
356
301
  }
357
302
 
358
303
  destroy() {
359
304
  this.isDestroying = true;
360
- // TODO do we still need this schedule?
361
- // eslint-disable-next-line @typescript-eslint/unbound-method
362
- emberBackburner.schedule('actions', this, this.willDestroy);
363
- }
364
- }
365
-
366
- function removeFromArray(array: RecordArray[], item: RecordArray): boolean {
367
- let index = array.indexOf(item);
368
-
369
- if (index !== -1) {
370
- array.splice(index, 1);
371
- return true;
305
+ this.clear();
306
+ this._live.clear();
307
+ this.isDestroyed = true;
372
308
  }
373
-
374
- return false;
375
309
  }
376
310
 
377
- function updateLiveRecordArray(store: Store, recordArray: RecordArray, identifiers: StableRecordIdentifier[]): void {
378
- let identifiersToAdd: StableRecordIdentifier[] = [];
379
- let identifiersToRemove: StableRecordIdentifier[] = [];
380
- const cache = store._instanceCache;
381
-
311
+ function associate(array: Collection, identifiers: StableRecordIdentifier[]) {
382
312
  for (let i = 0; i < identifiers.length; i++) {
383
313
  let identifier = identifiers[i];
384
- let recordArrays = recordArraysForIdentifier(identifier);
385
-
386
- if (cache.recordIsLoaded(identifier, true)) {
387
- if (!recordArrays.has(recordArray)) {
388
- identifiersToAdd.push(identifier);
389
- recordArrays.add(recordArray);
390
- }
391
- } else {
392
- identifiersToRemove.push(identifier);
393
- recordArrays.delete(recordArray);
314
+ let cache = RecordArraysCache.get(identifier);
315
+ if (!cache) {
316
+ cache = new Set();
317
+ RecordArraysCache.set(identifier, cache);
394
318
  }
395
- }
396
-
397
- if (identifiersToAdd.length > 0) {
398
- recordArray._pushIdentifiers(identifiersToAdd);
399
- }
400
- if (identifiersToRemove.length > 0) {
401
- recordArray._removeIdentifiers(identifiersToRemove);
319
+ cache.add(array);
402
320
  }
403
321
  }
404
322
 
405
- function removeFromAdapterPopulatedRecordArrays(store: Store, identifiers: StableRecordIdentifier[]): void {
323
+ function disassociate(array: Collection, identifiers: StableRecordIdentifier[]) {
406
324
  for (let i = 0; i < identifiers.length; i++) {
407
- removeFromAll(store, identifiers[i]);
325
+ disassociateIdentifier(array, identifiers[i]);
408
326
  }
409
327
  }
410
328
 
411
- function removeFromAll(store: Store, identifier: StableRecordIdentifier): void {
412
- const recordArrays = recordArraysForIdentifier(identifier);
329
+ export function disassociateIdentifier(array: Collection, identifier: StableRecordIdentifier) {
330
+ let cache = RecordArraysCache.get(identifier);
331
+ if (cache) {
332
+ cache.delete(array);
333
+ }
334
+ }
413
335
 
414
- recordArrays.forEach(function (recordArray) {
415
- recordArray._removeIdentifiers([identifier]);
336
+ function sync(array: IdentifierArray, changes: Map<StableRecordIdentifier, 'add' | 'del'>) {
337
+ let state = array[SOURCE];
338
+ const adds: StableRecordIdentifier[] = [];
339
+ const removes: StableRecordIdentifier[] = [];
340
+ changes.forEach((value, key) => {
341
+ if (value === 'add') {
342
+ // likely we want to keep a Set along-side
343
+ if (state.includes(key)) {
344
+ return;
345
+ }
346
+ adds.push(key);
347
+ } else {
348
+ removes.push(key);
349
+ }
416
350
  });
351
+ if (removes.length) {
352
+ if (removes.length === state.length) {
353
+ state.length = 0;
354
+ // changing the reference breaks the Proxy
355
+ // state = array[SOURCE] = [];
356
+ } else {
357
+ removes.forEach((i) => {
358
+ state.splice(state.indexOf(i), 1);
359
+ });
360
+ }
361
+ }
417
362
 
418
- recordArrays.clear();
363
+ if (adds.length) {
364
+ fastPush(state, adds);
365
+ // changing the reference breaks the Proxy
366
+ // else we could do this
367
+ /*
368
+ if (state.length === 0) {
369
+ array[SOURCE] = adds;
370
+ } else {
371
+ array[SOURCE] = state.concat(adds);
372
+ }
373
+ */
374
+ }
419
375
  }
420
376
 
421
377
  export default RecordArrayManager;