@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.
- package/dist-cjs/index.d.ts +1 -0
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/RecordType.js +1 -1
- package/dist-cjs/lib/RecordType.js.map +2 -2
- package/dist-cjs/lib/RecordsDiff.js +45 -9
- package/dist-cjs/lib/RecordsDiff.js.map +3 -3
- package/dist-cjs/lib/Store.js +18 -29
- package/dist-cjs/lib/Store.js.map +2 -2
- package/dist-cjs/lib/StoreQueries.js +7 -54
- package/dist-cjs/lib/StoreQueries.js.map +2 -2
- package/dist-cjs/lib/StoreSchema.js +8 -12
- package/dist-cjs/lib/StoreSchema.js.map +3 -3
- package/dist-cjs/lib/executeQuery.js +5 -2
- package/dist-cjs/lib/executeQuery.js.map +2 -2
- package/dist-cjs/lib/migrate.js +5 -7
- package/dist-cjs/lib/migrate.js.map +2 -2
- package/dist-esm/index.d.mts +1 -0
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/RecordType.mjs +1 -1
- package/dist-esm/lib/RecordType.mjs.map +2 -2
- package/dist-esm/lib/RecordsDiff.mjs +45 -9
- package/dist-esm/lib/RecordsDiff.mjs.map +3 -3
- package/dist-esm/lib/Store.mjs +19 -30
- package/dist-esm/lib/Store.mjs.map +2 -2
- package/dist-esm/lib/StoreQueries.mjs +7 -54
- package/dist-esm/lib/StoreQueries.mjs.map +2 -2
- package/dist-esm/lib/StoreSchema.mjs +8 -12
- package/dist-esm/lib/StoreSchema.mjs.map +3 -3
- package/dist-esm/lib/executeQuery.mjs +5 -2
- package/dist-esm/lib/executeQuery.mjs.map +2 -2
- package/dist-esm/lib/migrate.mjs +5 -7
- package/dist-esm/lib/migrate.mjs.map +2 -2
- package/package.json +3 -3
- package/src/lib/RecordType.ts +1 -1
- package/src/lib/RecordsDiff.ts +66 -10
- package/src/lib/Store.ts +21 -36
- package/src/lib/StoreQueries.test.ts +24 -0
- package/src/lib/StoreQueries.ts +7 -60
- package/src/lib/StoreSchema.ts +11 -17
- package/src/lib/executeQuery.ts +5 -2
- 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": "AAAA,SAAS,QAAQ,wBAAwB;AAKzC,SAAS,gBAAgB,UAA+D;AACvF,QAAM,SAAsB,CAAC;AAC7B,WAAS,IAAI,SAAS,SAAS,GAAG,KAAK,GAAG,KAAK;AAC9C,UAAM,OAAO,SAAS,CAAC;AACvB,QAAI,EAAE,QAAQ,OAAO;AACpB,YAAM,YAAY,KAAK;AACvB,YAAM,OAAO,OAAO,CAAC;AACrB,UAAI,MAAM;AACT,eAAO,CAAC,IAAI;AAAA,UACX,GAAG;AAAA,UACH,WAAW,UAAU,OAAO,KAAK,aAAa,CAAC,CAAC;AAAA,QACjD;AAAA,MACD;AAAA,IACD,OAAO;AACN,aAAO,QAAQ,IAAI;AAAA,IACpB;AAAA,EACD;AACA,SAAO;AACR;AA+BO,SAAS,wBAAwB;AAAA,EACvC;AAAA,EACA;AAAA,EACA,cAAc;AACf,GAIsB;AACrB,QAAM,aAAgC;AAAA,IACrC;AAAA,IACA;AAAA,IACA,UAAU,gBAAgB,QAAQ;AAAA,EACnC;AACA,qBAAmB,UAAU;AAC7B,SAAO;AACR;AA4BO,SAAS,mBAGd,YAAgB,UAAuE;
|
|
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.
|
|
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.
|
|
48
|
-
"@tldraw/utils": "5.
|
|
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"
|
package/src/lib/RecordType.ts
CHANGED
|
@@ -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
|
|
141
|
+
for (const [k, v] of objectMapEntries(properties)) {
|
|
142
142
|
if (v !== undefined) {
|
|
143
143
|
result[k] = v
|
|
144
144
|
}
|
package/src/lib/RecordsDiff.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
if (options?.mutateFirstDiff) {
|
|
164
|
+
const result = diffs[0]
|
|
165
|
+
squashRecordDiffsMutable(result, diffs, 1)
|
|
166
|
+
return result
|
|
167
|
+
}
|
|
165
168
|
|
|
166
|
-
|
|
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 (
|
|
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)
|
|
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)
|
|
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 (
|
|
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 {
|
|
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
|
-
|
|
508
|
-
document: new Set(
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
|
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 (
|
|
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)', () => {
|
package/src/lib/StoreQueries.ts
CHANGED
|
@@ -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
|
-
|
|
201
|
-
let numRemoved = 0
|
|
202
|
-
let numUpdated = 0
|
|
200
|
+
const size = squashRecordDiffsMutableByType(res, diff, typeName)
|
|
203
201
|
|
|
204
|
-
|
|
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 +
|
|
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 +
|
|
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 +
|
|
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 (
|
|
517
|
+
if (!hasAnyKey(query)) {
|
|
571
518
|
return this.getAllIdsForType(typeName)
|
|
572
519
|
}
|
|
573
520
|
|
package/src/lib/StoreSchema.ts
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/src/lib/executeQuery.ts
CHANGED
|
@@ -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 =
|
|
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(
|
|
191
|
+
return intersectSets(objectMapValues(matchIds)) as Set<IdOf<S>>
|
|
189
192
|
}
|