From 9859d30af34694cb41867e076964c3d21a97c9b2 Mon Sep 17 00:00:00 2001 From: Rich Glazerman Date: Fri, 20 Dec 2024 12:28:43 -0500 Subject: [PATCH 1/3] 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 961a3a59977..f576804daf9 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 4f6cbeb4fea..c95686aaa60 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({ From 37af71cab764660f33a9a61f182f138d6ae1ee09 Mon Sep 17 00:00:00 2001 From: Rich Glazerman Date: Fri, 10 Jan 2025 12:07:16 -0500 Subject: [PATCH 2/3] fix lint --- packages/diagnostic/server/default-setup.js | 2 +- ....timestamp-1734550993364-0a596e1e014eb.mjs | 30 ------------------- 2 files changed, 1 insertion(+), 31 deletions(-) delete mode 100644 packages/diagnostic/vite.config.mjs.timestamp-1734550993364-0a596e1e014eb.mjs diff --git a/packages/diagnostic/server/default-setup.js b/packages/diagnostic/server/default-setup.js index e0ac49ce1f6..58b4e84fcc6 100644 --- a/packages/diagnostic/server/default-setup.js +++ b/packages/diagnostic/server/default-setup.js @@ -35,7 +35,7 @@ export default async function launchDefault(overrides = {}) { Object.assign(overrides, flags); const RETRY_TESTS = - ('retry' in overrides ? overrides.retry : (process.env.CI ?? process.env.RETRY_TESTS)) && FAILURES.length; + ('retry' in overrides ? overrides.retry : process.env.CI ?? process.env.RETRY_TESTS) && FAILURES.length; const _parallel = process.env.DIAGNOSTIC_PARALLEL && !isNaN(Number(process.env.DIAGNOSTIC_PARALLEL)) ? Number(process.env.DIAGNOSTIC_PARALLEL) diff --git a/packages/diagnostic/vite.config.mjs.timestamp-1734550993364-0a596e1e014eb.mjs b/packages/diagnostic/vite.config.mjs.timestamp-1734550993364-0a596e1e014eb.mjs deleted file mode 100644 index 4c1da781a9d..00000000000 --- a/packages/diagnostic/vite.config.mjs.timestamp-1734550993364-0a596e1e014eb.mjs +++ /dev/null @@ -1,30 +0,0 @@ -// 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== From 8130185fdf3bb540ab396ee2625ac19a9e7942c3 Mon Sep 17 00:00:00 2001 From: Rich Glazerman Date: Fri, 10 Jan 2025 12:10:25 -0500 Subject: [PATCH 3/3] update comment --- packages/core-types/src/cache.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core-types/src/cache.ts b/packages/core-types/src/cache.ts index 295099a11c0..715425c3f15 100644 --- a/packages/core-types/src/cache.ts +++ b/packages/core-types/src/cache.ts @@ -142,7 +142,9 @@ export interface Cache { peek(identifier: StableDocumentIdentifier): ResourceDocument | null; /** - * Peek resource data from the Cache. + * Peek remote resource data from the Cache. + * + * This will give the data provided from the server without any local changes. * * In development, if the return value * is JSON the return value