@tldraw/store 5.3.0 → 5.4.0-next.ef99ab47e5ce

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist-cjs/index.d.ts +1 -0
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/RecordType.js +1 -1
  4. package/dist-cjs/lib/RecordType.js.map +2 -2
  5. package/dist-cjs/lib/RecordsDiff.js +45 -9
  6. package/dist-cjs/lib/RecordsDiff.js.map +3 -3
  7. package/dist-cjs/lib/Store.js +18 -29
  8. package/dist-cjs/lib/Store.js.map +2 -2
  9. package/dist-cjs/lib/StoreQueries.js +7 -54
  10. package/dist-cjs/lib/StoreQueries.js.map +2 -2
  11. package/dist-cjs/lib/StoreSchema.js +8 -12
  12. package/dist-cjs/lib/StoreSchema.js.map +3 -3
  13. package/dist-cjs/lib/executeQuery.js +5 -2
  14. package/dist-cjs/lib/executeQuery.js.map +2 -2
  15. package/dist-cjs/lib/migrate.js +5 -7
  16. package/dist-cjs/lib/migrate.js.map +2 -2
  17. package/dist-esm/index.d.mts +1 -0
  18. package/dist-esm/index.mjs +1 -1
  19. package/dist-esm/lib/RecordType.mjs +1 -1
  20. package/dist-esm/lib/RecordType.mjs.map +2 -2
  21. package/dist-esm/lib/RecordsDiff.mjs +45 -9
  22. package/dist-esm/lib/RecordsDiff.mjs.map +3 -3
  23. package/dist-esm/lib/Store.mjs +19 -30
  24. package/dist-esm/lib/Store.mjs.map +2 -2
  25. package/dist-esm/lib/StoreQueries.mjs +7 -54
  26. package/dist-esm/lib/StoreQueries.mjs.map +2 -2
  27. package/dist-esm/lib/StoreSchema.mjs +8 -12
  28. package/dist-esm/lib/StoreSchema.mjs.map +3 -3
  29. package/dist-esm/lib/executeQuery.mjs +5 -2
  30. package/dist-esm/lib/executeQuery.mjs.map +2 -2
  31. package/dist-esm/lib/migrate.mjs +5 -7
  32. package/dist-esm/lib/migrate.mjs.map +2 -2
  33. package/package.json +3 -3
  34. package/src/lib/RecordType.ts +1 -1
  35. package/src/lib/RecordsDiff.ts +66 -10
  36. package/src/lib/Store.ts +21 -36
  37. package/src/lib/StoreQueries.test.ts +24 -0
  38. package/src/lib/StoreQueries.ts +7 -60
  39. package/src/lib/StoreSchema.ts +11 -17
  40. package/src/lib/executeQuery.ts +5 -2
  41. package/src/lib/migrate.ts +8 -12
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/lib/migrate.ts"],
4
- "sourcesContent": ["import { assert, objectMapEntries } from '@tldraw/utils'\nimport { UnknownRecord } from './BaseRecord'\nimport { SerializedStore } from './Store'\nimport { SerializedSchema } from './StoreSchema'\n\nfunction squashDependsOn(sequence: Array<Migration | StandaloneDependsOn>): Migration[] {\n\tconst result: Migration[] = []\n\tfor (let i = sequence.length - 1; i >= 0; i--) {\n\t\tconst elem = sequence[i]\n\t\tif (!('id' in elem)) {\n\t\t\tconst dependsOn = elem.dependsOn\n\t\t\tconst prev = result[0]\n\t\t\tif (prev) {\n\t\t\t\tresult[0] = {\n\t\t\t\t\t...prev,\n\t\t\t\t\tdependsOn: dependsOn.concat(prev.dependsOn ?? []),\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tresult.unshift(elem)\n\t\t}\n\t}\n\treturn result\n}\n\n/**\n * Creates a migration sequence that defines how to transform data as your schema evolves.\n *\n * A migration sequence contains a series of migrations that are applied in order to transform\n * data from older versions to newer versions. Each migration is identified by a unique ID\n * and can operate at either the record level (transforming individual records) or store level\n * (transforming the entire store structure).\n *\n * See the [migration guide](https://tldraw.dev/docs/persistence#Migrations) for more info on how to use this API.\n * @param options - Configuration for the migration sequence\n * - sequenceId - Unique identifier for this migration sequence (e.g., 'com.myapp.book')\n * - sequence - Array of migrations or dependency declarations to include in the sequence\n * - retroactive - Whether migrations should apply to snapshots created before this sequence was added (defaults to true)\n * @returns A validated migration sequence that can be included in a store schema\n * @example\n * ```ts\n * const bookMigrations = createMigrationSequence({\n * sequenceId: 'com.myapp.book',\n * sequence: [\n * {\n * id: 'com.myapp.book/1',\n * scope: 'record',\n * up: (record) => ({ ...record, newField: 'default' })\n * }\n * ]\n * })\n * ```\n * @public\n */\nexport function createMigrationSequence({\n\tsequence,\n\tsequenceId,\n\tretroactive = true,\n}: {\n\tsequenceId: string\n\tretroactive?: boolean\n\tsequence: Array<Migration | StandaloneDependsOn>\n}): MigrationSequence {\n\tconst migrations: MigrationSequence = {\n\t\tsequenceId,\n\t\tretroactive,\n\t\tsequence: squashDependsOn(sequence),\n\t}\n\tvalidateMigrations(migrations)\n\treturn migrations\n}\n\n/**\n * Creates a named set of migration IDs from version numbers and a sequence ID.\n *\n * This utility function helps generate properly formatted migration IDs that follow\n * the required `sequenceId/version` pattern. It takes a sequence ID and a record\n * of named versions, returning migration IDs that can be used in migration definitions.\n *\n * See the [migration guide](https://tldraw.dev/docs/persistence#Migrations) for more info on how to use this API.\n * @param sequenceId - The sequence identifier (e.g., 'com.myapp.book')\n * @param versions - Record mapping version names to numbers\n * @returns Record mapping version names to properly formatted migration IDs\n * @example\n * ```ts\n * const migrationIds = createMigrationIds('com.myapp.book', {\n * addGenre: 1,\n * addPublisher: 2,\n * removeOldField: 3\n * })\n * // Result: {\n * // addGenre: 'com.myapp.book/1',\n * // addPublisher: 'com.myapp.book/2',\n * // removeOldField: 'com.myapp.book/3'\n * // }\n * ```\n * @public\n */\nexport function createMigrationIds<\n\tconst ID extends string,\n\tconst Versions extends Record<string, number>,\n>(sequenceId: ID, versions: Versions): { [K in keyof Versions]: `${ID}/${Versions[K]}` } {\n\treturn Object.fromEntries(\n\t\tobjectMapEntries(versions).map(([key, version]) => [key, `${sequenceId}/${version}`] as const)\n\t) as any\n}\n\n/**\n * Creates a migration sequence specifically for record-level migrations.\n *\n * This is a convenience function that creates a migration sequence where all migrations\n * operate at the record scope and are automatically filtered to apply only to records\n * of a specific type. Each migration in the sequence will be enhanced with the record\n * scope and appropriate filtering logic.\n * @param opts - Configuration for the record migration sequence\n * - recordType - The record type name these migrations should apply to\n * - filter - Optional additional filter function to determine which records to migrate\n * - retroactive - Whether migrations should apply to snapshots created before this sequence was added\n * - sequenceId - Unique identifier for this migration sequence\n * - sequence - Array of record migration definitions (scope will be added automatically)\n * @returns A migration sequence configured for record-level operations\n * @internal\n */\nexport function createRecordMigrationSequence(opts: {\n\trecordType: string\n\tfilter?(record: UnknownRecord): boolean\n\tretroactive?: boolean\n\tsequenceId: string\n\tsequence: Omit<Extract<Migration, { scope: 'record' }>, 'scope'>[]\n}): MigrationSequence {\n\tconst sequenceId = opts.sequenceId\n\treturn createMigrationSequence({\n\t\tsequenceId,\n\t\tretroactive: opts.retroactive ?? true,\n\t\tsequence: opts.sequence.map((m) =>\n\t\t\t'id' in m\n\t\t\t\t? {\n\t\t\t\t\t\t...m,\n\t\t\t\t\t\tscope: 'record',\n\t\t\t\t\t\tfilter: (r: UnknownRecord) =>\n\t\t\t\t\t\t\tr.typeName === opts.recordType &&\n\t\t\t\t\t\t\t(m.filter?.(r) ?? true) &&\n\t\t\t\t\t\t\t(opts.filter?.(r) ?? true),\n\t\t\t\t\t}\n\t\t\t\t: m\n\t\t),\n\t})\n}\n\n/**\n * Legacy migration interface for backward compatibility.\n *\n * This interface represents the old migration format that included both `up` and `down`\n * transformation functions. While still supported, new code should use the `Migration`\n * type which provides more flexibility and better integration with the current system.\n * @public\n */\nexport interface LegacyMigration<Before = any, After = any> {\n\t// eslint-disable-next-line tldraw/method-signature-style\n\tup: (oldState: Before) => After\n\t// eslint-disable-next-line tldraw/method-signature-style\n\tdown: (newState: After) => Before\n}\n\n/**\n * Unique identifier for a migration in the format `sequenceId/version`.\n *\n * Migration IDs follow a specific pattern where the sequence ID identifies the migration\n * sequence and the version number indicates the order within that sequence. For example:\n * 'com.myapp.book/1', 'com.myapp.book/2', etc.\n * @public\n */\nexport type MigrationId = `${string}/${number}`\n\n/**\n * Declares dependencies for migrations without being a migration itself.\n *\n * This interface allows you to specify that future migrations in a sequence depend on\n * migrations from other sequences, without defining an actual migration transformation.\n * It's used to establish cross-sequence dependencies in the migration graph.\n * @public\n */\nexport interface StandaloneDependsOn {\n\treadonly dependsOn: readonly MigrationId[]\n}\n\n/**\n * Defines a single migration that transforms data from one schema version to another.\n *\n * A migration can operate at two different scopes:\n * - `record`: Transforms individual records, with optional filtering to target specific records\n * - `store`: Transforms the entire serialized store structure\n *\n * Each migration has a unique ID and can declare dependencies on other migrations that must\n * be applied first. The `up` function performs the forward transformation, while the optional\n * `down` function can reverse the migration if needed.\n * @public\n */\nexport type Migration = {\n\treadonly id: MigrationId\n\treadonly dependsOn?: readonly MigrationId[] | undefined\n} & (\n\t| {\n\t\t\treadonly scope: 'record'\n\t\t\t// eslint-disable-next-line tldraw/method-signature-style\n\t\t\treadonly filter?: (record: UnknownRecord) => boolean\n\t\t\t// eslint-disable-next-line tldraw/method-signature-style\n\t\t\treadonly up: (oldState: UnknownRecord) => void | UnknownRecord\n\t\t\t// eslint-disable-next-line tldraw/method-signature-style\n\t\t\treadonly down?: (newState: UnknownRecord) => void | UnknownRecord\n\t }\n\t| {\n\t\t\treadonly scope: 'store'\n\t\t\t// eslint-disable-next-line tldraw/method-signature-style\n\t\t\treadonly up: (\n\t\t\t\toldState: SerializedStore<UnknownRecord>\n\t\t\t) => void | SerializedStore<UnknownRecord>\n\t\t\t// eslint-disable-next-line tldraw/method-signature-style\n\t\t\treadonly down?: (\n\t\t\t\tnewState: SerializedStore<UnknownRecord>\n\t\t\t) => void | SerializedStore<UnknownRecord>\n\t }\n\t| {\n\t\t\treadonly scope: 'storage'\n\t\t\t// eslint-disable-next-line tldraw/method-signature-style\n\t\t\treadonly up: (storage: SynchronousRecordStorage<UnknownRecord>) => void\n\t\t\treadonly down?: never\n\t }\n)\n\n/**\n * Abstraction over the store that can be used to perform migrations.\n * @public\n */\nexport interface SynchronousRecordStorage<R extends UnknownRecord> {\n\tget(id: string): R | undefined\n\tset(id: string, record: R): void\n\tdelete(id: string): void\n\tkeys(): Iterable<string>\n\tvalues(): Iterable<R>\n\tentries(): Iterable<[string, R]>\n}\n\n/**\n * Abstraction over the storage that can be used to perform migrations.\n * @public\n */\nexport interface SynchronousStorage<R extends UnknownRecord> extends SynchronousRecordStorage<R> {\n\tgetSchema(): SerializedSchema\n\tsetSchema(schema: SerializedSchema): void\n}\n\n/**\n * Base interface for legacy migration information.\n *\n * Contains the basic structure used by the legacy migration system, including version\n * range information and the migration functions indexed by version number. This is\n * maintained for backward compatibility with older migration definitions.\n * @public\n */\nexport interface LegacyBaseMigrationsInfo {\n\tfirstVersion: number\n\tcurrentVersion: number\n\tmigrators: { [version: number]: LegacyMigration }\n}\n\n/**\n * Legacy migration configuration with support for sub-type migrations.\n *\n * This interface extends the base legacy migration info to support migrations that\n * vary based on a sub-type key within records. This allows different migration paths\n * for different variants of the same record type, which was useful in older migration\n * systems but is now handled more elegantly by the current Migration system.\n * @public\n */\nexport interface LegacyMigrations extends LegacyBaseMigrationsInfo {\n\tsubTypeKey?: string\n\tsubTypeMigrations?: Record<string, LegacyBaseMigrationsInfo>\n}\n\n/**\n * A complete sequence of migrations that can be applied to transform data.\n *\n * A migration sequence represents a series of ordered migrations that belong together,\n * typically for a specific part of your schema. The sequence includes metadata about\n * whether it should be applied retroactively to existing data and contains the actual\n * migration definitions in execution order.\n * @public\n */\nexport interface MigrationSequence {\n\tsequenceId: string\n\t/**\n\t * retroactive should be true if the migrations should be applied to snapshots that were created before\n\t * this migration sequence was added to the schema.\n\t *\n\t * In general:\n\t *\n\t * - retroactive should be true when app developers create their own new migration sequences.\n\t * - 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.\n\t */\n\tretroactive: boolean\n\tsequence: Migration[]\n}\n\n/**\n * Sorts migrations using a distance-minimizing topological sort.\n *\n * This function respects two types of dependencies:\n * 1. Implicit sequence dependencies (foo/1 must come before foo/2)\n * 2. Explicit dependencies via `dependsOn` property\n *\n * The algorithm minimizes the total distance between migrations and their explicit\n * dependencies in the final ordering, while maintaining topological correctness.\n * This means when migration A depends on migration B, A will be scheduled as close\n * as possible to B (while respecting all constraints).\n *\n * Implementation uses Kahn's algorithm with priority scoring:\n * - Builds dependency graph and calculates in-degrees\n * - Uses priority queue that prioritizes migrations which unblock explicit dependencies\n * - Processes migrations in urgency order while maintaining topological constraints\n * - Detects cycles by ensuring all migrations are processed\n *\n * @param migrations - Array of migrations to sort\n * @returns Sorted array of migrations in execution order\n * @throws Assertion error if circular dependencies are detected\n * @example\n * ```ts\n * const sorted = sortMigrations([\n * { id: 'app/2', scope: 'record', up: (r) => r },\n * { id: 'app/1', scope: 'record', up: (r) => r },\n * { id: 'lib/1', scope: 'record', up: (r) => r, dependsOn: ['app/1'] }\n * ])\n * // Result: [app/1, app/2, lib/1] (respects both sequence and explicit deps)\n * ```\n * @public\n */\nexport function sortMigrations(migrations: Migration[]): Migration[] {\n\tif (migrations.length === 0) return []\n\n\t// Build dependency graph and calculate in-degrees\n\tconst byId = new Map(migrations.map((m) => [m.id, m]))\n\tconst dependents = new Map<MigrationId, Set<MigrationId>>() // who depends on this\n\tconst inDegree = new Map<MigrationId, number>()\n\tconst explicitDeps = new Map<MigrationId, Set<MigrationId>>() // explicit dependsOn relationships\n\n\t// Initialize\n\tfor (const m of migrations) {\n\t\tinDegree.set(m.id, 0)\n\t\tdependents.set(m.id, new Set())\n\t\texplicitDeps.set(m.id, new Set())\n\t}\n\n\t// Add implicit sequence dependencies and explicit dependencies\n\tfor (const m of migrations) {\n\t\tconst { version, sequenceId } = parseMigrationId(m.id)\n\n\t\t// Implicit dependency on previous in sequence\n\t\tconst prevId = `${sequenceId}/${version - 1}` as MigrationId\n\t\tif (byId.has(prevId)) {\n\t\t\tdependents.get(prevId)!.add(m.id)\n\t\t\tinDegree.set(m.id, inDegree.get(m.id)! + 1)\n\t\t}\n\n\t\t// Explicit dependencies\n\t\tif (m.dependsOn) {\n\t\t\tfor (const depId of m.dependsOn) {\n\t\t\t\tif (byId.has(depId)) {\n\t\t\t\t\tdependents.get(depId)!.add(m.id)\n\t\t\t\t\texplicitDeps.get(m.id)!.add(depId)\n\t\t\t\t\tinDegree.set(m.id, inDegree.get(m.id)! + 1)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Priority queue: migrations ready to process (in-degree 0)\n\tconst ready = migrations.filter((m) => inDegree.get(m.id) === 0)\n\tconst result: Migration[] = []\n\tconst processed = new Set<MigrationId>()\n\n\twhile (ready.length > 0) {\n\t\t// Calculate urgency scores for ready migrations and pick the best one\n\t\tlet bestCandidate: Migration | undefined\n\t\tlet bestCandidateScore = -Infinity\n\n\t\tfor (const m of ready) {\n\t\t\tlet urgencyScore = 0\n\n\t\t\tfor (const depId of dependents.get(m.id) || []) {\n\t\t\t\tif (!processed.has(depId)) {\n\t\t\t\t\t// Priority 1: Count all unprocessed dependents (to break ties)\n\t\t\t\t\turgencyScore += 1\n\n\t\t\t\t\t// Priority 2: If this migration is explicitly depended on by others, boost priority\n\t\t\t\t\tif (explicitDeps.get(depId)!.has(m.id)) {\n\t\t\t\t\t\turgencyScore += 100\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\turgencyScore > bestCandidateScore ||\n\t\t\t\t// Tiebreaker: prefer lower sequence/version\n\t\t\t\t(urgencyScore === bestCandidateScore && m.id.localeCompare(bestCandidate?.id ?? '') < 0)\n\t\t\t) {\n\t\t\t\tbestCandidate = m\n\t\t\t\tbestCandidateScore = urgencyScore\n\t\t\t}\n\t\t}\n\n\t\tconst nextMigration = bestCandidate!\n\t\tready.splice(ready.indexOf(nextMigration), 1)\n\n\t\t// Cycle detection - if we have processed everything and still have items left, there's a cycle\n\t\t// This is handled by Kahn's algorithm naturally - if we finish with items unprocessed, there's a cycle\n\n\t\t// Process this migration\n\t\tresult.push(nextMigration)\n\t\tprocessed.add(nextMigration.id)\n\n\t\t// Update in-degrees and add newly ready migrations\n\t\tfor (const depId of dependents.get(nextMigration.id) || []) {\n\t\t\tif (!processed.has(depId)) {\n\t\t\t\tinDegree.set(depId, inDegree.get(depId)! - 1)\n\t\t\t\tif (inDegree.get(depId) === 0) {\n\t\t\t\t\tready.push(byId.get(depId)!)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for cycles - if we didn't process all migrations, there's a cycle\n\tif (result.length !== migrations.length) {\n\t\tconst unprocessed = migrations.filter((m) => !processed.has(m.id))\n\t\tassert(false, `Circular dependency in migrations: ${unprocessed[0].id}`)\n\t}\n\n\treturn result\n}\n\n/**\n * Parses a migration ID to extract the sequence ID and version number.\n *\n * Migration IDs follow the format `sequenceId/version`, and this function splits\n * them into their component parts. This is used internally for sorting migrations\n * and understanding their relationships.\n * @param id - The migration ID to parse\n * @returns Object containing the sequence ID and numeric version\n * @example\n * ```ts\n * const { sequenceId, version } = parseMigrationId('com.myapp.book/5')\n * // sequenceId: 'com.myapp.book', version: 5\n * ```\n * @internal\n */\nexport function parseMigrationId(id: MigrationId): { sequenceId: string; version: number } {\n\tconst [sequenceId, version] = id.split('/')\n\treturn { sequenceId, version: parseInt(version) }\n}\n\nfunction validateMigrationId(id: string, expectedSequenceId?: string) {\n\tif (expectedSequenceId) {\n\t\tassert(\n\t\t\tid.startsWith(expectedSequenceId + '/'),\n\t\t\t`Every migration in sequence '${expectedSequenceId}' must have an id starting with '${expectedSequenceId}/'. Got invalid id: '${id}'`\n\t\t)\n\t}\n\n\tassert(id.match(/^(.*?)\\/(0|[1-9]\\d*)$/), `Invalid migration id: '${id}'`)\n}\n\n/**\n * Validates that a migration sequence is correctly structured.\n *\n * Performs several validation checks to ensure the migration sequence is valid:\n * - Sequence ID doesn't contain invalid characters\n * - All migration IDs belong to the expected sequence\n * - Migration versions start at 1 and increment by 1\n * - Migration IDs follow the correct format\n * @param migrations - The migration sequence to validate\n * @throws Assertion error if any validation checks fail\n * @example\n * ```ts\n * const sequence = createMigrationSequence({\n * sequenceId: 'com.myapp.book',\n * sequence: [{ id: 'com.myapp.book/1', scope: 'record', up: (r) => r }]\n * })\n * validateMigrations(sequence) // Passes validation\n * ```\n * @public\n */\nexport function validateMigrations(migrations: MigrationSequence) {\n\tassert(\n\t\t!migrations.sequenceId.includes('/'),\n\t\t`sequenceId cannot contain a '/', got ${migrations.sequenceId}`\n\t)\n\tassert(migrations.sequenceId.length, 'sequenceId must be a non-empty string')\n\n\tif (migrations.sequence.length === 0) {\n\t\treturn\n\t}\n\n\tvalidateMigrationId(migrations.sequence[0].id, migrations.sequenceId)\n\tlet n = parseMigrationId(migrations.sequence[0].id).version\n\tassert(\n\t\tn === 1,\n\t\t`Expected the first migrationId to be '${migrations.sequenceId}/1' but got '${migrations.sequence[0].id}'`\n\t)\n\tfor (let i = 1; i < migrations.sequence.length; i++) {\n\t\tconst id = migrations.sequence[i].id\n\t\tvalidateMigrationId(id, migrations.sequenceId)\n\t\tconst m = parseMigrationId(id).version\n\t\tassert(\n\t\t\tm === n + 1,\n\t\t\t`Migration id numbers must increase in increments of 1, expected ${migrations.sequenceId}/${n + 1} but got '${migrations.sequence[i].id}'`\n\t\t)\n\t\tn = m\n\t}\n}\n\n/**\n * Result type returned by migration operations.\n *\n * Migration operations can either succeed and return the transformed value,\n * or fail with a specific reason. This discriminated union type allows for\n * safe handling of both success and error cases when applying migrations.\n * @public\n */\nexport type MigrationResult<T> =\n\t| { type: 'success'; value: T }\n\t| { type: 'error'; reason: MigrationFailureReason }\n\n/** @public */\nexport const MigrationFailureReason = {\n\tIncompatibleSubtype: 'incompatible-subtype',\n\tUnknownType: 'unknown-type',\n\tTargetVersionTooNew: 'target-version-too-new',\n\tTargetVersionTooOld: 'target-version-too-old',\n\tMigrationError: 'migration-error',\n\tUnrecognizedSubtype: 'unrecognized-subtype',\n} as const\n\n/** @public */\nexport type MigrationFailureReason =\n\t(typeof MigrationFailureReason)[keyof typeof MigrationFailureReason]\n\n// This is the magic part for backward compat:\n/** @public */\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport declare namespace MigrationFailureReason {\n\texport type IncompatibleSubtype = typeof MigrationFailureReason.IncompatibleSubtype\n\texport type UnknownType = typeof MigrationFailureReason.UnknownType\n\texport type TargetVersionTooNew = typeof MigrationFailureReason.TargetVersionTooNew\n\texport type TargetVersionTooOld = typeof MigrationFailureReason.TargetVersionTooOld\n\texport type MigrationError = typeof MigrationFailureReason.MigrationError\n\texport type UnrecognizedSubtype = typeof MigrationFailureReason.UnrecognizedSubtype\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { assert, objectMapEntries } from '@tldraw/utils'\nimport { UnknownRecord } from './BaseRecord'\nimport { SerializedStore } from './Store'\nimport { SerializedSchema } from './StoreSchema'\n\nfunction squashDependsOn(sequence: Array<Migration | StandaloneDependsOn>): Migration[] {\n\tconst result: Migration[] = []\n\tfor (let i = sequence.length - 1; i >= 0; i--) {\n\t\tconst elem = sequence[i]\n\t\tif (!('id' in elem)) {\n\t\t\tconst dependsOn = elem.dependsOn\n\t\t\tconst prev = result[0]\n\t\t\tif (prev) {\n\t\t\t\tresult[0] = {\n\t\t\t\t\t...prev,\n\t\t\t\t\tdependsOn: dependsOn.concat(prev.dependsOn ?? []),\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tresult.unshift(elem)\n\t\t}\n\t}\n\treturn result\n}\n\n/**\n * Creates a migration sequence that defines how to transform data as your schema evolves.\n *\n * A migration sequence contains a series of migrations that are applied in order to transform\n * data from older versions to newer versions. Each migration is identified by a unique ID\n * and can operate at either the record level (transforming individual records) or store level\n * (transforming the entire store structure).\n *\n * See the [migration guide](https://tldraw.dev/docs/persistence#Migrations) for more info on how to use this API.\n * @param options - Configuration for the migration sequence\n * - sequenceId - Unique identifier for this migration sequence (e.g., 'com.myapp.book')\n * - sequence - Array of migrations or dependency declarations to include in the sequence\n * - retroactive - Whether migrations should apply to snapshots created before this sequence was added (defaults to true)\n * @returns A validated migration sequence that can be included in a store schema\n * @example\n * ```ts\n * const bookMigrations = createMigrationSequence({\n * sequenceId: 'com.myapp.book',\n * sequence: [\n * {\n * id: 'com.myapp.book/1',\n * scope: 'record',\n * up: (record) => ({ ...record, newField: 'default' })\n * }\n * ]\n * })\n * ```\n * @public\n */\nexport function createMigrationSequence({\n\tsequence,\n\tsequenceId,\n\tretroactive = true,\n}: {\n\tsequenceId: string\n\tretroactive?: boolean\n\tsequence: Array<Migration | StandaloneDependsOn>\n}): MigrationSequence {\n\tconst migrations: MigrationSequence = {\n\t\tsequenceId,\n\t\tretroactive,\n\t\tsequence: squashDependsOn(sequence),\n\t}\n\tvalidateMigrations(migrations)\n\treturn migrations\n}\n\n/**\n * Creates a named set of migration IDs from version numbers and a sequence ID.\n *\n * This utility function helps generate properly formatted migration IDs that follow\n * the required `sequenceId/version` pattern. It takes a sequence ID and a record\n * of named versions, returning migration IDs that can be used in migration definitions.\n *\n * See the [migration guide](https://tldraw.dev/docs/persistence#Migrations) for more info on how to use this API.\n * @param sequenceId - The sequence identifier (e.g., 'com.myapp.book')\n * @param versions - Record mapping version names to numbers\n * @returns Record mapping version names to properly formatted migration IDs\n * @example\n * ```ts\n * const migrationIds = createMigrationIds('com.myapp.book', {\n * addGenre: 1,\n * addPublisher: 2,\n * removeOldField: 3\n * })\n * // Result: {\n * // addGenre: 'com.myapp.book/1',\n * // addPublisher: 'com.myapp.book/2',\n * // removeOldField: 'com.myapp.book/3'\n * // }\n * ```\n * @public\n */\nexport function createMigrationIds<\n\tconst ID extends string,\n\tconst Versions extends Record<string, number>,\n>(sequenceId: ID, versions: Versions): { [K in keyof Versions]: `${ID}/${Versions[K]}` } {\n\t// Note: not objectMapFromEntries \u2014 `keyof Versions` is not narrowed to `string`, which that\n\t// helper requires.\n\treturn Object.fromEntries(\n\t\tobjectMapEntries(versions).map(([key, version]) => [key, `${sequenceId}/${version}`] as const)\n\t) as any\n}\n\n/**\n * Creates a migration sequence specifically for record-level migrations.\n *\n * This is a convenience function that creates a migration sequence where all migrations\n * operate at the record scope and are automatically filtered to apply only to records\n * of a specific type. Each migration in the sequence will be enhanced with the record\n * scope and appropriate filtering logic.\n * @param opts - Configuration for the record migration sequence\n * - recordType - The record type name these migrations should apply to\n * - filter - Optional additional filter function to determine which records to migrate\n * - retroactive - Whether migrations should apply to snapshots created before this sequence was added\n * - sequenceId - Unique identifier for this migration sequence\n * - sequence - Array of record migration definitions (scope will be added automatically)\n * @returns A migration sequence configured for record-level operations\n * @internal\n */\nexport function createRecordMigrationSequence(opts: {\n\trecordType: string\n\tfilter?(record: UnknownRecord): boolean\n\tretroactive?: boolean\n\tsequenceId: string\n\tsequence: Omit<Extract<Migration, { scope: 'record' }>, 'scope'>[]\n}): MigrationSequence {\n\tconst sequenceId = opts.sequenceId\n\treturn createMigrationSequence({\n\t\tsequenceId,\n\t\tretroactive: opts.retroactive ?? true,\n\t\tsequence: opts.sequence.map((m) => ({\n\t\t\t...m,\n\t\t\tscope: 'record',\n\t\t\tfilter: (r: UnknownRecord) =>\n\t\t\t\tr.typeName === opts.recordType && (m.filter?.(r) ?? true) && (opts.filter?.(r) ?? true),\n\t\t})),\n\t})\n}\n\n/**\n * Legacy migration interface for backward compatibility.\n *\n * This interface represents the old migration format that included both `up` and `down`\n * transformation functions. While still supported, new code should use the `Migration`\n * type which provides more flexibility and better integration with the current system.\n * @public\n */\nexport interface LegacyMigration<Before = any, After = any> {\n\t// eslint-disable-next-line tldraw/method-signature-style\n\tup: (oldState: Before) => After\n\t// eslint-disable-next-line tldraw/method-signature-style\n\tdown: (newState: After) => Before\n}\n\n/**\n * Unique identifier for a migration in the format `sequenceId/version`.\n *\n * Migration IDs follow a specific pattern where the sequence ID identifies the migration\n * sequence and the version number indicates the order within that sequence. For example:\n * 'com.myapp.book/1', 'com.myapp.book/2', etc.\n * @public\n */\nexport type MigrationId = `${string}/${number}`\n\n/**\n * Declares dependencies for migrations without being a migration itself.\n *\n * This interface allows you to specify that future migrations in a sequence depend on\n * migrations from other sequences, without defining an actual migration transformation.\n * It's used to establish cross-sequence dependencies in the migration graph.\n * @public\n */\nexport interface StandaloneDependsOn {\n\treadonly dependsOn: readonly MigrationId[]\n}\n\n/**\n * Defines a single migration that transforms data from one schema version to another.\n *\n * A migration can operate at two different scopes:\n * - `record`: Transforms individual records, with optional filtering to target specific records\n * - `store`: Transforms the entire serialized store structure\n *\n * Each migration has a unique ID and can declare dependencies on other migrations that must\n * be applied first. The `up` function performs the forward transformation, while the optional\n * `down` function can reverse the migration if needed.\n * @public\n */\nexport type Migration = {\n\treadonly id: MigrationId\n\treadonly dependsOn?: readonly MigrationId[] | undefined\n} & (\n\t| {\n\t\t\treadonly scope: 'record'\n\t\t\t// eslint-disable-next-line tldraw/method-signature-style\n\t\t\treadonly filter?: (record: UnknownRecord) => boolean\n\t\t\t// eslint-disable-next-line tldraw/method-signature-style\n\t\t\treadonly up: (oldState: UnknownRecord) => void | UnknownRecord\n\t\t\t// eslint-disable-next-line tldraw/method-signature-style\n\t\t\treadonly down?: (newState: UnknownRecord) => void | UnknownRecord\n\t }\n\t| {\n\t\t\treadonly scope: 'store'\n\t\t\t// eslint-disable-next-line tldraw/method-signature-style\n\t\t\treadonly up: (\n\t\t\t\toldState: SerializedStore<UnknownRecord>\n\t\t\t) => void | SerializedStore<UnknownRecord>\n\t\t\t// eslint-disable-next-line tldraw/method-signature-style\n\t\t\treadonly down?: (\n\t\t\t\tnewState: SerializedStore<UnknownRecord>\n\t\t\t) => void | SerializedStore<UnknownRecord>\n\t }\n\t| {\n\t\t\treadonly scope: 'storage'\n\t\t\t// eslint-disable-next-line tldraw/method-signature-style\n\t\t\treadonly up: (storage: SynchronousRecordStorage<UnknownRecord>) => void\n\t\t\treadonly down?: never\n\t }\n)\n\n/**\n * Abstraction over the store that can be used to perform migrations.\n * @public\n */\nexport interface SynchronousRecordStorage<R extends UnknownRecord> {\n\tget(id: string): R | undefined\n\tset(id: string, record: R): void\n\tdelete(id: string): void\n\tkeys(): Iterable<string>\n\tvalues(): Iterable<R>\n\tentries(): Iterable<[string, R]>\n}\n\n/**\n * Abstraction over the storage that can be used to perform migrations.\n * @public\n */\nexport interface SynchronousStorage<R extends UnknownRecord> extends SynchronousRecordStorage<R> {\n\tgetSchema(): SerializedSchema\n\tsetSchema(schema: SerializedSchema): void\n}\n\n/**\n * Base interface for legacy migration information.\n *\n * Contains the basic structure used by the legacy migration system, including version\n * range information and the migration functions indexed by version number. This is\n * maintained for backward compatibility with older migration definitions.\n * @public\n */\nexport interface LegacyBaseMigrationsInfo {\n\tfirstVersion: number\n\tcurrentVersion: number\n\tmigrators: { [version: number]: LegacyMigration }\n}\n\n/**\n * Legacy migration configuration with support for sub-type migrations.\n *\n * This interface extends the base legacy migration info to support migrations that\n * vary based on a sub-type key within records. This allows different migration paths\n * for different variants of the same record type, which was useful in older migration\n * systems but is now handled more elegantly by the current Migration system.\n * @public\n */\nexport interface LegacyMigrations extends LegacyBaseMigrationsInfo {\n\tsubTypeKey?: string\n\tsubTypeMigrations?: Record<string, LegacyBaseMigrationsInfo>\n}\n\n/**\n * A complete sequence of migrations that can be applied to transform data.\n *\n * A migration sequence represents a series of ordered migrations that belong together,\n * typically for a specific part of your schema. The sequence includes metadata about\n * whether it should be applied retroactively to existing data and contains the actual\n * migration definitions in execution order.\n * @public\n */\nexport interface MigrationSequence {\n\tsequenceId: string\n\t/**\n\t * retroactive should be true if the migrations should be applied to snapshots that were created before\n\t * this migration sequence was added to the schema.\n\t *\n\t * In general:\n\t *\n\t * - retroactive should be true when app developers create their own new migration sequences.\n\t * - 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.\n\t */\n\tretroactive: boolean\n\tsequence: Migration[]\n}\n\n/**\n * Sorts migrations using a distance-minimizing topological sort.\n *\n * This function respects two types of dependencies:\n * 1. Implicit sequence dependencies (foo/1 must come before foo/2)\n * 2. Explicit dependencies via `dependsOn` property\n *\n * The algorithm minimizes the total distance between migrations and their explicit\n * dependencies in the final ordering, while maintaining topological correctness.\n * This means when migration A depends on migration B, A will be scheduled as close\n * as possible to B (while respecting all constraints).\n *\n * Implementation uses Kahn's algorithm with priority scoring:\n * - Builds dependency graph and calculates in-degrees\n * - Uses priority queue that prioritizes migrations which unblock explicit dependencies\n * - Processes migrations in urgency order while maintaining topological constraints\n * - Detects cycles by ensuring all migrations are processed\n *\n * @param migrations - Array of migrations to sort\n * @returns Sorted array of migrations in execution order\n * @throws Assertion error if circular dependencies are detected\n * @example\n * ```ts\n * const sorted = sortMigrations([\n * { id: 'app/2', scope: 'record', up: (r) => r },\n * { id: 'app/1', scope: 'record', up: (r) => r },\n * { id: 'lib/1', scope: 'record', up: (r) => r, dependsOn: ['app/1'] }\n * ])\n * // Result: [app/1, app/2, lib/1] (respects both sequence and explicit deps)\n * ```\n * @public\n */\nexport function sortMigrations(migrations: Migration[]): Migration[] {\n\tif (migrations.length === 0) return []\n\n\t// Build dependency graph and calculate in-degrees\n\tconst byId = new Map(migrations.map((m) => [m.id, m]))\n\tconst dependents = new Map<MigrationId, Set<MigrationId>>() // who depends on this\n\tconst inDegree = new Map<MigrationId, number>()\n\tconst explicitDeps = new Map<MigrationId, Set<MigrationId>>() // explicit dependsOn relationships\n\n\t// Initialize\n\tfor (const m of migrations) {\n\t\tinDegree.set(m.id, 0)\n\t\tdependents.set(m.id, new Set())\n\t\texplicitDeps.set(m.id, new Set())\n\t}\n\n\t// Add implicit sequence dependencies and explicit dependencies\n\tfor (const m of migrations) {\n\t\tconst { version, sequenceId } = parseMigrationId(m.id)\n\n\t\t// Implicit dependency on previous in sequence\n\t\tconst prevId = `${sequenceId}/${version - 1}` as MigrationId\n\t\tif (byId.has(prevId)) {\n\t\t\tdependents.get(prevId)!.add(m.id)\n\t\t\tinDegree.set(m.id, inDegree.get(m.id)! + 1)\n\t\t}\n\n\t\t// Explicit dependencies\n\t\tif (m.dependsOn) {\n\t\t\tfor (const depId of m.dependsOn) {\n\t\t\t\tif (byId.has(depId)) {\n\t\t\t\t\tdependents.get(depId)!.add(m.id)\n\t\t\t\t\texplicitDeps.get(m.id)!.add(depId)\n\t\t\t\t\tinDegree.set(m.id, inDegree.get(m.id)! + 1)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Priority queue: migrations ready to process (in-degree 0)\n\tconst ready = migrations.filter((m) => inDegree.get(m.id) === 0)\n\tconst result: Migration[] = []\n\tconst processed = new Set<MigrationId>()\n\n\twhile (ready.length > 0) {\n\t\t// Calculate urgency scores for ready migrations and pick the best one\n\t\tlet bestCandidate: Migration | undefined\n\t\tlet bestCandidateScore = -Infinity\n\n\t\tfor (const m of ready) {\n\t\t\tlet urgencyScore = 0\n\n\t\t\tfor (const depId of dependents.get(m.id) || []) {\n\t\t\t\tif (!processed.has(depId)) {\n\t\t\t\t\t// Priority 1: Count all unprocessed dependents (to break ties)\n\t\t\t\t\turgencyScore += 1\n\n\t\t\t\t\t// Priority 2: If this migration is explicitly depended on by others, boost priority\n\t\t\t\t\tif (explicitDeps.get(depId)!.has(m.id)) {\n\t\t\t\t\t\turgencyScore += 100\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\turgencyScore > bestCandidateScore ||\n\t\t\t\t// Tiebreaker: prefer lower sequence/version\n\t\t\t\t(urgencyScore === bestCandidateScore && m.id.localeCompare(bestCandidate?.id ?? '') < 0)\n\t\t\t) {\n\t\t\t\tbestCandidate = m\n\t\t\t\tbestCandidateScore = urgencyScore\n\t\t\t}\n\t\t}\n\n\t\tconst nextMigration = bestCandidate!\n\t\tready.splice(ready.indexOf(nextMigration), 1)\n\n\t\t// Cycle detection - if we have processed everything and still have items left, there's a cycle\n\t\t// This is handled by Kahn's algorithm naturally - if we finish with items unprocessed, there's a cycle\n\n\t\t// Process this migration\n\t\tresult.push(nextMigration)\n\t\tprocessed.add(nextMigration.id)\n\n\t\t// Update in-degrees and add newly ready migrations\n\t\tfor (const depId of dependents.get(nextMigration.id) || []) {\n\t\t\tif (!processed.has(depId)) {\n\t\t\t\tinDegree.set(depId, inDegree.get(depId)! - 1)\n\t\t\t\tif (inDegree.get(depId) === 0) {\n\t\t\t\t\tready.push(byId.get(depId)!)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for cycles - if we didn't process all migrations, there's a cycle\n\tif (result.length !== migrations.length) {\n\t\tconst unprocessed = migrations.filter((m) => !processed.has(m.id))\n\t\tassert(false, `Circular dependency in migrations: ${unprocessed[0].id}`)\n\t}\n\n\treturn result\n}\n\n/**\n * Parses a migration ID to extract the sequence ID and version number.\n *\n * Migration IDs follow the format `sequenceId/version`, and this function splits\n * them into their component parts. This is used internally for sorting migrations\n * and understanding their relationships.\n * @param id - The migration ID to parse\n * @returns Object containing the sequence ID and numeric version\n * @example\n * ```ts\n * const { sequenceId, version } = parseMigrationId('com.myapp.book/5')\n * // sequenceId: 'com.myapp.book', version: 5\n * ```\n * @internal\n */\nexport function parseMigrationId(id: MigrationId): { sequenceId: string; version: number } {\n\tconst [sequenceId, version] = id.split('/')\n\treturn { sequenceId, version: parseInt(version) }\n}\n\nfunction validateMigrationId(id: string, expectedSequenceId?: string) {\n\tif (expectedSequenceId) {\n\t\tassert(\n\t\t\tid.startsWith(expectedSequenceId + '/'),\n\t\t\t`Every migration in sequence '${expectedSequenceId}' must have an id starting with '${expectedSequenceId}/'. Got invalid id: '${id}'`\n\t\t)\n\t}\n\n\tassert(id.match(/^(.*?)\\/(0|[1-9]\\d*)$/), `Invalid migration id: '${id}'`)\n}\n\n/**\n * Validates that a migration sequence is correctly structured.\n *\n * Performs several validation checks to ensure the migration sequence is valid:\n * - Sequence ID doesn't contain invalid characters\n * - All migration IDs belong to the expected sequence\n * - Migration versions start at 1 and increment by 1\n * - Migration IDs follow the correct format\n * @param migrations - The migration sequence to validate\n * @throws Assertion error if any validation checks fail\n * @example\n * ```ts\n * const sequence = createMigrationSequence({\n * sequenceId: 'com.myapp.book',\n * sequence: [{ id: 'com.myapp.book/1', scope: 'record', up: (r) => r }]\n * })\n * validateMigrations(sequence) // Passes validation\n * ```\n * @public\n */\nexport function validateMigrations(migrations: MigrationSequence) {\n\tassert(\n\t\t!migrations.sequenceId.includes('/'),\n\t\t`sequenceId cannot contain a '/', got ${migrations.sequenceId}`\n\t)\n\tassert(migrations.sequenceId.length, 'sequenceId must be a non-empty string')\n\n\tif (migrations.sequence.length === 0) {\n\t\treturn\n\t}\n\n\tvalidateMigrationId(migrations.sequence[0].id, migrations.sequenceId)\n\tlet n = parseMigrationId(migrations.sequence[0].id).version\n\tassert(\n\t\tn === 1,\n\t\t`Expected the first migrationId to be '${migrations.sequenceId}/1' but got '${migrations.sequence[0].id}'`\n\t)\n\tfor (let i = 1; i < migrations.sequence.length; i++) {\n\t\tconst id = migrations.sequence[i].id\n\t\tvalidateMigrationId(id, migrations.sequenceId)\n\t\tconst m = parseMigrationId(id).version\n\t\tassert(\n\t\t\tm === n + 1,\n\t\t\t`Migration id numbers must increase in increments of 1, expected ${migrations.sequenceId}/${n + 1} but got '${migrations.sequence[i].id}'`\n\t\t)\n\t\tn = m\n\t}\n}\n\n/**\n * Result type returned by migration operations.\n *\n * Migration operations can either succeed and return the transformed value,\n * or fail with a specific reason. This discriminated union type allows for\n * safe handling of both success and error cases when applying migrations.\n * @public\n */\nexport type MigrationResult<T> =\n\t| { type: 'success'; value: T }\n\t| { type: 'error'; reason: MigrationFailureReason }\n\n/** @public */\nexport const MigrationFailureReason = {\n\tIncompatibleSubtype: 'incompatible-subtype',\n\tUnknownType: 'unknown-type',\n\tTargetVersionTooNew: 'target-version-too-new',\n\tTargetVersionTooOld: 'target-version-too-old',\n\tMigrationError: 'migration-error',\n\tUnrecognizedSubtype: 'unrecognized-subtype',\n} as const\n\n/** @public */\nexport type MigrationFailureReason =\n\t(typeof MigrationFailureReason)[keyof typeof MigrationFailureReason]\n\n// This is the magic part for backward compat:\n/** @public */\n// eslint-disable-next-line @typescript-eslint/no-namespace\nexport declare namespace MigrationFailureReason {\n\texport type IncompatibleSubtype = typeof MigrationFailureReason.IncompatibleSubtype\n\texport type UnknownType = typeof MigrationFailureReason.UnknownType\n\texport type TargetVersionTooNew = typeof MigrationFailureReason.TargetVersionTooNew\n\texport type TargetVersionTooOld = typeof MigrationFailureReason.TargetVersionTooOld\n\texport type MigrationError = typeof MigrationFailureReason.MigrationError\n\texport type UnrecognizedSubtype = typeof MigrationFailureReason.UnrecognizedSubtype\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/store",
3
3
  "description": "tldraw infinite canvas SDK (store).",
4
- "version": "5.3.0",
4
+ "version": "5.4.0-next.ef99ab47e5ce",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -44,8 +44,8 @@
44
44
  "lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
45
45
  },
