import { ViewModelFieldPaths } from './fieldUtils'; import { ExtractFieldNames, ExtractPkFieldParseableValueType, ExtractStarFieldNames, FieldDataMappingRaw, FieldPath, FieldPaths, PartialViewModel, ViewModelConstructor, ViewModelInterface } from './ViewModelFactory'; /** * @quickinfo */ export declare type ChangeListener = (previous?: T | null, next?: T | null) => void; /** * @quickinfo */ export declare type MultiChangeListener = (previous?: (T | null)[], next?: (T | null)[]) => void; /** * Function that will unsubscribe the listener * @quickinfo */ export declare type ChangeListenerUnsubscribe = () => void; /** * @quickinfo */ export declare type AllChangesListener = () => void; /** * Cache for ViewModel instances based on the specified field names set. * * See the [ViewModel getting started guide](/docs/getting-started/viewmodel) for an overview of ViewModel's and how caching works * * Caching is based on the primary key, and the fields that are specified when creating an instance. For example, the * following two instances are cached separately: * * ```typescript * const user1 = User.cache.add({ id: 1, name: 'John' }); * const user2 = User.cache.add({ id: 1, email: 'john@example.com' }); * ``` * * When you read from the cache you specify the fields (or `"*"` for all fields): * * ```typescript * User.cache.get(1, ['name']); * // { id: 1, name: 'John' } * User.cache.get(1, ['email']); * // id: 1, email: 'john@example.com' * ``` * * An update to a superset of fields will update all cached subsets: * * ```typescript * User.cache.add({ * id: 1, * name: 'Johnny Smith', * email: 'johnny@test.com', * }); * console.log(User.cache.get(1, ['id', 'name'])); * // { id: 1, name: 'Johnny Smith' } * console.log(User.cache.get(1, ['id', 'email'])); * // { id: 1, email: 'johnny@test.com' } * ``` * * The motivation for this behaviour is that it's more desirable for have records be internally consistent than to have each individual * field reflect the latest value. Having partial records is useful for restricting the amount of data that is sent to the * frontend. * * ### Partial Models * * Partial models are instances where not all fields are specified. If a value isn't supplied for a * field, the instance is considered "partial" for that field. Note that a field with a null value is not considered partial - * it's only partial if no value is supplied at all. As described above, the key a record is cached under is based on * the fields it has set. * * To inspect which fields are specified in an instance, you can check the _assignedFields property: * * ```js * console.log(user._assignedFields); // Output: ['id', 'name'] * ``` * * To retrieve that record from the cache, you must specify the same fields: * * ```js * User.cache.get(1, ['id', 'name']); * ``` * * If all fields are set on the record you can use `'*'` to indicate all fields: * * ```js * User.cache.get(1, '*') * ``` * * * * ## Creating caches * * A cache requires a [ViewModel](doc:viewModelFactory), and every ViewModel has a cache created automatically: * * ```js * class User extends viewModelFactory({ * id: new IntegerField(), * name: new CharField(), * }, { pkFieldName: 'id' }) { * } * User.cache instanceof ViewModelCache; // true * ``` * * You can create a cache manually: * * ```js * const myCache = new ViewModelCache(User); * ``` * * But this is uncommon, unless you have custom caching requirements. In those cases it's recommended to override the * default cache: * * ```js * class CustomViewModelCache extends ViewModelCache {} * class User extends viewModelFactory({ * id: new IntegerField(), * name: new CharField(), * }, { pkFieldName: 'id' }) { * static cache = new CustomViewModelCache(User); * } * ``` * * ### Adding records * * Use [add](#Method-add) to add a single record. * * ```js * User.cache.add(new User({ id: 1, name: 'John' })); * ``` * * Using the ViewModel constructor is optional - you can pass the data directly * * ```js * User.cache.add({ id: 1, name: 'John' }); * ``` * * The primary key is always required, everything else is optional. * * Use [addList](#Method-addList) to add multiple records simultaneously. This is preferred to using `add` multiple times as it avoids * firing multiple change events. * * ```js * User.cache.addList([{ id: 1, name: 'John'} , { id: 2, name: 'Jane' }]); * ``` * * ### Updating records * * Updating a record is the same as adding it again: * * ```js * User.cache.add(new User({ id: 1, name: 'John' })); * ``` * * ### Retrieving records * * Use [get](#Method-get) to retrieve a single record by from the cache. You must specify the list of fields to include, or `"*"` * for all fields. * * ```js * const record = User.cache.get(1, ['name']); * ``` * * Note that the primary key is always included, so explicitly including it in the list of fields is optional. * * If you wish to get the latest version of a record you can pass the ViewModel instance itself instead of the id and fields: * * ```js * const latestRecord = User.cache.get(record); * ``` * * Use [getList](#Method-getList) to retrieve multiple records: * * ```js * const records = User.cache.getList([1, 2], ['name']); * ``` * * [getAll](#Method-getAll) can be used to retrieve all cached records: * * ```js * const records = User.cache.getAll(['name']); * ``` * * ### Deleting records * * Use [delete](#Method-delete) to delete a single record, either for a specific subset of fields or all cached records. * * ```js * // Delete a specific cache for a subset of fields * User.cache.delete(1, ['id', 'name']); * User.cache.get(1, ['id', 'name']); * // null * User.cache.get(1, ['id', 'name', 'email']) * // { id: 1, name: 'Johnny Smith', email: 'johnny@test.com' } * * // Or all fields * User.cache.delete(1); * User.cache.get(1, ['id', 'name', 'email']) * // null * ``` * * ### Listening to changes * * You can listen to changes using [addListener](#Method-addListener). * * > To listen for changes and re-render a component use the [useViewModelCache](doc:useViewModelCache) hook. * * ```js * User.cache.addListener(2, ['id', 'name'], (previous, next) => console.log(previous, 'change to', next)); * User.cache.add(new User({ id: 2, name: 'Bob' })); * // null changed to User({ id: 2, name: 'Bob' }) * User.cache.add(new User({ id: 2, name: 'Bobby' })); * // User({ id: 2, name: 'Bob' }) changed to User({ id: 2, name: 'Bobby' }) * User.cache.delete(2) * // User({ id: 2, name: 'Bobby' }) changed to null * ``` * * You can listen to changes to multiple records with [addListenerList](#Method-addListenerList). Used with `addList` and you will get one * notification for each batch of changes: * * ```js * // call for each change that occurs within addList * User.cache.addListenerList( * // Ids to listen for changes to * [3, 4], * // Only get updates for cached records with these field names * ['id', 'name'], * (previous, next) => console.log(previous, 'change to', next) * ); * User.cache.addList([new User({ id: 3, name: 'Jay' }), new User({ id: 4, name: 'Bee' })]); * // [null, null] changed to [User({ id: 3, name: 'Jay' }), User({ id: 4, name: 'Bee' })] * User.cache.addList([new User({ id: 3, name: 'Jayz' }), new User({ id: 4, name: 'Beeb' })]); * // [User({ id: 3, name: 'Jay' }), User({ id: 4, name: 'Bee' })] changed to [new User({ id: 3, name: 'Jayz' }), new User({ id: 4, name: 'Beeb' })] * User.cache.delete(3) * // [User({ id: 3, name: 'Jayz' }), User({ id: 4, name: 'Beeb' })] changed to [null, User({ id: 4, name: 'Beeb' })] * ``` * * * * ## Field notation * * If a model has a [RelatedViewModelField](doc:RelatedViewModelField), the data for a related field * can be retrieved using array notation: * * ```js * // Fetch the 'name' field and the related 'group' record and its 'label' field * ['name', ['group', 'label']] * ``` * * To fetch all fields from a relation you can just specify its name: * * ```js * ['name', 'group'] * // This will be expanded to include all non-relation fields on the related ViewModel * ['name', ['group', 'label'], ['group', 'ownerId']] * ``` * * Using the shorthand for a relation won't include any nested relation. To fetch deeply related records you must * explicitly opt in: * * ```js * ['name', 'group', ['group', 'owner']] * ``` * * **NOTE:** When accessing a relation its `sourceFieldName` is always included regardless * of whether you explicitly request it: * * ```js * User.cache.get(1, ['name', 'group']); * // { * // id: 1, * // name: 'Bob', * // groupId: 1, * // group: { * // id: 1, * // label: 'Staff', * // } * // } * ``` * * @extractdocs * @menugroup Caching * @typeParam ViewModelClassType The class of the ViewModel this cache is for */ export default class ViewModelCache> { /** * The ViewModel class this cache is for */ viewModel: ViewModelClassType; private fieldNameCache; /** * @ignore */ static listenerBatcher: { /** * Whether batch is in progress */ isActive: boolean; /** * This is items that need to be notified only once per change in a batch */ pending: Map; /** * This is items that don't need to respect batching rules. This is really for internal * use but is part of the public API. This exists as to make listeners work across * related fields we listen to changes on the related field cache */ pendingNoBatch: Map; /** * This is for callbacks that only listen to all changes on a model (not specific records or fields) */ pendingAll: Set; /** * Queue a call to a non-record/field specific listeners */ callAll(listener: AllChangesListener): void; /** * Queue a call to a record/field specific listener * * If `shouldBatch` is false then there's no guarantees about the listener only being called * once. */ call(listener: ChangeListener, before?: T | null | undefined, after?: T | null | undefined, shouldBatch?: boolean): void; /** * Start a batch. The passed function is called and any changes queued. Once function returns listeners * will be dispatched. The value returned from `run` will be returned. * * If any error occurs in `run` then no listeners will be called. * * If you nest batches then all listeners run at the end of the outer batch. */ batch(run: () => T_1): T_1; }; /** * @param viewModel The `ViewModel` this class is for */ constructor(viewModel: ViewModelClassType); private get cache(); /** * Checks if value `a` is an instance of the ViewModel this cache is for */ private isInstanceOfModel; /** * Get the cache key to use into for the primary key. Handles compound keys. */ private getPkCacheKey; /** * Acquire the field name cache specific to a primary key * * @param pk The primary key to get the cache for */ private acquireFieldNameCache; private get cacheClass(); /** * Batch changes made within provided function. This guarantees that any changes made * will result in a single call for each relevant listener. * * ```js * User.cache.addListener(listenerAll); * User.cache.addListener(1, ['id', 'name'], listener); * User.cache.addListenerList([1, 2], ['id', 'name'], listenerList); * User.cache.batch(() => { * // This value won't appear in changes at all as it's replaced 2 lines down * User.cache.add({ id: 1, name: 'Bob', groupId: 1 }); * User.cache.add({ id: 2, name: 'Sam', groupId: null }); * User.cache.add({ id: 1, name: 'Bobby', groupId: 1 }); * }); * // All listeners called once * ``` * @param run A function to run. Any changes made to the cache within this function will be batched. */ batch(run: () => T): T; /** * Add a ViewModel record to the cache. If the record is already cached then it will be updated. * * @overloadpreamble * Add a record, or records, to the cache. * * Records are cached based on the fields that are specified on that record. See [Partial Models](#Partial-Models) * for more information on how this works. * * You can pass raw data to this method, and it will be converted to a view model instance: * * ```js * const user = User.cache.add({ id: 1, name: 'Dave' }); * user instanceof User; // true * ``` * * You can pass an instance directly: * * ```js * User.cache.add(new User({ id: 1, name: 'Dave' })); * ``` * * If you pass an array then [addList](#Method-addList) will be called. * * > To add multiple records use [addList](#Method-addList) or pass an array to this method rather than calling * > `add` multiple times. This is more efficient as it will only trigger a single change event. * * @param data The record data to cache. An instance of the view model will be created and returned * * @returns The cached record as an instance of the view model. * @typeParam T The type of the record being added. See [PartialViewModel](BaseViewModel#PartialViewModel) */ add>(data: T): T; /** * @hidden */ add>(records: T[]): T[]; /** * Add a record from raw data to the cache. If the record is already cached then it will be updated. * * Returns the cached record as an instance of the ViewModel. * * @param data The data to cache as a plain object. * @typeParam FieldNames The type of fields being added. */ add>(data: FieldDataMappingRaw>): PartialViewModel; /** * @hidden */ add>(recordOrData: FieldDataMappingRaw>[]): PartialViewModel[]; /** * Add a list of ViewModel records to the cache. If any of the records are already cached then they will be updated. * * @overloadpreamble * * Adds a list of records to the cache. * * This method is preferred over manually invoking [add](#Method-add) for each record individually, as it ensures * listeners are only notified once about the changes to the list, rather than receiving a notification for each * individual record. * * You can pass an array of raw data to this method, and it will be converted to a view model instances: * * ```js * const users = User.cache.addList([{ id: 1, name: 'John'} , { id: 2, name: 'Jane' }]); * // users[0] instanceof User; // true * ``` * * Alternatively, you can pass an array of instances directly: * * ```js * User.cache.addList([new User({ id: 1, name: 'John'}), new User({ id: 2, name: 'Jane'})]); * ``` * * @param records The records to add * @typeParam T The type of the record being added when a ViewModel is passed */ addList>(records: T[]): T[]; /** * Add a list of records from raw data to the cache. If the records are already cached then they will be updated. * * @param records The records to add * @typeParam FieldNames The type of field names being added when raw data is passed */ addList>(records: FieldDataMappingRaw>[]): PartialViewModel[]; /** * Get a record with the specified `pk` and `fieldNames` from the cache. * * @overloadpreamble * * Get a record with the specified `fieldNames` set from the cache. * * ```js * User.cache.get(1, ['name']); * ``` * * Note that the primary key is always returned, so you do not need to specify it in `fieldNames`. * * To retrieve all fields use `"*"`. See [Field notation](#Field-notation) for supported format. * * ```js * User.cache.get(1, '*'); * ``` * * To get the latest version of a record you can pass the record directly and omit the fields: * * ```js * User.cache.get(user); * ``` * * See [Partial Models](#Partial-Models) for more details on how records are cached based on the fields they have. * * To get multiple records use [getList](#Method-getList). * * @param pk The primary key of the record to get * @param fieldNames The field names to use to look up the cache entry. Use '*' to indicate all fields. * See [Field notation](#Field_notation) for supported format. * * @returns The cached record, or null if none found */ get>(pk: ExtractPkFieldParseableValueType, fieldNames: T[]): PartialViewModel | null; /** @hidden */ get(pk: ExtractPkFieldParseableValueType, fieldNames: '*'): PartialViewModel> | null; /** * Get the latest version of a record from the cache. * * @param record The record to retrieve the latest version of */ get>(record: PartialViewModel): PartialViewModel | null; /** * @ignore */ _lastAllRecords: Map, PartialViewModel[]>; /** * Get all full records from the cache * * @overloadpreamble * * Get all records in the cache for the specified field names. This acts like `getList`, but returns * all records not just the records with the specified primary keys. * * This function guarantees to return the same array (i.e. passes strict equality check) if the underlying * records have not changed between calls. * * @param fieldNames List of field names, or `"*"`, to return records for. See [Field notation](#Field_notation) for the supported format. */ getAll(fieldNames: '*'): PartialViewModel>[]; /** * Get all records with the specified `fieldNames` from the cache * * @param fieldNames The fieldNames to return records for. See [Field notation](#Field_notation) for the supported format. * @typeParam T The type of the field names to return. This is inferred from the `fieldNames` parameter. */ getAll>(fieldNames: T[]): PartialViewModel[]; /** * Get a list of records with the specified `fieldNames` set from the cache * * @overloadpreamble * * Get a list of records with the specified `fieldNames` set from the cache. * * ```js * User.cache.getList([1, 5, 9], ['name']); * ``` * * Note that the primary key is always returned, so you do not need to specify it in `fieldNames`. * * To retrieve all fields use `"*"`. See [Field notation](#Field-notation) for supported format. * * ```js * User.cache.getList([1, 5, 9], '*'); * ``` * * To get the latest version of a record you can pass the record directly and omit the fields: * * ```js * User.cache.getList([user1, user2]); * ``` * * See [Partial Models](#Partial-Models) for more details on how records are cached based on the fields they have. * * @param pks The primary keys of the records to return * @param fieldNames The fieldNames to return records for. See [Field notation](#Field_notation) for the supported format. * @param removeNulls If `true` then any records that are not in the cache will be removed from the returned array. If `false` * then returned array will contain `null` for any records that are not in the cache. */ getList, RemoveNullsT extends boolean = true>(pks: ExtractPkFieldParseableValueType[], fieldNames: T[], removeNulls?: RemoveNullsT): RemoveNullsT extends true ? PartialViewModel[] : (PartialViewModel | null)[]; /** * @hidden */ getList(pks: ExtractPkFieldParseableValueType[], fieldNames: '*', removeNulls?: RemoveNullsT): RemoveNullsT extends true ? PartialViewModel>[] : (PartialViewModel> | null)[]; /** * Return a list of records from the cache * * @param records The records to return the latest version for * @param removeNulls If true, any records that are not in the cache will be removed from the returned array. */ getList, RemoveNullsT extends boolean = true>(records: T[], removeNulls?: RemoveNullsT): RemoveNullsT extends true ? T[] : (T | null)[]; /** * @hidden */ getList, RemoveNullsT extends boolean = true>(records: PartialViewModel[], removeNulls?: RemoveNullsT): RemoveNullsT extends true ? PartialViewModel[] : (PartialViewModel | null)[]; /** * Delete a record from the cache, optionally only for the specified `fieldNames` * * If `fieldNames` is omitted then the cache for the record is cleared in its entirety. * * @param pk The primary key of the record to delete * @param fieldNames Optionally only delete the entry with the specified field names. If * this is not set then all data for the record is removed. See [Field notation](#Field_notation) for supported format. * * @returns true if anything was removed, false otherwise */ delete(pk: ExtractPkFieldParseableValueType, fieldNames?: FieldPaths): boolean; /** * @ignore */ allChangeListeners: (() => void)[]; private onAnyChange; /** * Add a listener to any changes at all. The detail of the changes are not available. * * @overloadpreamble * * Add a listener for any changes, additions or deletions. * * You can pass just a function that will receive no arguments, in which case _all_ changes will trigger a notification: * * ```js * const unsubscribe = cache.addListener(() => { * console.log("Change occurred!") * }); * ``` * * The details of the changes are not available in this case. * * Alternatively you can pass either a single primary key or an array of primary keys, and the `fieldNames` to listen * for changes to (or `"*"` for complete records). In this case, the listener will be passed two arguments: the * previous value and next value: * * ```js * const unsubscribe = cache.addListener(1, '*', (prev, next) => { * console.log(prev, 'changed to', next); * }); * ``` * * In the case of a creation or deletion the previous or next value will be `null` respectively. * * If you pass an array of primary keys, then the listener will be passed an array with each element corresponding * to the primary key passed in: * * ```js * const unsubscribe = cache.addListener([1, 2], '*', (([prev1, prev2], [next1, next2]) => { * console.log(prev1, 'changed to', next1); * console.log(prev2, 'changed to', next2); * })); * ``` * * In the case of a creation or deletion the previous or next entry in the array will be `null` respectively. * * All forms return a function that will unsubscribe the listener when called. * * @param listener Function to that is called when any change occurs. The function is called with no parameters. * @returns A function that removes the listener */ addListener(listener: AllChangesListener): ChangeListenerUnsubscribe; /** * Add a listener for any changes, additions or deletions for the record(s) identified by * `pkOrPks` for the field names `fieldNames`. * * @param pk Primary key for the record to listen to changes/additions/deletions to * @param fieldNames Field names to listen to changes/additions/deletions to. See [Field notation](#Field_notation) for supported format. * @param listener Function to call with any changes * @param batch Whether or not to batch this call with other calls (defaults to true). You shouldn't need to change the default. * @returns A function that removes the listener */ addListener>(pk: ExtractPkFieldParseableValueType, fieldNames: T[], listener: ChangeListener>, batch?: boolean): ChangeListenerUnsubscribe; /** * @hidden */ addListener(pkOrPks: ExtractPkFieldParseableValueType, fieldNames: '*', listener: ChangeListener>>, batch?: boolean): ChangeListenerUnsubscribe; /** * Add a listener for any changes, additions or deletions for the record(s) identified by * `pkOrPks` for the field names `fieldNames`. * * @param pks The primary keys for the record to listen to changes/additions/deletions to * @param fieldNames Field names to listen to changes/additions/deletions to. See [Field notation](#Field_notation) for supported format. * @param listener Function to call with any changes * @param batch Whether or not to batch this call with other calls (defaults to true). You shouldn't need to change the default. * @returns A function that removes the listener */ addListener>(pks: ExtractPkFieldParseableValueType[], fieldNames: T[], listener: MultiChangeListener>, batch?: boolean): ChangeListenerUnsubscribe; /** * @hidden */ addListener(pkOrPksOrListener: ExtractPkFieldParseableValueType[], fieldNames: '*', listener: MultiChangeListener>>, batch?: boolean): ChangeListenerUnsubscribe; /** * Add a listener to any changes at all. The detail of the changes are not available. * * @overloadpreamble * * Add a listener for any changes, additions or deletions to a list of records * * See [addListener](#Method-addListener) for details. * * @param pks The primary keys to listen to changes/additions/deletions to * @param fieldNames The field names to listen to changes/additions/deletions to. See [Field notation](#Field_notation) for supported format. * @param listener The function that will be called when a change occurs. * @returns A function that removes the listener */ addListenerList>(pks: ExtractPkFieldParseableValueType[], fieldNames: T[], listener: MultiChangeListener>): ChangeListenerUnsubscribe; /** * @hidden */ addListenerList(pks: ExtractPkFieldParseableValueType[], fieldNames: '*', listener: MultiChangeListener>>): ChangeListenerUnsubscribe; /** * Deletes all records from the cache, optionally limiting to a subset of fields. * * @param fieldNames Optionally only delete the entries with the specified field names. If * this is not set then all data for every record is removed. See [Field notation](#Field_notation) for supported format. * * **NOTE:** When deleting a subset of fields be aware that if a superset of those fields exist in the * cache then the records with partial fields will be recreated next time they are accessed. */ deleteAll(fieldNames?: FieldPaths): void; }