Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cache): add peekRemoteState to cache to view remote state #9624

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions packages/core-types/src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,43 @@ export interface Cache {
peek<T = unknown>(identifier: StableRecordIdentifier<TypeFromInstanceOrString<T>>): T | null;
peek(identifier: StableDocumentIdentifier): ResourceDocument | null;

/**
* 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
* 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
2 changes: 1 addition & 1 deletion packages/diagnostic/server/default-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
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
Loading