46
46
  "dependencies": {
47
- "@tldraw/state": "5.3.0",
48
- "@tldraw/utils": "5.3.0"
47
+ "@tldraw/state": "5.4.0-next.ef99ab47e5ce",
48
+ "@tldraw/utils": "5.4.0-next.ef99ab47e5ce"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "react": "^18.2.0 || ^19.2.1"
@@ -138,7 +138,7 @@ export class RecordType<
138
138
  id: (properties as Partial<R>).id ?? this.createId(),
139
139
  } as any
140
140
 
141
- for (const [k, v] of Object.entries(properties)) {
141
+ for (const [k, v] of objectMapEntries(properties)) {
142
142
  if (v !== undefined) {
143
143
  result[k] = v
144
144
  }
@@ -1,3 +1,4 @@
1
+ import { objectMapValues } from '@tldraw/utils'
1
2
  import { IdOf, UnknownRecord } from './BaseRecord'
2
3
 
3
4
  /**
@@ -78,7 +79,7 @@ export function createEmptyRecordsDiff<R extends UnknownRecord>(): RecordsDiff<R
78
79
  */
79
80
  export function reverseRecordsDiff(diff: RecordsDiff<any>) {
80
81
  const result: RecordsDiff<any> = { added: diff.removed, removed: diff.added, updated: {} }
81
- for (const [from, to] of Object.values(diff.updated)) {
82
+ for (const [from, to] of objectMapValues(diff.updated)) {
82
83
  result.updated[from.id] = [to, from]
83
84
  }
84
85
  return result
@@ -159,11 +160,14 @@ export function squashRecordDiffs<T extends UnknownRecord>(
159
160
  mutateFirstDiff?: boolean
160
161
  }
161
162
  ): RecordsDiff<T> {
162
- const result = options?.mutateFirstDiff
163
- ? diffs[0]
164
- : ({ added: {}, removed: {}, updated: {} } as RecordsDiff<T>)
163
+ if (options?.mutateFirstDiff) {
164
+ const result = diffs[0]
165
+ squashRecordDiffsMutable(result, diffs, 1)
166
+ return result
167
+ }
165
168
 
166
- squashRecordDiffsMutable(result, options?.mutateFirstDiff ? diffs.slice(1) : diffs)
169
+ const result = { added: {}, removed: {}, updated: {} } as RecordsDiff<T>
170
+ squashRecordDiffsMutable(result, diffs)
167
171
  return result
168
172
  }
169
173
 
@@ -205,8 +209,38 @@ export function squashRecordDiffs<T extends UnknownRecord>(
205
209
  */
206
210
  export function squashRecordDiffsMutable<T extends UnknownRecord>(
207
211
  target: RecordsDiff<T>,
208
- diffs: RecordsDiff<T>[]
212
+ diffs: RecordsDiff<T>[],
213
+ fromIndex = 0
209
214
  ): void {
215
+ squashRecordDiffsMutableImpl(target, diffs, fromIndex)
216
+ }
217
+
218
+ /**
219
+ * Applies only records of a given type from an array of diffs to a target diff. Returns the net
220
+ * change in the number of entries in the target.
221
+ *
222
+ * @internal
223
+ */
224
+ export function squashRecordDiffsMutableByType<
225
+ T extends UnknownRecord,
226
+ TypeName extends T['typeName'],
227
+ >(
228
+ target: RecordsDiff<Extract<T, { typeName: TypeName }>>,
229
+ diffs: RecordsDiff<T>[],
230
+ typeName: TypeName
231
+ ): number {
232
+ return squashRecordDiffsMutableImpl(target, diffs, 0, typeName)
233
+ }
234
+
235
+ function squashRecordDiffsMutableImpl<T extends UnknownRecord>(
236
+ target: RecordsDiff<T>,
237
+ diffs: RecordsDiff<T>[],
238
+ fromIndex: number,
239
+ typeName?: T['typeName']
240
+ ): number {
241
+ let sizeChange = 0
242
+ const trackSize = typeName !== undefined
243
+
210
244
  // This runs on every history interceptor call — e.g. once per input tick while
211
245
  // resizing N shapes, with N entries in diff.updated — so the updated loop must not
212
246
  // allocate per entry. We use for-in instead of Object.entries, mutate the target's
@@ -214,7 +248,8 @@ export function squashRecordDiffsMutable<T extends UnknownRecord>(
214
248
  // know are empty. In-place tuple mutation is safe because the target exclusively
215
249
  // owns its updated tuples: they are always created here (never shared with a source
216
250
  // diff), and sources are never mutated.
217
- for (const diff of diffs) {
251
+ for (let i = fromIndex; i < diffs.length; i++) {
252
+ const diff = diffs[i]
218
253
  // target.removed can only lose entries before the removed loop below, so a
219
254
  // stale `true` is harmless (extra no-op deletes).
220
255
  const targetHasRemoved = hasAnyKey(target.removed)
@@ -222,13 +257,17 @@ export function squashRecordDiffsMutable<T extends UnknownRecord>(
222
257
  for (const _id in diff.added) {
223
258
  const id = _id as IdOf<T>
224
259
  const value = diff.added[id]
260
+ if (typeName !== undefined && value.typeName !== typeName) continue
225
261
  if (targetHasRemoved && target.removed[id]) {
226
262
  const original = target.removed[id]
227
263
  delete target.removed[id]
264
+ if (trackSize) sizeChange--
228
265
  if (original !== value) {
229
266
  target.updated[id] = [original, value]
267
+ if (trackSize) sizeChange++
230
268
  }
231
269
  } else {
270
+ if (trackSize && !target.added[id]) sizeChange++
232
271
  target.added[id] = value
233
272
  }
234
273
  }
@@ -239,35 +278,52 @@ export function squashRecordDiffsMutable<T extends UnknownRecord>(
239
278
  for (const _id in diff.updated) {
240
279
  const id = _id as IdOf<T>
241
280
  const to = diff.updated[id][1]
281
+ if (typeName !== undefined && to.typeName !== typeName) continue
242
282
  if (targetHasAdded && target.added[id]) {
243
283
  target.added[id] = to
284
+ if (trackSize && target.updated[id]) sizeChange--
244
285
  delete target.updated[id]
245
- if (targetHasRemoved) delete target.removed[id]
286
+ if (targetHasRemoved) {
287
+ if (trackSize && target.removed[id]) sizeChange--
288
+ delete target.removed[id]
289
+ }
246
290
  continue
247
291
  }
248
292
  const existing = target.updated[id]
249
293
  if (existing) {
250
294
  existing[1] = to
251
- if (targetHasRemoved) delete target.removed[id]
295
+ if (targetHasRemoved) {
296
+ if (trackSize && target.removed[id]) sizeChange--
297
+ delete target.removed[id]
298
+ }
252
299
  continue
253
300
  }
254
301
 
255
302
  // copy the tuple so the target owns it and can mutate it in place later
256
303
  target.updated[id] = [diff.updated[id][0], to]
257
- if (targetHasRemoved) delete target.removed[id]
304
+ if (trackSize) sizeChange++
305
+ if (targetHasRemoved) {
306
+ if (trackSize && target.removed[id]) sizeChange--
307
+ delete target.removed[id]
308
+ }
258
309
  }
259
310
 
260
311
  for (const _id in diff.removed) {
261
312
  const id = _id as IdOf<T>
313
+ if (typeName !== undefined && diff.removed[id].typeName !== typeName) continue
262
314
  // the same record was added in this diff sequence, just drop it
263
315
  if (target.added[id]) {
264
316
  delete target.added[id]
317
+ if (trackSize) sizeChange--
265
318
  } else if (target.updated[id]) {
266
319
  target.removed[id] = target.updated[id][0]
267
320
  delete target.updated[id]
268
321
  } else {
322
+ if (trackSize && !target.removed[id]) sizeChange++
269
323
  target.removed[id] = diff.removed[id]
270
324
  }
271
325
  }
272
326
  }
327
+
328
+ return sizeChange
273
329
  }
package/src/lib/Store.ts CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  import { AtomMap } from './AtomMap'
15
15
  import { IdOf, RecordId, UnknownRecord } from './BaseRecord'
16
16
  import { devFreeze } from './devFreeze'
17
- import { hasAnyKey, RecordsDiff, squashRecordDiffs } from './RecordsDiff'
17
+ import { isRecordsDiffEmpty, RecordsDiff, squashRecordDiffs } from './RecordsDiff'
18
18
  import { RecordScope } from './RecordType'
19
19
  import { StoreQueries } from './StoreQueries'
20
20
  import { SerializedSchema, StoreSchema } from './StoreSchema'
@@ -504,23 +504,15 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
504
504
  },
505
505
  { scheduleEffect: (cb) => (this.cancelHistoryReactor = throttleToNextFrame(cb)) }
506
506
  )
507
- this.scopedTypes = {
508
- document: new Set(
509
- objectMapValues(this.schema.types)
510
- .filter((t) => t.scope === 'document')
511
- .map((t) => t.typeName)
512
- ),
513
- session: new Set(
514
- objectMapValues(this.schema.types)
515
- .filter((t) => t.scope === 'session')
516
- .map((t) => t.typeName)
517
- ),
518
- presence: new Set(
519
- objectMapValues(this.schema.types)
520
- .filter((t) => t.scope === 'presence')
521
- .map((t) => t.typeName)
522
- ),
507
+ const scopedTypes: { [K in RecordScope]: Set<R['typeName']> } = {
508
+ document: new Set(),
509
+ session: new Set(),
510
+ presence: new Set(),
523
511
  }
512
+ for (const type of objectMapValues(this.schema.types)) {
513
+ scopedTypes[type.scope].add(type.typeName)
514
+ }
515
+ this.scopedTypes = scopedTypes
524
516
  }
525
517
 
526
518
  public _flushHistory() {
@@ -528,30 +520,23 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
528
520
  if (this.historyAccumulator.hasChanges()) {
529
521
  const entries = this.historyAccumulator.flush()
530
522
  for (const { changes, source } of entries) {
531
- let instanceChanges = null as null | RecordsDiff<R>
532
- let documentChanges = null as null | RecordsDiff<R>
533
- let presenceChanges = null as null | RecordsDiff<R>
523
+ // Filtered diffs are computed at most once per scope per entry, and shared by every
524
+ // listener watching that scope.
525
+ const scopedChanges = new Map<RecordScope, RecordsDiff<R> | null>()
534
526
  for (const { onHistory, filters } of this.listeners) {
535
527
  if (filters.source !== 'all' && filters.source !== source) {
536
528
  continue
537
529
  }
538
- if (filters.scope !== 'all') {
539
- if (filters.scope === 'document') {
540
- documentChanges ??= this.filterChangesByScope(changes, 'document')
541
- if (!documentChanges) continue
542
- onHistory({ changes: documentChanges, source })
543
- } else if (filters.scope === 'session') {
544
- instanceChanges ??= this.filterChangesByScope(changes, 'session')
545
- if (!instanceChanges) continue
546
- onHistory({ changes: instanceChanges, source })
547
- } else {
548
- presenceChanges ??= this.filterChangesByScope(changes, 'presence')
549
- if (!presenceChanges) continue
550
- onHistory({ changes: presenceChanges, source })
551
- }
552
- } else {
530
+ if (filters.scope === 'all') {
553
531
  onHistory({ changes, source })
532
+ continue
533
+ }
534
+ if (!scopedChanges.has(filters.scope)) {
535
+ scopedChanges.set(filters.scope, this.filterChangesByScope(changes, filters.scope))
554
536
  }
537
+ const filtered = scopedChanges.get(filters.scope)
538
+ if (!filtered) continue
539
+ onHistory({ changes: filtered, source })
555
540
  }
556
541
  }
557
542
  }
@@ -573,7 +558,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
573
558
  updated: filterEntries(change.updated, (_, r) => this.scopedTypes[scope].has(r[1].typeName)),
574
559
  removed: filterEntries(change.removed, (_, r) => this.scopedTypes[scope].has(r.typeName)),
575
560
  }
576
- if (!hasAnyKey(result.added) && !hasAnyKey(result.updated) && !hasAnyKey(result.removed)) {
561
+ if (isRecordsDiffEmpty(result)) {
577
562
  return null
578
563
  }
579
564
  return result
@@ -211,6 +211,30 @@ describe('filtered history (QH)', () => {
211
211
  },
212
212
  ])
213
213
  })
214
+
215
+ it('[QH2] squashes matching records while ignoring interleaved record types', () => {
216
+ const authorHistory = store.query.filterHistory('author')
217
+ authorHistory.get()
218
+
219
+ const lastChangedEpoch = authorHistory.lastChangedEpoch
220
+ const newAuthor = Author.create({ name: 'Stanley Briggs' })
221
+ const updatedNewAuthor = { ...newAuthor, age: 38 }
222
+
223
+ store.put([newAuthor])
224
+ store.put([updatedNewAuthor, { ...books.lotr, title: 'The Fellowship of the Ring' }])
225
+ store.put([{ ...authors.davidMitchellFunny, age: 38 }])
226
+ store.remove([authors.davidMitchellFunny.id])
227
+ store.remove([authors.tolkein.id])
228
+ store.put([authors.tolkein])
229
+
230
+ expect(authorHistory.getDiffSince(lastChangedEpoch)).toEqual([
231
+ {
232
+ added: { [newAuthor.id]: updatedNewAuthor },
233
+ updated: {},
234
+ removed: { [authors.davidMitchellFunny.id]: authors.davidMitchellFunny },
235
+ },
236
+ ])
237
+ })
214
238
  })
215
239
 
216
240
  describe('indexes (QI)', () => {
@@ -12,7 +12,7 @@ import { AtomMap } from './AtomMap'
12
12
  import { IdOf, UnknownRecord } from './BaseRecord'
13
13
  import { executeQuery, objectMatchesQuery, QueryExpression } from './executeQuery'
14
14
  import { IncrementalSetConstructor } from './IncrementalSetConstructor'
15
- import { RecordsDiff } from './RecordsDiff'
15
+ import { hasAnyKey, RecordsDiff, squashRecordDiffsMutableByType } from './RecordsDiff'
16
16
  import { diffSets } from './setUtils'
17
17
  import { CollectionDiff } from './Store'
18
18
 
@@ -197,62 +197,9 @@ export class StoreQueries<R extends UnknownRecord> {
197
197
  if (diff === RESET_VALUE) return this.history.get()
198
198
 
199
199
  const res = { added: {}, removed: {}, updated: {} } as RecordsDiff<S>
200
- let numAdded = 0
201
- let numRemoved = 0
202
- let numUpdated = 0
200
+ const size = squashRecordDiffsMutableByType(res, diff, typeName)
203
201
 
204
- for (const changes of diff) {
205
- for (const added of objectMapValues(changes.added)) {
206
- if (added.typeName === typeName) {
207
- if (res.removed[added.id as IdOf<S>]) {
208
- const original = res.removed[added.id as IdOf<S>]
209
- delete res.removed[added.id as IdOf<S>]
210
- numRemoved--
211
- if (original !== added) {
212
- res.updated[added.id as IdOf<S>] = [original, added as S]
213
- numUpdated++
214
- }
215
- } else {
216
- res.added[added.id as IdOf<S>] = added as S
217
- numAdded++
218
- }
219
- }
220
- }
221
-
222
- for (const [from, to] of objectMapValues(changes.updated)) {
223
- if (to.typeName === typeName) {
224
- if (res.added[to.id as IdOf<S>]) {
225
- res.added[to.id as IdOf<S>] = to as S
226
- } else if (res.updated[to.id as IdOf<S>]) {
227
- res.updated[to.id as IdOf<S>] = [res.updated[to.id as IdOf<S>][0], to as S]
228
- } else {
229
- res.updated[to.id as IdOf<S>] = [from as S, to as S]
230
- numUpdated++
231
- }
232
- }
233
- }
234
-
235
- for (const removed of objectMapValues(changes.removed)) {
236
- if (removed.typeName === typeName) {
237
- if (res.added[removed.id as IdOf<S>]) {
238
- // was added during this diff sequence, so just undo the add
239
- delete res.added[removed.id as IdOf<S>]
240
- numAdded--
241
- } else if (res.updated[removed.id as IdOf<S>]) {
242
- // remove oldest version
243
- res.removed[removed.id as IdOf<S>] = res.updated[removed.id as IdOf<S>][0]
244
- delete res.updated[removed.id as IdOf<S>]
245
- numUpdated--
246
- numRemoved++
247
- } else {
248
- res.removed[removed.id as IdOf<S>] = removed as S
249
- numRemoved++
250
- }
251
- }
252
- }
253
- }
254
-
255
- if (numAdded || numRemoved || numUpdated) {
202
+ if (size) {
256
203
  return withDiff(this.history.get(), res)
257
204
  } else {
258
205
  return lastValue
@@ -470,7 +417,7 @@ export class StoreQueries<R extends UnknownRecord> {
470
417
  record<TypeName extends R['typeName']>(
471
418
  typeName: TypeName,
472
419
  queryCreator: () => QueryExpression<Extract<R, { typeName: TypeName }>> = () => ({}),
473
- name = 'record:' + typeName + (queryCreator ? ':' + queryCreator.toString() : '')
420
+ name = 'record:' + typeName + ':' + queryCreator.toString()
474
421
  ): Computed<Extract<R, { typeName: TypeName }> | undefined> {
475
422
  type S = Extract<R, { typeName: TypeName }>
476
423
  const ids = this.ids(typeName, queryCreator, name)
@@ -510,7 +457,7 @@ export class StoreQueries<R extends UnknownRecord> {
510
457
  records<TypeName extends R['typeName']>(
511
458
  typeName: TypeName,
512
459
  queryCreator: () => QueryExpression<Extract<R, { typeName: TypeName }>> = () => ({}),
513
- name = 'records:' + typeName + (queryCreator ? ':' + queryCreator.toString() : '')
460
+ name = 'records:' + typeName + ':' + queryCreator.toString()
514
461
  ): Computed<Array<Extract<R, { typeName: TypeName }>>> {
515
462
  type S = Extract<R, { typeName: TypeName }>
516
463
  const ids = this.ids(typeName, queryCreator, 'ids:' + name)
@@ -554,7 +501,7 @@ export class StoreQueries<R extends UnknownRecord> {
554
501
  ids<TypeName extends R['typeName']>(
555
502
  typeName: TypeName,
556
503
  queryCreator: () => QueryExpression<Extract<R, { typeName: TypeName }>> = () => ({}),
557
- name = 'ids:' + typeName + (queryCreator ? ':' + queryCreator.toString() : '')
504
+ name = 'ids:' + typeName + ':' + queryCreator.toString()
558
505
  ): Computed<
559
506
  Set<IdOf<Extract<R, { typeName: TypeName }>>>,
560
507
  CollectionDiff<IdOf<Extract<R, { typeName: TypeName }>>>
@@ -567,7 +514,7 @@ export class StoreQueries<R extends UnknownRecord> {
567
514
  // deref type history early to allow first incremental update to use diffs
568
515
  typeHistory.get()
569
516
  const query: QueryExpression<S> = queryCreator()
570
- if (Object.keys(query).length === 0) {
517
+ if (!hasAnyKey(query)) {
571
518
  return this.getAllIdsForType(typeName)
572
519
  }
573
520
 
@@ -423,16 +423,21 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
423
423
  * @public
424
424
  */
425
425
  public getMigrationsSince(persistedSchema: SerializedSchema): Result<Migration[], string> {
426
- // Check cache first
426
+ // Every result — success, empty, or error — is cached, so cache once around the whole
427
+ // computation rather than at each of its exit points.
427
428
  const cached = this.migrationCache.get(persistedSchema)
428
429
  if (cached) {
429
430
  return cached
430
431
  }
431
432
 
433
+ const result = this.computeMigrationsSince(persistedSchema)
434
+ this.migrationCache.set(persistedSchema, result)
435
+ return result
436
+ }
437
+
438
+ private computeMigrationsSince(persistedSchema: SerializedSchema): Result<Migration[], string> {
432
439
  const upgradeResult = upgradeSchema(persistedSchema)
433
440
  if (!upgradeResult.ok) {
434
- // Cache the error result
435
- this.migrationCache.set(persistedSchema, upgradeResult)
436
441
  return upgradeResult
437
442
  }
438
443
  const schema = upgradeResult.value
@@ -449,10 +454,7 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
449
454
  }
450
455
 
451
456
  if (sequenceIdsToInclude.size === 0) {
452
- const result = Result.ok([])
453
- // Cache the empty result
454
- this.migrationCache.set(persistedSchema, result)
455
- return result
457
+ return Result.ok([])
456
458
  }
457
459
 
458
460
  const allMigrationsToInclude = new Set<MigrationId>()
@@ -471,10 +473,7 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
471
473
  const idx = this.migrations[sequenceId].sequence.findIndex((m) => m.id === theirVersionId)
472
474
  // todo: better error handling
473
475
  if (idx === -1) {
474
- const result = Result.err('Incompatible schema?')
475
- // Cache the error result
476
- this.migrationCache.set(persistedSchema, result)
477
- return result
476
+ return Result.err('Incompatible schema?')
478
477
  }
479
478
  for (const migration of this.migrations[sequenceId].sequence.slice(idx + 1)) {
480
479
  allMigrationsToInclude.add(migration.id)
@@ -482,12 +481,7 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
482
481
  }
483
482
 
484
483
  // collect any migrations
485
- const result = Result.ok(
486
- this.sortedMigrations.filter(({ id }) => allMigrationsToInclude.has(id))
487
- )
488
- // Cache the result
489
- this.migrationCache.set(persistedSchema, result)
490
- return result
484
+ return Result.ok(this.sortedMigrations.filter(({ id }) => allMigrationsToInclude.has(id)))
491
485
  }
492
486
 
493
487
  /**
@@ -1,3 +1,4 @@
1
+ import { objectMapFromEntries, objectMapValues } from '@tldraw/utils'
1
2
  import { IdOf, UnknownRecord } from './BaseRecord'
2
3
  import { intersectSets } from './setUtils'
3
4
  import { StoreQueries } from './StoreQueries'
@@ -147,7 +148,9 @@ export function executeQuery<R extends UnknownRecord, TypeName extends R['typeNa
147
148
  const matcherPaths = extractMatcherPaths(query)
148
149
 
149
150
  // Build a set of matching IDs for each path
150
- const matchIds = Object.fromEntries(matcherPaths.map(({ path }) => [path, new Set<IdOf<S>>()]))
151
+ const matchIds = objectMapFromEntries(
152
+ matcherPaths.map(({ path }) => [path, new Set<IdOf<S>>()] as const)
153
+ )
151
154
 
152
155
  // For each path, use the index to find matching IDs
153
156
  for (const { path, matcher } of matcherPaths) {
@@ -185,5 +188,5 @@ export function executeQuery<R extends UnknownRecord, TypeName extends R['typeNa
185
188
  }
186
189
 
187
190
  // Intersect all the match sets
188
- return intersectSets(Object.values(matchIds)) as Set<IdOf<S>>
191
+ return intersectSets(objectMapValues(matchIds)) as Set<IdOf<S>>
189
192
  }