import { createMigrationIds, createMigrationSequence } from '@tldraw/store' import { IndexKey, objectMapEntries } from '@tldraw/utils' import { TLPage } from './records/TLPage' import { TLShape } from './records/TLShape' import { TLLineShape } from './shapes/TLLineShape' /** * Migration version constants for store-level schema changes. * Each version represents a breaking change that requires data transformation. * * @internal */ const Versions = createMigrationIds('com.tldraw.store', { RemoveCodeAndIconShapeTypes: 1, AddInstancePresenceType: 2, RemoveTLUserAndPresenceAndAddPointer: 3, RemoveUserDocument: 4, FixIndexKeys: 5, } as const) /** * Migration version identifiers for store-level migrations. * These versions track changes to the overall store structure and data model. * * @example * ```ts * import { storeVersions } from '@tldraw/tlschema' * * // Check if a specific migration version exists * const hasRemoveCodeShapes = storeVersions.RemoveCodeAndIconShapeTypes * ``` * * @public */ export { Versions as storeVersions } /** * Store-level migration sequence that handles evolution of the tldraw data model. * These migrations run when the store schema version changes and ensure backward * compatibility by transforming old data structures to new formats. * * The migrations handle: * - Removal of deprecated shape types (code, icon) * - Addition of new record types (instance presence) * - Cleanup of obsolete user and presence data * - Removal of deprecated user document records * * @example * ```ts * import { storeMigrations } from '@tldraw/tlschema' * import { migrate } from '@tldraw/store' * * // Apply store migrations to old data * const migratedStore = migrate({ * store: oldStoreData, * migrations: storeMigrations, * fromVersion: 0, * toVersion: storeMigrations.currentVersion * }) * ``` * * @public */ export const storeMigrations = createMigrationSequence({ sequenceId: 'com.tldraw.store', retroactive: false, sequence: [ { id: Versions.RemoveCodeAndIconShapeTypes, scope: 'storage', up: (storage) => { for (const [id, record] of storage.entries()) { if ( record.typeName === 'shape' && 'type' in record && (record.type === 'icon' || record.type === 'code') ) { storage.delete(id) } } }, }, { id: Versions.AddInstancePresenceType, scope: 'storage', up(_storage) { // noop // there used to be a down migration for this but we made down migrations optional // and we don't use them on storage-level migrations so we can just remove it }, }, { // remove user and presence records and add pointer records id: Versions.RemoveTLUserAndPresenceAndAddPointer, scope: 'storage', up: (storage) => { for (const [id, record] of storage.entries()) { if (record.typeName.match(/^(user|user_presence)$/)) { storage.delete(id) } } }, }, { // remove user document records id: Versions.RemoveUserDocument, scope: 'storage', up: (storage) => { for (const [id, record] of storage.entries()) { if (record.typeName.match('user_document')) { storage.delete(id) } } }, }, { id: Versions.FixIndexKeys, scope: 'record', up: (record) => { if (['shape', 'page'].includes(record.typeName) && 'index' in record) { const recordWithIndex = record as TLShape | TLPage // Our newer fractional indexed library (more correctly) validates that indices // do not end with 0. ('a0' being an exception) if (recordWithIndex.index.endsWith('0') && recordWithIndex.index !== 'a0') { recordWithIndex.index = (recordWithIndex.index.slice(0, -1) + getNRandomBase62Digits(3)) as IndexKey } // Line shapes have 'points' that have indices as well. if (record.typeName === 'shape' && (recordWithIndex as TLShape).type === 'line') { const lineShape = recordWithIndex as TLLineShape for (const [_, point] of objectMapEntries(lineShape.props.points)) { if (point.index.endsWith('0') && point.index !== 'a0') { point.index = (point.index.slice(0, -1) + getNRandomBase62Digits(3)) as IndexKey } } } } }, down: () => { // noop // Enables tlsync to support older clients so as to not force people to refresh immediately after deploying. }, }, ], }) const BASE_62_DIGITS_WITHOUT_ZERO = '123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' const getRandomBase62Digit = () => { return BASE_62_DIGITS_WITHOUT_ZERO.charAt( Math.floor(Math.random() * BASE_62_DIGITS_WITHOUT_ZERO.length) ) } const getNRandomBase62Digits = (n: number) => { return Array.from({ length: n }, getRandomBase62Digit).join('') }