diff --git a/packages/core-types/src/cache.ts b/packages/core-types/src/cache.ts index a219c3b4f81..7eb3d137861 100644 --- a/packages/core-types/src/cache.ts +++ b/packages/core-types/src/cache.ts @@ -10,7 +10,7 @@ import type { StableDocumentIdentifier, StableRecordIdentifier } from './identif import type { Value } from './json/raw'; import type { TypeFromInstanceOrString } from './record'; import type { RequestContext, StructuredDataDocument, StructuredDocument } from './request'; -import type { ResourceDocument, SingleResourceDataDocument } from './spec/document'; +import type { ResourceDocument, SingleResourceDataDocument, V3ResourceDocument } from './spec/document'; import type { ApiError } from './spec/error'; /** @@ -42,10 +42,10 @@ export type RelationshipDiff = * A Cache handles in-memory storage of Document and Resource * data. * - * @class Cache + * @class CacheV2 * @public */ -export interface Cache { +export interface CacheV2 { /** * The Cache Version that this implementation implements. * @@ -150,12 +150,26 @@ export interface Cache { * whereas `peek` will return just the `content`. * * @method peekRequest - * @param {StableDocumentIdentifier} + * @param {StableDocumentIdentifier} identifier * @return {StructuredDocument | null} * @public */ peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null; + /** + * Remove a request from the cache. + * + * This does not guarantee release of any associated resources unless the cache determines + * they are no longer reachable. + * + * This method is a candidate to become a mutation + * + * @method removeRequest + * @param {StableDocumentIdentifier} identifier + * @return {boolean} whether a request was removed + */ + removeRequest(identifier: StableDocumentIdentifier): boolean; + /** * Push resource data from a remote source into the cache for this identifier * @@ -454,11 +468,7 @@ export interface Cache { * @param {string} field * @return resource relationship object */ - getRelationship( - identifier: StableRecordIdentifier, - field: string, - isCollection?: boolean - ): ResourceRelationship | CollectionRelationship; + getRelationship(identifier: StableRecordIdentifier, field: string): ResourceRelationship | CollectionRelationship; // Resource State // =============== @@ -529,3 +539,656 @@ export interface Cache { */ isDeletionCommitted(identifier: StableRecordIdentifier): boolean; } + +/** + * The interface for EmberData Caches. + * + * A Cache handles in-memory storage of Document and Resource + * data. + * + * @class CacheV3 + * @public + */ +export interface CacheV3 { + /** + * The Cache Version that this implementation implements. + * + * @type {'3'} + * @public + * @property version + */ + version: '3'; + + // Cache Management + // ================ + + /** + * Cache the response to a request + * + * Unlike `store.push` which has UPSERT + * semantics, `put` has `replace` semantics similar to + * the `http` method `PUT` + * + * the individually cacheable resource data it may contain + * should upsert, but the document data surrounding it should + * fully replace any existing information + * + * Note that in order to support inserting arbitrary data + * to the cache that did not originate from a request `put` + * should expect to sometimes encounter a document with only + * a `content` member and therefor must not assume the existence + * of `request` and `response` on the document. + * + * @method put + * @param {StructuredDocument} doc + * @return {V3ResourceDocument} + * @public + */ + put(doc: StructuredDocument | { content: T }): V3ResourceDocument; + + /** + * Update the "remote" or "canonical" (persisted) state of the Cache + * by merging new information into the existing state. + * + * Note: currently the only valid resource operation is a MergeOperation + * which occurs when a collision of identifiers is detected. + * + * @method patch + * @public + * @param {Operation} op the operation to perform + * @return {void} + */ + patch(op: Operation): void; + + /** + * Update the "local" or "current" (unpersisted) state of the Cache + * + * @method mutate + * @param {Mutation} mutation + * @return {void} + * @public + */ + mutate(mutation: Mutation): void; + + /** + * Peek resource or document data from the Cache. + * + * In V3, this method should return remote state + * (no mutations applied). In V2 it was not specified + * whether the return should or should not include + * mutated state, and the JSONPAPICache V2 implementation + * chose to return mutated state. + * + * The V2 peek behavior is replaced by `getResource` + * + * 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 + */ + peek(identifier: StableRecordIdentifier>): T | null; + peek(identifier: StableDocumentIdentifier): ResourceDocument | null; + + /** + * Added in V3 + * + * Peek the state of a resource from the cache with any + * mutations applied. + * + * In V2 it was not specified whether the return of peek should + * or should not include mutated state, and the JSONPAPICache V2 + * implementation chose to return mutated state. This method + * replaces that use case. + * + * 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 getResource + * @public + * @param {StableRecordIdentifier} identifier + * @return {ResourceBlob | null} the known resource data + */ + getResource(identifier: StableRecordIdentifier>): T | null; + + /** + * Peek the Cache for the existing request data associated with + * a cacheable request + * + * This is effectively the reverse of `put` for a request in + * that it will return the the request, response, and content + * whereas `peek` will return just the `content`. + * + * @method peekRequest + * @param {StableDocumentIdentifier} + * @return {StructuredDocument | null} + * @public + */ + peekRequest(identifier: StableDocumentIdentifier): StructuredDocument | null; + + /** + * Push resource data from a remote source into the cache for this identifier + * + * @method upsert + * @public + * @param identifier + * @param data + * @param hasRecord + * @return {void | string[]} if `hasRecord` is true then calculated key changes should be returned + */ + upsert(identifier: StableRecordIdentifier, data: ResourceBlob, hasRecord: boolean): void | string[]; + + // Cache Forking Support + // ===================== + + /** + * Create a fork of the cache from the current state. + * + * Applications should typically not call this method themselves, + * preferring instead to fork at the Store level, which will + * utilize this method to fork the cache. + * + * @method fork + * @public + * @return Promise + */ + fork(): Promise; + + /** + * Merge a fork back into a parent Cache. + * + * Applications should typically not call this method themselves, + * preferring instead to merge at the Store level, which will + * utilize this method to merge the caches. + * + * @method merge + * @param {Cache} cache + * @public + * @return Promise + */ + merge(cache: Cache): Promise; + + /** + * Generate the list of changes applied to all + * record in the store. + * + * Each individual resource or document that has + * been mutated should be described as an individual + * `Change` entry in the returned array. + * + * A `Change` is described by an object containing up to + * three properties: (1) the `identifier` of the entity that + * changed; (2) the `op` code of that change being one of + * `upsert` or `remove`, and if the op is `upsert` a `patch` + * containing the data to merge into the cache for the given + * entity. + * + * This `patch` is opaque to the Store but should be understood + * by the Cache and may expect to be utilized by an Adapter + * when generating data during a `save` operation. + * + * It is generally recommended that the `patch` contain only + * the updated state, ignoring fields that are unchanged + * + * ```ts + * interface Change { + * identifier: StableRecordIdentifier | StableDocumentIdentifier; + * op: 'upsert' | 'remove'; + * patch?: unknown; + * } + * ``` + * + * @method diff + * @public + */ + diff(): Promise; + + // SSR Support + // =========== + + /** + * Serialize the entire contents of the Cache into a Stream + * which may be fed back into a new instance of the same Cache + * via `cache.hydrate`. + * + * @method dump + * @return {Promise} + * @public + */ + dump(): Promise>; + + /** + * hydrate a Cache from a Stream with content previously serialized + * from another instance of the same Cache, resolving when hydration + * is complete. + * + * This method should expect to be called both in the context of restoring + * the Cache during application rehydration after SSR **AND** at unknown + * times during the lifetime of an already booted application when it is + * desired to bulk-load additional information into the cache. This latter + * behavior supports optimizing pre/fetching of data for route transitions + * via data-only SSR modes. + * + * @method hydrate + * @param {ReadableStream} stream + * @return {Promise} + * @public + */ + hydrate(stream: ReadableStream): Promise; + + // Resource Support + // ================ + + /** + * [LIFECYCLE] Signal to the cache that a new record has been instantiated on the client + * + * It returns properties from options that should be set on the record during the create + * process. This return value behavior is deprecated. + * + * @method clientDidCreate + * @public + * @param identifier + * @param createArgs + */ + clientDidCreate(identifier: StableRecordIdentifier, createArgs?: Record): Record; + + /** + * [LIFECYCLE] Signals to the cache that a resource + * will be part of a save transaction. + * + * @method willCommit + * @public + * @param identifier + */ + willCommit(identifier: StableRecordIdentifier, context: RequestContext): void; + + /** + * [LIFECYCLE] Signals to the cache that a resource + * was successfully updated as part of a save transaction. + * + * @method didCommit + * @public + * @param identifier - the primary identifier that was operated on + * @param data - a document in the cache format containing any updated data + * @return {SingleResourceDataDocument} + */ + didCommit(identifier: StableRecordIdentifier, result: StructuredDataDocument): SingleResourceDataDocument; + + /** + * [LIFECYCLE] Signals to the cache that a resource + * was update via a save transaction failed. + * + * @method commitWasRejected + * @public + * @param identifier + * @param errors + */ + commitWasRejected(identifier: StableRecordIdentifier, errors?: ApiError[]): void; + + /** + * [LIFECYCLE] Signals to the cache that all data for a resource + * should be cleared. + * + * This method is a candidate to become a mutation + * + * @method unloadRecord + * @public + * @param identifier + */ + unloadRecord(identifier: StableRecordIdentifier): void; + + // Granular Resource Data APIs + // =========================== + + /** + * Retrieve the data for an attribute from the cache + * without any mutations applied. + * + * @method peekAttr + * @public + * @param identifier + * @param field + * @return {unknown} + */ + peekAttr(identifier: StableRecordIdentifier, field: string | string[]): Value | undefined; + + /** + * Retrieve the data for an attribute from the cache, with any + * mutations applied + * + * @method getAttr + * @public + * @param identifier + * @param field + * @return {unknown} + */ + getAttr(identifier: StableRecordIdentifier, field: string | string[]): Value | undefined; + + /** + * Mutate the data for an attribute in the cache + * + * This method is a candidate to become a mutation + * + * @method setAttr + * @public + * @param identifier + * @param field + * @param value + */ + setAttr(identifier: StableRecordIdentifier, field: string | string[], value: Value): void; + + /** + * Query the cache for the changes to a specific attribute of a resource. + * + * Returns a tuple of [old, new] values, or `null` if no change exists + * + * ``` + * [, ] + * ``` + * + * @method changedAttrs + * @public + * @param identifier + * @param {string} field + * @return {[unknown, unknown] | null} [, ] | null + */ + changedAttr(identifier: StableRecordIdentifier, field: string): [Value | undefined, Value] | null; + + /** + * Query the cache for the changed attributes of a resource. + * + * Returns a map of field names to tuples of [old, new] values + * + * ``` + * { : [, ] } + * ``` + * + * @method changedAttrs + * @public + * @param identifier + * @return {Record} { : [, ] } + */ + changedAttrs(identifier: StableRecordIdentifier): ChangedAttributesHash; + + /** + * Query the cache for whether any mutated attributes exist + * + * @method hasChangedAttrs + * @public + * @param identifier + * @return {boolean} + */ + hasChangedAttrs(identifier: StableRecordIdentifier): boolean; + + /** + * Tell the cache to discard any uncommitted mutations to a specific + * attribute + * + * This method is a candidate to become a mutation + * + * @method rollbackAttr + * @public + * @param identifier + * @return {boolean} whether the field was restored + */ + rollbackAttr(identifier: StableRecordIdentifier, field: string): boolean; + + /** + * Tell the cache to discard any uncommitted mutations to attributes + * + * This method is a candidate to become a mutation + * + * @method rollbackAttrs + * @public + * @param identifier + * @return {string[]} the names of fields that were restored + */ + rollbackAttrs(identifier: StableRecordIdentifier): string[]; + + /** + * Query the cache for the changes to a specific relationship of a resource. + * + * Returns a RelationshipDiff object. + * + * ```ts + * type RelationshipDiff = + | { + kind: 'collection'; + remoteState: StableRecordIdentifier[]; + additions: Set; + removals: Set; + localState: StableRecordIdentifier[]; + reordered: boolean; + } + | { + kind: 'resource'; + remoteState: StableRecordIdentifier | null; + localState: StableRecordIdentifier | null; + }; + ``` + * + * @method changedRelationships + * @public + * @param {StableRecordIdentifier} identifier + * @param {string} field + * @return {RelationshipDiff} + */ + changedRelationship(identifier: StableRecordIdentifier, field: string): RelationshipDiff; + + /** + * Query the cache for the changes to relationships of a resource. + * + * Returns a map of relationship names to RelationshipDiff objects. + * + * ```ts + * type RelationshipDiff = + | { + kind: 'collection'; + remoteState: StableRecordIdentifier[]; + additions: Set; + removals: Set; + localState: StableRecordIdentifier[]; + reordered: boolean; + } + | { + kind: 'resource'; + remoteState: StableRecordIdentifier | null; + localState: StableRecordIdentifier | null; + }; + ``` + * + * @method changedRelationships + * @public + * @param {StableRecordIdentifier} identifier + * @return {Map} + */ + changedRelationships(identifier: StableRecordIdentifier): Map; + + /** + * Query the cache for whether any mutated attributes exist + * + * @method hasChangedRelationships + * @public + * @param {StableRecordIdentifier} identifier + * @return {boolean} + */ + hasChangedRelationships(identifier: StableRecordIdentifier): boolean; + + /** + * Tell the cache to discard any uncommitted mutations to a specific relationship. + * + * This will also discard the change on any appropriate inverses. + * + * This method is a candidate to become a mutation + * + * @method rollbackRelationship + * @public + * @param {StableRecordIdentifier} identifier + * @param {field} string + * @return {boolean} whether the relationship was restored + */ + rollbackRelationship(identifier: StableRecordIdentifier, field: string): boolean; + + /** + * Tell the cache to discard any uncommitted mutations to relationships. + * + * This will also discard the change on any appropriate inverses. + * + * This method is a candidate to become a mutation + * + * @method rollbackRelationships + * @public + * @param {StableRecordIdentifier} identifier + * @return {string[]} the names of relationships that were restored + */ + rollbackRelationships(identifier: StableRecordIdentifier): string[]; + + /** + * Query the cache for the current (remote/non-mutated state of a relationship property + * + * @method getRelationship + * @public + * @param {StableRecordIdentifier} identifier + * @param {string} field + * @return resource relationship object + */ + peekRelationship(identifier: StableRecordIdentifier, field: string): ResourceRelationship | CollectionRelationship; + + /** + * Query the cache for the current (mutated) state of a relationship property + * + * @method getRelationship + * @public + * @param {StableRecordIdentifier} identifier + * @param {string} field + * @return resource relationship object + */ + getRelationship(identifier: StableRecordIdentifier, field: string): ResourceRelationship | CollectionRelationship; + + // Resource State + // =============== + + /** + * Update the cache state for the given resource to be marked + * as locally deleted, or remove such a mark. + * + * This method is a candidate to become a mutation + * + * @method setIsDeleted + * @public + * @param identifier + * @param isDeleted {boolean} + */ + setIsDeleted(identifier: StableRecordIdentifier, isDeleted: boolean): void; + + /** + * Query the cache for any validation errors applicable to the given resource. + * + * @method getErrors + * @public + * @param identifier + * @return {JsonApiError[]} + */ + getErrors(identifier: StableRecordIdentifier): ApiError[]; + + /** + * Query the cache for whether a given resource has any available data + * + * @method isEmpty + * @public + * @param identifier + * @return {boolean} + */ + isEmpty(identifier: StableRecordIdentifier): boolean; + + /** + * Query the cache for whether a given resource was created locally and not + * yet persisted. + * + * @method isNew + * @public + * @param identifier + * @return {boolean} + */ + isNew(identifier: StableRecordIdentifier): boolean; + + /** + * Query the cache for whether a given resource is marked as deleted (but not + * necessarily persisted yet). + * + * @method isDeleted + * @public + * @param identifier + * @return {boolean} + */ + isDeleted(identifier: StableRecordIdentifier): boolean; + + /** + * Query the cache for whether a given resource has been deleted and that deletion + * has also been persisted. + * + * @method isDeletionCommitted + * @public + * @param identifier + * @return {boolean} + */ + isDeletionCommitted(identifier: StableRecordIdentifier): boolean; +} + +/** + * The interface for EmberData Caches. + * + * A Cache handles in-memory storage of Document and Resource + * data. + * + * Currently caches should implement V3 of the Cache interface, + * while V2 is still supported for compatibility. + * + * @class Cache + * @public + */ +export type Cache = CacheV2 | CacheV3; diff --git a/packages/core-types/src/schema/fields.ts b/packages/core-types/src/schema/fields.ts index 22b485841f2..8a4c0b9979a 100644 --- a/packages/core-types/src/schema/fields.ts +++ b/packages/core-types/src/schema/fields.ts @@ -35,6 +35,80 @@ export type GenericField = { options?: ObjectValue; }; + +/** + * A field that can be used to alias one key to another + * key present in the cache version of the resource. + * + * Unlike DerivedField, an AliasField may write to its + * source when a record is in an editable mode. + * + * AliasFields may utilize a transform, specified by type, + * to pre/post process the field. + * + * An AliasField may also specify a `kind` via options. + * `kind` may be any other valid field kind other than + * + * - `@hash` + * - `@id` + * - `@local` + * - `derived` + * + * This allows an AliasField to rename any field in the cache. + * + * Alias fields are generally intended to be used to support migrating + * between different schemas, though there are times where they are useful + * as a form of advanced derivation when used with a transform. For instance, + * an AliasField could be used to expose both a string and a Date version of the + * same field, with both being capable of being written to. + * + * @typedoc + */ +export type AliasField = { + kind: 'alias'; + name: string; + + /** + * the name of the transform to use, if any + * @typedoc + */ + type?: string; + + /** + * Additional configuratin for this field + * + * @typedoc + */ + options?: { + /** + * The name of the field in the cache from which to source + * the value for this field. + * + * @typedoc + */ + name: string; + + /** + * The kind of field that this alias should be treated as, defaults + * to 'field' + * + * @typedoc + */ + kind?: Exclude + + /** + * Options to pass to the transform, if any + * + * Must comply to the specific transform's options + * schema. + * + * @typedoc + */ + options?: ObjectValue; + } +}; + + /** * A field that can be used to alias one key to another * key present in the cache version of the resource. diff --git a/packages/core-types/src/spec/document.ts b/packages/core-types/src/spec/document.ts index 0ee5fa01399..2cd906cb4ca 100644 --- a/packages/core-types/src/spec/document.ts +++ b/packages/core-types/src/spec/document.ts @@ -39,8 +39,17 @@ export interface ResourceErrorDocument { errors: ApiError[]; } +export type MultiDocument = { + meta: { + isMulti: true; + }; + results: Record>; +}; + export type ResourceDocument = | ResourceMetaDocument | SingleResourceDataDocument | CollectionResourceDataDocument | ResourceErrorDocument; + +export type V3ResourceDocument = ResourceDocument; diff --git a/packages/experiments/src/persisted-cache/cache.ts b/packages/experiments/src/persisted-cache/cache.ts index b652d230a4d..9bc26ac1e6f 100644 --- a/packages/experiments/src/persisted-cache/cache.ts +++ b/packages/experiments/src/persisted-cache/cache.ts @@ -1,5 +1,5 @@ import type { StableRecordIdentifier } from '@warp-drive/core-types'; -import type { Cache, ChangedAttributesHash, RelationshipDiff } from '@warp-drive/core-types/cache'; +import type { Cache, CacheV2, ChangedAttributesHash, RelationshipDiff } from '@warp-drive/core-types/cache'; import type { ResourceBlob } from '@warp-drive/core-types/cache/aliases'; import type { Change } from '@warp-drive/core-types/cache/change'; import type { Mutation } from '@warp-drive/core-types/cache/mutations'; @@ -18,7 +18,7 @@ import type { ApiError } from '@warp-drive/core-types/spec/error'; * @class PersistedCache * @internal */ -export class PersistedCache implements Cache { +export class PersistedCache implements CacheV2 { declare _cache: Cache; declare _db: IDBDatabase; declare version: '2'; @@ -494,12 +494,8 @@ export class PersistedCache implements Cache { * @param propertyName * @returns resource relationship object */ - getRelationship( - identifier: StableRecordIdentifier, - field: string, - isCollection?: boolean - ): ResourceRelationship | CollectionRelationship { - return this._cache.getRelationship(identifier, field, isCollection); + getRelationship(identifier: StableRecordIdentifier, field: string): ResourceRelationship | CollectionRelationship { + return this._cache.getRelationship(identifier, field); } // Resource State diff --git a/packages/json-api/src/-private/cache.ts b/packages/json-api/src/-private/cache.ts index 5be8a7feb9a..ec8bfdf3d01 100644 --- a/packages/json-api/src/-private/cache.ts +++ b/packages/json-api/src/-private/cache.ts @@ -9,7 +9,7 @@ import { LOG_MUTATIONS, LOG_OPERATIONS, LOG_REQUESTS } from '@warp-drive/build-c import { DEPRECATE_RELATIONSHIP_REMOTE_UPDATE_CLEARING_LOCAL_STATE } from '@warp-drive/build-config/deprecations'; import { DEBUG } from '@warp-drive/build-config/env'; import { assert } from '@warp-drive/build-config/macros'; -import type { Cache, ChangedAttributesHash, RelationshipDiff } from '@warp-drive/core-types/cache'; +import type { Cache, CacheV2, ChangedAttributesHash, RelationshipDiff } from '@warp-drive/core-types/cache'; import type { Change } from '@warp-drive/core-types/cache/change'; import type { MergeOperation } from '@warp-drive/core-types/cache/operations'; import type { CollectionRelationship, ResourceRelationship } from '@warp-drive/core-types/cache/relationship'; @@ -127,7 +127,7 @@ function makeCache(): CachedResource { @public */ -export default class JSONAPICache implements Cache { +export default class JSONAPICache implements CacheV2 { /** * The Cache Version that this implementation implements. * diff --git a/packages/store/src/-private/managers/cache-manager.ts b/packages/store/src/-private/managers/cache-manager.ts index 8c335c2b8b8..664e9f99685 100644 --- a/packages/store/src/-private/managers/cache-manager.ts +++ b/packages/store/src/-private/managers/cache-manager.ts @@ -1,4 +1,4 @@ -import type { Cache, ChangedAttributesHash, RelationshipDiff } from '@warp-drive/core-types/cache'; +import type { Cache, CacheV2, ChangedAttributesHash, RelationshipDiff } from '@warp-drive/core-types/cache'; import type { Change } from '@warp-drive/core-types/cache/change'; import type { MergeOperation } from '@warp-drive/core-types/cache/operations'; import type { CollectionRelationship, ResourceRelationship } from '@warp-drive/core-types/cache/relationship'; @@ -29,7 +29,7 @@ import type { StoreRequestContext } from '../cache-handler/handler'; * @class CacheManager * @public */ -export class CacheManager implements Cache { +export class CacheManager implements CacheV2 { version = '2' as const; #cache: Cache;