diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index a3da7ed90..ed856f588 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -2495,9 +2495,9 @@ export type TLStoreOptions = { initialData?: SerializedStore; defaultName?: string; } & ({ - schema: StoreSchema; + schema?: StoreSchema; } | { - shapeUtils: readonly TLAnyShapeUtilConstructor[]; + shapeUtils?: readonly TLAnyShapeUtilConstructor[]; }); // @public (undocumented) diff --git a/packages/editor/src/lib/config/createTLStore.ts b/packages/editor/src/lib/config/createTLStore.ts index 2a4022f0c..7d04b5d8b 100644 --- a/packages/editor/src/lib/config/createTLStore.ts +++ b/packages/editor/src/lib/config/createTLStore.ts @@ -15,8 +15,8 @@ export type TLStoreOptions = { initialData?: SerializedStore defaultName?: string } & ( - | { shapeUtils: readonly TLAnyShapeUtilConstructor[] } - | { schema: StoreSchema } + | { shapeUtils?: readonly TLAnyShapeUtilConstructor[] } + | { schema?: StoreSchema } ) /** @public */ @@ -30,11 +30,16 @@ export type TLStoreEventInfo = HistoryEntry * @public */ export function createTLStore({ initialData, defaultName = '', ...rest }: TLStoreOptions): TLStore { const schema = - 'schema' in rest - ? rest.schema - : createTLSchema({ - shapes: currentPageShapesToShapeMap(checkShapesAndAddCore(rest.shapeUtils)), + 'schema' in rest && rest.schema + ? // we have a schema + rest.schema + : // we need a schema + createTLSchema({ + shapes: currentPageShapesToShapeMap( + checkShapesAndAddCore('shapeUtils' in rest && rest.shapeUtils ? rest.shapeUtils : []) + ), }) + return new Store({ schema, initialData, diff --git a/packages/store/api-report.md b/packages/store/api-report.md index 1537a867b..3be5d05b2 100644 --- a/packages/store/api-report.md +++ b/packages/store/api-report.md @@ -257,6 +257,7 @@ export class Store { // @internal (undocumented) markAsPossiblyCorrupted(): void; mergeRemoteChanges: (fn: () => void) => void; + migrateSnapshot(snapshot: StoreSnapshot): StoreSnapshot; onAfterChange?: (prev: R, next: R, source: 'remote' | 'user') => void; onAfterCreate?: (record: R, source: 'remote' | 'user') => void; onAfterDelete?: (prev: R, source: 'remote' | 'user') => void; diff --git a/packages/store/src/lib/Store.ts b/packages/store/src/lib/Store.ts index 56c958447..5418836ad 100644 --- a/packages/store/src/lib/Store.ts +++ b/packages/store/src/lib/Store.ts @@ -565,6 +565,7 @@ export class Store { * ``` * * @param scope - The scope of records to serialize. Defaults to 'document'. + * * @public */ getSnapshot(scope: RecordScope | 'all' = 'document'): StoreSnapshot { @@ -574,6 +575,30 @@ export class Store { } } + /** + * Migrate a serialized snapshot of the store and its schema. + * + * ```ts + * const snapshot = store.getSnapshot() + * store.migrateSnapshot(snapshot) + * ``` + * + * @param snapshot - The snapshot to load. + * @public + */ + migrateSnapshot(snapshot: StoreSnapshot): StoreSnapshot { + const migrationResult = this.schema.migrateStoreSnapshot(snapshot) + + if (migrationResult.type === 'error') { + throw new Error(`Failed to migrate snapshot: ${migrationResult.reason}`) + } + + return { + store: migrationResult.value, + schema: this.schema.serialize(), + } + } + /** * Load a serialized snapshot. * @@ -583,7 +608,6 @@ export class Store { * ``` * * @param snapshot - The snapshot to load. - * * @public */ loadSnapshot(snapshot: StoreSnapshot): void { diff --git a/packages/tldraw/src/test/Editor.test.tsx b/packages/tldraw/src/test/Editor.test.tsx index 8acae1d0f..66429bce9 100644 --- a/packages/tldraw/src/test/Editor.test.tsx +++ b/packages/tldraw/src/test/Editor.test.tsx @@ -1,4 +1,10 @@ -import { BaseBoxShapeUtil, PageRecordType, TLShape, createShapeId } from '@tldraw/editor' +import { + AssetRecordType, + BaseBoxShapeUtil, + PageRecordType, + TLShape, + createShapeId, +} from '@tldraw/editor' import { TestEditor } from './TestEditor' import { TL } from './test-jsx' @@ -504,3 +510,73 @@ describe('getShapeUtil', () => { ) }) }) + +describe('snapshots', () => { + it('creates and loads a snapshot', () => { + const ids = { + imageA: createShapeId('imageA'), + boxA: createShapeId('boxA'), + imageAssetA: AssetRecordType.createId('imageAssetA'), + } + + editor.createAssets([ + { + type: 'image', + id: ids.imageAssetA, + typeName: 'asset', + props: { + w: 1200, + h: 800, + name: '', + isAnimated: false, + mimeType: 'png', + src: '', + }, + meta: {}, + }, + ]) + + editor.createShapes([ + { type: 'geo', x: 0, y: 0 }, + { type: 'geo', x: 100, y: 0 }, + { + id: ids.imageA, + type: 'image', + props: { + playing: false, + url: '', + w: 1200, + h: 800, + assetId: ids.imageAssetA, + }, + x: 0, + y: 1200, + }, + ]) + + const page2Id = PageRecordType.createId('page2') + + editor.createPage({ + id: page2Id, + }) + + editor.setCurrentPage(page2Id) + + editor.createShapes([ + { type: 'geo', x: 0, y: 0 }, + { type: 'geo', x: 100, y: 0 }, + ]) + + editor.selectAll() + + // now serialize + + const snapshot = editor.store.getSnapshot() + + const newEditor = new TestEditor() + + newEditor.store.loadSnapshot(snapshot) + + expect(editor.store.serialize()).toEqual(newEditor.store.serialize()) + }) +}) diff --git a/packages/tlschema/api-report.md b/packages/tlschema/api-report.md index e97d82e97..0546e12c1 100644 --- a/packages/tlschema/api-report.md +++ b/packages/tlschema/api-report.md @@ -14,6 +14,7 @@ import { SerializedStore } from '@tldraw/store'; import { Signal } from '@tldraw/state'; import { Store } from '@tldraw/store'; import { StoreSchema } from '@tldraw/store'; +import { StoreSnapshot } from '@tldraw/store'; import { T } from '@tldraw/validate'; import { UnknownRecord } from '@tldraw/store'; @@ -1203,7 +1204,7 @@ export type TLStoreProps = { export type TLStoreSchema = StoreSchema; // @public (undocumented) -export type TLStoreSnapshot = SerializedStore; +export type TLStoreSnapshot = StoreSnapshot; // @public (undocumented) export type TLTextShape = TLBaseShape<'text', TLTextShapeProps>; diff --git a/packages/tlschema/src/TLStore.ts b/packages/tlschema/src/TLStore.ts index 87c79c74e..92a418655 100644 --- a/packages/tlschema/src/TLStore.ts +++ b/packages/tlschema/src/TLStore.ts @@ -1,4 +1,10 @@ -import { SerializedStore, Store, StoreSchema, StoreSchemaOptions } from '@tldraw/store' +import { + SerializedStore, + Store, + StoreSchema, + StoreSchemaOptions, + StoreSnapshot, +} from '@tldraw/store' import { annotateError, structuredClone } from '@tldraw/utils' import { CameraRecordType, TLCameraId } from './records/TLCamera' import { DocumentRecordType, TLDOCUMENT_ID } from './records/TLDocument' @@ -36,7 +42,7 @@ export type TLStoreSchema = StoreSchema export type TLSerializedStore = SerializedStore /** @public */ -export type TLStoreSnapshot = SerializedStore +export type TLStoreSnapshot = StoreSnapshot /** @public */ export type TLStoreProps = {