@tldraw/store
Version:
tldraw infinite canvas SDK (store).
1,517 lines (1,470 loc) • 100 kB
TypeScript
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