Skip to content

Commit

Permalink
feat(cache): add peekRemoteState to cache to view remote state
Browse files Browse the repository at this point in the history
  • Loading branch information
richgt committed Jan 10, 2025
1 parent 2f7d94a commit 9859d30
Show file tree
Hide file tree
Showing 6 changed files with 295 additions and 0 deletions.
35 changes: 35 additions & 0 deletions packages/core-types/src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,41 @@ export interface Cache {
peek<T = unknown>(identifier: StableRecordIdentifier<TypeFromInstanceOrString<T>>): 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<T = unknown>(identifier: StableRecordIdentifier<TypeFromInstanceOrString<T>>): T | null;
peekRemoteState(identifier: StableDocumentIdentifier): ResourceDocument | null;

/**
* Peek the Cache for the existing request data associated with
* a cacheable request
Expand Down
Original file line number Diff line number Diff line change
@@ -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==
38 changes: 38 additions & 0 deletions packages/experiments/src/persisted-cache/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T = unknown>(identifier: StableRecordIdentifier<TypeFromInstanceOrString<T>>): 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
Expand Down
58 changes: 58 additions & 0 deletions packages/json-api/src/-private/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions packages/store/src/-private/managers/cache-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ module('Integration | @ember-data/json-api Cache.put(<CollectionDataDocument>)',
'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: {
Expand Down Expand Up @@ -195,6 +203,127 @@ module('Integration | @ember-data/json-api Cache.put(<CollectionDataDocument>)',
);
});

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({
Expand Down

0 comments on commit 9859d30

Please sign in to comment.