@warp-drive/json-api 5.9.0-beta.0 → 5.9.0-beta.2

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