@tldraw/store 4.3.0-next.f13438eb7775 → 4.3.0
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 +49 -1
- package/dist-cjs/index.js +3 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/AtomSet.js +68 -0
- package/dist-cjs/lib/AtomSet.js.map +7 -0
- package/dist-cjs/lib/ImmutableMap.js +25 -23
- package/dist-cjs/lib/ImmutableMap.js.map +2 -2
- package/dist-cjs/lib/StoreSchema.js +84 -24
- package/dist-cjs/lib/StoreSchema.js.map +2 -2
- package/dist-cjs/lib/StoreSideEffects.js +1 -1
- package/dist-cjs/lib/StoreSideEffects.js.map +1 -1
- package/dist-cjs/lib/devFreeze.js +5 -3
- package/dist-cjs/lib/devFreeze.js.map +2 -2
- package/dist-cjs/lib/isDev.js +37 -0
- package/dist-cjs/lib/isDev.js.map +7 -0
- package/dist-cjs/lib/migrate.js.map +2 -2
- package/dist-esm/index.d.mts +49 -1
- package/dist-esm/index.mjs +3 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/AtomSet.mjs +48 -0
- package/dist-esm/lib/AtomSet.mjs.map +7 -0
- package/dist-esm/lib/ImmutableMap.mjs +25 -23
- package/dist-esm/lib/ImmutableMap.mjs.map +2 -2
- package/dist-esm/lib/StoreSchema.mjs +87 -25
- package/dist-esm/lib/StoreSchema.mjs.map +2 -2
- package/dist-esm/lib/StoreSideEffects.mjs +1 -1
- package/dist-esm/lib/StoreSideEffects.mjs.map +1 -1
- package/dist-esm/lib/devFreeze.mjs +5 -3
- package/dist-esm/lib/devFreeze.mjs.map +2 -2
- package/dist-esm/lib/isDev.mjs +16 -0
- package/dist-esm/lib/isDev.mjs.map +7 -0
- package/dist-esm/lib/migrate.mjs.map +2 -2
- package/package.json +4 -4
- package/src/index.ts +3 -0
- package/src/lib/AtomSet.ts +52 -0
- package/src/lib/ImmutableMap.ts +25 -33
- package/src/lib/StoreSchema.ts +97 -30
- package/src/lib/StoreSideEffects.ts +1 -1
- package/src/lib/devFreeze.test.ts +6 -2
- package/src/lib/devFreeze.ts +7 -3
- package/src/lib/isDev.ts +20 -0
- package/src/lib/migrate.ts +29 -0
- package/src/lib/test/recordStore.test.ts +182 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/StoreSideEffects.ts"],
|
|
4
|
-
"sourcesContent": ["import { UnknownRecord } from './BaseRecord'\nimport { Store } from './Store'\n\n/**\n * Handler function called before a record is created in the store.\n * The handler receives the record to be created and can return a modified version.\n * Use this to validate, transform, or modify records before they are added to the store.\n *\n * @param record - The record about to be created\n * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization\n * @returns The record to actually create (may be modified)\n *\n * @example\n * ```ts\n * const handler: StoreBeforeCreateHandler<MyRecord> = (record, source) => {\n * // Ensure all user-created records have a timestamp\n * if (source === 'user' && !record.createdAt) {\n * return { ...record, createdAt: Date.now() }\n * }\n * return record\n * }\n * ```\n *\n * @public\n */\nexport type StoreBeforeCreateHandler<R extends UnknownRecord> = (\n\trecord: R,\n\tsource: 'remote' | 'user'\n) => R\n/**\n * Handler function called after a record has been successfully created in the store.\n * Use this for side effects that should happen after record creation, such as updating\n * related records or triggering notifications.\n *\n * @param record - The record that was created\n * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization\n *\n * @example\n * ```ts\n * const handler: StoreAfterCreateHandler<BookRecord> = (book, source) => {\n * if (source === 'user') {\n * console.log(`New book added: ${book.title}`)\n * updateAuthorBookCount(book.authorId)\n * }\n * }\n * ```\n *\n * @public\n */\nexport type StoreAfterCreateHandler<R extends UnknownRecord> = (\n\trecord: R,\n\tsource: 'remote' | 'user'\n) => void\n/**\n * Handler function called before a record is updated in the store.\n * The handler receives the current and new versions of the record and can return\n * a modified version or the original to prevent the change.\n *\n * @param prev - The current version of the record in the store\n * @param next - The proposed new version of the record\n * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization\n * @returns The record version to actually store (may be modified or the original to block change)\n *\n * @example\n * ```ts\n * const handler: StoreBeforeChangeHandler<ShapeRecord> = (prev, next, source) => {\n * // Prevent shapes from being moved outside the canvas bounds\n * if (next.x < 0 || next.y < 0) {\n * return prev // Block the change\n * }\n * return next\n * }\n * ```\n *\n * @public\n */\nexport type StoreBeforeChangeHandler<R extends UnknownRecord> = (\n\tprev: R,\n\tnext: R,\n\tsource: 'remote' | 'user'\n) => R\n/**\n * Handler function called after a record has been successfully updated in the store.\n * Use this for side effects that should happen after record changes, such as\n * updating related records or maintaining consistency constraints.\n *\n * @param prev - The previous version of the record\n * @param next - The new version of the record that was stored\n * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization\n *\n * @example\n * ```ts\n * const handler: StoreAfterChangeHandler<ShapeRecord> = (prev, next, source) => {\n * // Update connected arrows when a shape moves\n * if (prev.x !== next.x || prev.y !== next.y) {\n * updateConnectedArrows(next.id)\n * }\n * }\n * ```\n *\n * @public\n */\nexport type StoreAfterChangeHandler<R extends UnknownRecord> = (\n\tprev: R,\n\tnext: R,\n\tsource: 'remote' | 'user'\n) => void\n/**\n * Handler function called before a record is deleted from the store.\n * The handler can return `false` to prevent the deletion from occurring.\n *\n * @param record - The record about to be deleted\n * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization\n * @returns `false` to prevent deletion, `void` or any other value to allow it\n *\n * @example\n * ```ts\n * const handler: StoreBeforeDeleteHandler<BookRecord> = (book, source) => {\n * // Prevent deletion of books that are currently checked out\n * if (book.isCheckedOut) {\n * console.warn('Cannot delete checked out book')\n * return false\n * }\n * // Allow deletion for other books\n * }\n * ```\n *\n * @public\n */\nexport type StoreBeforeDeleteHandler<R extends UnknownRecord> = (\n\trecord: R,\n\tsource: 'remote' | 'user'\n) => void | false\n/**\n * Handler function called after a record has been successfully deleted from the store.\n * Use this for cleanup operations and maintaining referential integrity.\n *\n * @param record - The record that was deleted\n * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization\n *\n * @example\n * ```ts\n * const handler: StoreAfterDeleteHandler<ShapeRecord> = (shape, source) => {\n * // Clean up arrows that were connected to this shape\n * const connectedArrows = findArrowsConnectedTo(shape.id)\n * store.remove(connectedArrows.map(arrow => arrow.id))\n * }\n * ```\n *\n * @public\n */\nexport type StoreAfterDeleteHandler<R extends UnknownRecord> = (\n\trecord: R,\n\tsource: 'remote' | 'user'\n) => void\n\n/**\n * Handler function called when a store operation (atomic transaction) completes.\n * This is useful for performing actions after a batch of changes has been applied,\n * such as triggering saves or sending notifications.\n *\n * @param source - Whether the operation originated from 'user' interaction or 'remote' synchronization\n *\n * @example\n * ```ts\n * const handler: StoreOperationCompleteHandler = (source) => {\n * if (source === 'user') {\n * // Auto-save after user operations complete\n * saveStoreSnapshot()\n * }\n * }\n * ```\n *\n * @public\n */\nexport type StoreOperationCompleteHandler = (source: 'remote' | 'user') => void\n\n/**\n * The side effect manager (aka a \"correct state enforcer\") is responsible\n * for making sure that the store's state is always correct and consistent. This includes\n * things like: deleting a shape if its parent is deleted; unbinding\n * arrows when their binding target is deleted; maintaining referential integrity; etc.\n *\n * Side effects are organized into lifecycle hooks that run before and after\n * record operations (create, change, delete), allowing you to validate data,\n * transform records, and maintain business rules.\n *\n * @example\n * ```ts\n * const sideEffects = new StoreSideEffects(store)\n *\n * // Ensure arrows are deleted when their target shape is deleted\n * sideEffects.registerAfterDeleteHandler('shape', (shape) => {\n * const arrows = store.query.records('arrow', () => ({\n * toId: { eq: shape.id }\n * })).get()\n * store.remove(arrows.map(arrow => arrow.id))\n * })\n * ```\n *\n * @public\n */\nexport class StoreSideEffects<R extends UnknownRecord> {\n\t/**\n\t * Creates a new side effects manager for the given store.\n\t *\n\t * store - The store instance to manage side effects for\n\t */\n\tconstructor(private readonly store: Store<R>) {}\n\n\tprivate _beforeCreateHandlers: { [K in string]?: StoreBeforeCreateHandler<any>[] } = {}\n\tprivate _afterCreateHandlers: { [K in string]?: StoreAfterCreateHandler<any>[] } = {}\n\tprivate _beforeChangeHandlers: { [K in string]?: StoreBeforeChangeHandler<any>[] } = {}\n\tprivate _afterChangeHandlers: { [K in string]?: StoreAfterChangeHandler<any>[] } = {}\n\tprivate _beforeDeleteHandlers: { [K in string]?: StoreBeforeDeleteHandler<any>[] } = {}\n\tprivate _afterDeleteHandlers: { [K in string]?: StoreAfterDeleteHandler<any>[] } = {}\n\tprivate _operationCompleteHandlers: StoreOperationCompleteHandler[] = []\n\n\tprivate _isEnabled = true\n\t/**\n\t * Checks whether side effects are currently enabled.\n\t * When disabled, all side effect handlers are bypassed.\n\t *\n\t * @returns `true` if side effects are enabled, `false` otherwise\n\t * @internal\n\t */\n\tisEnabled() {\n\t\treturn this._isEnabled\n\t}\n\t/**\n\t * Enables or disables side effects processing.\n\t * When disabled, no side effect handlers will be called.\n\t *\n\t * @param enabled - Whether to enable or disable side effects\n\t * @internal\n\t */\n\tsetIsEnabled(enabled: boolean) {\n\t\tthis._isEnabled = enabled\n\t}\n\n\t/**\n\t * Processes all registered 'before create' handlers for a record.\n\t * Handlers are called in registration order and can transform the record.\n\t *\n\t * @param record - The record about to be created\n\t * @param source - Whether the change originated from 'user' or 'remote'\n\t * @returns The potentially modified record to actually create\n\t * @internal\n\t */\n\thandleBeforeCreate(record: R, source: 'remote' | 'user') {\n\t\tif (!this._isEnabled) return record\n\n\t\tconst handlers = this._beforeCreateHandlers[record.typeName] as StoreBeforeCreateHandler<R>[]\n\t\tif (handlers) {\n\t\t\tlet r = record\n\t\t\tfor (const handler of handlers) {\n\t\t\t\tr = handler(r, source)\n\t\t\t}\n\t\t\treturn r\n\t\t}\n\n\t\treturn record\n\t}\n\n\t/**\n\t * Processes all registered 'after create' handlers for a record.\n\t * Handlers are called in registration order after the record is created.\n\t *\n\t * @param record - The record that was created\n\t * @param source - Whether the change originated from 'user' or 'remote'\n\t * @internal\n\t */\n\thandleAfterCreate(record: R, source: 'remote' | 'user') {\n\t\tif (!this._isEnabled) return\n\n\t\tconst handlers = this._afterCreateHandlers[record.typeName] as StoreAfterCreateHandler<R>[]\n\t\tif (handlers) {\n\t\t\tfor (const handler of handlers) {\n\t\t\t\thandler(record, source)\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Processes all registered 'before change' handlers for a record.\n\t * Handlers are called in registration order and can modify or block the change.\n\t *\n\t * @param prev - The current version of the record\n\t * @param next - The proposed new version of the record\n\t * @param source - Whether the change originated from 'user' or 'remote'\n\t * @returns The potentially modified record to actually store\n\t * @internal\n\t */\n\thandleBeforeChange(prev: R, next: R, source: 'remote' | 'user') {\n\t\tif (!this._isEnabled) return next\n\n\t\tconst handlers = this._beforeChangeHandlers[next.typeName] as StoreBeforeChangeHandler<R>[]\n\t\tif (handlers) {\n\t\t\tlet r = next\n\t\t\tfor (const handler of handlers) {\n\t\t\t\tr = handler(prev, r, source)\n\t\t\t}\n\t\t\treturn r\n\t\t}\n\n\t\treturn next\n\t}\n\n\t/**\n\t * Processes all registered 'after change' handlers for a record.\n\t * Handlers are called in registration order after the record is updated.\n\t *\n\t * @param prev - The previous version of the record\n\t * @param next - The new version of the record that was stored\n\t * @param source - Whether the change originated from 'user' or 'remote'\n\t * @internal\n\t */\n\thandleAfterChange(prev: R, next: R, source: 'remote' | 'user') {\n\t\tif (!this._isEnabled) return\n\n\t\tconst handlers = this._afterChangeHandlers[next.typeName] as StoreAfterChangeHandler<R>[]\n\t\tif (handlers) {\n\t\t\tfor (const handler of handlers) {\n\t\t\t\thandler(prev, next, source)\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Processes all registered 'before delete' handlers for a record.\n\t * If any handler returns `false`, the deletion is prevented.\n\t *\n\t * @param record - The record about to be deleted\n\t * @param source - Whether the change originated from 'user' or 'remote'\n\t * @returns `true` to allow deletion, `false` to prevent it\n\t * @internal\n\t */\n\thandleBeforeDelete(record: R, source: 'remote' | 'user') {\n\t\tif (!this._isEnabled) return true\n\n\t\tconst handlers = this._beforeDeleteHandlers[record.typeName] as StoreBeforeDeleteHandler<R>[]\n\t\tif (handlers) {\n\t\t\tfor (const handler of handlers) {\n\t\t\t\tif (handler(record, source) === false) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\n\t/**\n\t * Processes all registered 'after delete' handlers for a record.\n\t * Handlers are called in registration order after the record is deleted.\n\t *\n\t * @param record - The record that was deleted\n\t * @param source - Whether the change originated from 'user' or 'remote'\n\t * @internal\n\t */\n\thandleAfterDelete(record: R, source: 'remote' | 'user') {\n\t\tif (!this._isEnabled) return\n\n\t\tconst handlers = this._afterDeleteHandlers[record.typeName] as StoreAfterDeleteHandler<R>[]\n\t\tif (handlers) {\n\t\t\tfor (const handler of handlers) {\n\t\t\t\thandler(record, source)\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Processes all registered operation complete handlers.\n\t * Called after an atomic store operation finishes.\n\t *\n\t * @param source - Whether the operation originated from 'user' or 'remote'\n\t * @internal\n\t */\n\thandleOperationComplete(source: 'remote' | 'user') {\n\t\tif (!this._isEnabled) return\n\n\t\tfor (const handler of this._operationCompleteHandlers) {\n\t\t\thandler(source)\n\t\t}\n\t}\n\n\t/**\n\t * Internal helper for registering multiple side effect handlers at once and keeping them organized.\n\t * This provides a convenient way to register handlers for multiple record types and lifecycle events\n\t * in a single call, returning a single cleanup function.\n\t *\n\t * @param handlersByType - An object mapping record type names to their respective handlers\n\t * @returns A function that removes all registered handlers when called\n\t *\n\t * @example\n\t * ```ts\n\t * const cleanup = sideEffects.register({\n\t * shape: {\n\t * afterDelete: (shape) => console.log('Shape deleted:', shape.id),\n\t * beforeChange: (prev, next) => ({ ...next, lastModified: Date.now() })\n\t * },\n\t * arrow: {\n\t * afterCreate: (arrow) => updateConnectedShapes(arrow)\n\t * }\n\t * })\n\t *\n\t * // Later, remove all handlers\n\t * cleanup()\n\t * ```\n\t *\n\t * @internal\n\t */\n\tregister(handlersByType: {\n\t\t[T in R as T['typeName']]?: {\n\t\t\tbeforeCreate?: StoreBeforeCreateHandler<T>\n\t\t\tafterCreate?: StoreAfterCreateHandler<T>\n\t\t\tbeforeChange?: StoreBeforeChangeHandler<T>\n\t\t\tafterChange?: StoreAfterChangeHandler<T>\n\t\t\tbeforeDelete?: StoreBeforeDeleteHandler<T>\n\t\t\tafterDelete?: StoreAfterDeleteHandler<T>\n\t\t}\n\t}) {\n\t\tconst disposes: (() => void)[] = []\n\t\tfor (const [type, handlers] of Object.entries(handlersByType) as any) {\n\t\t\tif (handlers?.beforeCreate) {\n\t\t\t\tdisposes.push(this.registerBeforeCreateHandler(type, handlers.beforeCreate))\n\t\t\t}\n\t\t\tif (handlers?.afterCreate) {\n\t\t\t\tdisposes.push(this.registerAfterCreateHandler(type, handlers.afterCreate))\n\t\t\t}\n\t\t\tif (handlers?.beforeChange) {\n\t\t\t\tdisposes.push(this.registerBeforeChangeHandler(type, handlers.beforeChange))\n\t\t\t}\n\t\t\tif (handlers?.afterChange) {\n\t\t\t\tdisposes.push(this.registerAfterChangeHandler(type, handlers.afterChange))\n\t\t\t}\n\t\t\tif (handlers?.beforeDelete) {\n\t\t\t\tdisposes.push(this.registerBeforeDeleteHandler(type, handlers.beforeDelete))\n\t\t\t}\n\t\t\tif (handlers?.afterDelete) {\n\t\t\t\tdisposes.push(this.registerAfterDeleteHandler(type, handlers.afterDelete))\n\t\t\t}\n\t\t}\n\t\treturn () => {\n\t\t\tfor (const dispose of disposes) dispose()\n\t\t}\n\t}\n\n\t/**\n\t * Register a handler to be called before a record of a certain type is created. Return a\n\t * modified record from the handler to change the record that will be created.\n\t *\n\t * Use this handle only to modify the creation of the record itself. If you want to trigger a\n\t * side-effect on a different record (for example, moving one shape when another is created),\n\t * use {@link StoreSideEffects.registerAfterCreateHandler} instead.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.sideEffects.registerBeforeCreateHandler('shape', (shape, source) => {\n\t * // only modify shapes created by the user\n\t * if (source !== 'user') return shape\n\t *\n\t * //by default, arrow shapes have no label. Let's make sure they always have a label.\n\t * if (shape.type === 'arrow') {\n\t * return {...shape, props: {...shape.props, text: 'an arrow'}}\n\t * }\n\t *\n\t * // other shapes get returned unmodified\n\t * return shape\n\t * })\n\t * ```\n\t *\n\t * @param typeName - The type of record to listen for\n\t * @param handler - The handler to call\n\t *\n\t * @returns A callback that removes the handler.\n\t */\n\tregisterBeforeCreateHandler<T extends R['typeName']>(\n\t\ttypeName: T,\n\t\thandler: StoreBeforeCreateHandler<R & { typeName: T }>\n\t) {\n\t\tconst handlers = this._beforeCreateHandlers[typeName] as StoreBeforeCreateHandler<any>[]\n\t\tif (!handlers) this._beforeCreateHandlers[typeName] = []\n\t\tthis._beforeCreateHandlers[typeName]!.push(handler)\n\t\treturn () => remove(this._beforeCreateHandlers[typeName]!, handler)\n\t}\n\n\t/**\n\t * Register a handler to be called after a record is created. This is useful for side-effects\n\t * that would update _other_ records. If you want to modify the record being created use\n\t * {@link StoreSideEffects.registerBeforeCreateHandler} instead.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.sideEffects.registerAfterCreateHandler('page', (page, source) => {\n\t * // Automatically create a shape when a page is created\n\t * editor.createShape<TLTextShape>({\n\t * id: createShapeId(),\n\t * type: 'text',\n\t * props: { richText: toRichText(page.name) },\n\t * })\n\t * })\n\t * ```\n\t *\n\t * @param typeName - The type of record to listen for\n\t * @param handler - The handler to call\n\t *\n\t * @returns A callback that removes the handler.\n\t */\n\tregisterAfterCreateHandler<T extends R['typeName']>(\n\t\ttypeName: T,\n\t\thandler: StoreAfterCreateHandler<R & { typeName: T }>\n\t) {\n\t\tconst handlers = this._afterCreateHandlers[typeName] as StoreAfterCreateHandler<any>[]\n\t\tif (!handlers) this._afterCreateHandlers[typeName] = []\n\t\tthis._afterCreateHandlers[typeName]!.push(handler)\n\t\treturn () => remove(this._afterCreateHandlers[typeName]!, handler)\n\t}\n\n\t/**\n\t * Register a handler to be called before a record is changed. The handler is given the old and\n\t * new record - you can return a modified record to apply a different update, or the old record\n\t * to block the update entirely.\n\t *\n\t * Use this handler only for intercepting updates to the record itself. If you want to update\n\t * other records in response to a change, use\n\t * {@link StoreSideEffects.registerAfterChangeHandler} instead.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.sideEffects.registerBeforeChangeHandler('shape', (prev, next, source) => {\n\t * if (next.isLocked && !prev.isLocked) {\n\t * // prevent shapes from ever being locked:\n\t * return prev\n\t * }\n\t * // other types of change are allowed\n\t * return next\n\t * })\n\t * ```\n\t *\n\t * @param typeName - The type of record to listen for\n\t * @param handler - The handler to call\n\t *\n\t * @returns A callback that removes the handler.\n\t */\n\tregisterBeforeChangeHandler<T extends R['typeName']>(\n\t\ttypeName: T,\n\t\thandler: StoreBeforeChangeHandler<R & { typeName: T }>\n\t) {\n\t\tconst handlers = this._beforeChangeHandlers[typeName] as StoreBeforeChangeHandler<any>[]\n\t\tif (!handlers) this._beforeChangeHandlers[typeName] = []\n\t\tthis._beforeChangeHandlers[typeName]!.push(handler)\n\t\treturn () => remove(this._beforeChangeHandlers[typeName]!, handler)\n\t}\n\n\t/**\n\t * Register a handler to be called after a record is changed. This is useful for side-effects\n\t * that would update _other_ records - if you want to modify the record being changed, use\n\t * {@link StoreSideEffects.registerBeforeChangeHandler} instead.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.sideEffects.registerAfterChangeHandler('shape', (prev, next, source) => {\n\t * if (next.props.color === 'red') {\n\t * // there can only be one red shape at a time:\n\t * const otherRedShapes = editor.getCurrentPageShapes().filter(s => s.props.color === 'red' && s.id !== next.id)\n\t * editor.updateShapes(otherRedShapes.map(s => ({...s, props: {...s.props, color: 'blue'}})))\n\t * }\n\t * })\n\t * ```\n\t *\n\t * @param typeName - The type of record to listen for\n\t * @param handler - The handler to call\n\t *\n\t * @returns A callback that removes the handler.\n\t */\n\tregisterAfterChangeHandler<T extends R['typeName']>(\n\t\ttypeName: T,\n\t\thandler: StoreAfterChangeHandler<R & { typeName: T }>\n\t) {\n\t\tconst handlers = this._afterChangeHandlers[typeName] as StoreAfterChangeHandler<any>[]\n\t\tif (!handlers) this._afterChangeHandlers[typeName] = []\n\t\tthis._afterChangeHandlers[typeName]!.push(handler as StoreAfterChangeHandler<any>)\n\t\treturn () => remove(this._afterChangeHandlers[typeName]!, handler)\n\t}\n\n\t/**\n\t * Register a handler to be called before a record is deleted. The handler can return `false` to\n\t * prevent the deletion.\n\t *\n\t * Use this handler only for intercepting deletions of the record itself. If you want to do\n\t * something to other records in response to a deletion, use\n\t * {@link StoreSideEffects.registerAfterDeleteHandler} instead.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.sideEffects.registerBeforeDeleteHandler('shape', (shape, source) => {\n\t * if (shape.props.color === 'red') {\n\t * // prevent red shapes from being deleted\n\t * \t return false\n\t * }\n\t * })\n\t * ```\n\t *\n\t * @param typeName - The type of record to listen for\n\t * @param handler - The handler to call\n\t *\n\t * @returns A callback that removes the handler.\n\t */\n\tregisterBeforeDeleteHandler<T extends R['typeName']>(\n\t\ttypeName: T,\n\t\thandler: StoreBeforeDeleteHandler<R & { typeName: T }>\n\t) {\n\t\tconst handlers = this._beforeDeleteHandlers[typeName] as StoreBeforeDeleteHandler<any>[]\n\t\tif (!handlers) this._beforeDeleteHandlers[typeName] = []\n\t\tthis._beforeDeleteHandlers[typeName]!.push(handler as StoreBeforeDeleteHandler<any>)\n\t\treturn () => remove(this._beforeDeleteHandlers[typeName]!, handler)\n\t}\n\n\t/**\n\t * Register a handler to be called after a record is deleted. This is useful for side-effects\n\t * that would update _other_ records - if you want to block the deletion of the record itself,\n\t * use {@link StoreSideEffects.registerBeforeDeleteHandler} instead.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.sideEffects.registerAfterDeleteHandler('shape', (shape, source) => {\n\t * // if the last shape in a frame is deleted, delete the frame too:\n\t * const parentFrame = editor.getShape(shape.parentId)\n\t * if (!parentFrame || parentFrame.type !== 'frame') return\n\t *\n\t * const siblings = editor.getSortedChildIdsForParent(parentFrame)\n\t * if (siblings.length === 0) {\n\t * editor.deleteShape(parentFrame.id)\n\t * }\n\t * })\n\t * ```\n\t *\n\t * @param typeName - The type of record to listen for\n\t * @param handler - The handler to call\n\t *\n\t * @returns A callback that removes the handler.\n\t */\n\tregisterAfterDeleteHandler<T extends R['typeName']>(\n\t\ttypeName: T,\n\t\thandler: StoreAfterDeleteHandler<R & { typeName: T }>\n\t) {\n\t\tconst handlers = this._afterDeleteHandlers[typeName] as StoreAfterDeleteHandler<any>[]\n\t\tif (!handlers) this._afterDeleteHandlers[typeName] = []\n\t\tthis._afterDeleteHandlers[typeName]!.push(handler as StoreAfterDeleteHandler<any>)\n\t\treturn () => remove(this._afterDeleteHandlers[typeName]!, handler)\n\t}\n\n\t/**\n\t * Register a handler to be called when a store completes an atomic operation.\n\t *\n\t * @example\n\t * ```ts\n\t * let count = 0\n\t *\n\t * editor.sideEffects.registerOperationCompleteHandler(() => count++)\n\t *\n\t * editor.selectAll()\n\t * expect(count).toBe(1)\n\t *\n\t * editor.store.atomic(() => {\n\t *\teditor.selectNone()\n\t * \teditor.selectAll()\n\t * })\n\t *\n\t * expect(count).toBe(2)\n\t * ```\n\t *\n\t * @param handler - The handler to call\n\t *\n\t * @returns A callback that removes the handler.\n\t *\n\t * @public\n\t */\n\tregisterOperationCompleteHandler(handler: StoreOperationCompleteHandler) {\n\t\tthis._operationCompleteHandlers.push(handler)\n\t\treturn () => remove(this._operationCompleteHandlers, handler)\n\t}\n}\n\nfunction remove(array: any[], item: any) {\n\tconst index = array.indexOf(item)\n\tif (index >= 0) {\n\t\tarray.splice(index, 1)\n\t}\n}\n"],
|
|
4
|
+
"sourcesContent": ["import { UnknownRecord } from './BaseRecord'\nimport { Store } from './Store'\n\n/**\n * Handler function called before a record is created in the store.\n * The handler receives the record to be created and can return a modified version.\n * Use this to validate, transform, or modify records before they are added to the store.\n *\n * @param record - The record about to be created\n * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization\n * @returns The record to actually create (may be modified)\n *\n * @example\n * ```ts\n * const handler: StoreBeforeCreateHandler<MyRecord> = (record, source) => {\n * // Ensure all user-created records have a timestamp\n * if (source === 'user' && !record.createdAt) {\n * return { ...record, createdAt: Date.now() }\n * }\n * return record\n * }\n * ```\n *\n * @public\n */\nexport type StoreBeforeCreateHandler<R extends UnknownRecord> = (\n\trecord: R,\n\tsource: 'remote' | 'user'\n) => R\n/**\n * Handler function called after a record has been successfully created in the store.\n * Use this for side effects that should happen after record creation, such as updating\n * related records or triggering notifications.\n *\n * @param record - The record that was created\n * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization\n *\n * @example\n * ```ts\n * const handler: StoreAfterCreateHandler<BookRecord> = (book, source) => {\n * if (source === 'user') {\n * console.log(`New book added: ${book.title}`)\n * updateAuthorBookCount(book.authorId)\n * }\n * }\n * ```\n *\n * @public\n */\nexport type StoreAfterCreateHandler<R extends UnknownRecord> = (\n\trecord: R,\n\tsource: 'remote' | 'user'\n) => void\n/**\n * Handler function called before a record is updated in the store.\n * The handler receives the current and new versions of the record and can return\n * a modified version or the original to prevent the change.\n *\n * @param prev - The current version of the record in the store\n * @param next - The proposed new version of the record\n * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization\n * @returns The record version to actually store (may be modified or the original to block change)\n *\n * @example\n * ```ts\n * const handler: StoreBeforeChangeHandler<ShapeRecord> = (prev, next, source) => {\n * // Prevent shapes from being moved outside the canvas bounds\n * if (next.x < 0 || next.y < 0) {\n * return prev // Block the change\n * }\n * return next\n * }\n * ```\n *\n * @public\n */\nexport type StoreBeforeChangeHandler<R extends UnknownRecord> = (\n\tprev: R,\n\tnext: R,\n\tsource: 'remote' | 'user'\n) => R\n/**\n * Handler function called after a record has been successfully updated in the store.\n * Use this for side effects that should happen after record changes, such as\n * updating related records or maintaining consistency constraints.\n *\n * @param prev - The previous version of the record\n * @param next - The new version of the record that was stored\n * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization\n *\n * @example\n * ```ts\n * const handler: StoreAfterChangeHandler<ShapeRecord> = (prev, next, source) => {\n * // Update connected arrows when a shape moves\n * if (prev.x !== next.x || prev.y !== next.y) {\n * updateConnectedArrows(next.id)\n * }\n * }\n * ```\n *\n * @public\n */\nexport type StoreAfterChangeHandler<R extends UnknownRecord> = (\n\tprev: R,\n\tnext: R,\n\tsource: 'remote' | 'user'\n) => void\n/**\n * Handler function called before a record is deleted from the store.\n * The handler can return `false` to prevent the deletion from occurring.\n *\n * @param record - The record about to be deleted\n * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization\n * @returns `false` to prevent deletion, `void` or any other value to allow it\n *\n * @example\n * ```ts\n * const handler: StoreBeforeDeleteHandler<BookRecord> = (book, source) => {\n * // Prevent deletion of books that are currently checked out\n * if (book.isCheckedOut) {\n * console.warn('Cannot delete checked out book')\n * return false\n * }\n * // Allow deletion for other books\n * }\n * ```\n *\n * @public\n */\nexport type StoreBeforeDeleteHandler<R extends UnknownRecord> = (\n\trecord: R,\n\tsource: 'remote' | 'user'\n) => void | false\n/**\n * Handler function called after a record has been successfully deleted from the store.\n * Use this for cleanup operations and maintaining referential integrity.\n *\n * @param record - The record that was deleted\n * @param source - Whether the change originated from 'user' interaction or 'remote' synchronization\n *\n * @example\n * ```ts\n * const handler: StoreAfterDeleteHandler<ShapeRecord> = (shape, source) => {\n * // Clean up arrows that were connected to this shape\n * const connectedArrows = findArrowsConnectedTo(shape.id)\n * store.remove(connectedArrows.map(arrow => arrow.id))\n * }\n * ```\n *\n * @public\n */\nexport type StoreAfterDeleteHandler<R extends UnknownRecord> = (\n\trecord: R,\n\tsource: 'remote' | 'user'\n) => void\n\n/**\n * Handler function called when a store operation (atomic transaction) completes.\n * This is useful for performing actions after a batch of changes has been applied,\n * such as triggering saves or sending notifications.\n *\n * @param source - Whether the operation originated from 'user' interaction or 'remote' synchronization\n *\n * @example\n * ```ts\n * const handler: StoreOperationCompleteHandler = (source) => {\n * if (source === 'user') {\n * // Auto-save after user operations complete\n * saveStoreSnapshot()\n * }\n * }\n * ```\n *\n * @public\n */\nexport type StoreOperationCompleteHandler = (source: 'remote' | 'user') => void\n\n/**\n * The side effect manager (aka a \"correct state enforcer\") is responsible\n * for making sure that the store's state is always correct and consistent. This includes\n * things like: deleting a shape if its parent is deleted; unbinding\n * arrows when their binding target is deleted; maintaining referential integrity; etc.\n *\n * Side effects are organized into lifecycle hooks that run before and after\n * record operations (create, change, delete), allowing you to validate data,\n * transform records, and maintain business rules.\n *\n * @example\n * ```ts\n * const sideEffects = new StoreSideEffects(store)\n *\n * // Ensure arrows are deleted when their target shape is deleted\n * sideEffects.registerAfterDeleteHandler('shape', (shape) => {\n * const arrows = store.query.records('arrow', () => ({\n * toId: { eq: shape.id }\n * })).get()\n * store.remove(arrows.map(arrow => arrow.id))\n * })\n * ```\n *\n * @public\n */\nexport class StoreSideEffects<R extends UnknownRecord> {\n\t/**\n\t * Creates a new side effects manager for the given store.\n\t *\n\t * store - The store instance to manage side effects for\n\t */\n\tconstructor(private readonly store: Store<R>) {}\n\n\tprivate _beforeCreateHandlers: { [K in string]?: StoreBeforeCreateHandler<any>[] } = {}\n\tprivate _afterCreateHandlers: { [K in string]?: StoreAfterCreateHandler<any>[] } = {}\n\tprivate _beforeChangeHandlers: { [K in string]?: StoreBeforeChangeHandler<any>[] } = {}\n\tprivate _afterChangeHandlers: { [K in string]?: StoreAfterChangeHandler<any>[] } = {}\n\tprivate _beforeDeleteHandlers: { [K in string]?: StoreBeforeDeleteHandler<any>[] } = {}\n\tprivate _afterDeleteHandlers: { [K in string]?: StoreAfterDeleteHandler<any>[] } = {}\n\tprivate _operationCompleteHandlers: StoreOperationCompleteHandler[] = []\n\n\tprivate _isEnabled = true\n\t/**\n\t * Checks whether side effects are currently enabled.\n\t * When disabled, all side effect handlers are bypassed.\n\t *\n\t * @returns `true` if side effects are enabled, `false` otherwise\n\t * @internal\n\t */\n\tisEnabled() {\n\t\treturn this._isEnabled\n\t}\n\t/**\n\t * Enables or disables side effects processing.\n\t * When disabled, no side effect handlers will be called.\n\t *\n\t * @param enabled - Whether to enable or disable side effects\n\t * @internal\n\t */\n\tsetIsEnabled(enabled: boolean) {\n\t\tthis._isEnabled = enabled\n\t}\n\n\t/**\n\t * Processes all registered 'before create' handlers for a record.\n\t * Handlers are called in registration order and can transform the record.\n\t *\n\t * @param record - The record about to be created\n\t * @param source - Whether the change originated from 'user' or 'remote'\n\t * @returns The potentially modified record to actually create\n\t * @internal\n\t */\n\thandleBeforeCreate(record: R, source: 'remote' | 'user') {\n\t\tif (!this._isEnabled) return record\n\n\t\tconst handlers = this._beforeCreateHandlers[record.typeName] as StoreBeforeCreateHandler<R>[]\n\t\tif (handlers) {\n\t\t\tlet r = record\n\t\t\tfor (const handler of handlers) {\n\t\t\t\tr = handler(r, source)\n\t\t\t}\n\t\t\treturn r\n\t\t}\n\n\t\treturn record\n\t}\n\n\t/**\n\t * Processes all registered 'after create' handlers for a record.\n\t * Handlers are called in registration order after the record is created.\n\t *\n\t * @param record - The record that was created\n\t * @param source - Whether the change originated from 'user' or 'remote'\n\t * @internal\n\t */\n\thandleAfterCreate(record: R, source: 'remote' | 'user') {\n\t\tif (!this._isEnabled) return\n\n\t\tconst handlers = this._afterCreateHandlers[record.typeName] as StoreAfterCreateHandler<R>[]\n\t\tif (handlers) {\n\t\t\tfor (const handler of handlers) {\n\t\t\t\thandler(record, source)\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Processes all registered 'before change' handlers for a record.\n\t * Handlers are called in registration order and can modify or block the change.\n\t *\n\t * @param prev - The current version of the record\n\t * @param next - The proposed new version of the record\n\t * @param source - Whether the change originated from 'user' or 'remote'\n\t * @returns The potentially modified record to actually store\n\t * @internal\n\t */\n\thandleBeforeChange(prev: R, next: R, source: 'remote' | 'user') {\n\t\tif (!this._isEnabled) return next\n\n\t\tconst handlers = this._beforeChangeHandlers[next.typeName] as StoreBeforeChangeHandler<R>[]\n\t\tif (handlers) {\n\t\t\tlet r = next\n\t\t\tfor (const handler of handlers) {\n\t\t\t\tr = handler(prev, r, source)\n\t\t\t}\n\t\t\treturn r\n\t\t}\n\n\t\treturn next\n\t}\n\n\t/**\n\t * Processes all registered 'after change' handlers for a record.\n\t * Handlers are called in registration order after the record is updated.\n\t *\n\t * @param prev - The previous version of the record\n\t * @param next - The new version of the record that was stored\n\t * @param source - Whether the change originated from 'user' or 'remote'\n\t * @internal\n\t */\n\thandleAfterChange(prev: R, next: R, source: 'remote' | 'user') {\n\t\tif (!this._isEnabled) return\n\n\t\tconst handlers = this._afterChangeHandlers[next.typeName] as StoreAfterChangeHandler<R>[]\n\t\tif (handlers) {\n\t\t\tfor (const handler of handlers) {\n\t\t\t\thandler(prev, next, source)\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Processes all registered 'before delete' handlers for a record.\n\t * If any handler returns `false`, the deletion is prevented.\n\t *\n\t * @param record - The record about to be deleted\n\t * @param source - Whether the change originated from 'user' or 'remote'\n\t * @returns `true` to allow deletion, `false` to prevent it\n\t * @internal\n\t */\n\thandleBeforeDelete(record: R, source: 'remote' | 'user') {\n\t\tif (!this._isEnabled) return true\n\n\t\tconst handlers = this._beforeDeleteHandlers[record.typeName] as StoreBeforeDeleteHandler<R>[]\n\t\tif (handlers) {\n\t\t\tfor (const handler of handlers) {\n\t\t\t\tif (handler(record, source) === false) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\n\t/**\n\t * Processes all registered 'after delete' handlers for a record.\n\t * Handlers are called in registration order after the record is deleted.\n\t *\n\t * @param record - The record that was deleted\n\t * @param source - Whether the change originated from 'user' or 'remote'\n\t * @internal\n\t */\n\thandleAfterDelete(record: R, source: 'remote' | 'user') {\n\t\tif (!this._isEnabled) return\n\n\t\tconst handlers = this._afterDeleteHandlers[record.typeName] as StoreAfterDeleteHandler<R>[]\n\t\tif (handlers) {\n\t\t\tfor (const handler of handlers) {\n\t\t\t\thandler(record, source)\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Processes all registered operation complete handlers.\n\t * Called after an atomic store operation finishes.\n\t *\n\t * @param source - Whether the operation originated from 'user' or 'remote'\n\t * @internal\n\t */\n\thandleOperationComplete(source: 'remote' | 'user') {\n\t\tif (!this._isEnabled) return\n\n\t\tfor (const handler of this._operationCompleteHandlers) {\n\t\t\thandler(source)\n\t\t}\n\t}\n\n\t/**\n\t * Internal helper for registering multiple side effect handlers at once and keeping them organized.\n\t * This provides a convenient way to register handlers for multiple record types and lifecycle events\n\t * in a single call, returning a single cleanup function.\n\t *\n\t * @param handlersByType - An object mapping record type names to their respective handlers\n\t * @returns A function that removes all registered handlers when called\n\t *\n\t * @example\n\t * ```ts\n\t * const cleanup = sideEffects.register({\n\t * shape: {\n\t * afterDelete: (shape) => console.log('Shape deleted:', shape.id),\n\t * beforeChange: (prev, next) => ({ ...next, lastModified: Date.now() })\n\t * },\n\t * arrow: {\n\t * afterCreate: (arrow) => updateConnectedShapes(arrow)\n\t * }\n\t * })\n\t *\n\t * // Later, remove all handlers\n\t * cleanup()\n\t * ```\n\t *\n\t * @internal\n\t */\n\tregister(handlersByType: {\n\t\t[T in R as T['typeName']]?: {\n\t\t\tbeforeCreate?: StoreBeforeCreateHandler<T>\n\t\t\tafterCreate?: StoreAfterCreateHandler<T>\n\t\t\tbeforeChange?: StoreBeforeChangeHandler<T>\n\t\t\tafterChange?: StoreAfterChangeHandler<T>\n\t\t\tbeforeDelete?: StoreBeforeDeleteHandler<T>\n\t\t\tafterDelete?: StoreAfterDeleteHandler<T>\n\t\t}\n\t}) {\n\t\tconst disposes: (() => void)[] = []\n\t\tfor (const [type, handlers] of Object.entries(handlersByType) as any) {\n\t\t\tif (handlers?.beforeCreate) {\n\t\t\t\tdisposes.push(this.registerBeforeCreateHandler(type, handlers.beforeCreate))\n\t\t\t}\n\t\t\tif (handlers?.afterCreate) {\n\t\t\t\tdisposes.push(this.registerAfterCreateHandler(type, handlers.afterCreate))\n\t\t\t}\n\t\t\tif (handlers?.beforeChange) {\n\t\t\t\tdisposes.push(this.registerBeforeChangeHandler(type, handlers.beforeChange))\n\t\t\t}\n\t\t\tif (handlers?.afterChange) {\n\t\t\t\tdisposes.push(this.registerAfterChangeHandler(type, handlers.afterChange))\n\t\t\t}\n\t\t\tif (handlers?.beforeDelete) {\n\t\t\t\tdisposes.push(this.registerBeforeDeleteHandler(type, handlers.beforeDelete))\n\t\t\t}\n\t\t\tif (handlers?.afterDelete) {\n\t\t\t\tdisposes.push(this.registerAfterDeleteHandler(type, handlers.afterDelete))\n\t\t\t}\n\t\t}\n\t\treturn () => {\n\t\t\tfor (const dispose of disposes) dispose()\n\t\t}\n\t}\n\n\t/**\n\t * Register a handler to be called before a record of a certain type is created. Return a\n\t * modified record from the handler to change the record that will be created.\n\t *\n\t * Use this handle only to modify the creation of the record itself. If you want to trigger a\n\t * side-effect on a different record (for example, moving one shape when another is created),\n\t * use {@link StoreSideEffects.registerAfterCreateHandler} instead.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.sideEffects.registerBeforeCreateHandler('shape', (shape, source) => {\n\t * // only modify shapes created by the user\n\t * if (source !== 'user') return shape\n\t *\n\t * //by default, arrow shapes have no label. Let's make sure they always have a label.\n\t * if (shape.type === 'arrow') {\n\t * return {...shape, props: {...shape.props, text: 'an arrow'}}\n\t * }\n\t *\n\t * // other shapes get returned unmodified\n\t * return shape\n\t * })\n\t * ```\n\t *\n\t * @param typeName - The type of record to listen for\n\t * @param handler - The handler to call\n\t *\n\t * @returns A callback that removes the handler.\n\t */\n\tregisterBeforeCreateHandler<T extends R['typeName']>(\n\t\ttypeName: T,\n\t\thandler: StoreBeforeCreateHandler<R & { typeName: T }>\n\t) {\n\t\tconst handlers = this._beforeCreateHandlers[typeName] as StoreBeforeCreateHandler<any>[]\n\t\tif (!handlers) this._beforeCreateHandlers[typeName] = []\n\t\tthis._beforeCreateHandlers[typeName]!.push(handler)\n\t\treturn () => remove(this._beforeCreateHandlers[typeName]!, handler)\n\t}\n\n\t/**\n\t * Register a handler to be called after a record is created. This is useful for side-effects\n\t * that would update _other_ records. If you want to modify the record being created use\n\t * {@link StoreSideEffects.registerBeforeCreateHandler} instead.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.sideEffects.registerAfterCreateHandler('page', (page, source) => {\n\t * // Automatically create a shape when a page is created\n\t * editor.createShape({\n\t * id: createShapeId(),\n\t * type: 'text',\n\t * props: { richText: toRichText(page.name) },\n\t * })\n\t * })\n\t * ```\n\t *\n\t * @param typeName - The type of record to listen for\n\t * @param handler - The handler to call\n\t *\n\t * @returns A callback that removes the handler.\n\t */\n\tregisterAfterCreateHandler<T extends R['typeName']>(\n\t\ttypeName: T,\n\t\thandler: StoreAfterCreateHandler<R & { typeName: T }>\n\t) {\n\t\tconst handlers = this._afterCreateHandlers[typeName] as StoreAfterCreateHandler<any>[]\n\t\tif (!handlers) this._afterCreateHandlers[typeName] = []\n\t\tthis._afterCreateHandlers[typeName]!.push(handler)\n\t\treturn () => remove(this._afterCreateHandlers[typeName]!, handler)\n\t}\n\n\t/**\n\t * Register a handler to be called before a record is changed. The handler is given the old and\n\t * new record - you can return a modified record to apply a different update, or the old record\n\t * to block the update entirely.\n\t *\n\t * Use this handler only for intercepting updates to the record itself. If you want to update\n\t * other records in response to a change, use\n\t * {@link StoreSideEffects.registerAfterChangeHandler} instead.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.sideEffects.registerBeforeChangeHandler('shape', (prev, next, source) => {\n\t * if (next.isLocked && !prev.isLocked) {\n\t * // prevent shapes from ever being locked:\n\t * return prev\n\t * }\n\t * // other types of change are allowed\n\t * return next\n\t * })\n\t * ```\n\t *\n\t * @param typeName - The type of record to listen for\n\t * @param handler - The handler to call\n\t *\n\t * @returns A callback that removes the handler.\n\t */\n\tregisterBeforeChangeHandler<T extends R['typeName']>(\n\t\ttypeName: T,\n\t\thandler: StoreBeforeChangeHandler<R & { typeName: T }>\n\t) {\n\t\tconst handlers = this._beforeChangeHandlers[typeName] as StoreBeforeChangeHandler<any>[]\n\t\tif (!handlers) this._beforeChangeHandlers[typeName] = []\n\t\tthis._beforeChangeHandlers[typeName]!.push(handler)\n\t\treturn () => remove(this._beforeChangeHandlers[typeName]!, handler)\n\t}\n\n\t/**\n\t * Register a handler to be called after a record is changed. This is useful for side-effects\n\t * that would update _other_ records - if you want to modify the record being changed, use\n\t * {@link StoreSideEffects.registerBeforeChangeHandler} instead.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.sideEffects.registerAfterChangeHandler('shape', (prev, next, source) => {\n\t * if (next.props.color === 'red') {\n\t * // there can only be one red shape at a time:\n\t * const otherRedShapes = editor.getCurrentPageShapes().filter(s => s.props.color === 'red' && s.id !== next.id)\n\t * editor.updateShapes(otherRedShapes.map(s => ({...s, props: {...s.props, color: 'blue'}})))\n\t * }\n\t * })\n\t * ```\n\t *\n\t * @param typeName - The type of record to listen for\n\t * @param handler - The handler to call\n\t *\n\t * @returns A callback that removes the handler.\n\t */\n\tregisterAfterChangeHandler<T extends R['typeName']>(\n\t\ttypeName: T,\n\t\thandler: StoreAfterChangeHandler<R & { typeName: T }>\n\t) {\n\t\tconst handlers = this._afterChangeHandlers[typeName] as StoreAfterChangeHandler<any>[]\n\t\tif (!handlers) this._afterChangeHandlers[typeName] = []\n\t\tthis._afterChangeHandlers[typeName]!.push(handler as StoreAfterChangeHandler<any>)\n\t\treturn () => remove(this._afterChangeHandlers[typeName]!, handler)\n\t}\n\n\t/**\n\t * Register a handler to be called before a record is deleted. The handler can return `false` to\n\t * prevent the deletion.\n\t *\n\t * Use this handler only for intercepting deletions of the record itself. If you want to do\n\t * something to other records in response to a deletion, use\n\t * {@link StoreSideEffects.registerAfterDeleteHandler} instead.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.sideEffects.registerBeforeDeleteHandler('shape', (shape, source) => {\n\t * if (shape.props.color === 'red') {\n\t * // prevent red shapes from being deleted\n\t * \t return false\n\t * }\n\t * })\n\t * ```\n\t *\n\t * @param typeName - The type of record to listen for\n\t * @param handler - The handler to call\n\t *\n\t * @returns A callback that removes the handler.\n\t */\n\tregisterBeforeDeleteHandler<T extends R['typeName']>(\n\t\ttypeName: T,\n\t\thandler: StoreBeforeDeleteHandler<R & { typeName: T }>\n\t) {\n\t\tconst handlers = this._beforeDeleteHandlers[typeName] as StoreBeforeDeleteHandler<any>[]\n\t\tif (!handlers) this._beforeDeleteHandlers[typeName] = []\n\t\tthis._beforeDeleteHandlers[typeName]!.push(handler as StoreBeforeDeleteHandler<any>)\n\t\treturn () => remove(this._beforeDeleteHandlers[typeName]!, handler)\n\t}\n\n\t/**\n\t * Register a handler to be called after a record is deleted. This is useful for side-effects\n\t * that would update _other_ records - if you want to block the deletion of the record itself,\n\t * use {@link StoreSideEffects.registerBeforeDeleteHandler} instead.\n\t *\n\t * @example\n\t * ```ts\n\t * editor.sideEffects.registerAfterDeleteHandler('shape', (shape, source) => {\n\t * // if the last shape in a frame is deleted, delete the frame too:\n\t * const parentFrame = editor.getShape(shape.parentId)\n\t * if (!parentFrame || parentFrame.type !== 'frame') return\n\t *\n\t * const siblings = editor.getSortedChildIdsForParent(parentFrame)\n\t * if (siblings.length === 0) {\n\t * editor.deleteShape(parentFrame.id)\n\t * }\n\t * })\n\t * ```\n\t *\n\t * @param typeName - The type of record to listen for\n\t * @param handler - The handler to call\n\t *\n\t * @returns A callback that removes the handler.\n\t */\n\tregisterAfterDeleteHandler<T extends R['typeName']>(\n\t\ttypeName: T,\n\t\thandler: StoreAfterDeleteHandler<R & { typeName: T }>\n\t) {\n\t\tconst handlers = this._afterDeleteHandlers[typeName] as StoreAfterDeleteHandler<any>[]\n\t\tif (!handlers) this._afterDeleteHandlers[typeName] = []\n\t\tthis._afterDeleteHandlers[typeName]!.push(handler as StoreAfterDeleteHandler<any>)\n\t\treturn () => remove(this._afterDeleteHandlers[typeName]!, handler)\n\t}\n\n\t/**\n\t * Register a handler to be called when a store completes an atomic operation.\n\t *\n\t * @example\n\t * ```ts\n\t * let count = 0\n\t *\n\t * editor.sideEffects.registerOperationCompleteHandler(() => count++)\n\t *\n\t * editor.selectAll()\n\t * expect(count).toBe(1)\n\t *\n\t * editor.store.atomic(() => {\n\t *\teditor.selectNone()\n\t * \teditor.selectAll()\n\t * })\n\t *\n\t * expect(count).toBe(2)\n\t * ```\n\t *\n\t * @param handler - The handler to call\n\t *\n\t * @returns A callback that removes the handler.\n\t *\n\t * @public\n\t */\n\tregisterOperationCompleteHandler(handler: StoreOperationCompleteHandler) {\n\t\tthis._operationCompleteHandlers.push(handler)\n\t\treturn () => remove(this._operationCompleteHandlers, handler)\n\t}\n}\n\nfunction remove(array: any[], item: any) {\n\tconst index = array.indexOf(item)\n\tif (index >= 0) {\n\t\tarray.splice(index, 1)\n\t}\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { STRUCTURED_CLONE_OBJECT_PROTOTYPE } from "@tldraw/utils";
|
|
2
|
+
import { isDev } from "./isDev.mjs";
|
|
2
3
|
function devFreeze(object) {
|
|
3
|
-
if (
|
|
4
|
-
return object;
|
|
5
|
-
}
|
|
4
|
+
if (!isDev()) return object;
|
|
6
5
|
const proto = Object.getPrototypeOf(object);
|
|
7
6
|
if (proto && !(Array.isArray(object) || proto === Object.prototype || proto === null || proto === STRUCTURED_CLONE_OBJECT_PROTOTYPE)) {
|
|
8
7
|
console.error("cannot include non-js data in a record", object);
|
|
9
8
|
throw new Error("cannot include non-js data in a record");
|
|
10
9
|
}
|
|
10
|
+
if (Object.isFrozen(object)) {
|
|
11
|
+
return object;
|
|
12
|
+
}
|
|
11
13
|
const propNames = Object.getOwnPropertyNames(object);
|
|
12
14
|
for (const name of propNames) {
|
|
13
15
|
const value = object[name];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/devFreeze.ts"],
|
|
4
|
-
"sourcesContent": ["import { STRUCTURED_CLONE_OBJECT_PROTOTYPE } from '@tldraw/utils'\n\n/**\n * Freeze an object when in development mode. Copied from\n * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze\n *\n * @example\n *\n * ```ts\n * const frozen = devFreeze({ a: 1 })\n * ```\n *\n * @param object - The object to freeze.\n * @returns The frozen object when in development mode, or else the object when in other modes.\n * @public\n */\nexport function devFreeze<T>(object: T): T {\n\tif (
|
|
5
|
-
"mappings": "AAAA,SAAS,yCAAyC;
|
|
4
|
+
"sourcesContent": ["import { STRUCTURED_CLONE_OBJECT_PROTOTYPE } from '@tldraw/utils'\nimport { isDev } from './isDev'\n\n/**\n * Freeze an object when in development mode. Copied from\n * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze\n *\n * @example\n *\n * ```ts\n * const frozen = devFreeze({ a: 1 })\n * ```\n *\n * @param object - The object to freeze.\n * @returns The frozen object when in development mode, or else the object when in other modes.\n * @public\n */\nexport function devFreeze<T>(object: T): T {\n\tif (!isDev()) return object\n\n\tconst proto = Object.getPrototypeOf(object)\n\tif (\n\t\tproto &&\n\t\t!(\n\t\t\tArray.isArray(object) ||\n\t\t\tproto === Object.prototype ||\n\t\t\tproto === null ||\n\t\t\tproto === STRUCTURED_CLONE_OBJECT_PROTOTYPE\n\t\t)\n\t) {\n\t\tconsole.error('cannot include non-js data in a record', object)\n\t\tthrow new Error('cannot include non-js data in a record')\n\t}\n\n\tif (Object.isFrozen(object)) {\n\t\treturn object\n\t}\n\n\t// Retrieve the property names defined on object\n\tconst propNames = Object.getOwnPropertyNames(object)\n\n\t// Recursively freeze properties before freezing self\n\tfor (const name of propNames) {\n\t\tconst value = (object as any)[name]\n\n\t\tif (value && typeof value === 'object') {\n\t\t\tdevFreeze(value)\n\t\t}\n\t}\n\n\treturn Object.freeze(object)\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,yCAAyC;AAClD,SAAS,aAAa;AAgBf,SAAS,UAAa,QAAc;AAC1C,MAAI,CAAC,MAAM,EAAG,QAAO;AAErB,QAAM,QAAQ,OAAO,eAAe,MAAM;AAC1C,MACC,SACA,EACC,MAAM,QAAQ,MAAM,KACpB,UAAU,OAAO,aACjB,UAAU,QACV,UAAU,oCAEV;AACD,YAAQ,MAAM,0CAA0C,MAAM;AAC9D,UAAM,IAAI,MAAM,wCAAwC;AAAA,EACzD;AAEA,MAAI,OAAO,SAAS,MAAM,GAAG;AAC5B,WAAO;AAAA,EACR;AAGA,QAAM,YAAY,OAAO,oBAAoB,MAAM;AAGnD,aAAW,QAAQ,WAAW;AAC7B,UAAM,QAAS,OAAe,IAAI;AAElC,QAAI,SAAS,OAAO,UAAU,UAAU;AACvC,gBAAU,KAAK;AAAA,IAChB;AAAA,EACD;AAEA,SAAO,OAAO,OAAO,MAAM;AAC5B;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
let _isDev = false;
|
|
2
|
+
try {
|
|
3
|
+
_isDev = process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";
|
|
4
|
+
} catch (_e) {
|
|
5
|
+
}
|
|
6
|
+
try {
|
|
7
|
+
_isDev = _isDev || import.meta.env.DEV || import.meta.env.TEST || import.meta.env.MODE === "development" || import.meta.env.MODE === "test";
|
|
8
|
+
} catch (_e) {
|
|
9
|
+
}
|
|
10
|
+
function isDev() {
|
|
11
|
+
return _isDev;
|
|
12
|
+
}
|
|
13
|
+
export {
|
|
14
|
+
isDev
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=isDev.mjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/lib/isDev.ts"],
|
|
4
|
+
"sourcesContent": ["let _isDev = false\ntry {\n\t_isDev = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'\n} catch (_e) {\n\t/* noop */\n}\ntry {\n\t_isDev =\n\t\t_isDev ||\n\t\t(import.meta as any).env.DEV ||\n\t\t(import.meta as any).env.TEST ||\n\t\t(import.meta as any).env.MODE === 'development' ||\n\t\t(import.meta as any).env.MODE === 'test'\n} catch (_e) {\n\t/* noop */\n}\n\nexport function isDev() {\n\treturn _isDev\n}\n"],
|
|
5
|
+
"mappings": "AAAA,IAAI,SAAS;AACb,IAAI;AACH,WAAS,QAAQ,IAAI,aAAa,iBAAiB,QAAQ,IAAI,aAAa;AAC7E,SAAS,IAAI;AAEb;AACA,IAAI;AACH,WACC,UACC,YAAoB,IAAI,OACxB,YAAoB,IAAI,QACxB,YAAoB,IAAI,SAAS,iBACjC,YAAoB,IAAI,SAAS;AACpC,SAAS,IAAI;AAEb;AAEO,SAAS,QAAQ;AACvB,SAAO;AACR;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -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'\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 @typescript-eslint/method-signature-style\n\tup: (oldState: Before) => After\n\t// eslint-disable-next-line @typescript-eslint/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 @typescript-eslint/method-signature-style\n\t\t\treadonly filter?: (record: UnknownRecord) => boolean\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\treadonly up: (oldState: UnknownRecord) => void | UnknownRecord\n\t\t\t// eslint-disable-next-line @typescript-eslint/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 @typescript-eslint/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 @typescript-eslint/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)\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/**\n * Enumeration of possible reasons why a migration might fail.\n *\n * These reasons help identify what went wrong during migration processing,\n * allowing applications to handle different failure scenarios appropriately.\n * Common failures include incompatible data formats, unknown record types,\n * and version mismatches between the data and available migrations.\n * @public\n */\nexport enum 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}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,QAAQ,wBAAwB;
|
|
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 @typescript-eslint/method-signature-style\n\tup: (oldState: Before) => After\n\t// eslint-disable-next-line @typescript-eslint/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 @typescript-eslint/method-signature-style\n\t\t\treadonly filter?: (record: UnknownRecord) => boolean\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\treadonly up: (oldState: UnknownRecord) => void | UnknownRecord\n\t\t\t// eslint-disable-next-line @typescript-eslint/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 @typescript-eslint/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 @typescript-eslint/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 @typescript-eslint/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/**\n * Enumeration of possible reasons why a migration might fail.\n *\n * These reasons help identify what went wrong during migration processing,\n * allowing applications to handle different failure scenarios appropriately.\n * Common failures include incompatible data formats, unknown record types,\n * and version mismatches between the data and available migrations.\n * @public\n */\nexport enum 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}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["MigrationFailureReason"]
|
|
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": "4.3.0
|
|
4
|
+
"version": "4.3.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw Inc.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
"context": "yarn run -T tsx ../../internal/scripts/context.ts"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@tldraw/state": "4.3.0
|
|
48
|
-
"@tldraw/utils": "4.3.0
|
|
47
|
+
"@tldraw/state": "4.3.0",
|
|
48
|
+
"@tldraw/utils": "4.3.0"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"react": "^18.2.0 || ^19.
|
|
51
|
+
"react": "^18.2.0 || ^19.2.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@peculiar/webcrypto": "^1.5.0",
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { registerTldrawLibraryVersion } from '@tldraw/utils'
|
|
2
2
|
export { AtomMap } from './lib/AtomMap'
|
|
3
|
+
export { AtomSet } from './lib/AtomSet'
|
|
3
4
|
export type { BaseRecord, IdOf, RecordId, UnknownRecord } from './lib/BaseRecord'
|
|
4
5
|
export { devFreeze } from './lib/devFreeze'
|
|
5
6
|
export { type QueryExpression, type QueryValueMatcher } from './lib/executeQuery'
|
|
@@ -18,6 +19,8 @@ export {
|
|
|
18
19
|
type MigrationResult,
|
|
19
20
|
type MigrationSequence,
|
|
20
21
|
type StandaloneDependsOn,
|
|
22
|
+
type SynchronousRecordStorage,
|
|
23
|
+
type SynchronousStorage,
|
|
21
24
|
} from './lib/migrate'
|
|
22
25
|
export {
|
|
23
26
|
createEmptyRecordsDiff,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { AtomMap } from './AtomMap'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A drop-in replacement for Set that stores values in atoms and can be used in reactive contexts.
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export class AtomSet<T> {
|
|
8
|
+
private readonly map: AtomMap<T, T>
|
|
9
|
+
constructor(
|
|
10
|
+
private readonly name: string,
|
|
11
|
+
keys?: Iterable<T>
|
|
12
|
+
) {
|
|
13
|
+
const entries = keys ? Array.from(keys, (k) => [k, k] as const) : undefined
|
|
14
|
+
this.map = new AtomMap(name, entries)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
add(value: T): this {
|
|
18
|
+
this.map.set(value, value)
|
|
19
|
+
return this
|
|
20
|
+
}
|
|
21
|
+
clear(): void {
|
|
22
|
+
this.map.clear()
|
|
23
|
+
}
|
|
24
|
+
delete(value: T): boolean {
|
|
25
|
+
return this.map.delete(value)
|
|
26
|
+
}
|
|
27
|
+
forEach(callbackfn: (value: T, value2: T, set: AtomSet<T>) => void, thisArg?: any): void {
|
|
28
|
+
for (const value of this) {
|
|
29
|
+
callbackfn.call(thisArg, value, value, this)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
has(value: T): boolean {
|
|
33
|
+
return this.map.has(value)
|
|
34
|
+
}
|
|
35
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
36
|
+
get size(): number {
|
|
37
|
+
return this.map.size
|
|
38
|
+
}
|
|
39
|
+
entries(): Generator<[T, T], undefined, unknown> {
|
|
40
|
+
return this.map.entries()
|
|
41
|
+
}
|
|
42
|
+
keys(): Generator<T, undefined, unknown> {
|
|
43
|
+
return this.map.keys()
|
|
44
|
+
}
|
|
45
|
+
values(): Generator<T, undefined, unknown> {
|
|
46
|
+
return this.map.keys()
|
|
47
|
+
}
|
|
48
|
+
[Symbol.iterator](): Generator<T, undefined, unknown> {
|
|
49
|
+
return this.map.keys()
|
|
50
|
+
}
|
|
51
|
+
[Symbol.toStringTag]: string = 'AtomSet'
|
|
52
|
+
}
|
package/src/lib/ImmutableMap.ts
CHANGED
|
@@ -174,20 +174,10 @@ function SetRef(ref?: Ref): void {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
offset = offset || 0
|
|
180
|
-
const len = Math.max(0, arr.length - offset)
|
|
181
|
-
const newArr: Array<I> = new Array(len)
|
|
182
|
-
for (let ii = 0; ii < len; ii++) {
|
|
183
|
-
// We may want to guard for undefined values with `if (arr[ii + offset] !== undefined`, but ths should not happen by design
|
|
184
|
-
newArr[ii] = arr[ii + offset]
|
|
185
|
-
}
|
|
186
|
-
return newArr
|
|
177
|
+
function arrCopy<I>(arr: Array<I>, offset = 0): Array<I> {
|
|
178
|
+
return arr.slice(offset)
|
|
187
179
|
}
|
|
188
180
|
|
|
189
|
-
const is = Object.is
|
|
190
|
-
|
|
191
181
|
class OwnerID {}
|
|
192
182
|
|
|
193
183
|
/**
|
|
@@ -481,7 +471,7 @@ class ArrayMapNode<K, V> {
|
|
|
481
471
|
get(_shift: unknown, _keyHash: unknown, key: K, notSetValue?: V) {
|
|
482
472
|
const entries = this.entries
|
|
483
473
|
for (let ii = 0, len = entries.length; ii < len; ii++) {
|
|
484
|
-
if (is(key, entries[ii][0])) {
|
|
474
|
+
if (Object.is(key, entries[ii][0])) {
|
|
485
475
|
return entries[ii][1]
|
|
486
476
|
}
|
|
487
477
|
}
|
|
@@ -503,7 +493,7 @@ class ArrayMapNode<K, V> {
|
|
|
503
493
|
let idx = 0
|
|
504
494
|
const len = entries.length
|
|
505
495
|
for (; idx < len; idx++) {
|
|
506
|
-
if (is(key, entries[idx][0])) {
|
|
496
|
+
if (Object.is(key, entries[idx][0])) {
|
|
507
497
|
break
|
|
508
498
|
}
|
|
509
499
|
}
|
|
@@ -514,8 +504,7 @@ class ArrayMapNode<K, V> {
|
|
|
514
504
|
}
|
|
515
505
|
|
|
516
506
|
SetRef(didAlter)
|
|
517
|
-
|
|
518
|
-
;(removed || !exists) && SetRef(didChangeSize)
|
|
507
|
+
if (removed || !exists) SetRef(didChangeSize)
|
|
519
508
|
|
|
520
509
|
if (removed && entries.length === 1) {
|
|
521
510
|
return // undefined
|
|
@@ -530,8 +519,11 @@ class ArrayMapNode<K, V> {
|
|
|
530
519
|
|
|
531
520
|
if (exists) {
|
|
532
521
|
if (removed) {
|
|
533
|
-
|
|
534
|
-
|
|
522
|
+
if (idx === len - 1) {
|
|
523
|
+
newEntries.pop()
|
|
524
|
+
} else {
|
|
525
|
+
newEntries[idx] = newEntries.pop()!
|
|
526
|
+
}
|
|
535
527
|
} else {
|
|
536
528
|
newEntries[idx] = [key, value]
|
|
537
529
|
}
|
|
@@ -719,7 +711,7 @@ class HashCollisionNode<K, V> {
|
|
|
719
711
|
get(shift: number, keyHash: number, key: K, notSetValue?: V) {
|
|
720
712
|
const entries = this.entries
|
|
721
713
|
for (let ii = 0, len = entries.length; ii < len; ii++) {
|
|
722
|
-
if (is(key, entries[ii][0])) {
|
|
714
|
+
if (Object.is(key, entries[ii][0])) {
|
|
723
715
|
return entries[ii][1]
|
|
724
716
|
}
|
|
725
717
|
}
|
|
@@ -754,7 +746,7 @@ class HashCollisionNode<K, V> {
|
|
|
754
746
|
let idx = 0
|
|
755
747
|
const len = entries.length
|
|
756
748
|
for (; idx < len; idx++) {
|
|
757
|
-
if (is(key, entries[idx][0])) {
|
|
749
|
+
if (Object.is(key, entries[idx][0])) {
|
|
758
750
|
break
|
|
759
751
|
}
|
|
760
752
|
}
|
|
@@ -765,8 +757,7 @@ class HashCollisionNode<K, V> {
|
|
|
765
757
|
}
|
|
766
758
|
|
|
767
759
|
SetRef(didAlter)
|
|
768
|
-
|
|
769
|
-
;(removed || !exists) && SetRef(didChangeSize)
|
|
760
|
+
if (removed || !exists) SetRef(didChangeSize)
|
|
770
761
|
|
|
771
762
|
if (removed && len === 2) {
|
|
772
763
|
return new ValueNode(ownerID, this.keyHash, entries[idx ^ 1])
|
|
@@ -777,8 +768,11 @@ class HashCollisionNode<K, V> {
|
|
|
777
768
|
|
|
778
769
|
if (exists) {
|
|
779
770
|
if (removed) {
|
|
780
|
-
|
|
781
|
-
|
|
771
|
+
if (idx === len - 1) {
|
|
772
|
+
newEntries.pop()
|
|
773
|
+
} else {
|
|
774
|
+
newEntries[idx] = newEntries.pop()!
|
|
775
|
+
}
|
|
782
776
|
} else {
|
|
783
777
|
newEntries[idx] = [key, value]
|
|
784
778
|
}
|
|
@@ -803,7 +797,7 @@ class ValueNode<K, V> {
|
|
|
803
797
|
) {}
|
|
804
798
|
|
|
805
799
|
get(shift: number, keyHash: number, key: K, notSetValue?: V) {
|
|
806
|
-
return is(key, this.entry[0]) ? this.entry[1] : notSetValue
|
|
800
|
+
return Object.is(key, this.entry[0]) ? this.entry[1] : notSetValue
|
|
807
801
|
}
|
|
808
802
|
|
|
809
803
|
update(
|
|
@@ -816,7 +810,7 @@ class ValueNode<K, V> {
|
|
|
816
810
|
didAlter?: Ref
|
|
817
811
|
) {
|
|
818
812
|
const removed = value === NOT_SET
|
|
819
|
-
const keyMatch = is(key, this.entry[0])
|
|
813
|
+
const keyMatch = Object.is(key, this.entry[0])
|
|
820
814
|
if (keyMatch ? value === this.entry[1] : removed) {
|
|
821
815
|
return this
|
|
822
816
|
}
|
|
@@ -927,13 +921,11 @@ function iteratorValue<K, V>(
|
|
|
927
921
|
iteratorResult?: IteratorResult<any>
|
|
928
922
|
) {
|
|
929
923
|
const value = type === ITERATE_KEYS ? k : type === ITERATE_VALUES ? v : [k, v]
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
done: false,
|
|
936
|
-
})
|
|
924
|
+
if (iteratorResult) {
|
|
925
|
+
iteratorResult.value = value
|
|
926
|
+
} else {
|
|
927
|
+
iteratorResult = { value, done: false }
|
|
928
|
+
}
|
|
937
929
|
return iteratorResult
|
|
938
930
|
}
|
|
939
931
|
|