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:
parent
0b3e83be52
commit
48a1bb4d88
7 changed files with 126 additions and 13 deletions
|
@ -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)
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
Loading…
Reference in a new issue