From 91f47bba5c0968e76a03513260500ad8f397b4cd Mon Sep 17 00:00:00 2001 From: Rich Glazerman Date: Fri, 20 Dec 2024 12:28:43 -0500 Subject: [PATCH] feat(cache): add peekRemoteState to cache to view remote state --- packages/core-types/src/cache.ts | 35 +++++ ....timestamp-1734550993364-0a596e1e014eb.mjs | 30 ++++ .../experiments/src/persisted-cache/cache.ts | 38 ++++++ packages/json-api/src/-private/cache.ts | 58 ++++++++ .../src/-private/managers/cache-manager.ts | 5 + .../cache/collection-data-documents-test.ts | 129 ++++++++++++++++++ 6 files changed, 295 insertions(+) create mode 100644 packages/diagnostic/vite.config.mjs.timestamp-1734550993364-0a596e1e014eb.mjs diff --git a/packages/core-types/src/cache.ts b/packages/core-types/src/cache.ts index a219c3b4f81..295099a11c0 100644 --- a/packages/core-types/src/cache.ts +++ b/packages/core-types/src/cache.ts @@ -141,6 +141,41 @@ export interface Cache { peek(identifier: StableRecordIdentifier>): T | null; peek(identifier: StableDocumentIdentifier): ResourceDocument | null; + /** + * Peek resource data from the Cache. + * + * In development, if the return value + * is JSON the return value + * will be deep-cloned and deep-frozen + * to prevent mutation thereby enforcing cache + * Immutability. + * + * This form of peek is useful for implementations + * that want to feed raw-data from cache to the UI + * or which want to interact with a blob of data + * directly from the presentation cache. + * + * An implementation might want to do this because + * de-referencing records which read from their own + * blob is generally safer because the record does + * not require retainining connections to the Store + * and Cache to present data on a per-field basis. + * + * This generally takes the place of `getAttr` as + * an API and may even take the place of `getRelationship` + * depending on implementation specifics, though this + * latter usage is less recommended due to the advantages + * of the Graph handling necessary entanglements and + * notifications for relational data. + * + * @method peek + * @public + * @param {StableRecordIdentifier | StableDocumentIdentifier} identifier + * @return {ResourceDocument | ResourceBlob | null} the known resource data + */ + peekRemoteState(identifier: StableRecordIdentifier>): T | null; + peekRemoteState(identifier: StableDocumentIdentifier): ResourceDocument | null; + /** * Peek the Cache for the existing request data associated with * a cacheable request diff --git a/packages/diagnostic/vite.config.mjs.timestamp-1734550993364-0a596e1e014eb.mjs b/packages/diagnostic/vite.config.mjs.timestamp-1734550993364-0a596e1e014eb.mjs new file mode 100644 index 00000000000..4c1da781a9d --- /dev/null +++ b/packages/diagnostic/vite.config.mjs.timestamp-1734550993364-0a596e1e014eb.mjs @@ -0,0 +1,30 @@ +// vite.config.mjs +import { keepAssets } from "file:///Users/rglazerman/Development/ember-data/config/vite/keep-assets.js"; +import { createConfig } from "file:///Users/rglazerman/Development/ember-data/config/vite/config.js"; +var externals = [ + "@ember/runloop", + "@ember/test-helpers", + "ember-cli-test-loader/test-support/index", + "@glimmer/manager" +]; +var entryPoints = [ + "./src/index.ts", + "./src/reporters/dom.ts", + "./src/runners/dom.ts", + "./src/ember.ts", + "./src/-types.ts" +]; +var vite_config_default = createConfig( + { + entryPoints, + externals, + plugins: [keepAssets({ from: "src", include: ["./styles/**/*.css"], dist: "dist" })] + }, + import.meta.resolve +); +export { + vite_config_default as default, + entryPoints, + externals +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcubWpzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiL1VzZXJzL3JnbGF6ZXJtYW4vRGV2ZWxvcG1lbnQvZW1iZXItZGF0YS9wYWNrYWdlcy9kaWFnbm9zdGljXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvVXNlcnMvcmdsYXplcm1hbi9EZXZlbG9wbWVudC9lbWJlci1kYXRhL3BhY2thZ2VzL2RpYWdub3N0aWMvdml0ZS5jb25maWcubWpzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9Vc2Vycy9yZ2xhemVybWFuL0RldmVsb3BtZW50L2VtYmVyLWRhdGEvcGFja2FnZXMvZGlhZ25vc3RpYy92aXRlLmNvbmZpZy5tanNcIjtpbXBvcnQgeyBrZWVwQXNzZXRzIH0gZnJvbSAnQHdhcnAtZHJpdmUvaW50ZXJuYWwtY29uZmlnL3ZpdGUva2VlcC1hc3NldHMnO1xuaW1wb3J0IHsgY3JlYXRlQ29uZmlnIH0gZnJvbSAnQHdhcnAtZHJpdmUvaW50ZXJuYWwtY29uZmlnL3ZpdGUvY29uZmlnLmpzJztcblxuZXhwb3J0IGNvbnN0IGV4dGVybmFscyA9IFtcbiAgJ0BlbWJlci9ydW5sb29wJyxcbiAgJ0BlbWJlci90ZXN0LWhlbHBlcnMnLFxuICAnZW1iZXItY2xpLXRlc3QtbG9hZGVyL3Rlc3Qtc3VwcG9ydC9pbmRleCcsXG4gICdAZ2xpbW1lci9tYW5hZ2VyJyxcbl07XG5leHBvcnQgY29uc3QgZW50cnlQb2ludHMgPSBbXG4gICcuL3NyYy9pbmRleC50cycsXG4gICcuL3NyYy9yZXBvcnRlcnMvZG9tLnRzJyxcbiAgJy4vc3JjL3J1bm5lcnMvZG9tLnRzJyxcbiAgJy4vc3JjL2VtYmVyLnRzJyxcbiAgJy4vc3JjLy10eXBlcy50cycsXG5dO1xuXG5leHBvcnQgZGVmYXVsdCBjcmVhdGVDb25maWcoXG4gIHtcbiAgICBlbnRyeVBvaW50cyxcbiAgICBleHRlcm5hbHMsXG4gICAgcGx1Z2luczogW2tlZXBBc3NldHMoeyBmcm9tOiAnc3JjJywgaW5jbHVkZTogWycuL3N0eWxlcy8qKi8qLmNzcyddLCBkaXN0OiAnZGlzdCcgfSldLFxuICB9LFxuICBpbXBvcnQubWV0YS5yZXNvbHZlXG4pO1xuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUF3VyxTQUFTLGtCQUFrQjtBQUNuWSxTQUFTLG9CQUFvQjtBQUV0QixJQUFNLFlBQVk7QUFBQSxFQUN2QjtBQUFBLEVBQ0E7QUFBQSxFQUNBO0FBQUEsRUFDQTtBQUNGO0FBQ08sSUFBTSxjQUFjO0FBQUEsRUFDekI7QUFBQSxFQUNBO0FBQUEsRUFDQTtBQUFBLEVBQ0E7QUFBQSxFQUNBO0FBQ0Y7QUFFQSxJQUFPLHNCQUFRO0FBQUEsRUFDYjtBQUFBLElBQ0U7QUFBQSxJQUNBO0FBQUEsSUFDQSxTQUFTLENBQUMsV0FBVyxFQUFFLE1BQU0sT0FBTyxTQUFTLENBQUMsbUJBQW1CLEdBQUcsTUFBTSxPQUFPLENBQUMsQ0FBQztBQUFBLEVBQ3JGO0FBQUEsRUFDQSxZQUFZO0FBQ2Q7IiwKICAibmFtZXMiOiBbXQp9Cg== diff --git a/packages/experiments/src/persisted-cache/cache.ts b/packages/experiments/src/persisted-cache/cache.ts index b652d230a4d..d7b6bea4d78 100644 --- a/packages/experiments/src/persisted-cache/cache.ts +++ b/packages/experiments/src/persisted-cache/cache.ts @@ -152,6 +152,44 @@ export class PersistedCache implements Cache { return this._cache.peek(identifier); } + /** + * Peek resource data from the Cache. + * + * In development, if the return value + * is JSON the return value + * will be deep-cloned and deep-frozen + * to prevent mutation thereby enforcing cache + * Immutability. + * + * This form of peek is useful for implementations + * that want to feed raw-data from cache to the UI + * or which want to interact with a blob of data + * directly from the presentation cache. + * + * An implementation might want to do this because + * de-referencing records which read from their own + * blob is generally safer because the record does + * not require retainining connections to the Store + * and Cache to present data on a per-field basis. + * + * This generally takes the place of `getAttr` as + * an API and may even take the place of `getRelationship` + * depending on implementation specifics, though this + * latter usage is less recommended due to the advantages + * of the Graph handling necessary entanglements and + * notifications for relational data. + * + * @method peek + * @internal + * @param {StableRecordIdentifier | StableDocumentIdentifier} identifier + * @returns {ResourceDocument | ResourceBlob | null} the known resource data + */ + peekRemoteState(identifier: StableRecordIdentifier>): T | null; + peekRemoteState(identifier: StableDocumentIdentifier): ResourceDocument | null; + peekRemoteState(identifier: StableRecordIdentifier | StableDocumentIdentifier): unknown { + return this._cache.peekRemoteState(identifier); + } + /** * Peek the Cache for the existing request data associated with * a cacheable request diff --git a/packages/json-api/src/-private/cache.ts b/packages/json-api/src/-private/cache.ts index 9ab95958189..f319ed6eee0 100644 --- a/packages/json-api/src/-private/cache.ts +++ b/packages/json-api/src/-private/cache.ts @@ -483,6 +483,64 @@ export default class JSONAPICache implements Cache { return null; } + peekRemoteState(identifier: StableRecordIdentifier): ResourceObject | null; + peekRemoteState(identifier: StableDocumentIdentifier): ResourceDocument | null; + peekRemoteState( + identifier: StableDocumentIdentifier | StableRecordIdentifier + ): ResourceObject | ResourceDocument | null { + if ('type' in identifier) { + const peeked = this.__safePeek(identifier, false); + + if (!peeked) { + return null; + } + + const { type, id, lid } = identifier; + const attributes = Object.assign({}, peeked.remoteAttrs) as ObjectValue; + const relationships: ResourceObject['relationships'] = {}; + + const rels = this.__graph.identifiers.get(identifier); + if (rels) { + Object.keys(rels).forEach((key) => { + const rel = rels[key]; + if (rel.definition.isImplicit) { + return; + } else { + relationships[key] = this.__graph.getData(identifier, key); + } + }); + } + + upgradeCapabilities(this._capabilities); + const store = this._capabilities._store; + const attrs = this._capabilities.schema.fields(identifier); + attrs.forEach((attr, key) => { + if (key in attributes && attributes[key] !== undefined) { + return; + } + const defaultValue = getDefaultValue(attr, identifier, store); + + if (defaultValue !== undefined) { + attributes[key] = defaultValue; + } + }); + + return { + type, + id, + lid, + attributes, + relationships, + }; + } + + const document = this.peekRequest(identifier); + + if (document) { + if ('content' in document) return document.content!; + } + return null; + } /** * Peek the Cache for the existing request data associated with * a cacheable request. diff --git a/packages/store/src/-private/managers/cache-manager.ts b/packages/store/src/-private/managers/cache-manager.ts index 8c335c2b8b8..a7f12a37b43 100644 --- a/packages/store/src/-private/managers/cache-manager.ts +++ b/packages/store/src/-private/managers/cache-manager.ts @@ -133,6 +133,11 @@ export class CacheManager implements Cache { return this.#cache.peek(identifier); } + peekRemoteState(identifier: StableRecordIdentifier): unknown; + peekRemoteState(identifier: StableDocumentIdentifier): ResourceDocument | null; + peekRemoteState(identifier: StableRecordIdentifier | StableDocumentIdentifier): unknown { + return this.#cache.peekRemoteState(identifier); + } /** * Peek the Cache for the existing request data associated with * a cacheable request diff --git a/tests/ember-data__json-api/tests/integration/cache/collection-data-documents-test.ts b/tests/ember-data__json-api/tests/integration/cache/collection-data-documents-test.ts index d99aa8a37c1..0ed1be5a263 100644 --- a/tests/ember-data__json-api/tests/integration/cache/collection-data-documents-test.ts +++ b/tests/ember-data__json-api/tests/integration/cache/collection-data-documents-test.ts @@ -159,6 +159,14 @@ module('Integration | @ember-data/json-api Cache.put()', 'Resource Blob is kept updated in the cache after mutation' ); + const remoteData = store.cache.peekRemoteState(identifier); + + assert.deepEqual( + remoteData, + { type: 'user', id: '1', lid: '@lid:user-1', attributes: { name: 'Chris' }, relationships: {} }, + 'Remote State is not updated in the cache after mutation' + ); + store.cache.put( asStructuredDocument({ content: { @@ -195,6 +203,127 @@ module('Integration | @ember-data/json-api Cache.put()', ); }); + test('object fields are accessible via `peek`', function (assert) { + const store = new TestStore(); + store.schema.registerResource({ + identity: null, + type: 'user', + fields: [ + { kind: 'attribute', name: 'name', type: null }, + { + kind: 'object', + name: 'business', + }, + ], + }); + + let responseDocument: CollectionResourceDataDocument; + store._run(() => { + responseDocument = store.cache.put( + asStructuredDocument({ + content: { + data: [ + { + type: 'user', + id: '1', + attributes: { + name: 'Chris', + business: { + name: 'My Business', + address: { street: '123 Main Street', city: 'Anytown', state: 'NY', zip: '23456' }, + }, + }, + }, + ], + }, + }) + ); + }); + const identifier = store.identifierCache.getOrCreateRecordIdentifier({ type: 'user', id: '1' }); + assert.deepEqual(responseDocument!.data, [identifier], 'We were given the correct data back'); + + let resourceData = store.cache.peek(identifier); + + assert.deepEqual(resourceData, { + type: 'user', + id: '1', + lid: '@lid:user-1', + attributes: { + name: 'Chris', + business: { + name: 'My Business', + address: { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '23456', + }, + }, + }, + relationships: {}, + }); + + const record = store.peekRecord<{ + name: string | null; + business: { address: { street: string; city: string; state: string; zip: string } }; + }>(identifier); + + assert.equal(record?.business?.address?.street, '123 Main Street', 'record name is correct'); + + store.cache.setAttr(identifier, 'business', { + name: 'My Business', + address: { street: '456 Other Street', city: 'Anytown', state: 'NY', zip: '23456' }, + }); + resourceData = store.cache.peek(identifier); + + assert.deepEqual( + resourceData, + { + type: 'user', + id: '1', + lid: '@lid:user-1', + attributes: { + name: 'Chris', + business: { + name: 'My Business', + address: { + street: '456 Other Street', + city: 'Anytown', + state: 'NY', + zip: '23456', + }, + }, + }, + relationships: {}, + }, + 'Record is accessible via peek' + ); + + const remoteData = store.cache.peekRemoteState(identifier); + assert.deepEqual( + remoteData, + { + type: 'user', + id: '1', + lid: '@lid:user-1', + attributes: { + name: 'Chris', + business: { + name: 'My Business', + address: { + street: '123 Main Street', + city: 'Anytown', + state: 'NY', + zip: '23456', + }, + }, + }, + relationships: {}, + }, + 'Remote state is not updated after setAttr' + ); + }); + test('resource relationships are accessible via `peek`', function (assert) { const store = new TestStore(); store.schema.registerResource({