@warp-drive-mirror/json-api 5.8.0-alpha.4 → 5.8.0-alpha.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.
@@ -0,0 +1,1872 @@
1
+ import { graphFor, peekGraph, isBelongsTo } from '@warp-drive-mirror/core/graph/-private';
2
+ import { assertPrivateCapabilities, isResourceKey, isRequestKey } from '@warp-drive-mirror/core/store/-private';
3
+ import 'fuse.js';
4
+ import 'json-to-ast';
5
+
6
+ function isMetaDocument(doc) {
7
+ return !(doc instanceof Error) && doc.content && !('data' in doc.content) && !('included' in doc.content) && 'meta' in doc.content;
8
+ }
9
+ function isErrorDocument(doc) {
10
+ return doc instanceof Error;
11
+ }
12
+
13
+ function isImplicit(relationship) {
14
+ return relationship.definition.isImplicit;
15
+ }
16
+ const EMPTY_ITERATOR = {
17
+ iterator() {
18
+ return {
19
+ next() {
20
+ return {
21
+ done: true,
22
+ value: undefined
23
+ };
24
+ }
25
+ };
26
+ }
27
+ };
28
+ function makeCache() {
29
+ return {
30
+ id: null,
31
+ remoteAttrs: null,
32
+ localAttrs: null,
33
+ defaultAttrs: null,
34
+ inflightAttrs: null,
35
+ changes: null,
36
+ errors: null,
37
+ isNew: false,
38
+ isDeleted: false,
39
+ isDeletionCommitted: false
40
+ };
41
+ }
42
+
43
+ /**
44
+ * ```ts
45
+ * import { JSONAPICache } from '@warp-drive-mirror/json-api';
46
+ * ```
47
+ *
48
+ * A {@link Cache} implementation tuned for [{json:api}](https://jsonapi.org/)
49
+ *
50
+ * @categoryDescription Cache Management
51
+ * APIs for primary cache management functionality
52
+ * @categoryDescription Cache Forking
53
+ * APIs that support Cache Forking
54
+ * @categoryDescription SSR Support
55
+ * APIs that support SSR functionality
56
+ * @categoryDescription Resource Lifecycle
57
+ * APIs that support management of resource data
58
+ * @categoryDescription Resource Data
59
+ * APIs that support granular field level management of resource data
60
+ * @categoryDescription Resource State
61
+ * APIs that support managing Resource states
62
+ *
63
+ * @public
64
+ */
65
+ class JSONAPICache {
66
+ /**
67
+ * The Cache Version that this implementation implements.
68
+ *
69
+ * @public
70
+ */
71
+
72
+ /** @internal */
73
+
74
+ /** @internal */
75
+
76
+ /** @internal */
77
+
78
+ /** @internal */
79
+
80
+ /** @internal */
81
+
82
+ constructor(capabilities) {
83
+ this.version = '2';
84
+ this._capabilities = capabilities;
85
+ this.__cache = new Map();
86
+ this.__graph = graphFor(capabilities);
87
+ this.__destroyedCache = new Map();
88
+ this.__documents = new Map();
89
+ }
90
+
91
+ ////////// ================ //////////
92
+ ////////// Cache Management //////////
93
+ ////////// ================ //////////
94
+
95
+ /**
96
+ * Cache the response to a request
97
+ *
98
+ * Implements `Cache.put`.
99
+ *
100
+ * Expects a StructuredDocument whose `content` member is a JsonApiDocument.
101
+ *
102
+ * ```js
103
+ * cache.put({
104
+ * request: { url: 'https://api.example.com/v1/user/1' },
105
+ * content: {
106
+ * data: {
107
+ * type: 'user',
108
+ * id: '1',
109
+ * attributes: {
110
+ * name: 'Chris'
111
+ * }
112
+ * }
113
+ * }
114
+ * })
115
+ * ```
116
+ *
117
+ * > **Note**
118
+ * > The nested `content` and `data` members are not a mistake. This is because
119
+ * > there are two separate concepts involved here, the `StructuredDocument` which contains
120
+ * > the context of a given Request that has been issued with the returned contents as its
121
+ * > `content` property, and a `JSON:API Document` which is the json contents returned by
122
+ * > this endpoint and which uses its `data` property to signify which resources are the
123
+ * > primary resources associated with the request.
124
+ *
125
+ * StructuredDocument's with urls will be cached as full documents with
126
+ * associated resource membership order and contents preserved but linked
127
+ * into the cache.
128
+ *
129
+ * @category Cache Management
130
+ * @public
131
+ */
132
+
133
+ put(doc) {
134
+ if (isErrorDocument(doc)) {
135
+ return this._putDocument(doc, undefined, undefined);
136
+ } else if (isMetaDocument(doc)) {
137
+ return this._putDocument(doc, undefined, undefined);
138
+ }
139
+ const jsonApiDoc = doc.content;
140
+ const included = jsonApiDoc.included;
141
+ let i, length;
142
+ const {
143
+ cacheKeyManager
144
+ } = this._capabilities;
145
+ if (included) {
146
+ for (i = 0, length = included.length; i < length; i++) {
147
+ included[i] = putOne(this, cacheKeyManager, included[i]);
148
+ }
149
+ }
150
+ if (Array.isArray(jsonApiDoc.data)) {
151
+ length = jsonApiDoc.data.length;
152
+ const identifiers = [];
153
+ for (i = 0; i < length; i++) {
154
+ identifiers.push(putOne(this, cacheKeyManager, jsonApiDoc.data[i]));
155
+ }
156
+ return this._putDocument(doc, identifiers, included);
157
+ }
158
+ if (jsonApiDoc.data === null) {
159
+ return this._putDocument(doc, null, included);
160
+ }
161
+ const identifier = putOne(this, cacheKeyManager, jsonApiDoc.data);
162
+ return this._putDocument(doc, identifier, included);
163
+ }
164
+
165
+ /** @internal */
166
+
167
+ /** @internal */
168
+
169
+ /** @internal */
170
+
171
+ /** @internal */
172
+
173
+ /** @internal */
174
+ _putDocument(doc, data, included) {
175
+ // @ts-expect-error narrowing within is just horrible in TS :/
176
+ const resourceDocument = isErrorDocument(doc) ? fromStructuredError(doc) : fromBaseDocument(doc);
177
+ if (data !== undefined) {
178
+ resourceDocument.data = data;
179
+ }
180
+ if (included !== undefined) {
181
+ resourceDocument.included = included;
182
+ }
183
+ const request = doc.request;
184
+ const identifier = request ? this._capabilities.cacheKeyManager.getOrCreateDocumentIdentifier(request) : null;
185
+ if (identifier) {
186
+ resourceDocument.lid = identifier.lid;
187
+
188
+ // @ts-expect-error
189
+ doc.content = resourceDocument;
190
+ const hasExisting = this.__documents.has(identifier.lid);
191
+ this.__documents.set(identifier.lid, doc);
192
+ this._capabilities.notifyChange(identifier, hasExisting ? 'updated' : 'added', null);
193
+ }
194
+ if (doc.request?.op === 'findHasMany') {
195
+ const parentIdentifier = doc.request.options?.identifier;
196
+ const parentField = doc.request.options?.field;
197
+ if (parentField && parentIdentifier) {
198
+ this.__graph.push({
199
+ op: 'updateRelationship',
200
+ record: parentIdentifier,
201
+ field: parentField.name,
202
+ value: resourceDocument
203
+ });
204
+ }
205
+ }
206
+ return resourceDocument;
207
+ }
208
+
209
+ /**
210
+ * Update the "remote" or "canonical" (persisted) state of the Cache
211
+ * by merging new information into the existing state.
212
+ *
213
+ * @category Cache Management
214
+ * @public
215
+ * @param op the operation or list of operations to perform
216
+ */
217
+ patch(op) {
218
+ if (Array.isArray(op)) {
219
+ assertPrivateCapabilities(this._capabilities);
220
+ this._capabilities._store._join(() => {
221
+ for (const operation of op) {
222
+ patchCache(this, operation);
223
+ }
224
+ });
225
+ } else {
226
+ patchCache(this, op);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Update the "local" or "current" (unpersisted) state of the Cache
232
+ *
233
+ * @category Cache Management
234
+ * @public
235
+ */
236
+ mutate(mutation) {
237
+ this.__graph.update(mutation, false);
238
+ }
239
+
240
+ /**
241
+ * Peek resource data from the Cache.
242
+ *
243
+ * In development, if the return value
244
+ * is JSON the return value
245
+ * will be deep-cloned and deep-frozen
246
+ * to prevent mutation thereby enforcing cache
247
+ * Immutability.
248
+ *
249
+ * This form of peek is useful for implementations
250
+ * that want to feed raw-data from cache to the UI
251
+ * or which want to interact with a blob of data
252
+ * directly from the presentation cache.
253
+ *
254
+ * An implementation might want to do this because
255
+ * de-referencing records which read from their own
256
+ * blob is generally safer because the record does
257
+ * not require retainining connections to the Store
258
+ * and Cache to present data on a per-field basis.
259
+ *
260
+ * This generally takes the place of `getAttr` as
261
+ * an API and may even take the place of `getRelationship`
262
+ * depending on implementation specifics, though this
263
+ * latter usage is less recommended due to the advantages
264
+ * of the Graph handling necessary entanglements and
265
+ * notifications for relational data.
266
+ *
267
+ * @category Cache Management
268
+ * @public
269
+ */
270
+
271
+ peek(identifier) {
272
+ if (isResourceKey(identifier)) {
273
+ const peeked = this.__safePeek(identifier, false);
274
+ if (!peeked) {
275
+ return null;
276
+ }
277
+ const {
278
+ type,
279
+ id,
280
+ lid
281
+ } = identifier;
282
+ const attributes = structuredClone(Object.assign({}, peeked.remoteAttrs, peeked.inflightAttrs, peeked.localAttrs));
283
+ const relationships = {};
284
+ const rels = this.__graph.identifiers.get(identifier);
285
+ if (rels) {
286
+ Object.keys(rels).forEach(key => {
287
+ const rel = rels[key];
288
+ if (rel.definition.isImplicit) {
289
+ return;
290
+ } else {
291
+ relationships[key] = structuredClone(this.__graph.getData(identifier, key));
292
+ }
293
+ });
294
+ }
295
+ assertPrivateCapabilities(this._capabilities);
296
+ const store = this._capabilities._store;
297
+ const attrs = getCacheFields(this, identifier);
298
+ attrs.forEach((attr, key) => {
299
+ if (key in attributes && attributes[key] !== undefined) {
300
+ return;
301
+ }
302
+ const defaultValue = getDefaultValue(attr, identifier, store);
303
+ if (defaultValue !== undefined) {
304
+ attributes[key] = defaultValue;
305
+ }
306
+ });
307
+ return {
308
+ type,
309
+ id,
310
+ lid,
311
+ attributes,
312
+ relationships
313
+ };
314
+ }
315
+ const document = this.peekRequest(identifier);
316
+ if (document) {
317
+ if ('content' in document) return document.content;
318
+ }
319
+ return null;
320
+ }
321
+
322
+ /**
323
+ * Peek the remote resource data from the Cache.
324
+ *
325
+ * @category Cache Management
326
+ * @public
327
+ */
328
+
329
+ peekRemoteState(identifier) {
330
+ if (isResourceKey(identifier)) {
331
+ const peeked = this.__safePeek(identifier, false);
332
+ if (!peeked) {
333
+ return null;
334
+ }
335
+ const {
336
+ type,
337
+ id,
338
+ lid
339
+ } = identifier;
340
+ const attributes = structuredClone(peeked.remoteAttrs);
341
+ const relationships = {};
342
+ const rels = this.__graph.identifiers.get(identifier);
343
+ if (rels) {
344
+ Object.keys(rels).forEach(key => {
345
+ const rel = rels[key];
346
+ if (rel.definition.isImplicit) {
347
+ return;
348
+ } else {
349
+ relationships[key] = structuredClone(this.__graph.getData(identifier, key));
350
+ }
351
+ });
352
+ }
353
+ assertPrivateCapabilities(this._capabilities);
354
+ const store = this._capabilities._store;
355
+ const attrs = getCacheFields(this, identifier);
356
+ attrs.forEach((attr, key) => {
357
+ if (key in attributes && attributes[key] !== undefined) {
358
+ return;
359
+ }
360
+ const defaultValue = getDefaultValue(attr, identifier, store);
361
+ if (defaultValue !== undefined) {
362
+ attributes[key] = defaultValue;
363
+ }
364
+ });
365
+ return {
366
+ type,
367
+ id,
368
+ lid,
369
+ attributes,
370
+ relationships
371
+ };
372
+ }
373
+ const document = this.peekRequest(identifier);
374
+ if (document) {
375
+ if ('content' in document) return document.content;
376
+ }
377
+ return null;
378
+ }
379
+
380
+ /**
381
+ * Peek the Cache for the existing request data associated with
382
+ * a cacheable request.
383
+ *
384
+ * This is effectively the reverse of `put` for a request in
385
+ * that it will return the the request, response, and content
386
+ * whereas `peek` will return just the `content`.
387
+ *
388
+ * @category Cache Management
389
+ * @public
390
+ */
391
+ peekRequest(identifier) {
392
+ return this.__documents.get(identifier.lid) || null;
393
+ }
394
+
395
+ /**
396
+ * Push resource data from a remote source into the cache for this identifier
397
+ *
398
+ * @category Cache Management
399
+ * @public
400
+ * @return if `calculateChanges` is true then calculated key changes should be returned
401
+ */
402
+ upsert(identifier, data, calculateChanges) {
403
+ assertPrivateCapabilities(this._capabilities);
404
+ const store = this._capabilities._store;
405
+ if (!store._cbs) {
406
+ let result = undefined;
407
+ store._run(() => {
408
+ result = cacheUpsert(this, identifier, data, calculateChanges);
409
+ });
410
+ return result;
411
+ }
412
+ return cacheUpsert(this, identifier, data, calculateChanges);
413
+ }
414
+
415
+ ////////// ============= //////////
416
+ ////////// Cache Forking //////////
417
+ ////////// ============= //////////
418
+
419
+ /**
420
+ * Create a fork of the cache from the current state.
421
+ *
422
+ * Applications should typically not call this method themselves,
423
+ * preferring instead to fork at the Store level, which will
424
+ * utilize this method to fork the cache.
425
+ *
426
+ * @category Cache Forking
427
+ * @private
428
+ */
429
+ fork() {
430
+ throw new Error(`Not Implemented`);
431
+ }
432
+
433
+ /**
434
+ * Merge a fork back into a parent Cache.
435
+ *
436
+ * Applications should typically not call this method themselves,
437
+ * preferring instead to merge at the Store level, which will
438
+ * utilize this method to merge the caches.
439
+ *
440
+ * @category Cache Forking
441
+ * @private
442
+ */
443
+ merge(_cache) {
444
+ throw new Error(`Not Implemented`);
445
+ }
446
+
447
+ /**
448
+ * Generate the list of changes applied to all
449
+ * record in the store.
450
+ *
451
+ * Each individual resource or document that has
452
+ * been mutated should be described as an individual
453
+ * `Change` entry in the returned array.
454
+ *
455
+ * A `Change` is described by an object containing up to
456
+ * three properties: (1) the `identifier` of the entity that
457
+ * changed; (2) the `op` code of that change being one of
458
+ * `upsert` or `remove`, and if the op is `upsert` a `patch`
459
+ * containing the data to merge into the cache for the given
460
+ * entity.
461
+ *
462
+ * This `patch` is opaque to the Store but should be understood
463
+ * by the Cache and may expect to be utilized by an Adapter
464
+ * when generating data during a `save` operation.
465
+ *
466
+ * It is generally recommended that the `patch` contain only
467
+ * the updated state, ignoring fields that are unchanged
468
+ *
469
+ * ```ts
470
+ * interface Change {
471
+ * identifier: ResourceKey | RequestKey;
472
+ * op: 'upsert' | 'remove';
473
+ * patch?: unknown;
474
+ * }
475
+ * ```
476
+ *
477
+ * @category Cache Forking
478
+ * @private
479
+ */
480
+ diff() {
481
+ throw new Error(`Not Implemented`);
482
+ }
483
+
484
+ ////////// =========== //////////
485
+ ////////// SSR Support //////////
486
+ ////////// =========== //////////
487
+
488
+ /**
489
+ * Serialize the entire contents of the Cache into a Stream
490
+ * which may be fed back into a new instance of the same Cache
491
+ * via `cache.hydrate`.
492
+ *
493
+ * @category SSR Support
494
+ * @private
495
+ */
496
+ dump() {
497
+ throw new Error(`Not Implemented`);
498
+ }
499
+
500
+ /**
501
+ * hydrate a Cache from a Stream with content previously serialized
502
+ * from another instance of the same Cache, resolving when hydration
503
+ * is complete.
504
+ *
505
+ * This method should expect to be called both in the context of restoring
506
+ * the Cache during application rehydration after SSR **AND** at unknown
507
+ * times during the lifetime of an already booted application when it is
508
+ * desired to bulk-load additional information into the cache. This latter
509
+ * behavior supports optimizing pre/fetching of data for route transitions
510
+ * via data-only SSR modes.
511
+ *
512
+ * @category SSR Support
513
+ * @private
514
+ */
515
+ hydrate(stream) {
516
+ throw new Error('Not Implemented');
517
+ }
518
+
519
+ ////////// ================== //////////
520
+ ////////// Resource Lifecycle //////////
521
+ ////////// ================== //////////
522
+
523
+ /**
524
+ * [LIFECYCLE] Signal to the cache that a new record has been instantiated on the client
525
+ *
526
+ * It returns properties from options that should be set on the record during the create
527
+ * process. This return value behavior is deprecated.
528
+ *
529
+ * @category Resource Lifecycle
530
+ * @public
531
+ */
532
+ clientDidCreate(identifier, options) {
533
+ const cached = this._createCache(identifier);
534
+ cached.isNew = true;
535
+ const createOptions = {};
536
+ if (options !== undefined) {
537
+ const fields = getCacheFields(this, identifier);
538
+ const graph = this.__graph;
539
+ const propertyNames = Object.keys(options);
540
+ for (let i = 0; i < propertyNames.length; i++) {
541
+ const name = propertyNames[i];
542
+ const propertyValue = options[name];
543
+ if (name === 'id') {
544
+ continue;
545
+ }
546
+ const fieldType = fields.get(name);
547
+ const kind = fieldType !== undefined ? 'kind' in fieldType ? fieldType.kind : 'attribute' : null;
548
+ let relationship;
549
+ switch (kind) {
550
+ case 'attribute':
551
+ this.setAttr(identifier, name, propertyValue);
552
+ createOptions[name] = propertyValue;
553
+ break;
554
+ case 'belongsTo':
555
+ this.mutate({
556
+ op: 'replaceRelatedRecord',
557
+ field: name,
558
+ record: identifier,
559
+ value: propertyValue
560
+ });
561
+ relationship = graph.get(identifier, name);
562
+ relationship.state.hasReceivedData = true;
563
+ relationship.state.isEmpty = false;
564
+ break;
565
+ case 'hasMany':
566
+ this.mutate({
567
+ op: 'replaceRelatedRecords',
568
+ field: name,
569
+ record: identifier,
570
+ value: propertyValue
571
+ });
572
+ relationship = graph.get(identifier, name);
573
+ relationship.state.hasReceivedData = true;
574
+ relationship.state.isEmpty = false;
575
+ break;
576
+ default:
577
+ // reflect back (pass-thru) unknown properties
578
+ createOptions[name] = propertyValue;
579
+ }
580
+ }
581
+ }
582
+ this._capabilities.notifyChange(identifier, 'added', null);
583
+ return createOptions;
584
+ }
585
+
586
+ /**
587
+ * [LIFECYCLE] Signals to the cache that a resource
588
+ * will be part of a save transaction.
589
+ *
590
+ * @category Resource Lifecycle
591
+ * @public
592
+ */
593
+ willCommit(identifier, _context) {
594
+ if (Array.isArray(identifier)) {
595
+ for (const key of identifier) {
596
+ willCommit(this, key);
597
+ }
598
+ } else {
599
+ willCommit(this, identifier);
600
+ }
601
+ }
602
+
603
+ /**
604
+ * [LIFECYCLE] Signals to the cache that a resource
605
+ * was successfully updated as part of a save transaction.
606
+ *
607
+ * @category Resource Lifecycle
608
+ * @public
609
+ */
610
+
611
+ didCommit(committedIdentifier, result) {
612
+ const payload = result ? result.content : null;
613
+ const operation = result?.request?.op ?? null;
614
+ const data = payload && payload.data;
615
+ const responseIsCollection = Array.isArray(data);
616
+ const hasMultipleIdentifiers = Array.isArray(committedIdentifier) && committedIdentifier.length > 1;
617
+ if (Array.isArray(committedIdentifier)) {
618
+ if (responseIsCollection) {
619
+ for (let i = 0; i < committedIdentifier.length; i++) {
620
+ const identifier = committedIdentifier[i];
621
+ didCommit(this, identifier, data[i] ?? null, operation);
622
+ }
623
+ // but if we get back no data or a single entry, we apply
624
+ // the change back to the original identifier
625
+ } else {
626
+ for (let i = 0; i < committedIdentifier.length; i++) {
627
+ const identifier = committedIdentifier[i];
628
+ didCommit(this, identifier, i === 0 ? data : null, operation);
629
+ }
630
+ }
631
+ } else {
632
+ didCommit(this, committedIdentifier, data, operation);
633
+ }
634
+ const included = payload && payload.included;
635
+ const {
636
+ cacheKeyManager
637
+ } = this._capabilities;
638
+ if (included) {
639
+ for (let i = 0, length = included.length; i < length; i++) {
640
+ putOne(this, cacheKeyManager, included[i]);
641
+ }
642
+ }
643
+ return hasMultipleIdentifiers && responseIsCollection ? {
644
+ data: committedIdentifier
645
+ } : {
646
+ data: Array.isArray(committedIdentifier) ? committedIdentifier[0] : committedIdentifier
647
+ };
648
+ }
649
+
650
+ /**
651
+ * [LIFECYCLE] Signals to the cache that a resource
652
+ * was update via a save transaction failed.
653
+ *
654
+ * @category Resource Lifecycle
655
+ * @public
656
+ */
657
+ commitWasRejected(identifier, errors) {
658
+ if (Array.isArray(identifier)) {
659
+ for (let i = 0; i < identifier.length; i++) {
660
+ commitDidError(this, identifier[i], errors && i === 0 ? errors : null);
661
+ }
662
+ return;
663
+ }
664
+ return commitDidError(this, identifier, errors || null);
665
+ }
666
+
667
+ /**
668
+ * [LIFECYCLE] Signals to the cache that all data for a resource
669
+ * should be cleared.
670
+ *
671
+ * This method is a candidate to become a mutation
672
+ *
673
+ * @category Resource Lifecycle
674
+ * @public
675
+ */
676
+ unloadRecord(identifier) {
677
+ const storeWrapper = this._capabilities;
678
+ // TODO this is necessary because
679
+ // we maintain memebership inside InstanceCache
680
+ // for peekAll, so even though we haven't created
681
+ // any data we think this exists.
682
+ // TODO can we eliminate that membership now?
683
+ if (!this.__cache.has(identifier)) {
684
+ // the graph may still need to unload identity
685
+ peekGraph(storeWrapper)?.unload(identifier);
686
+ return;
687
+ }
688
+ const removeFromRecordArray = !this.isDeletionCommitted(identifier);
689
+ let removed = false;
690
+ const cached = this.__peek(identifier, false);
691
+ if (cached.isNew || cached.isDeletionCommitted) {
692
+ peekGraph(storeWrapper)?.push({
693
+ op: 'deleteRecord',
694
+ record: identifier,
695
+ isNew: cached.isNew
696
+ });
697
+ } else {
698
+ peekGraph(storeWrapper)?.unload(identifier);
699
+ }
700
+
701
+ // effectively clearing these is ensuring that
702
+ // we report as `isEmpty` during teardown.
703
+ cached.localAttrs = null;
704
+ cached.remoteAttrs = null;
705
+ cached.defaultAttrs = null;
706
+ cached.inflightAttrs = null;
707
+ const relatedIdentifiers = _allRelatedIdentifiers(storeWrapper, identifier);
708
+ if (areAllModelsUnloaded(storeWrapper, relatedIdentifiers)) {
709
+ for (let i = 0; i < relatedIdentifiers.length; ++i) {
710
+ const relatedIdentifier = relatedIdentifiers[i];
711
+ storeWrapper.notifyChange(relatedIdentifier, 'removed', null);
712
+ removed = true;
713
+ storeWrapper.disconnectRecord(relatedIdentifier);
714
+ }
715
+ }
716
+ this.__cache.delete(identifier);
717
+ this.__destroyedCache.set(identifier, cached);
718
+
719
+ /*
720
+ * The destroy cache is a hack to prevent applications
721
+ * from blowing up during teardown. Accessing state
722
+ * on a destroyed record is not safe, but historically
723
+ * was possible due to a combination of teardown timing
724
+ * and retention of cached state directly on the
725
+ * record itself.
726
+ *
727
+ * Once we have deprecated accessing state on a destroyed
728
+ * instance we may remove this. The timing isn't a huge deal
729
+ * as momentarily retaining the objects outside the bounds
730
+ * of a test won't cause issues.
731
+ */
732
+ if (this.__destroyedCache.size === 1) {
733
+ // TODO do we still need this?
734
+ setTimeout(() => {
735
+ this.__destroyedCache.clear();
736
+ }, 100);
737
+ }
738
+ if (!removed && removeFromRecordArray) {
739
+ storeWrapper.notifyChange(identifier, 'removed', null);
740
+ }
741
+ }
742
+
743
+ ////////// ============= //////////
744
+ ////////// Resource Data //////////
745
+ ////////// ============= //////////
746
+
747
+ /**
748
+ * Retrieve the data for an attribute from the cache
749
+ * with local mutations applied.
750
+ *
751
+ * @category Resource Data
752
+ * @public
753
+ */
754
+ getAttr(identifier, attr) {
755
+ const isSimplePath = !Array.isArray(attr) || attr.length === 1;
756
+ if (Array.isArray(attr) && attr.length === 1) {
757
+ attr = attr[0];
758
+ }
759
+ if (isSimplePath) {
760
+ const attribute = attr;
761
+ const cached = this.__peek(identifier, true);
762
+
763
+ // in Prod we try to recover when accessing something that
764
+ // doesn't exist
765
+ if (!cached) {
766
+ return undefined;
767
+ }
768
+ if (cached.localAttrs && attribute in cached.localAttrs) {
769
+ return cached.localAttrs[attribute];
770
+ } else if (cached.inflightAttrs && attribute in cached.inflightAttrs) {
771
+ return cached.inflightAttrs[attribute];
772
+ } else if (cached.remoteAttrs && attribute in cached.remoteAttrs) {
773
+ return cached.remoteAttrs[attribute];
774
+ } else if (cached.defaultAttrs && attribute in cached.defaultAttrs) {
775
+ return cached.defaultAttrs[attribute];
776
+ } else {
777
+ const attrSchema = getCacheFields(this, identifier).get(attribute);
778
+ assertPrivateCapabilities(this._capabilities);
779
+ const defaultValue = getDefaultValue(attrSchema, identifier, this._capabilities._store);
780
+ if (schemaHasLegacyDefaultValueFn(attrSchema)) {
781
+ cached.defaultAttrs = cached.defaultAttrs || Object.create(null);
782
+ cached.defaultAttrs[attribute] = defaultValue;
783
+ }
784
+ return defaultValue;
785
+ }
786
+ }
787
+
788
+ // TODO @runspired consider whether we need a defaultValue cache in ReactiveResource
789
+ // like we do for the simple case above.
790
+ const path = attr;
791
+ const cached = this.__peek(identifier, true);
792
+ const basePath = path[0];
793
+ let current = cached.localAttrs && basePath in cached.localAttrs ? cached.localAttrs[basePath] : undefined;
794
+ if (current === undefined) {
795
+ current = cached.inflightAttrs && basePath in cached.inflightAttrs ? cached.inflightAttrs[basePath] : undefined;
796
+ }
797
+ if (current === undefined) {
798
+ current = cached.remoteAttrs && basePath in cached.remoteAttrs ? cached.remoteAttrs[basePath] : undefined;
799
+ }
800
+ if (current === undefined) {
801
+ return undefined;
802
+ }
803
+ for (let i = 1; i < path.length; i++) {
804
+ current = current[path[i]];
805
+ if (current === undefined) {
806
+ return undefined;
807
+ }
808
+ }
809
+ return current;
810
+ }
811
+
812
+ /**
813
+ * Retrieve the remote data for an attribute from the cache
814
+ *
815
+ * @category Resource Data
816
+ * @public
817
+ */
818
+ getRemoteAttr(identifier, attr) {
819
+ const isSimplePath = !Array.isArray(attr) || attr.length === 1;
820
+ if (Array.isArray(attr) && attr.length === 1) {
821
+ attr = attr[0];
822
+ }
823
+ if (isSimplePath) {
824
+ const attribute = attr;
825
+ const cached = this.__peek(identifier, true);
826
+
827
+ // in Prod we try to recover when accessing something that
828
+ // doesn't exist
829
+ if (!cached) {
830
+ return undefined;
831
+ }
832
+ if (cached.remoteAttrs && attribute in cached.remoteAttrs) {
833
+ return cached.remoteAttrs[attribute];
834
+
835
+ // we still show defaultValues in the case of a remoteAttr access
836
+ } else if (cached.defaultAttrs && attribute in cached.defaultAttrs) {
837
+ return cached.defaultAttrs[attribute];
838
+ } else {
839
+ const attrSchema = getCacheFields(this, identifier).get(attribute);
840
+ assertPrivateCapabilities(this._capabilities);
841
+ const defaultValue = getDefaultValue(attrSchema, identifier, this._capabilities._store);
842
+ if (schemaHasLegacyDefaultValueFn(attrSchema)) {
843
+ cached.defaultAttrs = cached.defaultAttrs || Object.create(null);
844
+ cached.defaultAttrs[attribute] = defaultValue;
845
+ }
846
+ return defaultValue;
847
+ }
848
+ }
849
+
850
+ // TODO @runspired consider whether we need a defaultValue cache in ReactiveResource
851
+ // like we do for the simple case above.
852
+ const path = attr;
853
+ const cached = this.__peek(identifier, true);
854
+ const basePath = path[0];
855
+ let current = cached.remoteAttrs && basePath in cached.remoteAttrs ? cached.remoteAttrs[basePath] : undefined;
856
+ if (current === undefined) {
857
+ return undefined;
858
+ }
859
+ for (let i = 1; i < path.length; i++) {
860
+ current = current[path[i]];
861
+ if (current === undefined) {
862
+ return undefined;
863
+ }
864
+ }
865
+ return current;
866
+ }
867
+
868
+ /**
869
+ * Mutate the data for an attribute in the cache
870
+ *
871
+ * This method is a candidate to become a mutation
872
+ *
873
+ * @category Resource Data
874
+ * @public
875
+ */
876
+ setAttr(identifier, attr, value) {
877
+ const isSimplePath = !Array.isArray(attr) || attr.length === 1;
878
+ if (Array.isArray(attr) && attr.length === 1) {
879
+ attr = attr[0];
880
+ }
881
+ if (isSimplePath) {
882
+ const cached = this.__peek(identifier, false);
883
+ const currentAttr = attr;
884
+ const existing = cached.inflightAttrs && currentAttr in cached.inflightAttrs ? cached.inflightAttrs[currentAttr] : cached.remoteAttrs && currentAttr in cached.remoteAttrs ? cached.remoteAttrs[currentAttr] : undefined;
885
+ if (existing !== value) {
886
+ cached.localAttrs = cached.localAttrs || Object.create(null);
887
+ cached.localAttrs[currentAttr] = value;
888
+ cached.changes = cached.changes || Object.create(null);
889
+ cached.changes[currentAttr] = [existing, value];
890
+ } else if (cached.localAttrs) {
891
+ delete cached.localAttrs[currentAttr];
892
+ delete cached.changes[currentAttr];
893
+ }
894
+ if (cached.defaultAttrs && currentAttr in cached.defaultAttrs) {
895
+ delete cached.defaultAttrs[currentAttr];
896
+ }
897
+ this._capabilities.notifyChange(identifier, 'attributes', currentAttr);
898
+ return;
899
+ }
900
+
901
+ // get current value from local else inflight else remote
902
+ // structuredClone current if not local (or always?)
903
+ // traverse path, update value at path
904
+ // notify change at first link in path.
905
+ // second pass optimization is change notifyChange signature to take an array path
906
+
907
+ // guaranteed that we have path of at least 2 in length
908
+ const path = attr;
909
+ const cached = this.__peek(identifier, false);
910
+
911
+ // get existing cache record for base path
912
+ const basePath = path[0];
913
+ const existing = cached.inflightAttrs && basePath in cached.inflightAttrs ? cached.inflightAttrs[basePath] : cached.remoteAttrs && basePath in cached.remoteAttrs ? cached.remoteAttrs[basePath] : undefined;
914
+ let existingAttr;
915
+ if (existing) {
916
+ existingAttr = existing[path[1]];
917
+ for (let i = 2; i < path.length; i++) {
918
+ // the specific change we're making is at path[length - 1]
919
+ existingAttr = existingAttr[path[i]];
920
+ }
921
+ }
922
+ if (existingAttr !== value) {
923
+ cached.localAttrs = cached.localAttrs || Object.create(null);
924
+ cached.localAttrs[basePath] = cached.localAttrs[basePath] || structuredClone(existing);
925
+ cached.changes = cached.changes || Object.create(null);
926
+ let currentLocal = cached.localAttrs[basePath];
927
+ let nextLink = 1;
928
+ while (nextLink < path.length - 1) {
929
+ currentLocal = currentLocal[path[nextLink++]];
930
+ }
931
+ currentLocal[path[nextLink]] = value;
932
+ cached.changes[basePath] = [existing, cached.localAttrs[basePath]];
933
+
934
+ // since we initiaize the value as basePath as a clone of the value at the remote basePath
935
+ // then in theory we can use JSON.stringify to compare the two values as key insertion order
936
+ // ought to be consistent.
937
+ // we try/catch this because users have a habit of doing "Bad Things"TM wherein the cache contains
938
+ // stateful values that are not JSON serializable correctly such as Dates.
939
+ // in the case that we error, we fallback to not removing the local value
940
+ // so that any changes we don't understand are preserved. Thse objects would then sometimes
941
+ // appear to be dirty unnecessarily, and for folks that open an issue we can guide them
942
+ // to make their cache data less stateful.
943
+ } else if (cached.localAttrs) {
944
+ try {
945
+ if (!existing) {
946
+ return;
947
+ }
948
+ const existingStr = JSON.stringify(existing);
949
+ const newStr = JSON.stringify(cached.localAttrs[basePath]);
950
+ if (existingStr !== newStr) {
951
+ delete cached.localAttrs[basePath];
952
+ delete cached.changes[basePath];
953
+ }
954
+ } catch {
955
+ // noop
956
+ }
957
+ }
958
+ this._capabilities.notifyChange(identifier, 'attributes', basePath);
959
+ }
960
+
961
+ /**
962
+ * Query the cache for the changed attributes of a resource.
963
+ *
964
+ * @category Resource Data
965
+ * @public
966
+ * @return `{ '<field>': ['<old>', '<new>'] }`
967
+ */
968
+ changedAttrs(identifier) {
969
+ const cached = this.__peek(identifier, false);
970
+
971
+ // in Prod we try to recover when accessing something that
972
+ // doesn't exist
973
+ if (!cached) {
974
+ return Object.create(null);
975
+ }
976
+
977
+ // TODO freeze in dev
978
+ return cached.changes || Object.create(null);
979
+ }
980
+
981
+ /**
982
+ * Query the cache for whether any mutated attributes exist
983
+ *
984
+ * @category Resource Data
985
+ * @public
986
+ */
987
+ hasChangedAttrs(identifier) {
988
+ const cached = this.__peek(identifier, true);
989
+
990
+ // in Prod we try to recover when accessing something that
991
+ // doesn't exist
992
+ if (!cached) {
993
+ return false;
994
+ }
995
+ return cached.inflightAttrs !== null && Object.keys(cached.inflightAttrs).length > 0 || cached.localAttrs !== null && Object.keys(cached.localAttrs).length > 0;
996
+ }
997
+
998
+ /**
999
+ * Tell the cache to discard any uncommitted mutations to attributes
1000
+ *
1001
+ * This method is a candidate to become a mutation
1002
+ *
1003
+ * @category Resource Data
1004
+ * @public
1005
+ * @return the names of fields that were restored
1006
+ */
1007
+ rollbackAttrs(identifier) {
1008
+ const cached = this.__peek(identifier, false);
1009
+ let dirtyKeys;
1010
+ cached.isDeleted = false;
1011
+ if (cached.localAttrs !== null) {
1012
+ dirtyKeys = Object.keys(cached.localAttrs);
1013
+ cached.localAttrs = null;
1014
+ cached.changes = null;
1015
+ }
1016
+ if (cached.isNew) {
1017
+ // > Note: Graph removal handled by unloadRecord
1018
+ cached.isDeletionCommitted = true;
1019
+ cached.isDeleted = true;
1020
+ cached.isNew = false;
1021
+ }
1022
+ cached.inflightAttrs = null;
1023
+ cached.defaultAttrs = null;
1024
+ if (cached.errors) {
1025
+ cached.errors = null;
1026
+ this._capabilities.notifyChange(identifier, 'errors', null);
1027
+ }
1028
+ this._capabilities.notifyChange(identifier, 'state', null);
1029
+ if (dirtyKeys && dirtyKeys.length) {
1030
+ notifyAttributes(this._capabilities, identifier, new Set(dirtyKeys));
1031
+ }
1032
+ return dirtyKeys || [];
1033
+ }
1034
+
1035
+ /**
1036
+ * Query the cache for the changes to relationships of a resource.
1037
+ *
1038
+ * Returns a map of relationship names to RelationshipDiff objects.
1039
+ *
1040
+ * ```ts
1041
+ * type RelationshipDiff =
1042
+ | {
1043
+ kind: 'collection';
1044
+ remoteState: ResourceKey[];
1045
+ additions: Set<ResourceKey>;
1046
+ removals: Set<ResourceKey>;
1047
+ localState: ResourceKey[];
1048
+ reordered: boolean;
1049
+ }
1050
+ | {
1051
+ kind: 'resource';
1052
+ remoteState: ResourceKey | null;
1053
+ localState: ResourceKey | null;
1054
+ };
1055
+ ```
1056
+ *
1057
+ * @category Resource Data
1058
+ * @public
1059
+ */
1060
+ changedRelationships(identifier) {
1061
+ return this.__graph.getChanged(identifier);
1062
+ }
1063
+
1064
+ /**
1065
+ * Query the cache for whether any mutated relationships exist
1066
+ *
1067
+ * @category Resource Data
1068
+ * @public
1069
+ */
1070
+ hasChangedRelationships(identifier) {
1071
+ return this.__graph.hasChanged(identifier);
1072
+ }
1073
+
1074
+ /**
1075
+ * Tell the cache to discard any uncommitted mutations to relationships.
1076
+ *
1077
+ * This will also discard the change on any appropriate inverses.
1078
+ *
1079
+ * This method is a candidate to become a mutation
1080
+ *
1081
+ * @category Resource Data
1082
+ * @public
1083
+ * @return the names of relationships that were restored
1084
+ */
1085
+ rollbackRelationships(identifier) {
1086
+ assertPrivateCapabilities(this._capabilities);
1087
+ let result;
1088
+ this._capabilities._store._join(() => {
1089
+ result = this.__graph.rollback(identifier);
1090
+ });
1091
+ return result;
1092
+ }
1093
+
1094
+ /**
1095
+ * Query the cache for the current state of a relationship property
1096
+ *
1097
+ * @category Resource Data
1098
+ * @public
1099
+ * @return resource relationship object
1100
+ */
1101
+ getRelationship(identifier, field) {
1102
+ return this.__graph.getData(identifier, field);
1103
+ }
1104
+
1105
+ /**
1106
+ * Query the cache for the remote state of a relationship property
1107
+ *
1108
+ * @category Resource Data
1109
+ * @public
1110
+ * @return resource relationship object
1111
+ */
1112
+ getRemoteRelationship(identifier, field) {
1113
+ return this.__graph.getRemoteData(identifier, field);
1114
+ }
1115
+
1116
+ ////////// ============== //////////
1117
+ ////////// Resource State //////////
1118
+ ////////// ============== //////////
1119
+
1120
+ /**
1121
+ * Update the cache state for the given resource to be marked
1122
+ * as locally deleted, or remove such a mark.
1123
+ *
1124
+ * This method is a candidate to become a mutation
1125
+ *
1126
+ * @category Resource State
1127
+ * @public
1128
+ */
1129
+ setIsDeleted(identifier, isDeleted) {
1130
+ const cached = this.__peek(identifier, false);
1131
+ cached.isDeleted = isDeleted;
1132
+ // > Note: Graph removal for isNew handled by unloadRecord
1133
+ this._capabilities.notifyChange(identifier, 'state', null);
1134
+ }
1135
+
1136
+ /**
1137
+ * Query the cache for any validation errors applicable to the given resource.
1138
+ *
1139
+ * @category Resource State
1140
+ * @public
1141
+ */
1142
+ getErrors(identifier) {
1143
+ return this.__peek(identifier, true).errors || [];
1144
+ }
1145
+
1146
+ /**
1147
+ * Query the cache for whether a given resource has any available data
1148
+ *
1149
+ * @category Resource State
1150
+ * @public
1151
+ */
1152
+ isEmpty(identifier) {
1153
+ const cached = this.__safePeek(identifier, true);
1154
+ return cached ? cached.remoteAttrs === null && cached.inflightAttrs === null && cached.localAttrs === null : true;
1155
+ }
1156
+
1157
+ /**
1158
+ * Query the cache for whether a given resource was created locally and not
1159
+ * yet persisted.
1160
+ *
1161
+ * @category Resource State
1162
+ * @public
1163
+ */
1164
+ isNew(identifier) {
1165
+ // TODO can we assert here?
1166
+ return this.__safePeek(identifier, true)?.isNew || false;
1167
+ }
1168
+
1169
+ /**
1170
+ * Query the cache for whether a given resource is marked as deleted (but not
1171
+ * necessarily persisted yet).
1172
+ *
1173
+ * @category Resource State
1174
+ * @public
1175
+ */
1176
+ isDeleted(identifier) {
1177
+ // TODO can we assert here?
1178
+ return this.__safePeek(identifier, true)?.isDeleted || false;
1179
+ }
1180
+
1181
+ /**
1182
+ * Query the cache for whether a given resource has been deleted and that deletion
1183
+ * has also been persisted.
1184
+ *
1185
+ * @category Resource State
1186
+ * @public
1187
+ */
1188
+ isDeletionCommitted(identifier) {
1189
+ // TODO can we assert here?
1190
+ return this.__safePeek(identifier, true)?.isDeletionCommitted || false;
1191
+ }
1192
+
1193
+ /**
1194
+ * Private method used to populate an entry for the identifier
1195
+ *
1196
+ * @internal
1197
+ */
1198
+ _createCache(identifier) {
1199
+ const cache = makeCache();
1200
+ this.__cache.set(identifier, cache);
1201
+ return cache;
1202
+ }
1203
+
1204
+ /**
1205
+ * Peek whether we have cached resource data matching the identifier
1206
+ * without asserting if the resource data is missing.
1207
+ *
1208
+ * @internal
1209
+ */
1210
+ __safePeek(identifier, allowDestroyed) {
1211
+ let resource = this.__cache.get(identifier);
1212
+ if (!resource && allowDestroyed) {
1213
+ resource = this.__destroyedCache.get(identifier);
1214
+ }
1215
+ return resource;
1216
+ }
1217
+
1218
+ /**
1219
+ * Peek whether we have cached resource data matching the identifier
1220
+ * Asserts if the resource data is missing.
1221
+ *
1222
+ * @internal
1223
+ */
1224
+ __peek(identifier, allowDestroyed) {
1225
+ const resource = this.__safePeek(identifier, allowDestroyed);
1226
+ return resource;
1227
+ }
1228
+ }
1229
+ function addResourceToDocument(cache, op) {
1230
+ const doc = cache.__documents.get(op.record.lid);
1231
+ const {
1232
+ content
1233
+ } = doc;
1234
+ if (op.field === 'data') {
1235
+ let shouldNotify = false;
1236
+
1237
+ // if data is not an array, we set the data property directly
1238
+ if (!Array.isArray(content.data)) {
1239
+ shouldNotify = content.data !== op.value;
1240
+ if (shouldNotify) content.data = op.value;
1241
+ } else {
1242
+ if (Array.isArray(op.value)) {
1243
+ if (op.index !== undefined) {
1244
+ // for collections, because we allow duplicates we are always changed.
1245
+ shouldNotify = true;
1246
+ content.data.splice(op.index, 0, ...op.value);
1247
+ } else {
1248
+ // for collections, because we allow duplicates we are always changed.
1249
+ shouldNotify = true;
1250
+ content.data.push(...op.value);
1251
+ }
1252
+ } else {
1253
+ if (op.index !== undefined) {
1254
+ // for collections, because we allow duplicates we are always changed.
1255
+ shouldNotify = true;
1256
+ content.data.splice(op.index, 0, op.value);
1257
+ } else {
1258
+ // for collections, because we allow duplicates we are always changed.
1259
+ shouldNotify = true;
1260
+ content.data.push(op.value);
1261
+ }
1262
+ }
1263
+ }
1264
+
1265
+ // notify
1266
+ if (shouldNotify) cache._capabilities.notifyChange(op.record, 'updated', null);
1267
+ return;
1268
+ }
1269
+ content.included = content.included || [];
1270
+ if (Array.isArray(op.value)) {
1271
+ content.included = content.included.concat(op.value);
1272
+ } else {
1273
+ content.included.push(op.value);
1274
+ }
1275
+
1276
+ // we don't notify in the included case because this is not reactively
1277
+ // exposed. We should possibly consider doing so though for subscribers
1278
+ }
1279
+ function removeResourceFromDocument(cache, op) {
1280
+ const doc = cache.__documents.get(op.record.lid);
1281
+ const {
1282
+ content
1283
+ } = doc;
1284
+ if (op.field === 'data') {
1285
+ let shouldNotify = false;
1286
+
1287
+ // if data is not an array, we set the data property directly
1288
+ if (!Array.isArray(content.data)) {
1289
+ shouldNotify = content.data === op.value;
1290
+ // we only remove the value if it was our existing value
1291
+ if (shouldNotify) content.data = null;
1292
+ } else {
1293
+ const toRemove = Array.isArray(op.value) ? op.value : [op.value];
1294
+ for (let i = 0; i < toRemove.length; i++) {
1295
+ const value = toRemove[i];
1296
+ if (op.index !== undefined) {
1297
+ // in production we want to recover gracefully
1298
+ // so we fallback to first-index-of
1299
+ const index = op.index < content.data.length && content.data[op.index] === value ? op.index : content.data.indexOf(value);
1300
+ if (index !== -1) {
1301
+ // we remove the first occurrence of the value
1302
+ shouldNotify = true;
1303
+ content.data.splice(index, 1);
1304
+ }
1305
+ } else {
1306
+ // we remove the first occurrence of the value
1307
+ const index = content.data.indexOf(value);
1308
+ if (index !== -1) {
1309
+ shouldNotify = true;
1310
+ content.data.splice(index, 1);
1311
+ }
1312
+ }
1313
+ }
1314
+ }
1315
+
1316
+ // notify
1317
+ if (shouldNotify) cache._capabilities.notifyChange(op.record, 'updated', null);
1318
+ } else {
1319
+ content.included = content.included || [];
1320
+ const toRemove = Array.isArray(op.value) ? op.value : [op.value];
1321
+ for (const identifier of toRemove) {
1322
+ const index = content.included.indexOf(identifier);
1323
+ if (index !== -1) {
1324
+ content.included.splice(index, 1);
1325
+ }
1326
+ }
1327
+
1328
+ // we don't notify in the included case because this is not reactively
1329
+ // exposed. We should possibly consider doing so though for subscribers
1330
+ }
1331
+ }
1332
+ function areAllModelsUnloaded(wrapper, identifiers) {
1333
+ for (let i = 0; i < identifiers.length; ++i) {
1334
+ const identifier = identifiers[i];
1335
+ if (wrapper.hasRecord(identifier)) {
1336
+ return false;
1337
+ }
1338
+ }
1339
+ return true;
1340
+ }
1341
+ function getLocalState(rel) {
1342
+ if (isBelongsTo(rel)) {
1343
+ return rel.localState ? [rel.localState] : [];
1344
+ }
1345
+ return rel.additions ? [...rel.additions] : [];
1346
+ }
1347
+ function getRemoteState(rel) {
1348
+ if (isBelongsTo(rel)) {
1349
+ return rel.remoteState ? [rel.remoteState] : [];
1350
+ }
1351
+ return rel.remoteState;
1352
+ }
1353
+ function schemaHasLegacyDefaultValueFn(schema) {
1354
+ if (!schema) return false;
1355
+ return hasLegacyDefaultValueFn(schema.options);
1356
+ }
1357
+ function hasLegacyDefaultValueFn(options) {
1358
+ return !!options && typeof options.defaultValue === 'function';
1359
+ }
1360
+ function getDefaultValue(schema, identifier, store) {
1361
+ const options = schema?.options;
1362
+ if (!schema || !options && !schema.type) {
1363
+ return;
1364
+ }
1365
+ if (schema.kind !== 'attribute' && schema.kind !== 'field') {
1366
+ return;
1367
+ }
1368
+
1369
+ // legacy support for defaultValues that are functions
1370
+ if (hasLegacyDefaultValueFn(options)) {
1371
+ // If anyone opens an issue for args not working right, we'll restore + deprecate it via a Proxy
1372
+ // that lazily instantiates the record. We don't want to provide any args here
1373
+ // because in a non @ember-data-mirror/model world they don't make sense.
1374
+ return options.defaultValue();
1375
+ // legacy support for defaultValues that are primitives
1376
+ } else if (options && 'defaultValue' in options) {
1377
+ const defaultValue = options.defaultValue;
1378
+ return defaultValue;
1379
+
1380
+ // new style transforms
1381
+ } else if (schema.kind !== 'attribute' && schema.type) {
1382
+ const transform = store.schema.transformation(schema);
1383
+ if (transform?.defaultValue) {
1384
+ return transform.defaultValue(options || null, identifier);
1385
+ }
1386
+ }
1387
+ }
1388
+ function notifyAttributes(storeWrapper, identifier, keys) {
1389
+ if (!keys) {
1390
+ storeWrapper.notifyChange(identifier, 'attributes', null);
1391
+ return;
1392
+ }
1393
+ for (const key of keys) {
1394
+ storeWrapper.notifyChange(identifier, 'attributes', key);
1395
+ }
1396
+ }
1397
+
1398
+ /*
1399
+ TODO @deprecate IGOR DAVID
1400
+ There seems to be a potential bug here, where we will return keys that are not
1401
+ in the schema
1402
+ */
1403
+ function calculateChangedKeys(cached, updates, fields) {
1404
+ const changedKeys = new Set();
1405
+ const keys = Object.keys(updates);
1406
+ const length = keys.length;
1407
+ const localAttrs = cached.localAttrs;
1408
+ const original = Object.assign(Object.create(null), cached.remoteAttrs, cached.inflightAttrs);
1409
+ for (let i = 0; i < length; i++) {
1410
+ const key = keys[i];
1411
+ if (!fields.has(key)) {
1412
+ continue;
1413
+ }
1414
+ const value = updates[key];
1415
+
1416
+ // A value in localAttrs means the user has a local change to
1417
+ // this attribute. We never override this value when merging
1418
+ // updates from the backend so we should not sent a change
1419
+ // notification if the server value differs from the original.
1420
+ if (localAttrs && localAttrs[key] !== undefined) {
1421
+ continue;
1422
+ }
1423
+ if (original[key] !== value) {
1424
+ changedKeys.add(key);
1425
+ }
1426
+ }
1427
+ return changedKeys;
1428
+ }
1429
+ function cacheIsEmpty(cached) {
1430
+ return !cached || cached.remoteAttrs === null && cached.inflightAttrs === null && cached.localAttrs === null;
1431
+ }
1432
+ function _isEmpty(peeked) {
1433
+ if (!peeked) {
1434
+ return true;
1435
+ }
1436
+ const isNew = peeked.isNew;
1437
+ const isDeleted = peeked.isDeleted;
1438
+ const isEmpty = cacheIsEmpty(peeked);
1439
+ return (!isNew || isDeleted) && isEmpty;
1440
+ }
1441
+ function recordIsLoaded(cached, filterDeleted = false) {
1442
+ if (!cached) {
1443
+ return false;
1444
+ }
1445
+ const isNew = cached.isNew;
1446
+ const isEmpty = cacheIsEmpty(cached);
1447
+
1448
+ // if we are new we must consider ourselves loaded
1449
+ if (isNew) {
1450
+ return !cached.isDeleted;
1451
+ }
1452
+ // even if we have a past request, if we are now empty we are not loaded
1453
+ // typically this is true after an unloadRecord call
1454
+
1455
+ // if we are not empty, not new && we have a fulfilled request then we are loaded
1456
+ // we should consider allowing for something to be loaded that is simply "not empty".
1457
+ // which is how RecordState currently handles this case; however, RecordState is buggy
1458
+ // in that it does not account for unloading.
1459
+ return filterDeleted && cached.isDeletionCommitted ? false : !isEmpty;
1460
+ }
1461
+ function _isLoading(peeked, capabilities, identifier) {
1462
+ assertPrivateCapabilities(capabilities);
1463
+ // TODO refactor things such that the cache is not required to know
1464
+ // about isLoading
1465
+ const req = capabilities._store.getRequestStateService();
1466
+ // const fulfilled = req.getLastRequestForRecord(identifier);
1467
+ const isLoaded = recordIsLoaded(peeked);
1468
+ return !isLoaded &&
1469
+ // fulfilled === null &&
1470
+ req.getPendingRequestsForRecord(identifier).some(r => r.type === 'query');
1471
+ }
1472
+ function setupRelationships(graph, fields, identifier, data) {
1473
+ for (const name in data.relationships) {
1474
+ const relationshipData = data.relationships[name];
1475
+ const field = fields.get(name);
1476
+ // TODO consider asserting if the relationship is not in the schema
1477
+ // we intentionally ignore relationships that are not in the schema
1478
+ if (!relationshipData || !field || !isRelationship(field)) continue;
1479
+ graph.push({
1480
+ op: 'updateRelationship',
1481
+ record: identifier,
1482
+ field: name,
1483
+ value: relationshipData
1484
+ });
1485
+ }
1486
+ }
1487
+ function isRelationship(field) {
1488
+ const {
1489
+ kind
1490
+ } = field;
1491
+ return kind === 'hasMany' || kind === 'belongsTo' || kind === 'resource' || kind === 'collection';
1492
+ }
1493
+ function patchLocalAttributes(cached, changedRemoteKeys) {
1494
+ const {
1495
+ localAttrs,
1496
+ remoteAttrs,
1497
+ inflightAttrs,
1498
+ defaultAttrs,
1499
+ changes
1500
+ } = cached;
1501
+ if (!localAttrs) {
1502
+ cached.changes = null;
1503
+ return false;
1504
+ }
1505
+ let hasAppliedPatch = false;
1506
+ const mutatedKeys = Object.keys(localAttrs);
1507
+ for (let i = 0, length = mutatedKeys.length; i < length; i++) {
1508
+ const attr = mutatedKeys[i];
1509
+ const existing = inflightAttrs && attr in inflightAttrs ? inflightAttrs[attr] : remoteAttrs && attr in remoteAttrs ? remoteAttrs[attr] : undefined;
1510
+ if (existing === localAttrs[attr]) {
1511
+ hasAppliedPatch = true;
1512
+
1513
+ // if the local change is committed, then
1514
+ // the remoteKeyChange is no longer relevant
1515
+ changedRemoteKeys?.delete(attr);
1516
+ delete localAttrs[attr];
1517
+ delete changes[attr];
1518
+ }
1519
+ if (defaultAttrs && attr in defaultAttrs) {
1520
+ delete defaultAttrs[attr];
1521
+ }
1522
+ }
1523
+ return hasAppliedPatch;
1524
+ }
1525
+ function putOne(cache, identifiers, resource) {
1526
+ let identifier = identifiers.peekResourceKey(resource);
1527
+ if (identifier) {
1528
+ identifier = identifiers.updateRecordIdentifier(identifier, resource);
1529
+ } else {
1530
+ identifier = identifiers.getOrCreateRecordIdentifier(resource);
1531
+ }
1532
+ cache.upsert(identifier, resource, cache._capabilities.hasRecord(identifier));
1533
+ // even if the identifier was not "existing" before, it is now
1534
+ return identifier;
1535
+ }
1536
+
1537
+ /*
1538
+ Iterates over the set of internal models reachable from `this` across exactly one
1539
+ relationship.
1540
+ */
1541
+ function _directlyRelatedIdentifiersIterable(storeWrapper, originating) {
1542
+ const graph = peekGraph(storeWrapper);
1543
+ const initializedRelationships = graph?.identifiers.get(originating);
1544
+ if (!initializedRelationships) {
1545
+ return EMPTY_ITERATOR;
1546
+ }
1547
+ const initializedRelationshipsArr = [];
1548
+ Object.keys(initializedRelationships).forEach(key => {
1549
+ const rel = initializedRelationships[key];
1550
+ if (rel && !isImplicit(rel)) {
1551
+ initializedRelationshipsArr.push(rel);
1552
+ }
1553
+ });
1554
+ let i = 0;
1555
+ let j = 0;
1556
+ let k = 0;
1557
+ const findNext = () => {
1558
+ while (i < initializedRelationshipsArr.length) {
1559
+ while (j < 2) {
1560
+ const relatedIdentifiers = j === 0 ? getLocalState(initializedRelationshipsArr[i]) : getRemoteState(initializedRelationshipsArr[i]);
1561
+ while (k < relatedIdentifiers.length) {
1562
+ const relatedIdentifier = relatedIdentifiers[k++];
1563
+ if (relatedIdentifier !== null) {
1564
+ return relatedIdentifier;
1565
+ }
1566
+ }
1567
+ k = 0;
1568
+ j++;
1569
+ }
1570
+ j = 0;
1571
+ i++;
1572
+ }
1573
+ return undefined;
1574
+ };
1575
+ return {
1576
+ iterator() {
1577
+ return {
1578
+ next: () => {
1579
+ const value = findNext();
1580
+ return {
1581
+ value,
1582
+ done: value === undefined
1583
+ };
1584
+ }
1585
+ };
1586
+ }
1587
+ };
1588
+ }
1589
+
1590
+ /*
1591
+ Computes the set of Identifiers reachable from this Identifier.
1592
+
1593
+ Reachability is determined over the relationship graph (ie a graph where
1594
+ nodes are identifiers and edges are belongs to or has many
1595
+ relationships).
1596
+
1597
+ Returns an array including `this` and all identifiers reachable
1598
+ from `this.identifier`.
1599
+ */
1600
+ function _allRelatedIdentifiers(storeWrapper, originating) {
1601
+ const array = [];
1602
+ const queue = [];
1603
+ const seen = new Set();
1604
+ queue.push(originating);
1605
+ while (queue.length > 0) {
1606
+ const identifier = queue.shift();
1607
+ array.push(identifier);
1608
+ seen.add(identifier);
1609
+ const iterator = _directlyRelatedIdentifiersIterable(storeWrapper, originating).iterator();
1610
+ for (let obj = iterator.next(); !obj.done; obj = iterator.next()) {
1611
+ const relatedIdentifier = obj.value;
1612
+ if (relatedIdentifier && !seen.has(relatedIdentifier)) {
1613
+ seen.add(relatedIdentifier);
1614
+ queue.push(relatedIdentifier);
1615
+ }
1616
+ }
1617
+ }
1618
+ return array;
1619
+ }
1620
+ function fromBaseDocument(doc) {
1621
+ const resourceDocument = {};
1622
+ const jsonApiDoc = doc.content;
1623
+ if (jsonApiDoc) {
1624
+ copyLinksAndMeta(resourceDocument, jsonApiDoc);
1625
+ }
1626
+ return resourceDocument;
1627
+ }
1628
+ function fromStructuredError(doc) {
1629
+ const errorDoc = {};
1630
+ if (doc.content) {
1631
+ copyLinksAndMeta(errorDoc, doc.content);
1632
+ if ('errors' in doc.content) {
1633
+ errorDoc.errors = doc.content.errors;
1634
+ } else if (typeof doc.error === 'object' && 'errors' in doc.error) {
1635
+ errorDoc.errors = doc.error.errors;
1636
+ } else {
1637
+ errorDoc.errors = [{
1638
+ title: doc.message
1639
+ }];
1640
+ }
1641
+ }
1642
+ return errorDoc;
1643
+ }
1644
+ function copyLinksAndMeta(target, source) {
1645
+ if ('links' in source) {
1646
+ target.links = source.links;
1647
+ }
1648
+ if ('meta' in source) {
1649
+ target.meta = source.meta;
1650
+ }
1651
+ }
1652
+ function cacheUpsert(cache, identifier, data, calculateChanges) {
1653
+ let changedKeys;
1654
+ const peeked = cache.__safePeek(identifier, false);
1655
+ const existed = !!peeked;
1656
+ const cached = peeked || cache._createCache(identifier);
1657
+ const isLoading = /*#__NOINLINE__*/_isLoading(peeked, cache._capabilities, identifier) || !recordIsLoaded(peeked);
1658
+ const isUpdate = /*#__NOINLINE__*/!_isEmpty(peeked) && !isLoading;
1659
+ if (cached.isNew) {
1660
+ cached.isNew = false;
1661
+ cache._capabilities.notifyChange(identifier, 'identity', null);
1662
+ cache._capabilities.notifyChange(identifier, 'state', null);
1663
+ }
1664
+ const fields = getCacheFields(cache, identifier);
1665
+
1666
+ // if no cache entry existed, no record exists / property has been accessed
1667
+ // and thus we do not need to notify changes to any properties.
1668
+ if (calculateChanges && existed && data.attributes) {
1669
+ changedKeys = calculateChangedKeys(cached, data.attributes, fields);
1670
+ }
1671
+ cached.remoteAttrs = Object.assign(cached.remoteAttrs || Object.create(null), data.attributes);
1672
+ if (cached.localAttrs) {
1673
+ if (patchLocalAttributes(cached, changedKeys)) {
1674
+ cache._capabilities.notifyChange(identifier, 'state', null);
1675
+ }
1676
+ }
1677
+ if (!isUpdate) {
1678
+ cache._capabilities.notifyChange(identifier, 'added', null);
1679
+ }
1680
+ if (data.id) {
1681
+ cached.id = data.id;
1682
+ }
1683
+ if (data.relationships) {
1684
+ setupRelationships(cache.__graph, fields, identifier, data);
1685
+ }
1686
+ if (changedKeys?.size) {
1687
+ notifyAttributes(cache._capabilities, identifier, changedKeys);
1688
+ }
1689
+ return changedKeys?.size ? Array.from(changedKeys) : undefined;
1690
+ }
1691
+ function patchCache(Cache, op) {
1692
+ const isRecord = isResourceKey(op.record);
1693
+ !isRecord && isRequestKey(op.record);
1694
+ switch (op.op) {
1695
+ case 'mergeIdentifiers':
1696
+ {
1697
+ const cache = Cache.__cache.get(op.record);
1698
+ if (cache) {
1699
+ Cache.__cache.set(op.value, cache);
1700
+ Cache.__cache.delete(op.record);
1701
+ }
1702
+ Cache.__graph.update(op, true);
1703
+ break;
1704
+ }
1705
+ case 'update':
1706
+ {
1707
+ if (isRecord) {
1708
+ if ('field' in op) {
1709
+ const field = getCacheFields(Cache, op.record).get(op.field);
1710
+ if (isRelationship(field)) {
1711
+ Cache.__graph.push(op);
1712
+ } else {
1713
+ Cache.upsert(op.record, {
1714
+ type: op.record.type,
1715
+ id: op.record.id,
1716
+ attributes: {
1717
+ [op.field]: op.value
1718
+ }
1719
+ }, Cache._capabilities.hasRecord(op.record));
1720
+ }
1721
+ } else {
1722
+ Cache.upsert(op.record, op.value, Cache._capabilities.hasRecord(op.record));
1723
+ }
1724
+ }
1725
+ break;
1726
+ }
1727
+ case 'add':
1728
+ {
1729
+ if (isRecord) {
1730
+ if ('field' in op) {
1731
+ Cache.__graph.push(op);
1732
+ } else {
1733
+ Cache.upsert(op.record, op.value, Cache._capabilities.hasRecord(op.record));
1734
+ }
1735
+ } else {
1736
+ addResourceToDocument(Cache, op);
1737
+ }
1738
+ break;
1739
+ }
1740
+ case 'remove':
1741
+ {
1742
+ if (isRecord) {
1743
+ if ('field' in op) {
1744
+ Cache.__graph.push(op);
1745
+ } else {
1746
+ const cached = Cache.__safePeek(op.record, false);
1747
+ if (cached) {
1748
+ cached.isDeleted = true;
1749
+ cached.isDeletionCommitted = true;
1750
+ Cache.unloadRecord(op.record);
1751
+ } else {
1752
+ peekGraph(Cache._capabilities)?.push({
1753
+ op: 'deleteRecord',
1754
+ record: op.record,
1755
+ isNew: false
1756
+ });
1757
+ }
1758
+ }
1759
+ } else {
1760
+ if ('field' in op) {
1761
+ removeResourceFromDocument(Cache, op);
1762
+ }
1763
+ }
1764
+ break;
1765
+ }
1766
+ }
1767
+ }
1768
+ function getCacheFields(cache, identifier) {
1769
+ if (cache._capabilities.schema.cacheFields) {
1770
+ return cache._capabilities.schema.cacheFields(identifier);
1771
+ }
1772
+
1773
+ // the model schema service cannot process fields that are not cache fields
1774
+ return cache._capabilities.schema.fields(identifier);
1775
+ }
1776
+ function commitDidError(cache, identifier, errors) {
1777
+ const cached = cache.__peek(identifier, false);
1778
+ if (cached.inflightAttrs) {
1779
+ const keys = Object.keys(cached.inflightAttrs);
1780
+ if (keys.length > 0) {
1781
+ const attrs = cached.localAttrs = cached.localAttrs || Object.create(null);
1782
+ for (let i = 0; i < keys.length; i++) {
1783
+ if (attrs[keys[i]] === undefined) {
1784
+ attrs[keys[i]] = cached.inflightAttrs[keys[i]];
1785
+ }
1786
+ }
1787
+ }
1788
+ cached.inflightAttrs = null;
1789
+ }
1790
+ if (errors) {
1791
+ cached.errors = errors;
1792
+ }
1793
+ cache._capabilities.notifyChange(identifier, 'errors', null);
1794
+ }
1795
+ function didCommit(cache, committedIdentifier, data, op) {
1796
+ const {
1797
+ cacheKeyManager
1798
+ } = cache._capabilities;
1799
+ const existingId = committedIdentifier.id;
1800
+ const identifier = op !== 'deleteRecord' && data ? cacheKeyManager.updateRecordIdentifier(committedIdentifier, data) : committedIdentifier;
1801
+ const cached = cache.__peek(identifier, false);
1802
+ if (cached.isDeleted || op === 'deleteRecord') {
1803
+ cache.__graph.push({
1804
+ op: 'deleteRecord',
1805
+ record: identifier,
1806
+ isNew: false
1807
+ });
1808
+ cached.isDeleted = true;
1809
+ cached.isDeletionCommitted = true;
1810
+ cache._capabilities.notifyChange(identifier, 'removed', null);
1811
+ // TODO @runspired should we early exit here?
1812
+ }
1813
+ const fields = getCacheFields(cache, identifier);
1814
+ cached.isNew = false;
1815
+ let newCanonicalAttributes;
1816
+ if (data) {
1817
+ if (data.id && !cached.id) {
1818
+ cached.id = data.id;
1819
+ }
1820
+ if (identifier === committedIdentifier && identifier.id !== existingId) {
1821
+ cache._capabilities.notifyChange(identifier, 'identity', null);
1822
+ }
1823
+ if (data.relationships) {
1824
+ setupRelationships(cache.__graph, fields, identifier, data);
1825
+ }
1826
+ newCanonicalAttributes = data.attributes;
1827
+ }
1828
+ const changedKeys = newCanonicalAttributes && calculateChangedKeys(cached, newCanonicalAttributes, fields);
1829
+ cached.remoteAttrs = Object.assign(cached.remoteAttrs || Object.create(null), cached.inflightAttrs, newCanonicalAttributes);
1830
+ cached.inflightAttrs = null;
1831
+ patchLocalAttributes(cached, changedKeys);
1832
+ if (cached.errors) {
1833
+ cached.errors = null;
1834
+ cache._capabilities.notifyChange(identifier, 'errors', null);
1835
+ }
1836
+ if (changedKeys?.size) notifyAttributes(cache._capabilities, identifier, changedKeys);
1837
+ cache._capabilities.notifyChange(identifier, 'state', null);
1838
+ }
1839
+ function willCommit(cache, identifier) {
1840
+ const cached = cache.__peek(identifier, false);
1841
+
1842
+ /*
1843
+ if we have multiple saves in flight at once then
1844
+ we have information loss no matter what. This
1845
+ attempts to lose the least information.
1846
+ If we were to clear inflightAttrs, previous requests
1847
+ would not be able to use it during their didCommit.
1848
+ If we upsert inflightattrs, previous requests incorrectly
1849
+ see more recent inflight changes as part of their own and
1850
+ will incorrectly mark the new state as the correct remote state.
1851
+ We choose this latter behavior to avoid accidentally removing
1852
+ earlier changes.
1853
+ If apps do not want this behavior they can either
1854
+ - chain save requests serially vs allowing concurrent saves
1855
+ - move to using a request handler that caches the inflight state
1856
+ on a per-request basis
1857
+ - change their save requests to only send a "PATCH" instead of a "PUT"
1858
+ so that only latest changes are involved in each request, and then also
1859
+ ensure that the API or their handler reflects only those changes back
1860
+ for upsert into the cache.
1861
+ */
1862
+ if (cached.inflightAttrs) {
1863
+ if (cached.localAttrs) {
1864
+ Object.assign(cached.inflightAttrs, cached.localAttrs);
1865
+ }
1866
+ } else {
1867
+ cached.inflightAttrs = cached.localAttrs;
1868
+ }
1869
+ cached.localAttrs = null;
1870
+ }
1871
+
1872
+ export { JSONAPICache };