UNPKG

@tldraw/store

Version:

tldraw infinite canvas SDK (store).

1,517 lines (1,470 loc) • 100 kB
import { Atom } from '@tldraw/state'; import { Computed } from '@tldraw/state'; import { Expand } from '@tldraw/utils'; import { Result } from '@tldraw/utils'; import { Signal } from '@tldraw/state'; import { UNINITIALIZED } from '@tldraw/state'; /** * Assert whether an id correspond to a record type. * * @example * * ```ts * assertIdType(myId, "shape") * ``` * * @param id - The id to check. * @param type - The type of the record. * @public */ export declare function assertIdType<R extends UnknownRecord>(id: string | undefined, type: RecordType<R, any>): asserts id is IdOf<R>; /** * A drop-in replacement for Map that stores values in atoms and can be used in reactive contexts. * @public */ export declare class AtomMap<K, V> implements Map<K, V> { private readonly name; private atoms; /** * Creates a new AtomMap instance. * * name - A unique name for this map, used for atom identification * entries - Optional initial entries to populate the map with * @example * ```ts * // Create an empty map * const map = new AtomMap('userMap') * * // Create a map with initial data * const initialData: [string, number][] = [['a', 1], ['b', 2]] * const mapWithData = new AtomMap('numbersMap', initialData) * ``` */ constructor(name: string, entries?: Iterable<readonly [K, V]>); /* Excluded from this release type: getAtom */ /** * Gets the value associated with a key. Returns undefined if the key doesn't exist. * This method is reactive and will cause reactive contexts to update when the value changes. * * @param key - The key to retrieve the value for * @returns The value associated with the key, or undefined if not found * @example * ```ts * const map = new AtomMap('myMap') * map.set('name', 'Alice') * console.log(map.get('name')) // 'Alice' * console.log(map.get('missing')) // undefined * ``` */ get(key: K): undefined | V; /** * Gets the value associated with a key without creating reactive dependencies. * This method will not cause reactive contexts to update when the value changes. * * @param key - The key to retrieve the value for * @returns The value associated with the key, or undefined if not found * @example * ```ts * const map = new AtomMap('myMap') * map.set('count', 42) * const value = map.__unsafe__getWithoutCapture('count') // No reactive subscription * ``` */ __unsafe__getWithoutCapture(key: K): undefined | V; /** * Checks whether a key exists in the map. * This method is reactive and will cause reactive contexts to update when keys are added or removed. * * @param key - The key to check for * @returns True if the key exists in the map, false otherwise * @example * ```ts * const map = new AtomMap('myMap') * console.log(map.has('name')) // false * map.set('name', 'Alice') * console.log(map.has('name')) // true * ``` */ has(key: K): boolean; /** * Checks whether a key exists in the map without creating reactive dependencies. * This method will not cause reactive contexts to update when keys are added or removed. * * @param key - The key to check for * @returns True if the key exists in the map, false otherwise * @example * ```ts * const map = new AtomMap('myMap') * map.set('active', true) * const exists = map.__unsafe__hasWithoutCapture('active') // No reactive subscription * ``` */ __unsafe__hasWithoutCapture(key: K): boolean; /** * Sets a value for the given key. If the key already exists, its value is updated. * If the key doesn't exist, a new entry is created. * * @param key - The key to set the value for * @param value - The value to associate with the key * @returns This AtomMap instance for method chaining * @example * ```ts * const map = new AtomMap('myMap') * map.set('name', 'Alice').set('age', 30) * ``` */ set(key: K, value: V): this; /** * Updates an existing value using an updater function. * * @param key - The key of the value to update * @param updater - A function that receives the current value and returns the new value * @throws Error if the key doesn't exist in the map * @example * ```ts * const map = new AtomMap('myMap') * map.set('count', 5) * map.update('count', count => count + 1) // count is now 6 * ``` */ update(key: K, updater: (value: V) => V): void; /** * Removes a key-value pair from the map. * * @param key - The key to remove * @returns True if the key existed and was removed, false if it didn't exist * @example * ```ts * const map = new AtomMap('myMap') * map.set('temp', 'value') * console.log(map.delete('temp')) // true * console.log(map.delete('missing')) // false * ``` */ delete(key: K): boolean; /** * Removes multiple key-value pairs from the map in a single transaction. * * @param keys - An iterable of keys to remove * @returns An array of [key, value] pairs that were actually deleted * @example * ```ts * const map = new AtomMap('myMap') * map.set('a', 1).set('b', 2).set('c', 3) * const deleted = map.deleteMany(['a', 'c', 'missing']) * console.log(deleted) // [['a', 1], ['c', 3]] * ``` */ deleteMany(keys: Iterable<K>): [K, V][]; /** * Removes all key-value pairs from the map. * * @example * ```ts * const map = new AtomMap('myMap') * map.set('a', 1).set('b', 2) * map.clear() * console.log(map.size) // 0 * ``` */ clear(): void; /** * Returns an iterator that yields [key, value] pairs for each entry in the map. * This method is reactive and will cause reactive contexts to update when entries change. * * @returns A generator that yields [key, value] tuples * @example * ```ts * const map = new AtomMap('myMap') * map.set('a', 1).set('b', 2) * for (const [key, value] of map.entries()) { * console.log(`${key}: ${value}`) * } * ``` */ entries(): Generator<[K, V], undefined, unknown>; /** * Returns an iterator that yields all keys in the map. * This method is reactive and will cause reactive contexts to update when keys change. * * @returns A generator that yields keys * @example * ```ts * const map = new AtomMap('myMap') * map.set('name', 'Alice').set('age', 30) * for (const key of map.keys()) { * console.log(key) // 'name', 'age' * } * ``` */ keys(): Generator<K, undefined, unknown>; /** * Returns an iterator that yields all values in the map. * This method is reactive and will cause reactive contexts to update when values change. * * @returns A generator that yields values * @example * ```ts * const map = new AtomMap('myMap') * map.set('name', 'Alice').set('age', 30) * for (const value of map.values()) { * console.log(value) // 'Alice', 30 * } * ``` */ values(): Generator<V, undefined, unknown>; /** * The number of key-value pairs in the map. * This property is reactive and will cause reactive contexts to update when the size changes. * * @returns The number of entries in the map * @example * ```ts * const map = new AtomMap('myMap') * console.log(map.size) // 0 * map.set('a', 1) * console.log(map.size) // 1 * ``` */ get size(): number; /** * Executes a provided function once for each key-value pair in the map. * This method is reactive and will cause reactive contexts to update when entries change. * * @param callbackfn - Function to execute for each entry * - value - The value of the current entry * - key - The key of the current entry * - map - The AtomMap being traversed * @param thisArg - Value to use as `this` when executing the callback * @example * ```ts * const map = new AtomMap('myMap') * map.set('a', 1).set('b', 2) * map.forEach((value, key) => { * console.log(`${key} = ${value}`) * }) * ``` */ forEach(callbackfn: (value: V, key: K, map: AtomMap<K, V>) => void, thisArg?: any): void; /** * Returns the default iterator for the map, which is the same as entries(). * This allows the map to be used in for...of loops and other iterable contexts. * * @returns The same iterator as entries() * @example * ```ts * const map = new AtomMap('myMap') * map.set('a', 1).set('b', 2) * * // These are equivalent: * for (const [key, value] of map) { * console.log(`${key}: ${value}`) * } * * for (const [key, value] of map.entries()) { * console.log(`${key}: ${value}`) * } * ``` */ [Symbol.iterator](): Generator<[K, V], undefined, unknown>; /** * The string tag used by Object.prototype.toString for this class. * * @example * ```ts * const map = new AtomMap('myMap') * console.log(Object.prototype.toString.call(map)) // '[object AtomMap]' * ``` */ [Symbol.toStringTag]: string; } /** * A drop-in replacement for Set that stores values in atoms and can be used in reactive contexts. * @public */ export declare class AtomSet<T> { private readonly name; private readonly map; constructor(name: string, keys?: Iterable<T>); add(value: T): this; clear(): void; delete(value: T): boolean; forEach(callbackfn: (value: T, value2: T, set: AtomSet<T>) => void, thisArg?: any): void; has(value: T): boolean; get size(): number; entries(): Generator<[T, T], undefined, unknown>; keys(): Generator<T, undefined, unknown>; values(): Generator<T, undefined, unknown>; [Symbol.iterator](): Generator<T, undefined, unknown>; [Symbol.toStringTag]: string; } /** * The base record interface that all records in the store must extend. * This interface provides the fundamental structure required for all records: a unique ID and a type name. * The type parameters ensure type safety and prevent mixing of different record types. * * @example * ```ts * // Define a Book record that extends BaseRecord * interface Book extends BaseRecord<'book', RecordId<Book>> { * title: string * author: string * publishedYear: number * } * * // Define an Author record * interface Author extends BaseRecord<'author', RecordId<Author>> { * name: string * birthYear: number * } * * // Usage with RecordType * const Book = createRecordType<Book>('book', { scope: 'document' }) * const book = Book.create({ * title: '1984', * author: 'George Orwell', * publishedYear: 1949 * }) * // Results in: { id: 'book:abc123', typeName: 'book', title: '1984', ... } * ``` * * @public */ export declare interface BaseRecord<TypeName extends string, Id extends RecordId<UnknownRecord>> { readonly id: Id; readonly typeName: TypeName; } /** * The source of a change to the store. * - `'user'` - Changes originating from local user actions * - `'remote'` - Changes originating from remote synchronization * * @public */ export declare type ChangeSource = 'remote' | 'user'; /** * A diff describing the changes to a collection. * * @example * ```ts * const diff: CollectionDiff<string> = { * added: new Set(['newItem']), * removed: new Set(['oldItem']) * } * ``` * * @public */ export declare interface CollectionDiff<T> { /** Items that were added to the collection */ added?: Set<T>; /** Items that were removed from the collection */ removed?: Set<T>; } /** * A computed cache that stores derived data for records. * The cache automatically updates when underlying records change and cleans up when records are deleted. * * @example * ```ts * const expensiveCache = store.createComputedCache( * 'expensive', * (book: Book) => performExpensiveCalculation(book) * ) * * const result = expensiveCache.get(bookId) * ``` * * @public */ export declare interface ComputedCache<Data, R extends UnknownRecord> { /** * Get the cached data for a record by its ID. * * @param id - The ID of the record * @returns The cached data or undefined if the record doesn't exist */ get(id: IdOf<R>): Data | undefined; } /** * Create a computed cache that works with any StoreObject (store or object containing a store). * This is a standalone version of Store.createComputedCache that can work with multiple store instances. * * @example * ```ts * const expensiveCache = createComputedCache( * 'expensiveData', * (context: { store: Store<Book> }, book: Book) => { * return performExpensiveCalculation(book) * } * ) * * // Use with different store instances * const result1 = expensiveCache.get(storeObject1, bookId) * const result2 = expensiveCache.get(storeObject2, bookId) * ``` * * @param name - A unique name for the cache (used for debugging) * @param derive - Function that derives a value from the context and record * @param opts - Optional configuration for equality checks * @returns A cache that can be used with multiple store instances * @public */ export declare function createComputedCache<Context extends StoreObject<any>, Result, Record extends StoreObjectRecordType<Context> = StoreObjectRecordType<Context>>(name: string, derive: (context: Context, record: Record) => Result | undefined, opts?: CreateComputedCacheOpts<Result, Record>): { get(context: Context, id: IdOf<Record>): Result | undefined; }; /** * Options for creating a computed cache. * * @example * ```ts * const options: CreateComputedCacheOpts<string[], Book> = { * areRecordsEqual: (a, b) => a.title === b.title, * areResultsEqual: (a, b) => JSON.stringify(a) === JSON.stringify(b) * } * ``` * * @public */ export declare interface CreateComputedCacheOpts<Data, R extends UnknownRecord> { /** Custom equality function for comparing records */ areRecordsEqual?(a: R, b: R): boolean; /** Custom equality function for comparing results */ areResultsEqual?(a: Data, b: Data): boolean; } /* Excluded from this release type: createEmptyRecordsDiff */ /** * Creates a named set of migration IDs from version numbers and a sequence ID. * * This utility function helps generate properly formatted migration IDs that follow * the required `sequenceId/version` pattern. It takes a sequence ID and a record * of named versions, returning migration IDs that can be used in migration definitions. * * See the [migration guide](https://tldraw.dev/docs/persistence#Migrations) for more info on how to use this API. * @param sequenceId - The sequence identifier (e.g., 'com.myapp.book') * @param versions - Record mapping version names to numbers * @returns Record mapping version names to properly formatted migration IDs * @example * ```ts * const migrationIds = createMigrationIds('com.myapp.book', { * addGenre: 1, * addPublisher: 2, * removeOldField: 3 * }) * // Result: { * // addGenre: 'com.myapp.book/1', * // addPublisher: 'com.myapp.book/2', * // removeOldField: 'com.myapp.book/3' * // } * ``` * @public */ export declare function createMigrationIds<const ID extends string, const Versions extends Record<string, number>>(sequenceId: ID, versions: Versions): { [K in keyof Versions]: `${ID}/${Versions[K]}`; }; /** * Creates a migration sequence that defines how to transform data as your schema evolves. * * A migration sequence contains a series of migrations that are applied in order to transform * data from older versions to newer versions. Each migration is identified by a unique ID * and can operate at either the record level (transforming individual records) or store level * (transforming the entire store structure). * * See the [migration guide](https://tldraw.dev/docs/persistence#Migrations) for more info on how to use this API. * @param options - Configuration for the migration sequence * - sequenceId - Unique identifier for this migration sequence (e.g., 'com.myapp.book') * - sequence - Array of migrations or dependency declarations to include in the sequence * - retroactive - Whether migrations should apply to snapshots created before this sequence was added (defaults to true) * @returns A validated migration sequence that can be included in a store schema * @example * ```ts * const bookMigrations = createMigrationSequence({ * sequenceId: 'com.myapp.book', * sequence: [ * { * id: 'com.myapp.book/1', * scope: 'record', * up: (record) => ({ ...record, newField: 'default' }) * } * ] * }) * ``` * @public */ export declare function createMigrationSequence({ sequence, sequenceId, retroactive, }: { retroactive?: boolean; sequence: Array<Migration | StandaloneDependsOn>; sequenceId: string; }): MigrationSequence; /* Excluded from this release type: createRecordMigrationSequence */ /** * Creates a new RecordType with the specified configuration. * * This factory function creates a RecordType that can be used to create, validate, and manage * records of a specific type within a store. The resulting RecordType can be extended with * default properties using the withDefaultProperties method. * * @example * ```ts * interface BookRecord extends BaseRecord<'book', RecordId<BookRecord>> { * title: string * author: string * inStock: boolean * } * * const Book = createRecordType<BookRecord>('book', { * scope: 'document', * validator: bookValidator * }) * ``` * * @param typeName - The unique type name for this record type * @param config - Configuration object containing validator, scope, and ephemeral keys * @returns A new RecordType instance for creating and managing records * @public */ export declare function createRecordType<R extends UnknownRecord>(typeName: R['typeName'], config: { ephemeralKeys?: { readonly [K in Exclude<keyof R, 'id' | 'typeName'>]: boolean; }; scope: RecordScope; validator?: StoreValidator<R>; }): RecordType<R, keyof Omit<R, 'id' | 'typeName'>>; /** * Freeze an object when in development mode. Copied from * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze * * @example * * ```ts * const frozen = devFreeze({ a: 1 }) * ``` * * @param object - The object to freeze. * @returns The frozen object when in development mode, or else the object when in other modes. * @public */ export declare function devFreeze<T>(object: T): T; /** * An entry containing changes that originated either by user actions or remote changes. * History entries are used to track and replay changes to the store. * * @example * ```ts * const entry: HistoryEntry<Book> = { * changes: { * added: { 'book:123': bookRecord }, * updated: {}, * removed: {} * }, * source: 'user' * } * ``` * * @public */ export declare interface HistoryEntry<R extends UnknownRecord = UnknownRecord> { /** The changes that occurred in this history entry */ changes: RecordsDiff<R>; /** The source of these changes */ source: ChangeSource; } /** * Utility type that extracts the ID type from a record type. * This is useful when you need to work with record IDs without having the full record type. * * @example * ```ts * interface Book extends BaseRecord<'book', RecordId<Book>> { * title: string * author: string * } * * // Extract the ID type from the Book record * type BookId = IdOf<Book> // RecordId<Book> * * function findBook(id: IdOf<Book>): Book | undefined { * return store.get(id) * } * ``` * * @public */ export declare type IdOf<R extends UnknownRecord> = R['id']; /* Excluded from this release type: IncrementalSetConstructor */ /** * Checks whether a RecordsDiff contains any changes. A diff is considered empty * if it has no added, updated, or removed records. * * @param diff - The diff to check * @returns True if the diff contains no changes, false otherwise * @example * ```ts * const emptyDiff = createEmptyRecordsDiff<Book>() * console.log(isRecordsDiffEmpty(emptyDiff)) // true * * const nonEmptyDiff: RecordsDiff<Book> = { * added: { 'book:1': someBook }, * updated: {}, * removed: {} * } * console.log(isRecordsDiffEmpty(nonEmptyDiff)) // false * ``` * * @public */ export declare function isRecordsDiffEmpty<T extends UnknownRecord>(diff: RecordsDiff<T>): boolean; /** * Base interface for legacy migration information. * * Contains the basic structure used by the legacy migration system, including version * range information and the migration functions indexed by version number. This is * maintained for backward compatibility with older migration definitions. * @public */ export declare interface LegacyBaseMigrationsInfo { firstVersion: number; currentVersion: number; migrators: { [version: number]: LegacyMigration; }; } /** * Legacy migration interface for backward compatibility. * * This interface represents the old migration format that included both `up` and `down` * transformation functions. While still supported, new code should use the `Migration` * type which provides more flexibility and better integration with the current system. * @public */ export declare interface LegacyMigration<Before = any, After = any> { up: (oldState: Before) => After; down: (newState: After) => Before; } /** * Legacy migration configuration with support for sub-type migrations. * * This interface extends the base legacy migration info to support migrations that * vary based on a sub-type key within records. This allows different migration paths * for different variants of the same record type, which was useful in older migration * systems but is now handled more elegantly by the current Migration system. * @public */ export declare interface LegacyMigrations extends LegacyBaseMigrationsInfo { subTypeKey?: string; subTypeMigrations?: Record<string, LegacyBaseMigrationsInfo>; } /** * Defines a single migration that transforms data from one schema version to another. * * A migration can operate at two different scopes: * - `record`: Transforms individual records, with optional filtering to target specific records * - `store`: Transforms the entire serialized store structure * * Each migration has a unique ID and can declare dependencies on other migrations that must * be applied first. The `up` function performs the forward transformation, while the optional * `down` function can reverse the migration if needed. * @public */ export declare type Migration = { readonly dependsOn?: readonly MigrationId[] | undefined; readonly id: MigrationId; } & ({ readonly down?: (newState: SerializedStore<UnknownRecord>) => SerializedStore<UnknownRecord> | void; readonly scope: 'store'; readonly up: (oldState: SerializedStore<UnknownRecord>) => SerializedStore<UnknownRecord> | void; } | { readonly down?: (newState: UnknownRecord) => UnknownRecord | void; readonly filter?: (record: UnknownRecord) => boolean; readonly scope: 'record'; readonly up: (oldState: UnknownRecord) => UnknownRecord | void; } | { readonly down?: never; readonly scope: 'storage'; readonly up: (storage: SynchronousRecordStorage<UnknownRecord>) => void; }); /** * Enumeration of possible reasons why a migration might fail. * * These reasons help identify what went wrong during migration processing, * allowing applications to handle different failure scenarios appropriately. * Common failures include incompatible data formats, unknown record types, * and version mismatches between the data and available migrations. * @public */ export declare enum MigrationFailureReason { IncompatibleSubtype = "incompatible-subtype", UnknownType = "unknown-type", TargetVersionTooNew = "target-version-too-new", TargetVersionTooOld = "target-version-too-old", MigrationError = "migration-error", UnrecognizedSubtype = "unrecognized-subtype" } /** * Unique identifier for a migration in the format `sequenceId/version`. * * Migration IDs follow a specific pattern where the sequence ID identifies the migration * sequence and the version number indicates the order within that sequence. For example: * 'com.myapp.book/1', 'com.myapp.book/2', etc. * @public */ export declare type MigrationId = `${string}/${number}`; /** * Result type returned by migration operations. * * Migration operations can either succeed and return the transformed value, * or fail with a specific reason. This discriminated union type allows for * safe handling of both success and error cases when applying migrations. * @public */ export declare type MigrationResult<T> = { reason: MigrationFailureReason; type: 'error'; } | { type: 'success'; value: T; }; /** * A complete sequence of migrations that can be applied to transform data. * * A migration sequence represents a series of ordered migrations that belong together, * typically for a specific part of your schema. The sequence includes metadata about * whether it should be applied retroactively to existing data and contains the actual * migration definitions in execution order. * @public */ export declare interface MigrationSequence { sequenceId: string; /** * retroactive should be true if the migrations should be applied to snapshots that were created before * this migration sequence was added to the schema. * * In general: * * - retroactive should be true when app developers create their own new migration sequences. * - retroactive should be false when library developers ship a migration sequence. When you install a library for the first time, any migrations that were added in the library before that point should generally _not_ be applied to your existing data. */ retroactive: boolean; sequence: Migration[]; } /* Excluded from this release type: parseMigrationId */ /** * Query expression for filtering records by their property values. Maps record property names * to matching criteria. * * @example * ```ts * // Query for books published after 2020 that are in stock * const bookQuery: QueryExpression<Book> = { * publishedYear: { gt: 2020 }, * inStock: { eq: true } * } * * // Query for books not by a specific author * const notByAuthor: QueryExpression<Book> = { * authorId: { neq: 'author:tolkien' } * } * * // Query with nested properties * const nestedQuery: QueryExpression<Book> = { * metadata: { sessionId: { eq: 'session:alpha' } } * } * ``` * * @public */ /** @public */ export declare type QueryExpression<R extends object> = { [k in keyof R & string]?: R[k] extends boolean | null | number | string | undefined ? QueryValueMatcher<R[k]> : R[k] extends object ? QueryExpression<R[k]> : QueryValueMatcher<R[k]>; }; /** * Defines matching criteria for query values. Supports equality, inequality, and greater-than comparisons. * * @example * ```ts * // Exact match * const exactMatch: QueryValueMatcher<string> = { eq: 'Science Fiction' } * * // Not equal to * const notMatch: QueryValueMatcher<string> = { neq: 'Romance' } * * // Greater than (numeric values only) * const greaterThan: QueryValueMatcher<number> = { gt: 2020 } * ``` * * @public */ export declare type QueryValueMatcher<T> = { eq: T; } | { gt: number; } | { neq: T; }; /** * Extracts the record type from a record ID type. * * @example * ```ts * type BookId = RecordId<Book> * type BookType = RecordFromId<BookId> // Book * ``` * * @public */ export declare type RecordFromId<K extends RecordId<UnknownRecord>> = K extends RecordId<infer R> ? R : never; /** * A branded string type that represents a unique identifier for a record. * The brand ensures type safety by preventing mixing of IDs between different record types. * * @example * ```ts * // Define a Book record * interface Book extends BaseRecord<'book', RecordId<Book>> { * title: string * author: string * } * * const bookId: RecordId<Book> = 'book:abc123' as RecordId<Book> * const authorId: RecordId<Author> = 'author:xyz789' as RecordId<Author> * * // TypeScript prevents mixing different record ID types * // bookId = authorId // Type error! * ``` * * @public */ export declare type RecordId<R extends UnknownRecord> = string & { __type__: R; }; /** * Defines the scope of the record * * session: The record belongs to a single instance of the store. It should not be synced, and any persistence logic should 'de-instance-ize' the record before persisting it, and apply the reverse when rehydrating. * document: The record is persisted and synced. It is available to all store instances. * presence: The record belongs to a single instance of the store. It may be synced to other instances, but other instances should not make changes to it. It should not be persisted. * * @public * */ export declare type RecordScope = 'document' | 'presence' | 'session'; /** * A diff describing the changes to records, containing collections of records that were added, * updated, or removed. This is the fundamental data structure used throughout the store system * to track and communicate changes. * * @example * ```ts * const diff: RecordsDiff<Book> = { * added: { * 'book:1': { id: 'book:1', typeName: 'book', title: 'New Book' } * }, * updated: { * 'book:2': [ * { id: 'book:2', typeName: 'book', title: 'Old Title' }, // from * { id: 'book:2', typeName: 'book', title: 'New Title' } // to * ] * }, * removed: { * 'book:3': { id: 'book:3', typeName: 'book', title: 'Deleted Book' } * } * } * ``` * * @public */ export declare interface RecordsDiff<R extends UnknownRecord> { /** Records that were created, keyed by their ID */ added: Record<IdOf<R>, R>; /** Records that were modified, keyed by their ID. Each entry contains [from, to] tuple */ updated: Record<IdOf<R>, [from: R, to: R]>; /** Records that were deleted, keyed by their ID */ removed: Record<IdOf<R>, R>; } /** * A record type is a type that can be stored in a record store. It is created with * `createRecordType`. * * @public */ export declare class RecordType<R extends UnknownRecord, RequiredProperties extends keyof Omit<R, 'id' | 'typeName'>> { /** * The unique type associated with this record. * * @public * @readonly */ readonly typeName: R['typeName']; /** * Factory function that creates default properties for new records. * @public */ readonly createDefaultProperties: () => Exclude<Omit<R, 'id' | 'typeName'>, RequiredProperties>; /** * Validator function used to validate records of this type. * @public */ readonly validator: StoreValidator<R>; /** * Optional configuration specifying which record properties are ephemeral. * Ephemeral properties are not included in snapshots or synchronization. * @public */ readonly ephemeralKeys?: { readonly [K in Exclude<keyof R, 'id' | 'typeName'>]: boolean; }; /** * Set of property names that are marked as ephemeral for efficient lookup. * @public */ readonly ephemeralKeySet: ReadonlySet<string>; /** * The scope that determines how records of this type are persisted and synchronized. * @public */ readonly scope: RecordScope; /** * Creates a new RecordType instance. * * typeName - The unique type name for records created by this RecordType * config - Configuration object for the RecordType * - createDefaultProperties - Function that returns default properties for new records * - validator - Optional validator function for record validation * - scope - Optional scope determining persistence behavior (defaults to 'document') * - ephemeralKeys - Optional mapping of property names to ephemeral status * @public */ constructor( /** * The unique type associated with this record. * * @public * @readonly */ typeName: R['typeName'], config: { readonly createDefaultProperties: () => Exclude<Omit<R, 'id' | 'typeName'>, RequiredProperties>; readonly ephemeralKeys?: { readonly [K in Exclude<keyof R, 'id' | 'typeName'>]: boolean; }; readonly scope?: RecordScope; readonly validator?: StoreValidator<R>; }); /** * Creates a new record of this type with the given properties. * * Properties are merged with default properties from the RecordType configuration. * If no id is provided, a unique id will be generated automatically. * * @example * ```ts * const book = Book.create({ * title: 'The Great Gatsby', * author: 'F. Scott Fitzgerald' * }) * // Result: { id: 'book:abc123', typeName: 'book', title: 'The Great Gatsby', author: 'F. Scott Fitzgerald', inStock: true } * ``` * * @param properties - The properties for the new record, including both required and optional fields * @returns The newly created record with generated id and typeName * @public */ create(properties: Expand<Pick<R, RequiredProperties> & Omit<Partial<R>, RequiredProperties>>): R; /** * Creates a deep copy of an existing record with a new unique id. * * This method performs a deep clone of all properties while generating a fresh id, * making it useful for duplicating records without id conflicts. * * @example * ```ts * const originalBook = Book.create({ title: '1984', author: 'George Orwell' }) * const duplicatedBook = Book.clone(originalBook) * // duplicatedBook has same properties but different id * ``` * * @param record - The record to clone * @returns A new record with the same properties but a different id * @public */ clone(record: R): R; /** * Create a new ID for this record type. * * @example * * ```ts * const id = recordType.createId() * ``` * * @returns The new ID. * @public */ createId(customUniquePart?: string): IdOf<R>; /** * Extracts the unique identifier part from a full record id. * * Record ids have the format `typeName:uniquePart`. This method returns just the unique part. * * @example * ```ts * const bookId = Book.createId() // 'book:abc123' * const uniquePart = Book.parseId(bookId) // 'abc123' * ``` * * @param id - The full record id to parse * @returns The unique identifier portion after the colon * @throws Error if the id is not valid for this record type * @public */ parseId(id: IdOf<R>): string; /** * Type guard that checks whether a record belongs to this RecordType. * * This method performs a runtime check by comparing the record's typeName * against this RecordType's typeName. * * @example * ```ts * if (Book.isInstance(someRecord)) { * // someRecord is now typed as a book record * console.log(someRecord.title) * } * ``` * * @param record - The record to check, may be undefined * @returns True if the record is an instance of this record type * @public */ isInstance(record?: UnknownRecord): record is R; /** * Type guard that checks whether an id string belongs to this RecordType. * * Validates that the id starts with this RecordType's typeName followed by a colon. * This is more efficient than parsing the full id when you only need to verify the type. * * @example * ```ts * if (Book.isId(someId)) { * // someId is now typed as IdOf<BookRecord> * const book = store.get(someId) * } * ``` * * @param id - The id string to check, may be undefined * @returns True if the id belongs to this record type * @public */ isId(id?: string): id is IdOf<R>; /** * Create a new RecordType that has the same type name as this RecordType and includes the given * default properties. * * @example * * ```ts * const authorType = createRecordType('author', () => ({ living: true })) * const deadAuthorType = authorType.withDefaultProperties({ living: false }) * ``` * * @param createDefaultProperties - A function that returns the default properties of the new RecordType. * @returns The new RecordType. */ withDefaultProperties<DefaultProps extends Omit<Partial<R>, 'id' | 'typeName'>>(createDefaultProperties: () => DefaultProps): RecordType<R, Exclude<RequiredProperties, keyof DefaultProps>>; /** * Validates a record against this RecordType's validator and returns it with proper typing. * * This method runs the configured validator function and throws an error if validation fails. * If a previous version of the record is provided, it may use optimized validation. * * @example * ```ts * try { * const validBook = Book.validate(untrustedData) * // validBook is now properly typed and validated * } catch (error) { * console.log('Validation failed:', error.message) * } * ``` * * @param record - The unknown record data to validate * @param recordBefore - Optional previous version for optimized validation * @returns The validated and properly typed record * @throws Error if validation fails * @public */ validate(record: unknown, recordBefore?: R): R; } /** * Creates the inverse of a RecordsDiff, effectively reversing all changes. * Added records become removed, removed records become added, and updated records * have their from/to values swapped. This is useful for implementing undo operations. * * @param diff - The diff to reverse * @returns A new RecordsDiff that represents the inverse of the input diff * @example * ```ts * const originalDiff: RecordsDiff<Book> = { * added: { 'book:1': newBook }, * updated: { 'book:2': [oldBook, updatedBook] }, * removed: { 'book:3': deletedBook } * } * * const reversedDiff = reverseRecordsDiff(originalDiff) * // Result: { * // added: { 'book:3': deletedBook }, * // updated: { 'book:2': [updatedBook, oldBook] }, * // removed: { 'book:1': newBook } * // } * ``` * * @public */ export declare function reverseRecordsDiff(diff: RecordsDiff<any>): RecordsDiff<any>; /** * A reactive computed index that provides efficient lookups of records by property values. * Returns a computed value containing an RSIndexMap with diffs for change tracking. * * @example * ```ts * // Create an index on book authors * const authorIndex: RSIndex<Book, 'authorId'> = store.query.index('book', 'authorId') * * // Get all books by a specific author * const leguinBooks = authorIndex.get().get('author:leguin') * ``` * * @public */ export declare type RSIndex<R extends UnknownRecord> = Computed<RSIndexMap<R>, RSIndexDiff<R>>; /** * A type representing the diff of changes to a reactive store index. * Maps property values to the collection differences for record IDs that have that property value. * * @example * ```ts * // For an index on book titles, the diff might look like: * const titleIndexDiff: RSIndexDiff<Book, 'title'> = new Map([ * ['The Lathe of Heaven', { added: new Set(['book:1']), removed: new Set() }], * ['Animal Farm', { added: new Set(), removed: new Set(['book:2']) }] * ]) * ``` * * @public */ export declare type RSIndexDiff<R extends UnknownRecord> = Map<any, CollectionDiff<IdOf<R>>>; /** * A type representing a reactive store index as a map from property values to sets of record IDs. * This is used to efficiently look up records by a specific property value. * * @example * ```ts * // Index mapping book titles to the IDs of books with that title * const titleIndex: RSIndexMap<Book, 'title'> = new Map([ * ['The Lathe of Heaven', new Set(['book:1'])], * ['Animal Farm', new Set(['book:2', 'book:3'])] * ]) * ``` * * @public */ export declare type RSIndexMap<R extends UnknownRecord> = Map<any, Set<IdOf<R>>>; /** * Union type representing all supported serialized schema formats. * * This type allows the store to handle both legacy (V1) and current (V2) * schema formats during deserialization and migration. * * @example * ```ts * function handleSchema(schema: SerializedSchema) { * if (schema.schemaVersion === 1) { * // Handle V1 format * console.log('Store version:', schema.storeVersion) * } else { * // Handle V2 format * console.log('Sequences:', schema.sequences) * } * } * ``` * * @public */ export declare type SerializedSchema = SerializedSchemaV1 | SerializedSchemaV2; /** * Version 1 format for serialized store schema information. * * This is the legacy format used before schema version 2. Version 1 schemas * separate store-level versioning from record-level versioning, and support * subtypes for complex record types like shapes. * * @example * ```ts * const schemaV1: SerializedSchemaV1 = { * schemaVersion: 1, * storeVersion: 2, * recordVersions: { * book: { version: 3 }, * shape: { * version: 2, * subTypeVersions: { rectangle: 1, circle: 2 }, * subTypeKey: 'type' * } * } * } * ``` * * @public */ export declare interface SerializedSchemaV1 { /** Schema version is the version for this type you're looking at right now */ schemaVersion: 1; /** * Store version is the version for the structure of the store. e.g. higher level structure like * removing or renaming a record type. */ storeVersion: number; /** Record versions are the versions for each record type. e.g. adding a new field to a record */ recordVersions: Record<string, { subTypeKey: string; subTypeVersions: Record<string, number>; version: number; } | { version: number; }>; } /** * Version 2 format for serialized store schema information. * * This is the current format that uses a unified sequence-based approach * for tracking versions across all migration sequences. Each sequence ID * maps to the latest version number for that sequence. * * @example * ```ts * const schemaV2: SerializedSchemaV2 = { * schemaVersion: 2, * sequences: { * 'com.tldraw.store': 3, * 'com.tldraw.book': 2, * 'com.tldraw.shape': 4, * 'com.tldraw.shape.rectangle': 1 * } * } * ``` * * @public */ export declare interface SerializedSchemaV2 { schemaVersion: 2; sequences: { [sequenceId: string]: number; }; } /** * A serialized snapshot of the record store's values. * This is a plain JavaScript object that can be saved to storage or transmitted over the network. * * @example * ```ts * const serialized: SerializedStore<Book> = { * 'book:123': { id: 'book:123', typeName: 'book', title: 'The Lathe of Heaven' }, * 'book:456': { id: 'book:456', typeName: 'book', title: 'The Left Hand of Darkness' } * } * ``` * * @public */ export declare type SerializedStore<R extends UnknownRecord> = Record<IdOf<R>, R>; /** * Combines multiple RecordsDiff objects into a single consolidated diff. * This function intelligently merges changes, handling cases where the same record * is modified multiple times across different diffs. For example, if a record is * added in one diff and then updated in another, the result will show it as added * with the final state. * * @param diffs - An array of diffs to combine into a single diff * @param options - Configuration options for the squashing operation * - mutateFirstDiff - If true, modifies the first diff in place instead of creating a new one * @returns A single diff that represents the cumulative effect of all input diffs * @example * ```ts * const diff1: RecordsDiff<Book> = { * added: { 'book:1': { id: 'book:1', title: 'New Book' } }, * updated: {}, * removed: {} * } * * const diff2: RecordsDiff<Book> = { * added: {}, * updated: { 'book:1': [{ id: 'book:1', title: 'New Book' }, { id: 'book:1', title: 'Updated Title' }] }, * removed: {} * } * * const squashed = squashRecordDiffs([diff1, diff2]) * // Result: { * // added: { 'book:1': { id: 'book:1', title: 'Updated Title' } }, * // updated: {}, * // removed: {} * // } * ``` * * @public */ export declare function squashRecordDiffs<T extends UnknownRecord>(diffs: RecordsDiff<T>[], options?: { mutateFirstDiff?: boolean; }): RecordsDiff<T>; /* Excluded from this release type: squashRecordDiffsMutable */ /** * Declares dependencies for migrations without being a migration itself. * * This interface allows you to specify that future migrations in a sequence depend on * migrations from other sequences, without defining an actual migration transformation. * It's used to establish cross-sequence dependencies in the migration graph. * @public */ export declare interface StandaloneDependsOn { readonly dependsOn: readonly MigrationId[]; } /** * A reactive store that manages collections of typed records. * * The Store is the central container for your application's data, providing: * - Reactive state management with automatic updates * - Type-safe record operations * - History tracking and change notifications * - Schema validation and migrations * - Side effects and business logic hooks * - Efficient querying and indexing * * @example * ```ts * // Create a store with schema * const schema = StoreSchema.create({ * book: Book, * author: Author * }) * * const store = new Store({ * schema, * props: {} * }) * * // Add records * const book = Book.create({ title: 'The Lathe of Heaven', author: 'Le Guin' }) * store.put([book]) * * // Listen to changes * store.listen((entry) => { * console.log('Changes:', entry.changes) * }) * ``` * * @public */ export declare class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> { /** * The unique identifier of the store instance. * * @public */ readonly id: string; /* Excluded from this release type: records */ /** * An atom containing the store's history. * * @public * @readonly */ readonly history: Atom<number, RecordsDiff<R>>; /** * Reactive queries and indexes for efficiently accessing store data. * Provides methods for filtering, indexing, and subscribing to subsets of records. * * @example * ```ts * // Create an index by a property * const booksByAuthor = store.query.index('book', 'author') * * // Get records matching criteria * const inStockBooks = store.query.records('book', () => ({ * inStock: { eq: true } * })) * ``` * * @public * @readonly */ readonly query: StoreQueries<R>; /* Excluded from this release type: listeners */ /* Excluded from this release type: historyAccumulator */ /* Excluded from this release type: historyReactor */ /* Excluded from this release type: cancelHistoryReactor */ /** * The schema that defines the structure and validation rules for records in this store. * * @public */ readonly schema: StoreSchema<R, Props>; /** * Custom properties associated with this store instance. * * @public */ readonly props: Props; /** * A mapping of record scopes to the set of record type names that belong to each scope. * Used to filter records by their persistence and synchronization behavior. * * @public */ readonly scopedTypes: { readonly [K in RecordScope]: ReadonlySet<R['typeName']>; }; /** * Side effects manager that handles lifecycle events for record operations. * Allows registration of callbacks for create, update, delete, and validation events. * * @example * ```ts * store.sideEffects.registerAfterCreateHandler('book', (book) => { * console.log('Book created:', book.title) * }) * ``` * * @public */ readonly sideEffects: StoreSideEffects<R>; /** * Creates a new Store instance. * * @example * ```ts * const store = new Store({ * schema: StoreSchema.create({ book: Book }), * props: { appName: 'MyLibrary' }, * initialData: savedData * }) * ``` * * @param config - Configuration object for the store */ constructor(config: { /** Custom properties for the store instance */ props: Props; /** Optional unique identifier for the store */ id?: string; /** The schema defining record types, validation, and migrations */ schema: StoreSchema<R, Props>; /** The store's initial data to populate on creation */ initialData?: SerializedStore<R>; }); _flushHistory(): void; dispose(): void; /** * Filters out non-document changes from a diff. Returns null if there are no changes left. * @param change - the records diff * @param scope - the records scope * @returns */ filterChangesByScope(change: RecordsDiff<R>, scope: RecordScope): { added: { [K in IdOf<R>]: R; }; remo