Skip to content

Commit

Permalink
feat(cache): add peekRemoteState to cache to view remote state (#9624)
Browse files Browse the repository at this point in the history
* feat(cache): add peekRemoteState to cache to view remote state

* fix lint

* update comment

* commit working state

* Add test for getRemoteRelationship

* lint fixes

* another change to expected docs

* prettier fix

* Test fixes
  • Loading branch information
richgt authored Feb 21, 2025
1 parent 1c214fd commit 0badb69
Show file tree
Hide file tree
Showing 14 changed files with 604 additions and 8 deletions.
63 changes: 63 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 Expand Up @@ -341,6 +378,17 @@ export interface Cache {
*/
getAttr(identifier: StableRecordIdentifier, field: string | string[]): Value | undefined;

/**
* Retrieve remote state without any local changes for a specific attribute
*
* @method getRemoteAttr
* @public
* @param identifier
* @param field
* @return {unknown}
*/
getRemoteAttr(identifier: StableRecordIdentifier, field: string | string[]): Value | undefined;

/**
* Mutate the data for an attribute in the cache
*
Expand Down Expand Up @@ -460,6 +508,21 @@ export interface Cache {
isCollection?: boolean
): ResourceRelationship | CollectionRelationship;

/**
* Query the cache for the server state of a relationship property without any local changes
*
* @method getRelationship
* @public
* @param {StableRecordIdentifier} identifier
* @param {string} field
* @return resource relationship object
*/
getRemoteRelationship(
identifier: StableRecordIdentifier,
field: string,
isCollection?: boolean
): ResourceRelationship | CollectionRelationship;

// Resource State
// ===============

Expand Down
68 changes: 68 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 Expand Up @@ -374,6 +412,19 @@ export class PersistedCache implements Cache {
return this._cache.getAttr(identifier, field);
}

/**
* Retrieve the remote state for an attribute from the cache
*
* @method getAttr
* @internal
* @param identifier
* @param propertyName
* @return {unknown}
*/
getRemoteAttr(identifier: StableRecordIdentifier, field: string): Value | undefined {
return this._cache.getRemoteAttr(identifier, field);
}

/**
* Mutate the data for an attribute in the cache
*
Expand Down Expand Up @@ -502,6 +553,23 @@ export class PersistedCache implements Cache {
return this._cache.getRelationship(identifier, field, isCollection);
}

/**
* Query the remote state for the current state of a relationship property
*
* @method getRelationship
* @internal
* @param identifier
* @param propertyName
* @return resource relationship object
*/
getRemoteRelationship(
identifier: StableRecordIdentifier,
field: string,
isCollection?: boolean
): ResourceRelationship | CollectionRelationship {
return this._cache.getRemoteRelationship(identifier, field, isCollection);
}

// Resource State
// ===============

Expand Down
7 changes: 5 additions & 2 deletions packages/graph/src/-private/edges/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,15 @@ export function createCollectionEdge(definition: UpgradedMeta, identifier: Stabl
};
}

export function legacyGetCollectionRelationshipData(source: CollectionEdge): CollectionRelationship {
export function legacyGetCollectionRelationshipData(
source: CollectionEdge,
getRemoteState: boolean
): CollectionRelationship {
source.accessed = true;
const payload: CollectionRelationship = {};

if (source.state.hasReceivedData) {
payload.data = computeLocalState(source);
payload.data = getRemoteState ? source.remoteState.slice() : computeLocalState(source);
}

if (source.links) {
Expand Down
9 changes: 6 additions & 3 deletions packages/graph/src/-private/edges/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,19 @@ export function createResourceEdge(definition: UpgradedMeta, identifier: StableR
};
}

export function legacyGetResourceRelationshipData(source: ResourceEdge): ResourceRelationship {
export function legacyGetResourceRelationshipData(source: ResourceEdge, getRemoteState: boolean): ResourceRelationship {
source.accessed = true;
let data: StableRecordIdentifier | null | undefined;
const payload: ResourceRelationship = {};
if (source.localState) {
if (getRemoteState && source.remoteState) {
data = source.remoteState;
} else if (!getRemoteState && source.localState) {
data = source.localState;
}
if (source.localState === null && source.state.hasReceivedData) {
if (((getRemoteState && source.remoteState === null) || source.localState === null) && source.state.hasReceivedData) {
data = null;
}

if (source.links) {
payload.links = source.links;
}
Expand Down
21 changes: 18 additions & 3 deletions packages/graph/src/-private/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,25 @@ export class Graph {
assert(`Cannot getData() on an implicit relationship`, !isImplicit(relationship));

if (isBelongsTo(relationship)) {
return legacyGetResourceRelationshipData(relationship);
return legacyGetResourceRelationshipData(relationship, false);
}

return legacyGetCollectionRelationshipData(relationship);
return legacyGetCollectionRelationshipData(relationship, false);
}

getRemoteData(
identifier: StableRecordIdentifier,
propertyName: string
): ResourceRelationship | CollectionRelationship {
const relationship = this.get(identifier, propertyName);

assert(`Cannot getRemoteData() on an implicit relationship`, !isImplicit(relationship));

if (isBelongsTo(relationship)) {
return legacyGetResourceRelationshipData(relationship, true);
}

return legacyGetCollectionRelationshipData(relationship, true);
}

/*
Expand Down Expand Up @@ -334,7 +349,7 @@ export class Graph {
additions: new Set(relationship.additions),
removals: new Set(relationship.removals),
remoteState: relationship.remoteState,
localState: legacyGetCollectionRelationshipData(relationship).data || [],
localState: legacyGetCollectionRelationshipData(relationship, false).data || [],
reordered,
});
}
Expand Down
122 changes: 122 additions & 0 deletions packages/json-api/src/-private/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,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 Expand Up @@ -1176,6 +1234,63 @@ export default class JSONAPICache implements Cache {
return current;
}

getRemoteAttr(identifier: StableRecordIdentifier, attr: string | string[]): Value | undefined {
const isSimplePath = !Array.isArray(attr) || attr.length === 1;
if (Array.isArray(attr) && attr.length === 1) {
attr = attr[0];
}

if (isSimplePath) {
const attribute = attr as string;
const cached = this.__peek(identifier, true);
assert(
`Cannot retrieve remote attributes for identifier ${String(identifier)} as it is not present in the cache`,
cached
);

// in Prod we try to recover when accessing something that
// doesn't exist
if (!cached) {
return undefined;
}

if (cached.remoteAttrs && attribute in cached.remoteAttrs) {
return cached.remoteAttrs[attribute];
} else if (cached.defaultAttrs && attribute in cached.defaultAttrs) {
return cached.defaultAttrs[attribute];
} else {
const attrSchema = this._capabilities.schema.fields(identifier).get(attribute);

upgradeCapabilities(this._capabilities);
const defaultValue = getDefaultValue(attrSchema, identifier, this._capabilities._store);
if (schemaHasLegacyDefaultValueFn(attrSchema)) {
cached.defaultAttrs = cached.defaultAttrs || (Object.create(null) as Record<string, Value>);
cached.defaultAttrs[attribute] = defaultValue;
}
return defaultValue;
}
}

// TODO @runspired consider whether we need a defaultValue cache in SchemaRecord
// like we do for the simple case above.
const path: string[] = attr as string[];
const cached = this.__peek(identifier, true);
const basePath = path[0];
let current = cached.remoteAttrs && basePath in cached.remoteAttrs ? cached.remoteAttrs[basePath] : undefined;

if (current === undefined) {
return undefined;
}

for (let i = 1; i < path.length; i++) {
current = (current as ObjectValue)[path[i]];
if (current === undefined) {
return undefined;
}
}
return current;
}

/**
* Mutate the data for an attribute in the cache
*
Expand Down Expand Up @@ -1471,6 +1586,13 @@ export default class JSONAPICache implements Cache {
return this.__graph.getData(identifier, field);
}

getRemoteRelationship(
identifier: StableRecordIdentifier,
field: string
): ResourceRelationship | CollectionRelationship {
return this.__graph.getRemoteData(identifier, field);
}

// Resource State
// ===============

Expand Down
Loading

0 comments on commit 0badb69

Please sign in to comment.