@tldraw/store
Version:
tldraw infinite canvas SDK (store).
5 lines • 57.2 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../../src/lib/Store.ts"],
"sourcesContent": ["import { Atom, Reactor, Signal, atom, computed, reactor, transact } from '@tldraw/state'\nimport {\n\tWeakCache,\n\tassert,\n\tfilterEntries,\n\tgetOwnProperty,\n\tisEqual,\n\tobjectMapEntries,\n\tobjectMapKeys,\n\tobjectMapValues,\n\tthrottleToNextFrame,\n\tuniqueId,\n} from '@tldraw/utils'\nimport { AtomMap } from './AtomMap'\nimport { IdOf, RecordId, UnknownRecord } from './BaseRecord'\nimport { RecordScope } from './RecordType'\nimport { RecordsDiff, squashRecordDiffs } from './RecordsDiff'\nimport { StoreQueries } from './StoreQueries'\nimport { SerializedSchema, StoreSchema } from './StoreSchema'\nimport { StoreSideEffects } from './StoreSideEffects'\nimport { devFreeze } from './devFreeze'\n\n/**\n * Extracts the record type from a record ID type.\n *\n * @example\n * ```ts\n * type BookId = RecordId<Book>\n * type BookType = RecordFromId<BookId> // Book\n * ```\n *\n * @public\n */\nexport type RecordFromId<K extends RecordId<UnknownRecord>> =\n\tK extends RecordId<infer R> ? R : never\n\n/**\n * A diff describing the changes to a collection.\n *\n * @example\n * ```ts\n * const diff: CollectionDiff<string> = {\n * added: new Set(['newItem']),\n * removed: new Set(['oldItem'])\n * }\n * ```\n *\n * @public\n */\nexport interface CollectionDiff<T> {\n\t/** Items that were added to the collection */\n\tadded?: Set<T>\n\t/** Items that were removed from the collection */\n\tremoved?: Set<T>\n}\n\n/**\n * The source of a change to the store.\n * - `'user'` - Changes originating from local user actions\n * - `'remote'` - Changes originating from remote synchronization\n *\n * @public\n */\nexport type ChangeSource = 'user' | 'remote'\n\n/**\n * Filters for store listeners to control which changes trigger the listener.\n *\n * @example\n * ```ts\n * const filters: StoreListenerFilters = {\n * source: 'user', // Only listen to user changes\n * scope: 'document' // Only listen to document-scoped records\n * }\n * ```\n *\n * @public\n */\nexport interface StoreListenerFilters {\n\t/** Filter by the source of changes */\n\tsource: ChangeSource | 'all'\n\t/** Filter by the scope of records */\n\tscope: RecordScope | 'all'\n}\n\n/**\n * An entry containing changes that originated either by user actions or remote changes.\n * History entries are used to track and replay changes to the store.\n *\n * @example\n * ```ts\n * const entry: HistoryEntry<Book> = {\n * changes: {\n * added: { 'book:123': bookRecord },\n * updated: {},\n * removed: {}\n * },\n * source: 'user'\n * }\n * ```\n *\n * @public\n */\nexport interface HistoryEntry<R extends UnknownRecord = UnknownRecord> {\n\t/** The changes that occurred in this history entry */\n\tchanges: RecordsDiff<R>\n\t/** The source of these changes */\n\tsource: ChangeSource\n}\n\n/**\n * A function that will be called when the history changes.\n *\n * @example\n * ```ts\n * const listener: StoreListener<Book> = (entry) => {\n * console.log('Changes:', entry.changes)\n * console.log('Source:', entry.source)\n * }\n *\n * store.listen(listener)\n * ```\n *\n * @param entry - The history entry containing the changes\n *\n * @public\n */\nexport type StoreListener<R extends UnknownRecord> = (entry: HistoryEntry<R>) => void\n\n/**\n * A computed cache that stores derived data for records.\n * The cache automatically updates when underlying records change and cleans up when records are deleted.\n *\n * @example\n * ```ts\n * const expensiveCache = store.createComputedCache(\n * 'expensive',\n * (book: Book) => performExpensiveCalculation(book)\n * )\n *\n * const result = expensiveCache.get(bookId)\n * ```\n *\n * @public\n */\nexport interface ComputedCache<Data, R extends UnknownRecord> {\n\t/**\n\t * Get the cached data for a record by its ID.\n\t *\n\t * @param id - The ID of the record\n\t * @returns The cached data or undefined if the record doesn't exist\n\t */\n\tget(id: IdOf<R>): Data | undefined\n}\n\n/**\n * Options for creating a computed cache.\n *\n * @example\n * ```ts\n * const options: CreateComputedCacheOpts<string[], Book> = {\n * areRecordsEqual: (a, b) => a.title === b.title,\n * areResultsEqual: (a, b) => JSON.stringify(a) === JSON.stringify(b)\n * }\n * ```\n *\n * @public\n */\nexport interface CreateComputedCacheOpts<Data, R extends UnknownRecord> {\n\t/** Custom equality function for comparing records */\n\tareRecordsEqual?(a: R, b: R): boolean\n\t/** Custom equality function for comparing results */\n\tareResultsEqual?(a: Data, b: Data): boolean\n}\n\n/**\n * A serialized snapshot of the record store's values.\n * This is a plain JavaScript object that can be saved to storage or transmitted over the network.\n *\n * @example\n * ```ts\n * const serialized: SerializedStore<Book> = {\n * 'book:123': { id: 'book:123', typeName: 'book', title: 'The Lathe of Heaven' },\n * 'book:456': { id: 'book:456', typeName: 'book', title: 'The Left Hand of Darkness' }\n * }\n * ```\n *\n * @public\n */\nexport type SerializedStore<R extends UnknownRecord> = Record<IdOf<R>, R>\n\n/**\n * A snapshot of the store including both data and schema information.\n * This enables proper migration when loading data from different schema versions.\n *\n * @example\n * ```ts\n * const snapshot = store.getStoreSnapshot()\n * // Later...\n * store.loadStoreSnapshot(snapshot)\n * ```\n *\n * @public\n */\nexport interface StoreSnapshot<R extends UnknownRecord> {\n\t/** The serialized store data */\n\tstore: SerializedStore<R>\n\t/** The serialized schema information */\n\tschema: SerializedSchema\n}\n\n/**\n * A validator for store records that ensures data integrity.\n * Validators are called when records are created or updated.\n *\n * @example\n * ```ts\n * const bookValidator: StoreValidator<Book> = {\n * validate(record: unknown): Book {\n * // Validate and return the record\n * if (typeof record !== 'object' || !record.title) {\n * throw new Error('Invalid book')\n * }\n * return record as Book\n * }\n * }\n * ```\n *\n * @public\n */\nexport interface StoreValidator<R extends UnknownRecord> {\n\t/**\n\t * Validate a record.\n\t *\n\t * @param record - The record to validate\n\t * @returns The validated record\n\t * @throws When validation fails\n\t */\n\tvalidate(record: unknown): R\n\t/**\n\t * Validate a record using a known good version for reference.\n\t *\n\t * @param knownGoodVersion - A known valid version of the record\n\t * @param record - The record to validate\n\t * @returns The validated record\n\t */\n\tvalidateUsingKnownGoodVersion?(knownGoodVersion: R, record: unknown): R\n}\n\n/**\n * A map of validators for each record type in the store.\n *\n * @example\n * ```ts\n * const validators: StoreValidators<Book | Author> = {\n * book: bookValidator,\n * author: authorValidator\n * }\n * ```\n *\n * @public\n */\nexport type StoreValidators<R extends UnknownRecord> = {\n\t[K in R['typeName']]: StoreValidator<Extract<R, { typeName: K }>>\n}\n\n/**\n * Information about an error that occurred in the store.\n *\n * @example\n * ```ts\n * const error: StoreError = {\n * error: new Error('Validation failed'),\n * phase: 'updateRecord',\n * recordBefore: oldRecord,\n * recordAfter: newRecord,\n * isExistingValidationIssue: false\n * }\n * ```\n *\n * @public\n */\nexport interface StoreError {\n\t/** The error that occurred */\n\terror: Error\n\t/** The phase during which the error occurred */\n\tphase: 'initialize' | 'createRecord' | 'updateRecord' | 'tests'\n\t/** The record state before the operation (if applicable) */\n\trecordBefore?: unknown\n\t/** The record state after the operation */\n\trecordAfter: unknown\n\t/** Whether this is an existing validation issue */\n\tisExistingValidationIssue: boolean\n}\n\n/**\n * Extract the record type from a Store type.\n * Used internally for type inference.\n *\n * @internal\n */\nexport type StoreRecord<S extends Store<any>> = S extends Store<infer R> ? R : never\n\n/**\n * A reactive store that manages collections of typed records.\n *\n * The Store is the central container for your application's data, providing:\n * - Reactive state management with automatic updates\n * - Type-safe record operations\n * - History tracking and change notifications\n * - Schema validation and migrations\n * - Side effects and business logic hooks\n * - Efficient querying and indexing\n *\n * @example\n * ```ts\n * // Create a store with schema\n * const schema = StoreSchema.create({\n * book: Book,\n * author: Author\n * })\n *\n * const store = new Store({\n * schema,\n * props: {}\n * })\n *\n * // Add records\n * const book = Book.create({ title: 'The Lathe of Heaven', author: 'Le Guin' })\n * store.put([book])\n *\n * // Listen to changes\n * store.listen((entry) => {\n * console.log('Changes:', entry.changes)\n * })\n * ```\n *\n * @public\n */\nexport class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {\n\t/**\n\t * The unique identifier of the store instance.\n\t *\n\t * @public\n\t */\n\tpublic readonly id: string\n\t/**\n\t * An AtomMap containing the stores records.\n\t *\n\t * @internal\n\t * @readonly\n\t */\n\tprivate readonly records: AtomMap<IdOf<R>, R>\n\n\t/**\n\t * An atom containing the store's history.\n\t *\n\t * @public\n\t * @readonly\n\t */\n\treadonly history: Atom<number, RecordsDiff<R>> = atom('history', 0, {\n\t\thistoryLength: 1000,\n\t})\n\n\t/**\n\t * Reactive queries and indexes for efficiently accessing store data.\n\t * Provides methods for filtering, indexing, and subscribing to subsets of records.\n\t *\n\t * @example\n\t * ```ts\n\t * // Create an index by a property\n\t * const booksByAuthor = store.query.index('book', 'author')\n\t *\n\t * // Get records matching criteria\n\t * const inStockBooks = store.query.records('book', () => ({\n\t * inStock: { eq: true }\n\t * }))\n\t * ```\n\t *\n\t * @public\n\t * @readonly\n\t */\n\treadonly query: StoreQueries<R>\n\n\t/**\n\t * A set containing listeners that have been added to this store.\n\t *\n\t * @internal\n\t */\n\tprivate listeners = new Set<{ onHistory: StoreListener<R>; filters: StoreListenerFilters }>()\n\n\t/**\n\t * An array of history entries that have not yet been flushed.\n\t *\n\t * @internal\n\t */\n\tprivate historyAccumulator = new HistoryAccumulator<R>()\n\n\t/**\n\t * A reactor that responds to changes to the history by squashing the accumulated history and\n\t * notifying listeners of the changes.\n\t *\n\t * @internal\n\t */\n\tprivate historyReactor: Reactor\n\n\t/**\n\t * Function to dispose of any in-flight timeouts.\n\t *\n\t * @internal\n\t */\n\tprivate cancelHistoryReactor(): void {\n\t\t/* noop */\n\t}\n\n\t/**\n\t * The schema that defines the structure and validation rules for records in this store.\n\t *\n\t * @public\n\t */\n\treadonly schema: StoreSchema<R, Props>\n\n\t/**\n\t * Custom properties associated with this store instance.\n\t *\n\t * @public\n\t */\n\treadonly props: Props\n\n\t/**\n\t * A mapping of record scopes to the set of record type names that belong to each scope.\n\t * Used to filter records by their persistence and synchronization behavior.\n\t *\n\t * @public\n\t */\n\tpublic readonly scopedTypes: { readonly [K in RecordScope]: ReadonlySet<R['typeName']> }\n\n\t/**\n\t * Side effects manager that handles lifecycle events for record operations.\n\t * Allows registration of callbacks for create, update, delete, and validation events.\n\t *\n\t * @example\n\t * ```ts\n\t * store.sideEffects.registerAfterCreateHandler('book', (book) => {\n\t * console.log('Book created:', book.title)\n\t * })\n\t * ```\n\t *\n\t * @public\n\t */\n\tpublic readonly sideEffects = new StoreSideEffects<R>(this)\n\n\t/**\n\t * Creates a new Store instance.\n\t *\n\t * @example\n\t * ```ts\n\t * const store = new Store({\n\t * schema: StoreSchema.create({ book: Book }),\n\t * props: { appName: 'MyLibrary' },\n\t * initialData: savedData\n\t * })\n\t * ```\n\t *\n\t * @param config - Configuration object for the store\n\t */\n\tconstructor(config: {\n\t\t/** Optional unique identifier for the store */\n\t\tid?: string\n\t\t/** The store's initial data to populate on creation */\n\t\tinitialData?: SerializedStore<R>\n\t\t/** The schema defining record types, validation, and migrations */\n\t\tschema: StoreSchema<R, Props>\n\t\t/** Custom properties for the store instance */\n\t\tprops: Props\n\t}) {\n\t\tconst { initialData, schema, id } = config\n\n\t\tthis.id = id ?? uniqueId()\n\t\tthis.schema = schema\n\t\tthis.props = config.props\n\n\t\tif (initialData) {\n\t\t\tthis.records = new AtomMap(\n\t\t\t\t'store',\n\t\t\t\tobjectMapEntries(initialData).map(([id, record]) => [\n\t\t\t\t\tid,\n\t\t\t\t\tdevFreeze(this.schema.validateRecord(this, record, 'initialize', null)),\n\t\t\t\t])\n\t\t\t)\n\t\t} else {\n\t\t\tthis.records = new AtomMap('store')\n\t\t}\n\n\t\tthis.query = new StoreQueries<R>(this.records, this.history)\n\n\t\tthis.historyReactor = reactor(\n\t\t\t'Store.historyReactor',\n\t\t\t() => {\n\t\t\t\t// deref to make sure we're subscribed regardless of whether we need to propagate\n\t\t\t\tthis.history.get()\n\t\t\t\t// If we have accumulated history, flush it and update listeners\n\t\t\t\tthis._flushHistory()\n\t\t\t},\n\t\t\t{ scheduleEffect: (cb) => (this.cancelHistoryReactor = throttleToNextFrame(cb)) }\n\t\t)\n\t\tthis.scopedTypes = {\n\t\t\tdocument: new Set(\n\t\t\t\tobjectMapValues(this.schema.types)\n\t\t\t\t\t.filter((t) => t.scope === 'document')\n\t\t\t\t\t.map((t) => t.typeName)\n\t\t\t),\n\t\t\tsession: new Set(\n\t\t\t\tobjectMapValues(this.schema.types)\n\t\t\t\t\t.filter((t) => t.scope === 'session')\n\t\t\t\t\t.map((t) => t.typeName)\n\t\t\t),\n\t\t\tpresence: new Set(\n\t\t\t\tobjectMapValues(this.schema.types)\n\t\t\t\t\t.filter((t) => t.scope === 'presence')\n\t\t\t\t\t.map((t) => t.typeName)\n\t\t\t),\n\t\t}\n\t}\n\n\tpublic _flushHistory() {\n\t\t// If we have accumulated history, flush it and update listeners\n\t\tif (this.historyAccumulator.hasChanges()) {\n\t\t\tconst entries = this.historyAccumulator.flush()\n\t\t\tfor (const { changes, source } of entries) {\n\t\t\t\tlet instanceChanges = null as null | RecordsDiff<R>\n\t\t\t\tlet documentChanges = null as null | RecordsDiff<R>\n\t\t\t\tlet presenceChanges = null as null | RecordsDiff<R>\n\t\t\t\tfor (const { onHistory, filters } of this.listeners) {\n\t\t\t\t\tif (filters.source !== 'all' && filters.source !== source) {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tif (filters.scope !== 'all') {\n\t\t\t\t\t\tif (filters.scope === 'document') {\n\t\t\t\t\t\t\tdocumentChanges ??= this.filterChangesByScope(changes, 'document')\n\t\t\t\t\t\t\tif (!documentChanges) continue\n\t\t\t\t\t\t\tonHistory({ changes: documentChanges, source })\n\t\t\t\t\t\t} else if (filters.scope === 'session') {\n\t\t\t\t\t\t\tinstanceChanges ??= this.filterChangesByScope(changes, 'session')\n\t\t\t\t\t\t\tif (!instanceChanges) continue\n\t\t\t\t\t\t\tonHistory({ changes: instanceChanges, source })\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tpresenceChanges ??= this.filterChangesByScope(changes, 'presence')\n\t\t\t\t\t\t\tif (!presenceChanges) continue\n\t\t\t\t\t\t\tonHistory({ changes: presenceChanges, source })\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tonHistory({ changes, source })\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tdispose() {\n\t\tthis.cancelHistoryReactor()\n\t}\n\n\t/**\n\t * Filters out non-document changes from a diff. Returns null if there are no changes left.\n\t * @param change - the records diff\n\t * @param scope - the records scope\n\t * @returns\n\t */\n\tfilterChangesByScope(change: RecordsDiff<R>, scope: RecordScope) {\n\t\tconst result = {\n\t\t\tadded: filterEntries(change.added, (_, r) => this.scopedTypes[scope].has(r.typeName)),\n\t\t\tupdated: filterEntries(change.updated, (_, r) => this.scopedTypes[scope].has(r[1].typeName)),\n\t\t\tremoved: filterEntries(change.removed, (_, r) => this.scopedTypes[scope].has(r.typeName)),\n\t\t}\n\t\tif (\n\t\t\tObject.keys(result.added).length === 0 &&\n\t\t\tObject.keys(result.updated).length === 0 &&\n\t\t\tObject.keys(result.removed).length === 0\n\t\t) {\n\t\t\treturn null\n\t\t}\n\t\treturn result\n\t}\n\n\t/**\n\t * Update the history with a diff of changes.\n\t *\n\t * @param changes - The changes to add to the history.\n\t */\n\tprivate updateHistory(changes: RecordsDiff<R>): void {\n\t\tthis.historyAccumulator.add({\n\t\t\tchanges,\n\t\t\tsource: this.isMergingRemoteChanges ? 'remote' : 'user',\n\t\t})\n\t\tif (this.listeners.size === 0) {\n\t\t\tthis.historyAccumulator.clear()\n\t\t}\n\t\tthis.history.set(this.history.get() + 1, changes)\n\t}\n\n\tvalidate(phase: 'initialize' | 'createRecord' | 'updateRecord' | 'tests') {\n\t\tthis.allRecords().forEach((record) => this.schema.validateRecord(this, record, phase, null))\n\t}\n\n\t/**\n\t * Add or update records in the store. If a record with the same ID already exists, it will be updated.\n\t * Otherwise, a new record will be created.\n\t *\n\t * @example\n\t * ```ts\n\t * // Add new records\n\t * const book = Book.create({ title: 'Lathe Of Heaven', author: 'Le Guin' })\n\t * store.put([book])\n\t *\n\t * // Update existing record\n\t * store.put([{ ...book, title: 'The Lathe of Heaven' }])\n\t * ```\n\t *\n\t * @param records - The records to add or update\n\t * @param phaseOverride - Override the validation phase (used internally)\n\t * @public\n\t */\n\tput(records: R[], phaseOverride?: 'initialize'): void {\n\t\tthis.atomic(() => {\n\t\t\tconst updates: Record<IdOf<UnknownRecord>, [from: R, to: R]> = {}\n\t\t\tconst additions: Record<IdOf<UnknownRecord>, R> = {}\n\n\t\t\t// Iterate through all records, creating, updating or removing as needed\n\t\t\tlet record: R\n\n\t\t\t// There's a chance that, despite having records, all of the values are\n\t\t\t// identical to what they were before; and so we'd end up with an \"empty\"\n\t\t\t// history entry. Let's keep track of whether we've actually made any\n\t\t\t// changes (e.g. additions, deletions, or updates that produce a new value).\n\t\t\tlet didChange = false\n\n\t\t\tconst source = this.isMergingRemoteChanges ? 'remote' : 'user'\n\n\t\t\tfor (let i = 0, n = records.length; i < n; i++) {\n\t\t\t\trecord = records[i]\n\n\t\t\t\tconst initialValue = this.records.__unsafe__getWithoutCapture(record.id)\n\t\t\t\t// If we already have an atom for this record, update its value.\n\t\t\t\tif (initialValue) {\n\t\t\t\t\t// If we have a beforeUpdate callback, run it against the initial and next records\n\t\t\t\t\trecord = this.sideEffects.handleBeforeChange(initialValue, record, source)\n\n\t\t\t\t\t// Validate the record\n\t\t\t\t\tconst validated = this.schema.validateRecord(\n\t\t\t\t\t\tthis,\n\t\t\t\t\t\trecord,\n\t\t\t\t\t\tphaseOverride ?? 'updateRecord',\n\t\t\t\t\t\tinitialValue\n\t\t\t\t\t)\n\n\t\t\t\t\tif (validated === initialValue) continue\n\n\t\t\t\t\trecord = devFreeze(record)\n\t\t\t\t\tthis.records.set(record.id, record)\n\n\t\t\t\t\tdidChange = true\n\t\t\t\t\tupdates[record.id] = [initialValue, record]\n\t\t\t\t\tthis.addDiffForAfterEvent(initialValue, record)\n\t\t\t\t} else {\n\t\t\t\t\trecord = this.sideEffects.handleBeforeCreate(record, source)\n\n\t\t\t\t\tdidChange = true\n\n\t\t\t\t\t// If we don't have an atom, create one.\n\n\t\t\t\t\t// Validate the record\n\t\t\t\t\trecord = this.schema.validateRecord(\n\t\t\t\t\t\tthis,\n\t\t\t\t\t\trecord as R,\n\t\t\t\t\t\tphaseOverride ?? 'createRecord',\n\t\t\t\t\t\tnull\n\t\t\t\t\t)\n\n\t\t\t\t\t// freeze it\n\t\t\t\t\trecord = devFreeze(record)\n\n\t\t\t\t\t// Mark the change as a new addition.\n\t\t\t\t\tadditions[record.id] = record\n\t\t\t\t\tthis.addDiffForAfterEvent(null, record)\n\n\t\t\t\t\tthis.records.set(record.id, record)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// If we did change, update the history\n\t\t\tif (!didChange) return\n\t\t\tthis.updateHistory({\n\t\t\t\tadded: additions,\n\t\t\t\tupdated: updates,\n\t\t\t\tremoved: {} as Record<IdOf<R>, R>,\n\t\t\t})\n\t\t})\n\t}\n\n\t/**\n\t * Remove records from the store by their IDs.\n\t *\n\t * @example\n\t * ```ts\n\t * // Remove a single record\n\t * store.remove([book.id])\n\t *\n\t * // Remove multiple records\n\t * store.remove([book1.id, book2.id, book3.id])\n\t * ```\n\t *\n\t * @param ids - The IDs of the records to remove\n\t * @public\n\t */\n\tremove(ids: IdOf<R>[]): void {\n\t\tthis.atomic(() => {\n\t\t\tconst toDelete = new Set<IdOf<R>>(ids)\n\t\t\tconst source = this.isMergingRemoteChanges ? 'remote' : 'user'\n\n\t\t\tif (this.sideEffects.isEnabled()) {\n\t\t\t\tfor (const id of ids) {\n\t\t\t\t\tconst record = this.records.__unsafe__getWithoutCapture(id)\n\t\t\t\t\tif (!record) continue\n\n\t\t\t\t\tif (this.sideEffects.handleBeforeDelete(record, source) === false) {\n\t\t\t\t\t\ttoDelete.delete(id)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst actuallyDeleted = this.records.deleteMany(toDelete)\n\t\t\tif (actuallyDeleted.length === 0) return\n\n\t\t\tconst removed = {} as RecordsDiff<R>['removed']\n\t\t\tfor (const [id, record] of actuallyDeleted) {\n\t\t\t\tremoved[id] = record\n\t\t\t\tthis.addDiffForAfterEvent(record, null)\n\t\t\t}\n\n\t\t\t// Update the history with the removed records.\n\t\t\tthis.updateHistory({ added: {}, updated: {}, removed } as RecordsDiff<R>)\n\t\t})\n\t}\n\n\t/**\n\t * Get a record by its ID. This creates a reactive subscription to the record.\n\t *\n\t * @example\n\t * ```ts\n\t * const book = store.get(bookId)\n\t * if (book) {\n\t * console.log(book.title)\n\t * }\n\t * ```\n\t *\n\t * @param id - The ID of the record to get\n\t * @returns The record if it exists, undefined otherwise\n\t * @public\n\t */\n\tget<K extends IdOf<R>>(id: K): RecordFromId<K> | undefined {\n\t\treturn this.records.get(id) as RecordFromId<K> | undefined\n\t}\n\n\t/**\n\t * Get a record by its ID without creating a reactive subscription.\n\t * Use this when you need to access a record but don't want reactive updates.\n\t *\n\t * @example\n\t * ```ts\n\t * // Won't trigger reactive updates when this record changes\n\t * const book = store.unsafeGetWithoutCapture(bookId)\n\t * ```\n\t *\n\t * @param id - The ID of the record to get\n\t * @returns The record if it exists, undefined otherwise\n\t * @public\n\t */\n\tunsafeGetWithoutCapture<K extends IdOf<R>>(id: K): RecordFromId<K> | undefined {\n\t\treturn this.records.__unsafe__getWithoutCapture(id) as RecordFromId<K> | undefined\n\t}\n\n\t/**\n\t * Serialize the store's records to a plain JavaScript object.\n\t * Only includes records matching the specified scope.\n\t *\n\t * @example\n\t * ```ts\n\t * // Serialize only document records (default)\n\t * const documentData = store.serialize('document')\n\t *\n\t * // Serialize all records\n\t * const allData = store.serialize('all')\n\t * ```\n\t *\n\t * @param scope - The scope of records to serialize. Defaults to 'document'\n\t * @returns The serialized store data\n\t * @public\n\t */\n\tserialize(scope: RecordScope | 'all' = 'document'): SerializedStore<R> {\n\t\tconst result = {} as SerializedStore<R>\n\t\tfor (const [id, record] of this.records) {\n\t\t\tif (scope === 'all' || this.scopedTypes[scope].has(record.typeName)) {\n\t\t\t\tresult[id as IdOf<R>] = record\n\t\t\t}\n\t\t}\n\t\treturn result\n\t}\n\n\t/**\n\t * Get a serialized snapshot of the store and its schema.\n\t * This includes both the data and schema information needed for proper migration.\n\t *\n\t * @example\n\t * ```ts\n\t * const snapshot = store.getStoreSnapshot()\n\t * localStorage.setItem('myApp', JSON.stringify(snapshot))\n\t *\n\t * // Later...\n\t * const saved = JSON.parse(localStorage.getItem('myApp'))\n\t * store.loadStoreSnapshot(saved)\n\t * ```\n\t *\n\t * @param scope - The scope of records to serialize. Defaults to 'document'\n\t * @returns A snapshot containing both store data and schema information\n\t * @public\n\t */\n\tgetStoreSnapshot(scope: RecordScope | 'all' = 'document'): StoreSnapshot<R> {\n\t\treturn {\n\t\t\tstore: this.serialize(scope),\n\t\t\tschema: this.schema.serialize(),\n\t\t}\n\t}\n\n\t/**\n\t * Migrate a serialized snapshot to the current schema version.\n\t * This applies any necessary migrations to bring old data up to date.\n\t *\n\t * @example\n\t * ```ts\n\t * const oldSnapshot = JSON.parse(localStorage.getItem('myApp'))\n\t * const migratedSnapshot = store.migrateSnapshot(oldSnapshot)\n\t * ```\n\t *\n\t * @param snapshot - The snapshot to migrate\n\t * @returns The migrated snapshot with current schema version\n\t * @throws Error if migration fails\n\t * @public\n\t */\n\tmigrateSnapshot(snapshot: StoreSnapshot<R>): StoreSnapshot<R> {\n\t\tconst migrationResult = this.schema.migrateStoreSnapshot(snapshot)\n\n\t\tif (migrationResult.type === 'error') {\n\t\t\tthrow new Error(`Failed to migrate snapshot: ${migrationResult.reason}`)\n\t\t}\n\n\t\treturn {\n\t\t\tstore: migrationResult.value,\n\t\t\tschema: this.schema.serialize(),\n\t\t}\n\t}\n\n\t/**\n\t * Load a serialized snapshot into the store, replacing all current data.\n\t * The snapshot will be automatically migrated to the current schema version if needed.\n\t *\n\t * @example\n\t * ```ts\n\t * const snapshot = JSON.parse(localStorage.getItem('myApp'))\n\t * store.loadStoreSnapshot(snapshot)\n\t * ```\n\t *\n\t * @param snapshot - The snapshot to load\n\t * @throws Error if migration fails or snapshot is invalid\n\t * @public\n\t */\n\tloadStoreSnapshot(snapshot: StoreSnapshot<R>): void {\n\t\tconst migrationResult = this.schema.migrateStoreSnapshot(snapshot)\n\n\t\tif (migrationResult.type === 'error') {\n\t\t\tthrow new Error(`Failed to migrate snapshot: ${migrationResult.reason}`)\n\t\t}\n\n\t\tconst prevSideEffectsEnabled = this.sideEffects.isEnabled()\n\t\ttry {\n\t\t\tthis.sideEffects.setIsEnabled(false)\n\t\t\tthis.atomic(() => {\n\t\t\t\tthis.clear()\n\t\t\t\tthis.put(Object.values(migrationResult.value))\n\t\t\t\tthis.ensureStoreIsUsable()\n\t\t\t})\n\t\t} finally {\n\t\t\tthis.sideEffects.setIsEnabled(prevSideEffectsEnabled)\n\t\t}\n\t}\n\n\t/**\n\t * Get an array of all records in the store.\n\t *\n\t * @example\n\t * ```ts\n\t * const allRecords = store.allRecords()\n\t * const books = allRecords.filter(r => r.typeName === 'book')\n\t * ```\n\t *\n\t * @returns An array containing all records in the store\n\t * @public\n\t */\n\tallRecords(): R[] {\n\t\treturn Array.from(this.records.values())\n\t}\n\n\t/**\n\t * Remove all records from the store.\n\t *\n\t * @example\n\t * ```ts\n\t * store.clear()\n\t * console.log(store.allRecords().length) // 0\n\t * ```\n\t *\n\t * @public\n\t */\n\tclear(): void {\n\t\tthis.remove(Array.from(this.records.keys()))\n\t}\n\n\t/**\n\t * Update a single record using an updater function. To update multiple records at once,\n\t * use the `update` method of the `TypedStore` class.\n\t *\n\t * @example\n\t * ```ts\n\t * store.update(book.id, (book) => ({\n\t * ...book,\n\t * title: 'Updated Title'\n\t * }))\n\t * ```\n\t *\n\t * @param id - The ID of the record to update\n\t * @param updater - A function that receives the current record and returns the updated record\n\t * @public\n\t */\n\tupdate<K extends IdOf<R>>(id: K, updater: (record: RecordFromId<K>) => RecordFromId<K>) {\n\t\tconst existing = this.unsafeGetWithoutCapture(id)\n\t\tif (!existing) {\n\t\t\tconsole.error(`Record ${id} not found. This is probably an error`)\n\t\t\treturn\n\t\t}\n\n\t\tthis.put([updater(existing) as any])\n\t}\n\n\t/**\n\t * Check whether a record with the given ID exists in the store.\n\t *\n\t * @example\n\t * ```ts\n\t * if (store.has(bookId)) {\n\t * console.log('Book exists!')\n\t * }\n\t * ```\n\t *\n\t * @param id - The ID of the record to check\n\t * @returns True if the record exists, false otherwise\n\t * @public\n\t */\n\thas<K extends IdOf<R>>(id: K): boolean {\n\t\treturn this.records.has(id)\n\t}\n\n\t/**\n\t * Add a listener that will be called when the store changes.\n\t * Returns a function to remove the listener.\n\t *\n\t * @example\n\t * ```ts\n\t * const removeListener = store.listen((entry) => {\n\t * console.log('Changes:', entry.changes)\n\t * console.log('Source:', entry.source)\n\t * })\n\t *\n\t * // Listen only to user changes to document records\n\t * const removeDocumentListener = store.listen(\n\t * (entry) => console.log('Document changed:', entry),\n\t * { source: 'user', scope: 'document' }\n\t * )\n\t *\n\t * // Later, remove the listener\n\t * removeListener()\n\t * ```\n\t *\n\t * @param onHistory - The listener function to call when changes occur\n\t * @param filters - Optional filters to control when the listener is called\n\t * @returns A function that removes the listener when called\n\t * @public\n\t */\n\tlisten(onHistory: StoreListener<R>, filters?: Partial<StoreListenerFilters>) {\n\t\t// flush history so that this listener's history starts from exactly now\n\t\tthis._flushHistory()\n\n\t\tconst listener = {\n\t\t\tonHistory,\n\t\t\tfilters: {\n\t\t\t\tsource: filters?.source ?? 'all',\n\t\t\t\tscope: filters?.scope ?? 'all',\n\t\t\t},\n\t\t}\n\n\t\tif (!this.historyReactor.scheduler.isActivelyListening) {\n\t\t\tthis.historyReactor.start()\n\t\t\tthis.historyReactor.scheduler.execute()\n\t\t}\n\n\t\tthis.listeners.add(listener)\n\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener)\n\n\t\t\tif (this.listeners.size === 0) {\n\t\t\t\tthis.historyReactor.stop()\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate isMergingRemoteChanges = false\n\n\t/**\n\t * Merge changes from a remote source. Changes made within the provided function\n\t * will be marked with source 'remote' instead of 'user'.\n\t *\n\t * @example\n\t * ```ts\n\t * // Changes from sync/collaboration\n\t * store.mergeRemoteChanges(() => {\n\t * store.put(remoteRecords)\n\t * store.remove(deletedIds)\n\t * })\n\t * ```\n\t *\n\t * @param fn - A function that applies the remote changes\n\t * @public\n\t */\n\tmergeRemoteChanges(fn: () => void) {\n\t\tif (this.isMergingRemoteChanges) {\n\t\t\treturn fn()\n\t\t}\n\n\t\tif (this._isInAtomicOp) {\n\t\t\tthrow new Error('Cannot merge remote changes while in atomic operation')\n\t\t}\n\n\t\ttry {\n\t\t\tthis.atomic(fn, true, true)\n\t\t} finally {\n\t\t\tthis.ensureStoreIsUsable()\n\t\t}\n\t}\n\n\t/**\n\t * Run `fn` and return a {@link RecordsDiff} of the changes that occurred as a result.\n\t */\n\textractingChanges(fn: () => void): RecordsDiff<R> {\n\t\tconst changes: Array<RecordsDiff<R>> = []\n\t\tconst dispose = this.historyAccumulator.addInterceptor((entry) => changes.push(entry.changes))\n\t\ttry {\n\t\t\ttransact(fn)\n\t\t\treturn squashRecordDiffs(changes)\n\t\t} finally {\n\t\t\tdispose()\n\t\t}\n\t}\n\n\tapplyDiff(\n\t\tdiff: RecordsDiff<R>,\n\t\t{\n\t\t\trunCallbacks = true,\n\t\t\tignoreEphemeralKeys = false,\n\t\t}: { runCallbacks?: boolean; ignoreEphemeralKeys?: boolean } = {}\n\t) {\n\t\tthis.atomic(() => {\n\t\t\tconst toPut = objectMapValues(diff.added)\n\n\t\t\tfor (const [_from, to] of objectMapValues(diff.updated)) {\n\t\t\t\tconst type = this.schema.getType(to.typeName)\n\t\t\t\tif (ignoreEphemeralKeys && type.ephemeralKeySet.size) {\n\t\t\t\t\tconst existing = this.get(to.id)\n\t\t\t\t\tif (!existing) {\n\t\t\t\t\t\ttoPut.push(to)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tlet changed: R | null = null\n\t\t\t\t\tfor (const [key, value] of Object.entries(to)) {\n\t\t\t\t\t\tif (type.ephemeralKeySet.has(key) || Object.is(value, getOwnProperty(existing, key))) {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (!changed) changed = { ...existing } as R\n\t\t\t\t\t\t;(changed as any)[key] = value\n\t\t\t\t\t}\n\t\t\t\t\tif (changed) toPut.push(changed)\n\t\t\t\t} else {\n\t\t\t\t\ttoPut.push(to)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst toRemove = objectMapKeys(diff.removed)\n\t\t\tif (toPut.length) {\n\t\t\t\tthis.put(toPut)\n\t\t\t}\n\t\t\tif (toRemove.length) {\n\t\t\t\tthis.remove(toRemove)\n\t\t\t}\n\t\t}, runCallbacks)\n\t}\n\n\t/**\n\t * Create a cache based on values in the store. Pass in a function that takes and ID and a\n\t * signal for the underlying record. Return a signal (usually a computed) for the cached value.\n\t * For simple derivations, use {@link Store.createComputedCache}. This function is useful if you\n\t * need more precise control over intermediate values.\n\t */\n\tcreateCache<Result, Record extends R = R>(\n\t\tcreate: (id: IdOf<Record>, recordSignal: Signal<R>) => Signal<Result>\n\t) {\n\t\tconst cache = new WeakCache<Atom<any>, Signal<Result>>()\n\t\treturn {\n\t\t\tget: (id: IdOf<Record>) => {\n\t\t\t\tconst atom = this.records.getAtom(id)\n\t\t\t\tif (!atom) return undefined\n\t\t\t\treturn cache.get(atom, () => create(id, atom as Signal<R>)).get()\n\t\t\t},\n\t\t}\n\t}\n\n\t/**\n\t * Create a computed cache.\n\t *\n\t * @param name - The name of the derivation cache.\n\t * @param derive - A function used to derive the value of the cache.\n\t * @param opts - Options for the computed cache.\n\t * @public\n\t */\n\tcreateComputedCache<Result, Record extends R = R>(\n\t\tname: string,\n\t\tderive: (record: Record) => Result | undefined,\n\t\topts?: CreateComputedCacheOpts<Result, Record>\n\t): ComputedCache<Result, Record> {\n\t\treturn this.createCache((id, record) => {\n\t\t\tconst recordSignal = opts?.areRecordsEqual\n\t\t\t\t? computed(`${name}:${id}:isEqual`, () => record.get(), { isEqual: opts.areRecordsEqual })\n\t\t\t\t: record\n\n\t\t\treturn computed<Result | undefined>(\n\t\t\t\tname + ':' + id,\n\t\t\t\t() => {\n\t\t\t\t\treturn derive(recordSignal.get() as Record)\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tisEqual: opts?.areResultsEqual,\n\t\t\t\t}\n\t\t\t)\n\t\t})\n\t}\n\n\tprivate _integrityChecker?: () => void | undefined\n\n\t/** @internal */\n\tensureStoreIsUsable() {\n\t\tthis.atomic(() => {\n\t\t\tthis._integrityChecker ??= this.schema.createIntegrityChecker(this)\n\t\t\tthis._integrityChecker?.()\n\t\t})\n\t}\n\n\tprivate _isPossiblyCorrupted = false\n\t/** @internal */\n\tmarkAsPossiblyCorrupted() {\n\t\tthis._isPossiblyCorrupted = true\n\t}\n\t/** @internal */\n\tisPossiblyCorrupted() {\n\t\treturn this._isPossiblyCorrupted\n\t}\n\n\tprivate pendingAfterEvents: Map<IdOf<R>, { before: R | null; after: R | null }> | null = null\n\tprivate addDiffForAfterEvent(before: R | null, after: R | null) {\n\t\tassert(this.pendingAfterEvents, 'must be in event operation')\n\t\tif (before === after) return\n\t\tif (before && after) assert(before.id === after.id)\n\t\tif (!before && !after) return\n\t\tconst id = (before || after)!.id\n\t\tconst existing = this.pendingAfterEvents.get(id)\n\t\tif (existing) {\n\t\t\texisting.after = after\n\t\t} else {\n\t\t\tthis.pendingAfterEvents.set(id, { before, after })\n\t\t}\n\t}\n\tprivate flushAtomicCallbacks(isMergingRemoteChanges: boolean) {\n\t\tlet updateDepth = 0\n\t\tlet source: ChangeSource = isMergingRemoteChanges ? 'remote' : 'user'\n\t\twhile (this.pendingAfterEvents) {\n\t\t\tconst events = this.pendingAfterEvents\n\t\t\tthis.pendingAfterEvents = null\n\n\t\t\tif (!this.sideEffects.isEnabled()) continue\n\n\t\t\tupdateDepth++\n\t\t\tif (updateDepth > 100) {\n\t\t\t\tthrow new Error('Maximum store update depth exceeded, bailing out')\n\t\t\t}\n\n\t\t\tfor (const { before, after } of events.values()) {\n\t\t\t\tif (before && after && before !== after && !isEqual(before, after)) {\n\t\t\t\t\tthis.sideEffects.handleAfterChange(before, after, source)\n\t\t\t\t} else if (before && !after) {\n\t\t\t\t\tthis.sideEffects.handleAfterDelete(before, source)\n\t\t\t\t} else if (!before && after) {\n\t\t\t\t\tthis.sideEffects.handleAfterCreate(after, source)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!this.pendingAfterEvents) {\n\t\t\t\tthis.sideEffects.handleOperationComplete(source)\n\t\t\t} else {\n\t\t\t\t// if the side effects triggered by a remote operation resulted in more effects,\n\t\t\t\t// those extra effects should not be marked as originating remotely.\n\t\t\t\tsource = 'user'\n\t\t\t}\n\t\t}\n\t}\n\tprivate _isInAtomicOp = false\n\t/** @internal */\n\tatomic<T>(fn: () => T, runCallbacks = true, isMergingRemoteChanges = false): T {\n\t\treturn transact(() => {\n\t\t\tif (this._isInAtomicOp) {\n\t\t\t\tif (!this.pendingAfterEvents) this.pendingAfterEvents = new Map()\n\t\t\t\tconst prevSideEffectsEnabled = this.sideEffects.isEnabled()\n\t\t\t\tassert(!isMergingRemoteChanges, 'cannot call mergeRemoteChanges while in atomic operation')\n\t\t\t\ttry {\n\t\t\t\t\t// if we are in an atomic context with side effects ON allow switching before* callbacks OFF.\n\t\t\t\t\t// but don't allow switching them ON if they had been marked OFF before.\n\t\t\t\t\tif (prevSideEffectsEnabled && !runCallbacks) {\n\t\t\t\t\t\tthis.sideEffects.setIsEnabled(false)\n\t\t\t\t\t}\n\t\t\t\t\treturn fn()\n\t\t\t\t} finally {\n\t\t\t\t\tthis.sideEffects.setIsEnabled(prevSideEffectsEnabled)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthis.pendingAfterEvents = new Map()\n\t\t\tconst prevSideEffectsEnabled = this.sideEffects.isEnabled()\n\t\t\tthis.sideEffects.setIsEnabled(runCallbacks ?? prevSideEffectsEnabled)\n\t\t\tthis._isInAtomicOp = true\n\n\t\t\tif (isMergingRemoteChanges) {\n\t\t\t\tthis.isMergingRemoteChanges = true\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\tconst result = fn()\n\t\t\t\tthis.isMergingRemoteChanges = false\n\n\t\t\t\tthis.flushAtomicCallbacks(isMergingRemoteChanges)\n\n\t\t\t\treturn result\n\t\t\t} finally {\n\t\t\t\tthis.pendingAfterEvents = null\n\t\t\t\tthis.sideEffects.setIsEnabled(prevSideEffectsEnabled)\n\t\t\t\tthis._isInAtomicOp = false\n\t\t\t\tthis.isMergingRemoteChanges = false\n\t\t\t}\n\t\t})\n\t}\n\n\t/** @internal */\n\taddHistoryInterceptor(fn: (entry: HistoryEntry<R>, source: ChangeSource) => void) {\n\t\treturn this.historyAccumulator.addInterceptor((entry) =>\n\t\t\tfn(entry, this.isMergingRemoteChanges ? 'remote' : 'user')\n\t\t)\n\t}\n}\n\n/**\n * Collect and squash history entries by their adjacent sources.\n * Adjacent entries from the same source are combined into a single entry.\n *\n * For example: [user, user, remote, remote, user] becomes [user, remote, user]\n *\n * @example\n * ```ts\n * const entries = [\n * { source: 'user', changes: userChanges1 },\n * { source: 'user', changes: userChanges2 },\n * { source: 'remote', changes: remoteChanges }\n * ]\n *\n * const squashed = squashHistoryEntries(entries)\n * // Results in 2 entries: combined user changes + remote changes\n * ```\n *\n * @param entries - The array of history entries to squash\n * @returns An array of squashed history entries\n * @public\n */\nfunction squashHistoryEntries<T extends UnknownRecord>(\n\tentries: HistoryEntry<T>[]\n): HistoryEntry<T>[] {\n\tif (entries.length === 0) return []\n\n\tconst chunked: HistoryEntry<T>[][] = []\n\tlet chunk: HistoryEntry<T>[] = [entries[0]]\n\tlet entry: HistoryEntry<T>\n\n\tfor (let i = 1, n = entries.length; i < n; i++) {\n\t\tentry = entries[i]\n\t\tif (chunk[0].source !== entry.source) {\n\t\t\tchunked.push(chunk)\n\t\t\tchunk = []\n\t\t}\n\t\tchunk.push(entry)\n\t}\n\t// Push the last chunk\n\tchunked.push(chunk)\n\n\treturn devFreeze(\n\t\tchunked.map((chunk) => ({\n\t\t\tsource: chunk[0].source,\n\t\t\tchanges: squashRecordDiffs(chunk.map((e) => e.changes)),\n\t\t}))\n\t)\n}\n\n/**\n * Internal class that accumulates history entries before they are flushed to listeners.\n * Handles batching and squashing of adjacent entries from the same source.\n *\n * @internal\n */\nclass HistoryAccumulator<T extends UnknownRecord> {\n\tprivate _history: HistoryEntry<T>[] = []\n\n\tprivate _interceptors: Set<(entry: HistoryEntry<T>) => void> = new Set()\n\n\t/**\n\t * Add an interceptor that will be called for each history entry.\n\t * Returns a function to remove the interceptor.\n\t */\n\taddInterceptor(fn: (entry: HistoryEntry<T>) => void) {\n\t\tthis._interceptors.add(fn)\n\t\treturn () => {\n\t\t\tthis._interceptors.delete(fn)\n\t\t}\n\t}\n\n\t/**\n\t * Add a history entry to the accumulator.\n\t * Calls all registered interceptors with the entry.\n\t */\n\tadd(entry: HistoryEntry<T>) {\n\t\tthis._history.push(entry)\n\t\tfor (const interceptor of this._interceptors) {\n\t\t\tinterceptor(entry)\n\t\t}\n\t}\n\n\t/**\n\t * Flush all accumulated history entries, squashing adjacent entries from the same source.\n\t * Clears the internal history buffer.\n\t */\n\tflush() {\n\t\tconst history = squashHistoryEntries(this._history)\n\t\tthis._history = []\n\t\treturn history\n\t}\n\n\t/**\n\t * Clear all accumulated history entries without flushing.\n\t */\n\tclear() {\n\t\tthis._history = []\n\t}\n\n\t/**\n\t * Check if there are any accumulated history entries.\n\t */\n\thasChanges() {\n\t\treturn this._history.length > 0\n\t}\n}\n\n/**\n * A store or an object containing a store.\n * This type is used for APIs that can accept either a store directly or an object with a store property.\n *\n * @example\n * ```ts\n * function useStore(storeOrObject: StoreObject<MyRecord>) {\n * const store = storeOrObject instanceof Store ? storeOrObject : storeOrObject.store\n * return store\n * }\n * ```\n *\n * @public\n */\nexport type StoreObject<R extends UnknownRecord> = Store<R> | { store: Store<R> }\n/**\n * Extract the record type from a StoreObject.\n *\n * @example\n * ```ts\n * type MyStoreObject = { store: Store<Book | Author> }\n * type Records = StoreObjectRecordType<MyStoreObject> // Book | Author\n * ```\n *\n * @public\n */\nexport type StoreObjectRecordType<Context extends StoreObject<any>> =\n\tContext extends Store<infer R> ? R : Context extends { store: Store<infer R> } ? R : never\n\n/**\n * Create a computed cache that works with any StoreObject (store or object containing a store).\n * This is a standalone version of Store.createComputedCache that can work with multiple store instances.\n *\n * @example\n * ```ts\n * const expensiveCache = createComputedCache(\n * 'expensiveData',\n * (context: { store: Store<Book> }, book: Book) => {\n * return performExpensiveCalculation(book)\n * }\n * )\n *\n * // Use with different store instances\n * const result1 = expensiveCache.get(storeObject1, bookId)\n * const result2 = expensiveCache.get(storeObject2, bookId)\n * ```\n *\n * @param name - A unique name for the cache (used for debugging)\n * @param derive - Function that derives a value from the context and record\n * @param opts - Optional configuration for equality checks\n * @returns A cache that can be used with multiple store instances\n * @public\n */\nexport function createComputedCache<\n\tContext extends StoreObject<any>,\n\tResult,\n\tRecord extends StoreObjectRecordType<Context> = StoreObjectRecordType<Context>,\n>(\n\tname: string,\n\tderive: (context: Context, record: Record) => Result | undefined,\n\topts?: CreateComputedCacheOpts<Result, Record>\n) {\n\tconst cache = new WeakCache<Context, ComputedCache<Result, Record>>()\n\treturn {\n\t\tget(context: Context, id: IdOf<Record>) {\n\t\t\tconst computedCache = cache.get(context, () => {\n\t\t\t\tconst store = (context instanceof Store ? context : context.store) as Store<Record>\n\t\t\t\treturn store.createComputedCache(name, (record) => derive(context, record), opts)\n\t\t\t})\n\t\t\treturn computedCache.get(id)\n\t\t},\n\t}\n}\n"],
"mappings": "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