@ember-data/store 4.8.0-alpha.5 → 4.8.0-alpha.6

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.
@@ -4,10 +4,9 @@ import { DEBUG } from '@glimmer/env';
4
4
  import { importSync } from '@embroider/macros';
5
5
  import { resolve } from 'rsvp';
6
6
 
7
- import { V2CACHE_SINGLETON_MANAGER } from '@ember-data/canary-features';
8
7
  import { HAS_RECORD_DATA_PACKAGE } from '@ember-data/private-build-infra';
9
8
  import { LOG_INSTANCE_CACHE } from '@ember-data/private-build-infra/debugging';
10
- import { DEPRECATE_V1CACHE_STORE_APIS } from '@ember-data/private-build-infra/deprecations';
9
+ import { DEPRECATE_V1_RECORD_DATA, DEPRECATE_V1CACHE_STORE_APIS } from '@ember-data/private-build-infra/deprecations';
11
10
  import type { Graph, peekGraph } from '@ember-data/record-data/-private/graph/index';
12
11
  import type {
13
12
  ExistingResourceIdentifierObject,
@@ -324,23 +323,23 @@ export class InstanceCache {
324
323
  identifier.lid,
325
324
  this._storeWrapper
326
325
  );
327
- if (V2CACHE_SINGLETON_MANAGER) {
326
+ if (DEPRECATE_V1_RECORD_DATA) {
327
+ recordData = new NonSingletonRecordDataManager(this.store, recordDataInstance, identifier);
328
+ } else {
328
329
  recordData = this.__cacheManager =
329
330
  this.__cacheManager || new NonSingletonRecordDataManager(this.store, recordDataInstance, identifier);
330
- } else {
331
- recordData = new NonSingletonRecordDataManager(this.store, recordDataInstance, identifier);
332
331
  }
333
332
  } else {
334
333
  let recordDataInstance = this.store.createRecordDataFor(identifier, this._storeWrapper);
335
- if (V2CACHE_SINGLETON_MANAGER) {
334
+ if (DEPRECATE_V1_RECORD_DATA) {
335
+ recordData = new NonSingletonRecordDataManager(this.store, recordDataInstance, identifier);
336
+ } else {
336
337
  if (DEBUG) {
337
- recordData = this.__cacheManager = this.__cacheManager || new SingletonRecordDataManager(this.store);
338
+ recordData = this.__cacheManager = this.__cacheManager || new SingletonRecordDataManager();
338
339
  (recordData as SingletonRecordDataManager)._addRecordData(identifier, recordDataInstance as RecordData);
339
340
  } else {
340
341
  recordData = recordDataInstance as RecordData;
341
342
  }
342
- } else {
343
- recordData = new NonSingletonRecordDataManager(this.store, recordDataInstance, identifier);
344
343
  }
345
344
  }
346
345
  setRecordDataFor(identifier, recordData);
@@ -18,6 +18,7 @@ export {
18
18
  setIdentifierUpdateMethod,
19
19
  setIdentifierForgetMethod,
20
20
  setIdentifierResetMethod,
21
+ isStableIdentifier,
21
22
  } from './caches/identifier-cache';
22
23
 
23
24
  export function normalizeModelName(modelName: string) {
@@ -41,12 +42,15 @@ export function normalizeModelName(modelName: string) {
41
42
  // to also eliminate
42
43
  export { default as coerceId } from './utils/coerce-id';
43
44
 
44
- export { PromiseArray, PromiseObject, deprecatedPromiseObject } from './proxies/promise-proxies';
45
-
46
- export { default as RecordArray } from './record-arrays/record-array';
47
- export { default as AdapterPopulatedRecordArray } from './record-arrays/adapter-populated-record-array';
48
-
49
- export { default as RecordArrayManager } from './managers/record-array-manager';
45
+ export {
46
+ default as RecordArray,
47
+ default as IdentifierArray,
48
+ Collection as AdapterPopulatedRecordArray,
49
+ SOURCE,
50
+ MUTATE,
51
+ IDENTIFIER_ARRAY_TAG,
52
+ } from './record-arrays/identifier-array';
53
+ export { default as RecordArrayManager, fastPush } from './managers/record-array-manager';
50
54
 
51
55
  // // Used by tests
52
56
  export { default as SnapshotRecordArray } from './network/snapshot-record-array';
@@ -15,6 +15,7 @@ export function getShimClass(store: Store, modelName: string): ShimModelClass {
15
15
  shims = Object.create(null) as Dict<ShimModelClass>;
16
16
  AvailableShims.set(store, shims);
17
17
  }
18
+
18
19
  let shim = shims[modelName];
19
20
  if (shim === undefined) {
20
21
  shim = shims[modelName] = new ShimModelClass(store, modelName);
@@ -1,23 +1,72 @@
1
1
  /**
2
2
  @module @ember-data/store
3
3
  */
4
-
5
- import { A } from '@ember/array';
6
- import { _backburner as emberBackburner } from '@ember/runloop';
7
-
8
4
  import type { CollectionResourceDocument } from '@ember-data/types/q/ember-data-json-api';
9
5
  import type { StableRecordIdentifier } from '@ember-data/types/q/identifier';
10
6
  import type { Dict } from '@ember-data/types/q/utils';
11
7
 
12
- import AdapterPopulatedRecordArray, {
13
- AdapterPopulatedRecordArrayCreateArgs,
14
- } 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
15
 
18
- const RecordArraysCache = new Map<StableRecordIdentifier, Set<AdapterPopulatedRecordArray>>();
16
+ const RecordArraysCache = new Map<StableRecordIdentifier, Set<Collection>>();
19
17
  const FAKE_ARR = {};
20
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));
68
+ }
69
+
21
70
  type ChangeSet = Map<StableRecordIdentifier, 'add' | 'del'>;
22
71
 
23
72
  /**
@@ -28,11 +77,10 @@ class RecordArrayManager {
28
77
  declare store: Store;
29
78
  declare isDestroying: boolean;
30
79
  declare isDestroyed: boolean;
31
- declare _live: Map<string, RecordArray>;
32
- declare _managed: Set<AdapterPopulatedRecordArray>;
33
- declare _pending: Map<RecordArray | AdapterPopulatedRecordArray, ChangeSet>;
34
- declare _willFlush: boolean;
35
- declare _identifiers: Map<StableRecordIdentifier, Set<AdapterPopulatedRecordArray>>;
80
+ declare _live: Map<string, IdentifierArray>;
81
+ declare _managed: Set<IdentifierArray>;
82
+ declare _pending: Map<IdentifierArray, ChangeSet>;
83
+ declare _identifiers: Map<StableRecordIdentifier, Set<Collection>>;
36
84
  declare _staged: Map<string, ChangeSet>;
37
85
 
38
86
  constructor(options: { store: Store }) {
@@ -43,18 +91,17 @@ class RecordArrayManager {
43
91
  this._managed = new Set();
44
92
  this._pending = new Map();
45
93
  this._staged = new Map();
46
- this._willFlush = false;
47
94
  this._identifiers = RecordArraysCache;
48
95
  }
49
96
 
50
- _syncArray(array: RecordArray | AdapterPopulatedRecordArray) {
97
+ _syncArray(array: IdentifierArray) {
51
98
  const pending = this._pending.get(array);
52
99
 
53
100
  if (!pending || this.isDestroying || this.isDestroyed) {
54
101
  return;
55
102
  }
56
103
 
57
- array._updateState(pending);
104
+ sync(array, pending);
58
105
  this._pending.delete(array);
59
106
  }
60
107
 
@@ -67,7 +114,7 @@ class RecordArrayManager {
67
114
  @param {String} modelName
68
115
  @return {RecordArray}
69
116
  */
70
- liveArrayFor(type: string): RecordArray {
117
+ liveArrayFor(type: string): IdentifierArray {
71
118
  let array = this._live.get(type);
72
119
  let identifiers: StableRecordIdentifier[] = [];
73
120
  let staged = this._staged.get(type);
@@ -81,19 +128,14 @@ class RecordArrayManager {
81
128
  }
82
129
 
83
130
  if (!array) {
84
- array = RecordArray.create({
85
- modelName: type,
86
- content: A(identifiers),
131
+ array = new IdentifierArray({
132
+ type,
133
+ identifiers,
87
134
  store: this.store,
88
- isLoaded: true,
135
+ allowMutation: false,
89
136
  manager: this,
90
137
  });
91
138
  this._live.set(type, array);
92
- } else {
93
- let pending = this._pending.get(array);
94
- if (pending) {
95
- array._notify();
96
- }
97
139
  }
98
140
 
99
141
  return array;
@@ -104,18 +146,19 @@ class RecordArrayManager {
104
146
  query?: Dict<unknown>;
105
147
  identifiers?: StableRecordIdentifier[];
106
148
  doc?: CollectionResourceDocument;
107
- }): AdapterPopulatedRecordArray {
108
- let options: AdapterPopulatedRecordArrayCreateArgs = {
109
- modelName: config.type,
149
+ }): Collection {
150
+ let options: CollectionCreateOptions = {
151
+ type: config.type,
110
152
  links: config.doc?.links || null,
111
153
  meta: config.doc?.meta || null,
112
154
  query: config.query || null,
113
- content: A(config.identifiers || []),
155
+ identifiers: config.identifiers || [],
114
156
  isLoaded: !!config.identifiers?.length,
157
+ allowMutation: false,
115
158
  store: this.store,
116
159
  manager: this,
117
160
  };
118
- let array = AdapterPopulatedRecordArray.create(options);
161
+ let array = new Collection(options);
119
162
  this._managed.add(array);
120
163
  if (config.identifiers) {
121
164
  associate(array, config.identifiers);
@@ -124,35 +167,29 @@ class RecordArrayManager {
124
167
  return array;
125
168
  }
126
169
 
127
- dirtyArray(array: RecordArray | AdapterPopulatedRecordArray): void {
128
- if (array === FAKE_ARR || this._willFlush) {
170
+ dirtyArray(array: IdentifierArray): void {
171
+ if (array === FAKE_ARR) {
129
172
  return;
130
173
  }
131
- this._willFlush = true;
132
- // eslint-disable-next-line @typescript-eslint/unbound-method
133
- emberBackburner.schedule('actions', this, this._flush);
134
- }
135
-
136
- _flush() {
137
- this._willFlush = false;
138
- let pending = this._pending;
139
- pending.forEach((_changes, recordArray) => {
140
- recordArray._notify();
141
- });
174
+ let tag = array[IDENTIFIER_ARRAY_TAG];
175
+ if (!tag.shouldReset) {
176
+ tag.shouldReset = true;
177
+ tag.ref = null;
178
+ }
142
179
  }
143
180
 
144
181
  _getPendingFor(
145
182
  identifier: StableRecordIdentifier,
146
183
  includeManaged: boolean,
147
184
  isRemove?: boolean
148
- ): Map<RecordArray | AdapterPopulatedRecordArray, ChangeSet> | void {
185
+ ): Map<IdentifierArray, ChangeSet> | void {
149
186
  if (this.isDestroying || this.isDestroyed) {
150
187
  return;
151
188
  }
152
189
 
153
190
  let liveArray = this._live.get(identifier.type);
154
191
  const allPending = this._pending;
155
- let pending: Map<RecordArray | AdapterPopulatedRecordArray, ChangeSet> = new Map();
192
+ let pending: Map<IdentifierArray, ChangeSet> = new Map();
156
193
 
157
194
  if (includeManaged) {
158
195
  let managed = RecordArraysCache.get(identifier);
@@ -170,7 +207,7 @@ class RecordArrayManager {
170
207
 
171
208
  // during unloadAll we can ignore removes since we've already
172
209
  // cleared the array.
173
- if (liveArray && liveArray.content.length === 0 && isRemove) {
210
+ if (liveArray && liveArray[SOURCE].length === 0 && isRemove) {
174
211
  return pending;
175
212
  }
176
213
 
@@ -182,7 +219,7 @@ class RecordArrayManager {
182
219
  changes = new Map();
183
220
  this._staged.set(identifier.type, changes);
184
221
  }
185
- pending.set(FAKE_ARR as RecordArray, changes);
222
+ pending.set(FAKE_ARR as IdentifierArray, changes);
186
223
  } else {
187
224
  let changes = allPending.get(liveArray);
188
225
  if (!changes) {
@@ -195,14 +232,13 @@ class RecordArrayManager {
195
232
  return pending;
196
233
  }
197
234
 
198
- populateManagedArray(
199
- array: AdapterPopulatedRecordArray,
200
- identifiers: StableRecordIdentifier[],
201
- payload: CollectionResourceDocument
202
- ) {
235
+ populateManagedArray(array: Collection, identifiers: StableRecordIdentifier[], payload: CollectionResourceDocument) {
203
236
  this._pending.delete(array);
204
- const old = array.content;
205
- array.content.setObjects(identifiers); // this will also notify
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;
206
242
  array.meta = payload.meta || null;
207
243
  array.links = payload.links || null;
208
244
  array.isLoaded = true;
@@ -272,7 +308,7 @@ class RecordArrayManager {
272
308
  }
273
309
  }
274
310
 
275
- function associate(array: AdapterPopulatedRecordArray, identifiers: StableRecordIdentifier[]) {
311
+ function associate(array: Collection, identifiers: StableRecordIdentifier[]) {
276
312
  for (let i = 0; i < identifiers.length; i++) {
277
313
  let identifier = identifiers[i];
278
314
  let cache = RecordArraysCache.get(identifier);
@@ -283,16 +319,59 @@ function associate(array: AdapterPopulatedRecordArray, identifiers: StableRecord
283
319
  cache.add(array);
284
320
  }
285
321
  }
286
- function disassociate(array: AdapterPopulatedRecordArray, identifiers: StableRecordIdentifier[]) {
322
+
323
+ function disassociate(array: Collection, identifiers: StableRecordIdentifier[]) {
287
324
  for (let i = 0; i < identifiers.length; i++) {
288
325
  disassociateIdentifier(array, identifiers[i]);
289
326
  }
290
327
  }
291
- export function disassociateIdentifier(array: AdapterPopulatedRecordArray, identifier: StableRecordIdentifier) {
328
+
329
+ export function disassociateIdentifier(array: Collection, identifier: StableRecordIdentifier) {
292
330
  let cache = RecordArraysCache.get(identifier);
293
331
  if (cache) {
294
332
  cache.delete(array);
295
333
  }
296
334
  }
297
335
 
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
+ }
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
+ }
362
+
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
+ }
375
+ }
376
+
298
377
  export default RecordArrayManager;