Migrate snapshot (#1843)

Add `Store.migrateSnapshot`, another surface API alongside getSnapshot
and loadSnapshot.

### Change Type

- [x] `minor` — New feature

### Release Notes

- [editor] add `Store.migrateSnapshot`
This commit is contained in:
Steve Ruiz 2023-09-08 18:04:53 +01:00 committed by GitHub
parent 0b3e83be52
commit 48a1bb4d88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 126 additions and 13 deletions

View file

@ -2495,9 +2495,9 @@ export type TLStoreOptions = {
initialData?: SerializedStore<TLRecord>; initialData?: SerializedStore<TLRecord>;
defaultName?: string; defaultName?: string;
} & ({ } & ({
schema: StoreSchema<TLRecord, TLStoreProps>; schema?: StoreSchema<TLRecord, TLStoreProps>;
} | { } | {
shapeUtils: readonly TLAnyShapeUtilConstructor[]; shapeUtils?: readonly TLAnyShapeUtilConstructor[];
}); });
// @public (undocumented) // @public (undocumented)

View file

@ -15,8 +15,8 @@ export type TLStoreOptions = {
initialData?: SerializedStore<TLRecord> initialData?: SerializedStore<TLRecord>
defaultName?: string defaultName?: string
} & ( } & (
| { shapeUtils: readonly TLAnyShapeUtilConstructor[] } | { shapeUtils?: readonly TLAnyShapeUtilConstructor[] }
| { schema: StoreSchema<TLRecord, TLStoreProps> } | { schema?: StoreSchema<TLRecord, TLStoreProps> }
) )
/** @public */ /** @public */
@ -30,11 +30,16 @@ export type TLStoreEventInfo = HistoryEntry<TLRecord>
* @public */ * @public */
export function createTLStore({ initialData, defaultName = '', ...rest }: TLStoreOptions): TLStore { export function createTLStore({ initialData, defaultName = '', ...rest }: TLStoreOptions): TLStore {
const schema = const schema =
'schema' in rest 'schema' in rest && rest.schema
? rest.schema ? // we have a schema
: createTLSchema({ rest.schema
shapes: currentPageShapesToShapeMap(checkShapesAndAddCore(rest.shapeUtils)), : // we need a schema
createTLSchema({
shapes: currentPageShapesToShapeMap(
checkShapesAndAddCore('shapeUtils' in rest && rest.shapeUtils ? rest.shapeUtils : [])
),
}) })
return new Store({ return new Store({
schema, schema,
initialData, initialData,

View file

@ -257,6 +257,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
// @internal (undocumented) // @internal (undocumented)
markAsPossiblyCorrupted(): void; markAsPossiblyCorrupted(): void;
mergeRemoteChanges: (fn: () => void) => void; mergeRemoteChanges: (fn: () => void) => void;
migrateSnapshot(snapshot: StoreSnapshot<R>): StoreSnapshot<R>;
onAfterChange?: (prev: R, next: R, source: 'remote' | 'user') => void; onAfterChange?: (prev: R, next: R, source: 'remote' | 'user') => void;
onAfterCreate?: (record: R, source: 'remote' | 'user') => void; onAfterCreate?: (record: R, source: 'remote' | 'user') => void;
onAfterDelete?: (prev: R, source: 'remote' | 'user') => void; onAfterDelete?: (prev: R, source: 'remote' | 'user') => void;

View file

@ -565,6 +565,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
* ``` * ```
* *
* @param scope - The scope of records to serialize. Defaults to 'document'. * @param scope - The scope of records to serialize. Defaults to 'document'.
*
* @public * @public
*/ */
getSnapshot(scope: RecordScope | 'all' = 'document'): StoreSnapshot<R> { getSnapshot(scope: RecordScope | 'all' = 'document'): StoreSnapshot<R> {
@ -574,6 +575,30 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
} }
} }
/**
* 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<R>): StoreSnapshot<R> {
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. * Load a serialized snapshot.
* *
@ -583,7 +608,6 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
* ``` * ```
* *
* @param snapshot - The snapshot to load. * @param snapshot - The snapshot to load.
*
* @public * @public
*/ */
loadSnapshot(snapshot: StoreSnapshot<R>): void { loadSnapshot(snapshot: StoreSnapshot<R>): void {

View file

@ -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 { TestEditor } from './TestEditor'
import { TL } from './test-jsx' 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())
})
})

View file

@ -14,6 +14,7 @@ import { SerializedStore } from '@tldraw/store';
import { Signal } from '@tldraw/state'; import { Signal } from '@tldraw/state';
import { Store } from '@tldraw/store'; import { Store } from '@tldraw/store';
import { StoreSchema } from '@tldraw/store'; import { StoreSchema } from '@tldraw/store';
import { StoreSnapshot } from '@tldraw/store';
import { T } from '@tldraw/validate'; import { T } from '@tldraw/validate';
import { UnknownRecord } from '@tldraw/store'; import { UnknownRecord } from '@tldraw/store';
@ -1203,7 +1204,7 @@ export type TLStoreProps = {
export type TLStoreSchema = StoreSchema<TLRecord, TLStoreProps>; export type TLStoreSchema = StoreSchema<TLRecord, TLStoreProps>;
// @public (undocumented) // @public (undocumented)
export type TLStoreSnapshot = SerializedStore<TLRecord>; export type TLStoreSnapshot = StoreSnapshot<TLRecord>;
// @public (undocumented) // @public (undocumented)
export type TLTextShape = TLBaseShape<'text', TLTextShapeProps>; export type TLTextShape = TLBaseShape<'text', TLTextShapeProps>;

View file

@ -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 { annotateError, structuredClone } from '@tldraw/utils'
import { CameraRecordType, TLCameraId } from './records/TLCamera' import { CameraRecordType, TLCameraId } from './records/TLCamera'
import { DocumentRecordType, TLDOCUMENT_ID } from './records/TLDocument' import { DocumentRecordType, TLDOCUMENT_ID } from './records/TLDocument'
@ -36,7 +42,7 @@ export type TLStoreSchema = StoreSchema<TLRecord, TLStoreProps>
export type TLSerializedStore = SerializedStore<TLRecord> export type TLSerializedStore = SerializedStore<TLRecord>
/** @public */ /** @public */
export type TLStoreSnapshot = SerializedStore<TLRecord> export type TLStoreSnapshot = StoreSnapshot<TLRecord>
/** @public */ /** @public */
export type TLStoreProps = { export type TLStoreProps = {