Snapshots pit of success (#3811)

Lots of people are having a bad time with loading/restoring snapshots
and there's a few reasons for that:

- It's not clear how to preserve UI state independently of document
state.
- Loading a snapshot wipes the instance state, which means we almost
always need to
  - update the viewport page bounds
  - refocus the editor
  - preserver some other sneaky properties of the `instance` record

### Change Type

<!--  Please select a 'Scope' label ️ -->

- [x] `sdk` — Changes the tldraw SDK
- [ ] `dotcom` — Changes the tldraw.com web app
- [ ] `docs` — Changes to the documentation, examples, or templates.
- [ ] `vs code` — Changes to the vscode plugin
- [ ] `internal` — Does not affect user-facing stuff

<!--  Please select a 'Type' label ️ -->

- [ ] `bugfix` — Bug fix
- [ ] `feature` — New feature
- [ ] `improvement` — Improving existing features
- [ ] `chore` — Updating dependencies, other boring stuff
- [ ] `galaxy brain` — Architectural changes
- [ ] `tests` — Changes to any test code
- [ ] `tools` — Changes to infrastructure, CI, internal scripts,
debugging tools, etc.
- [ ] `dunno` — I don't know


### Test Plan

1. Add a step-by-step description of how to test your PR here.
2.

- [ ] Unit Tests
- [ ] End to end tests

### Release Notes

- Add a brief release note for your PR here.
This commit is contained in:
David Sheldrick 2024-06-03 16:58:00 +01:00 committed by GitHub
parent 9422a0ecc2
commit 19d051c188
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1831 additions and 1715 deletions

View file

@ -54,19 +54,22 @@ export default function () {
In the example above, both editors would synchronize their document locally. They would still have two independent instance states (e.g. selections) but the document would be kept in sync and persisted under the same key.
## Document Snapshots
## State Snapshots
You can get a JSON snapshot of the editor's content using the [Editor#store](?)'s [Store#getSnapshot](?) method.
You can get a JSON snapshot of the document content and the user 'session' state using the [getSnapshot](?) function.
```tsx
function SaveButton() {
function SaveButton({ documentId, userId }) {
const editor = useEditor()
return (
<button
onClick={() => {
const snapshot = editor.store.getSnapshot()
const stringified = JSON.stringify(snapshot)
localStorage.setItem('my-editor-snapshot', stringified)
const { document, session } = getSnapshot(editor.store)
// If you are building a multi-user app, you probably want to store
// the document and session states separately because the
// session state is user-specific and normally shouldn't be shared.
await saveDocumentState(documentId, document)
await saveSessionState(documentId, userId, session)
}}
>
Save
@ -75,17 +78,18 @@ function SaveButton() {
}
```
You can load the snapshot into a new editor with [Store#loadSnapshot](?).
To load the snapshot back into an existing editor, use the [loadSnapshot](?) function.
```tsx
function LoadButton() {
function LoadButton({ documentId, userId }) {
const editor = useEditor()
return (
<button
onClick={() => {
const stringified = localStorage.getItem('my-editor-snapshot')
const snapshot = JSON.parse(stringified)
editor.store.loadSnapshot(snapshot)
const document = await loadDocumentState(documentId)
const session = await loadSessionState(documentId, userId)
editor.setCurrentTool('select') // need to reset tool state separately
loadSnapshot(editor.store, { document, session })
}}
>
Load
@ -94,11 +98,27 @@ function LoadButton() {
}
```
A [snapshot](/reference/store/StoreSnapshot) includes both the store's [serialized records](/reference/store/SerializedStore) and its [serialized schema](/reference/store/SerializedSchema), which is used for migrations.
You can also pass a snapshot as a prop to set the initial editor state.
> By default, the `getSnapshot` method returns only the editor's document data. If you want to get records from a different scope, you can pass in `session`, `document`, `presence`, or else `all` for all scopes.
```tsx
function MyApp({ userId, documentId }) {
const [snapshot, setSnapshot] = useState(null)
Note that loading a snapshot does not reset the editor's in memory state or UI state. For example, loading a snapshot during a resizing operation may lead to a crash. This is because the resizing state maintains its own cache of information about which shapes it is resizing, and its possible that those shapes may no longer exist!
useEffect(() => {
async function load() {
const document = await getDocumentState(documentId)
const session = await getSessionState(documentId, userId)
setSnapshot({ document, session })
}
load()
}, [documentId, userId])
return snapshot ? <Tldraw snapshot={snapshot} /> : null
}
```
When tldraw loads a snapshot, it will run any necessary migrations to bring the data up to the latest tldraw schema version.
## The `"store"` prop
@ -112,6 +132,7 @@ export default function () {
// Create the store
const newStore = createTLStore({
shapeUtils: defaultShapeUtils,
bindingUtils: defaultBindingUtils,
})
// Get the snapshot
@ -119,7 +140,7 @@ export default function () {
const snapshot = JSON.parse(stringified)
// Load the snapshot
newStore.loadSnapshot(snapshot)
loadSnapshot(newStore, snapshot)
return newStore
})
@ -146,10 +167,11 @@ export default function () {
// Create the store
const newStore = createTLStore({
shapeUtils: defaultShapeUtils,
bindingUtils: defaultBindingUtils,
})
// Load the snapshot
newStore.loadSnapshot(snapshot)
loadSnapshot(newStore, snapshot)
// Update the store with status
setStoreWithStatus({
@ -211,7 +233,7 @@ myRemoteSource.on('change', (changes) => {
## Migrations
Tldraw uses migrations to bring data from old snapshots up to date. These run automatically when calling `editor.store.loadSnapshot`.
Tldraw uses migrations to bring data from old snapshots up to date. These run automatically when calling [loadSnapshot](?).
### Running migrations manually

View file

@ -1,7 +1,7 @@
import { ROOM_PREFIX } from '@tldraw/dotcom-shared'
import { RoomSnapshot } from '@tldraw/tlsync'
import { useCallback, useState } from 'react'
import { Tldraw, createTLStore, defaultShapeUtils } from 'tldraw'
import { Tldraw } from 'tldraw'
import '../../../styles/core.css'
import { assetUrls } from '../../utils/assetUrls'
import { useFileSystem } from '../../utils/useFileSystem'
@ -17,14 +17,10 @@ export function BoardHistorySnapshot({
timestamp: string
token?: string
}) {
const [store] = useState(() => {
const store = createTLStore({ shapeUtils: defaultShapeUtils })
store.loadSnapshot({
const [snapshot] = useState(() => ({
schema: data.schema!,
store: Object.fromEntries(data.documents.map((doc) => [doc.state.id, doc.state])) as any,
})
return store
})
}))
const fileSystemUiOverrides = useFileSystem({ isMultiplayer: true })
@ -57,7 +53,7 @@ export function BoardHistorySnapshot({
<>
<div className="tldraw__editor">
<Tldraw
store={store}
snapshot={snapshot}
assetUrls={assetUrls}
onMount={(editor) => {
editor.updateInstanceState({ isReadonly: true })

View file

@ -8,6 +8,7 @@ import {
TLStoreSnapshot,
Tldraw,
TldrawImage,
getSnapshot,
} from 'tldraw'
import 'tldraw/tldraw.css'
import initialSnapshot from './snapshot.json'
@ -38,7 +39,7 @@ export default function TldrawImageExample() {
setShowBackground(editor.getInstanceState().exportBackground)
setViewportPageBounds(editor.getViewportPageBounds())
setCurrentPageId(editor.getCurrentPageId())
setSnapshot(editor.store.getSnapshot())
setSnapshot(getSnapshot(editor.store).document)
setIsEditing(false)
} else {
setIsEditing(true)

View file

@ -1,5 +1,13 @@
import { useLayoutEffect, useState } from 'react'
import { Tldraw, createTLStore, defaultShapeUtils, throttle } from 'tldraw'
import {
Tldraw,
createTLStore,
defaultBindingUtils,
defaultShapeUtils,
getSnapshot,
loadSnapshot,
throttle,
} from 'tldraw'
import 'tldraw/tldraw.css'
// There's a guide at the bottom of this file!
@ -8,7 +16,9 @@ const PERSISTENCE_KEY = 'example-3'
export default function PersistenceExample() {
//[1]
const [store] = useState(() => createTLStore({ shapeUtils: defaultShapeUtils }))
const [store] = useState(() =>
createTLStore({ shapeUtils: defaultShapeUtils, bindingUtils: defaultBindingUtils })
)
//[2]
const [loadingState, setLoadingState] = useState<
{ status: 'loading' } | { status: 'ready' } | { status: 'error'; error: string }
@ -25,7 +35,7 @@ export default function PersistenceExample() {
if (persistedSnapshot) {
try {
const snapshot = JSON.parse(persistedSnapshot)
store.loadSnapshot(snapshot)
loadSnapshot(store, snapshot)
setLoadingState({ status: 'ready' })
} catch (error: any) {
setLoadingState({ status: 'error', error: error.message }) // Something went wrong
@ -37,7 +47,7 @@ export default function PersistenceExample() {
// Each time the store changes, run the (debounced) persist function
const cleanupFn = store.listen(
throttle(() => {
const snapshot = store.getSnapshot()
const snapshot = getSnapshot(store)
localStorage.setItem(PERSISTENCE_KEY, JSON.stringify(snapshot))
}, 500)
)

View file

@ -9,4 +9,4 @@ Load a snapshot of the editor's contents.
---
Use `editor.store.getSnapshot()` and `editor.store.loadSnapshot()` to save and restore the editor's contents.
Use `getSnapshot()` and `loadSnapshot()` to save and restore the editor's contents.

View file

@ -1,47 +1,99 @@
import { TLStoreSnapshot, Tldraw } from 'tldraw'
import { useCallback, useEffect, useState } from 'react'
import { TLEditorSnapshot, Tldraw, getSnapshot, loadSnapshot, useEditor } from 'tldraw'
import 'tldraw/tldraw.css'
import _jsonSnapshot from './snapshot.json'
const jsonSnapshot = _jsonSnapshot as TLStoreSnapshot
// There's a guide at the bottom of this file!
const LOAD_SNAPSHOT_WITH_INITIAL_DATA = true
const jsonSnapshot = _jsonSnapshot as any as TLEditorSnapshot
// [1]
function SnapshotToolbar() {
const editor = useEditor()
const save = useCallback(() => {
// [2]
const { document, session } = getSnapshot(editor.store)
// [3]
localStorage.setItem('snapshot', JSON.stringify({ document, session }))
}, [editor])
const load = useCallback(() => {
const snapshot = localStorage.getItem('snapshot')
if (!snapshot) return
// [4]
loadSnapshot(editor.store, JSON.parse(snapshot))
}, [editor])
const [showCheckMark, setShowCheckMark] = useState(false)
useEffect(() => {
if (showCheckMark) {
const timeout = setTimeout(() => {
setShowCheckMark(false)
}, 1000)
return () => clearTimeout(timeout)
}
return
})
//[1]
export default function SnapshotExample() {
if (LOAD_SNAPSHOT_WITH_INITIAL_DATA) {
return (
<div className="tldraw__editor">
<Tldraw snapshot={jsonSnapshot} />
<div style={{ padding: 20, pointerEvents: 'all', display: 'flex', gap: '10px' }}>
<span
style={{
display: 'inline-block',
transition: 'transform 0.2s ease, opacity 0.2s ease',
transform: showCheckMark ? `scale(1)` : `scale(0.5)`,
opacity: showCheckMark ? 1 : 0,
}}
>
Saved
</span>
<button
onClick={() => {
save()
setShowCheckMark(true)
}}
>
Save Snapshot
</button>
<button onClick={load}>Load Snapshot</button>
</div>
)
}
//[2]
}
export default function SnapshotExample() {
return (
<div className="tldraw__editor">
<Tldraw
onMount={(editor) => {
editor.store.loadSnapshot(jsonSnapshot)
// [5]
snapshot={jsonSnapshot}
components={{
SharePanel: SnapshotToolbar,
}}
/>
</div>
)
}
/*
This example shows how to load a snapshot into the editor. Thanks to our
migration system, you can load snapshots from any version of Tldraw. The
snapshot we're using can be found in the snapshot.json file in this folder.
You can generate a snapshot by using editor.store.getSnapshot().
There are two ways to load a snapshot:
[1] We'll add a toolbar to the top-right of the editor viewport that allows the user to save and load snapshots.
[1] Via the `snapshot` prop of the Tldraw component.
[2] Call `getSnapshot(editor.store)` to get the current state of the editor
[2] Using editor.store.loadSnapshot() in the callback of the onMount prop of the
Tldraw component.
[3] The 'document' state is the set of shapes and pages and images etc.
The 'session' state is the state of the editor like the current page, camera positions, zoom level, etc.
You probably need to store these separately if you're building a multi-user app, so that you can store per-user session state.
For this example we'll just store them together in localStorage.
[4] Call `loadSnapshot()` to load a snapshot into the editor
You can omit the `session` state, or load it later on it's own.
e.g.
loadSnapshot(editor.store, { document })
then optionally later
loadSnapshot(editor.store, { session })
Tips:
Want to migrate a snapshot but not load it? Use `editor.store.migrateSnapshot()`
*/
[5] You can load an initial snapshot into the editor by passing it to the `snapshot` prop.
*/

File diff suppressed because one or more lines are too long

View file

@ -1,16 +1,22 @@
import { TldrawFile, createTLStore, defaultShapeUtils } from 'tldraw'
import { TldrawFile, createTLStore, defaultBindingUtils, defaultShapeUtils } from 'tldraw'
import * as vscode from 'vscode'
import { nicelog } from './utils'
export const defaultFileContents: TldrawFile = {
tldrawFileFormatVersion: 1,
schema: createTLStore({ shapeUtils: defaultShapeUtils }).schema.serialize(),
schema: createTLStore({
shapeUtils: defaultShapeUtils,
bindingUtils: defaultBindingUtils,
}).schema.serialize(),
records: [],
}
export const fileContentWithErrors: TldrawFile = {
tldrawFileFormatVersion: 1,
schema: createTLStore({ shapeUtils: defaultShapeUtils }).schema.serialize(),
schema: createTLStore({
shapeUtils: defaultShapeUtils,
bindingUtils: defaultBindingUtils,
}).schema.serialize(),
records: [{ typeName: 'shape', id: null } as any],
}

View file

@ -38,7 +38,6 @@ import { Signal } from '@tldraw/state';
import { Store } from '@tldraw/store';
import { StoreSchema } from '@tldraw/store';
import { StoreSideEffects } from '@tldraw/store';
import { StoreSnapshot } from '@tldraw/store';
import { StyleProp } from '@tldraw/tlschema';
import { StylePropValue } from '@tldraw/tlschema';
import { TLAsset } from '@tldraw/tlschema';
@ -67,11 +66,13 @@ import { TLParentId } from '@tldraw/tlschema';
import { TLPropsMigrations } from '@tldraw/tlschema';
import { TLRecord } from '@tldraw/tlschema';
import { TLScribble } from '@tldraw/tlschema';
import { TLSerializedStore } from '@tldraw/tlschema';
import { TLShape } from '@tldraw/tlschema';
import { TLShapeId } from '@tldraw/tlschema';
import { TLShapePartial } from '@tldraw/tlschema';
import { TLStore } from '@tldraw/tlschema';
import { TLStoreProps } from '@tldraw/tlschema';
import { TLStoreSnapshot } from '@tldraw/tlschema';
import { TLUnknownBinding } from '@tldraw/tlschema';
import { TLUnknownShape } from '@tldraw/tlschema';
import { TLVideoAsset } from '@tldraw/tlschema';
@ -1318,6 +1319,9 @@ export function getRotationSnapshot({ editor }: {
editor: Editor;
}): null | TLRotationSnapshot;
// @public (undocumented)
export function getSnapshot(store: TLStore): TLEditorSnapshot;
// @public
export function getSvgPathFromPoints(points: VecLike[], closed?: boolean): string;
@ -1492,6 +1496,9 @@ export function LoadingScreen({ children }: {
// @public
export function loadSessionStateSnapshotIntoStore(store: TLStore, snapshot: TLSessionStateSnapshot): void;
// @public
export function loadSnapshot(store: TLStore, _snapshot: Partial<TLEditorSnapshot> | TLStoreSnapshot): void;
// @public (undocumented)
export function loopToHtmlElement(elm: Element): HTMLElement;
@ -2311,11 +2318,11 @@ export interface TldrawEditorBaseProps {
// @public
export type TldrawEditorProps = Expand<TldrawEditorBaseProps & ({
defaultName?: string;
initialData?: SerializedStore<TLRecord>;
initialData?: TLSerializedStore;
migrations?: readonly MigrationSequence[];
persistenceKey?: string;
sessionId?: string;
snapshot?: StoreSnapshot<TLRecord>;
snapshot?: TLEditorSnapshot | TLStoreSnapshot;
store?: undefined;
} | {
store: TLStore | TLStoreWithStatus;
@ -2398,6 +2405,14 @@ export interface TLEditorOptions {
user?: TLUser;
}
// @public (undocumented)
export interface TLEditorSnapshot {
// (undocumented)
document: TLStoreSnapshot;
// (undocumented)
session: TLSessionStateSnapshot;
}
// @public (undocumented)
export type TLEnterEventHandler = (info: any, from: string) => void;
@ -3065,7 +3080,7 @@ export function useIsEditing(shapeId: TLShapeId): boolean;
export function useLocalStore({ persistenceKey, sessionId, ...rest }: {
persistenceKey?: string;
sessionId?: string;
snapshot?: StoreSnapshot<TLRecord>;
snapshot?: TLEditorSnapshot | TLStoreSnapshot;
} & TLStoreOptions): TLStoreWithStatus;
// @internal (undocumented)
@ -3104,7 +3119,7 @@ export function useSvgExportContext(): {
// @public (undocumented)
export function useTLStore(opts: TLStoreOptions & {
snapshot?: StoreSnapshot<TLRecord>;
snapshot?: TLEditorSnapshot | TLStoreSnapshot;
}): TLStore;
// @public (undocumented)

View file

@ -83,6 +83,7 @@ export {
} from './lib/components/default-components/DefaultSnapIndictor'
export { DefaultSpinner } from './lib/components/default-components/DefaultSpinner'
export { DefaultSvgDefs } from './lib/components/default-components/DefaultSvgDefs'
export { getSnapshot, loadSnapshot, type TLEditorSnapshot } from './lib/config/TLEditorSnapshot'
export {
TAB_ID,
createSessionStateSnapshotSignal,

View file

@ -1,5 +1,5 @@
import { MigrationSequence, SerializedStore, Store, StoreSnapshot } from '@tldraw/store'
import { TLRecord, TLStore } from '@tldraw/tlschema'
import { MigrationSequence, Store } from '@tldraw/store'
import { TLSerializedStore, TLStore, TLStoreSnapshot } from '@tldraw/tlschema'
import { Expand, Required, annotateError } from '@tldraw/utils'
import React, {
ReactNode,
@ -14,6 +14,7 @@ import React, {
import classNames from 'classnames'
import { OptionalErrorBoundary } from './components/ErrorBoundary'
import { DefaultErrorFallback } from './components/default-components/DefaultErrorFallback'
import { TLEditorSnapshot } from './config/TLEditorSnapshot'
import { TLUser, createTLUser } from './config/createTLUser'
import { TLAnyBindingUtilConstructor } from './config/defaultBindings'
import { TLAnyShapeUtilConstructor } from './config/defaultShapes'
@ -51,8 +52,8 @@ export type TldrawEditorProps = Expand<
| {
store?: undefined
migrations?: readonly MigrationSequence[]
snapshot?: StoreSnapshot<TLRecord>
initialData?: SerializedStore<TLRecord>
snapshot?: TLEditorSnapshot | TLStoreSnapshot
initialData?: TLSerializedStore
persistenceKey?: string
sessionId?: string
defaultName?: string

View file

@ -0,0 +1,185 @@
import { PageRecordType, TLStore } from '@tldraw/tlschema'
import { IndexKey } from '@tldraw/utils'
import { Editor } from '../editor/Editor'
import { Box } from '../primitives/Box'
import { TLEditorSnapshot, getSnapshot, loadSnapshot } from './TLEditorSnapshot'
import { createTLStore } from './createTLStore'
const createEditor = (store: TLStore) => {
return new Editor({
store,
bindingUtils: [],
shapeUtils: [],
getContainer: () => document.createElement('div'),
tools: [],
})
}
describe('getSnapshot', () => {
it('should return a TLEditorSnapshot', () => {
const store = createTLStore({})
store.ensureStoreIsUsable()
const snapshot = getSnapshot(store)
expect(snapshot).toMatchObject({
document: {
schema: {},
store: {
'document:document': {},
'page:page': {},
},
},
session: {
currentPageId: 'page:page',
exportBackground: true,
isDebugMode: false,
isFocusMode: false,
isGridMode: false,
isToolLocked: false,
pageStates: [
{
camera: { x: 0, y: 0, z: 1 },
focusedGroupId: null,
pageId: 'page:page',
selectedShapeIds: [],
},
],
version: 0,
},
})
const editor = createEditor(store)
editor.updateInstanceState({ isDebugMode: true })
editor.createPage({ id: PageRecordType.createId('page2') })
editor.setCurrentPage(PageRecordType.createId('page2'))
const snapshot2 = getSnapshot(store)
expect(snapshot2).toMatchObject({
document: {
schema: {},
store: {
'document:document': {},
'page:page': {},
'page:page2': {},
},
},
session: {
currentPageId: 'page:page2',
isDebugMode: true,
pageStates: [{}, {}],
version: 0,
},
})
})
})
const page2Id = PageRecordType.createId('page2')
function addPage2(snapshot: TLEditorSnapshot) {
// sneakily add a page
snapshot.document.store[page2Id] = PageRecordType.create({
id: page2Id,
name: 'my lovely page',
index: 'a4' as IndexKey,
})
// and set the current page id
snapshot.session.currentPageId = page2Id
}
describe('loadSnapshot', () => {
it('loads a snapshot into the store', () => {
const store = createTLStore({})
const editor = createEditor(store)
const snapshot = getSnapshot(store)
addPage2(snapshot)
loadSnapshot(store, snapshot)
expect(
editor
.getPages()
.map((p) => p.id)
.sort()
).toEqual(['page:page', 'page:page2'])
expect(editor.getCurrentPageId()).toBe(page2Id)
})
it('does not overwrite changes to the viewport page bounds', () => {
const store = createTLStore({})
const editor = createEditor(store)
const snapshot = getSnapshot(store)
expect(editor.getViewportScreenBounds()).not.toEqual(new Box(0, 0, 100, 100))
editor.updateViewportScreenBounds(new Box(0, 0, 100, 100))
expect(editor.getViewportScreenBounds()).toEqual(new Box(0, 0, 100, 100))
addPage2(snapshot)
loadSnapshot(store, snapshot)
expect(editor.getViewportScreenBounds()).toEqual(new Box(0, 0, 100, 100))
})
it('works with just the document bits (1)', () => {
const store = createTLStore({})
const editor = createEditor(store)
const snapshot = getSnapshot(store)
addPage2(snapshot)
loadSnapshot(store, snapshot.document)
expect(
editor
.getPages()
.map((p) => p.id)
.sort()
).toEqual(['page:page', 'page:page2'])
expect(editor.getCurrentPageId()).toBe('page:page')
})
it('works with just the document bits (2)', () => {
const store = createTLStore({})
const editor = createEditor(store)
const snapshot = getSnapshot(store)
addPage2(snapshot)
loadSnapshot(store, { document: snapshot.document })
expect(
editor
.getPages()
.map((p) => p.id)
.sort()
).toEqual(['page:page', 'page:page2'])
expect(editor.getCurrentPageId()).toBe('page:page')
})
it('allows loading the session bits later', () => {
const store = createTLStore({})
const editor = createEditor(store)
const snapshot = getSnapshot(store)
addPage2(snapshot)
loadSnapshot(store, snapshot.document)
expect(
editor
.getPages()
.map((p) => p.id)
.sort()
).toEqual(['page:page', 'page:page2'])
expect(editor.getCurrentPageId()).toBe('page:page')
loadSnapshot(store, { session: snapshot.session })
expect(editor.getCurrentPageId()).toBe('page:page2')
})
})

View file

@ -0,0 +1,84 @@
import { Signal } from '@tldraw/state'
import { TLINSTANCE_ID, TLStore, TLStoreSnapshot, pluckPreservingValues } from '@tldraw/tlschema'
import { WeakCache, filterEntries } from '@tldraw/utils'
import {
TLSessionStateSnapshot,
createSessionStateSnapshotSignal,
loadSessionStateSnapshotIntoStore,
} from './TLSessionStateSnapshot'
/** @public */
export interface TLEditorSnapshot {
document: TLStoreSnapshot
session: TLSessionStateSnapshot
}
/**
* Loads a snapshot into a store.
* @public
*/
export function loadSnapshot(
store: TLStore,
_snapshot: Partial<TLEditorSnapshot> | TLStoreSnapshot
) {
let snapshot: Partial<TLEditorSnapshot> = {}
if ('store' in _snapshot) {
// regular old TLStoreSnapshot
// let's migrate it and then filter out the non-doc state to help folks out
const migrationResult = store.schema.migrateStoreSnapshot(_snapshot)
if (migrationResult.type !== 'success') {
throw new Error('Failed to migrate store snapshot: ' + migrationResult.reason)
}
snapshot.document = {
schema: store.schema.serialize(),
store: filterEntries(migrationResult.value, (_, { typeName }) =>
store.scopedTypes.document.has(typeName)
),
}
} else {
// TLEditorSnapshot
snapshot = _snapshot
}
// We need to preserve a bunch of instance state properties that the Editor sets
// to avoid breaking the editor or causing jarring changes when loading a snapshot.
const preservingInstanceState = pluckPreservingValues(store.get(TLINSTANCE_ID))
store.atomic(() => {
// first load the document state (this will wipe the store if it happens)
if (snapshot.document) {
store.loadStoreSnapshot(snapshot.document)
}
// then make sure we preserve those instance state properties that must be preserved
// this is a noop if the document state wasn't loaded above
if (preservingInstanceState) {
store.update(TLINSTANCE_ID, (r) => ({ ...r, ...preservingInstanceState }))
}
// finally reinstate the UI state
if (snapshot.session) {
loadSessionStateSnapshotIntoStore(store, snapshot.session)
}
})
}
const sessionStateCache = new WeakCache<
TLStore,
Signal<TLSessionStateSnapshot | undefined | null>
>()
/** @public */
export function getSnapshot(store: TLStore): TLEditorSnapshot {
const sessionState$ = sessionStateCache.get(store, createSessionStateSnapshotSignal)
const session = sessionState$.get()
if (!session) {
throw new Error('Session state is not ready yet')
}
return {
document: store.getStoreSnapshot(),
session,
}
}

View file

@ -1,20 +1,19 @@
import { Signal, computed, transact } from '@tldraw/state'
import { RecordsDiff, UnknownRecord, squashRecordDiffs } from '@tldraw/store'
import { Signal, computed } from '@tldraw/state'
import { UnknownRecord } from '@tldraw/store'
import {
CameraRecordType,
InstancePageStateRecordType,
TLINSTANCE_ID,
TLPageId,
TLRecord,
TLShapeId,
TLStore,
pageIdValidator,
pluckPreservingValues,
shapeIdValidator,
} from '@tldraw/tlschema'
import {
deleteFromSessionStorage,
getFromSessionStorage,
objectMapFromEntries,
setInSessionStorage,
structuredClone,
} from '@tldraw/utils'
@ -209,58 +208,43 @@ export function loadSessionStateSnapshotIntoStore(
const res = migrateAndValidateSessionStateSnapshot(snapshot)
if (!res) return
// remove all page states and cameras and the instance state
const allPageStatesAndCameras = store
.allRecords()
.filter((r) => r.typeName === 'instance_page_state' || r.typeName === 'camera')
const removeDiff: RecordsDiff<TLRecord> = {
added: {},
updated: {},
removed: {
...objectMapFromEntries(allPageStatesAndCameras.map((r) => [r.id, r])),
},
}
if (store.has(TLINSTANCE_ID)) {
removeDiff.removed[TLINSTANCE_ID] = store.get(TLINSTANCE_ID)!
}
const addDiff: RecordsDiff<TLRecord> = {
removed: {},
updated: {},
added: {
[TLINSTANCE_ID]: store.schema.types.instance.create({
const instanceState = store.schema.types.instance.create({
id: TLINSTANCE_ID,
...pluckPreservingValues(store.get(TLINSTANCE_ID)),
currentPageId: res.currentPageId,
isDebugMode: res.isDebugMode,
isFocusMode: res.isFocusMode,
isToolLocked: res.isToolLocked,
isGridMode: res.isGridMode,
exportBackground: res.exportBackground,
}),
},
}
})
// remove all page states and cameras and the instance state
const allPageStatesAndCameras = store
.allRecords()
.filter((r) => r.typeName === 'instance_page_state' || r.typeName === 'camera')
store.atomic(() => {
store.remove(allPageStatesAndCameras.map((r) => r.id))
// replace them with new ones
for (const ps of res.pageStates) {
const cameraId = CameraRecordType.createId(ps.pageId)
const pageStateId = InstancePageStateRecordType.createId(ps.pageId)
addDiff.added[cameraId] = CameraRecordType.create({
store.put([
CameraRecordType.create({
id: CameraRecordType.createId(ps.pageId),
x: ps.camera.x,
y: ps.camera.y,
z: ps.camera.z,
})
addDiff.added[pageStateId] = InstancePageStateRecordType.create({
}),
InstancePageStateRecordType.create({
id: InstancePageStateRecordType.createId(ps.pageId),
pageId: ps.pageId,
selectedShapeIds: ps.selectedShapeIds,
focusedGroupId: ps.focusedGroupId,
})
}),
])
}
transact(() => {
store.applyDiff(squashRecordDiffs([removeDiff, addDiff]))
store.put([instanceState])
store.ensureStoreIsUsable()
})
}

View file

@ -21,7 +21,7 @@ export type TLStoreOptions = {
export type TLStoreEventInfo = HistoryEntry<TLRecord>
/**
* A helper for creating a TLStore. Custom shapes cannot override default shapes.
* A helper for creating a TLStore.
*
* @param opts - Options for creating the store.
*

View file

@ -2981,8 +2981,8 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @example
* ```ts
* editor.updateViewportScreenBounds()
* editor.updateViewportScreenBounds(true)
* editor.updateViewportScreenBounds(new Box(0, 0, 1280, 1024))
* editor.updateViewportScreenBounds(new Box(0, 0, 1280, 1024), true)
* ```
*
* @param center - Whether to preserve the viewport page center as the viewport changes.
@ -3004,16 +3004,18 @@ export class Editor extends EventEmitter<TLEventMap> {
screenBounds.minX !== 0,
]
const boundsAreEqual = screenBounds.equals(this.getViewportScreenBounds())
const { screenBounds: prevScreenBounds, insets: prevInsets } = this.getInstanceState()
if (screenBounds.equals(prevScreenBounds) && insets.every((v, i) => v === prevInsets[i])) {
// nothing to do
return this
}
const { _willSetInitialBounds } = this
if (boundsAreEqual) {
this._willSetInitialBounds = false
} else {
if (_willSetInitialBounds) {
// If we have just received the initial bounds, don't center the camera.
this._willSetInitialBounds = false
this.updateInstanceState({ screenBounds: screenBounds.toJson(), insets })
this.setCamera(this.getCamera())
} else {
@ -3028,7 +3030,6 @@ export class Editor extends EventEmitter<TLEventMap> {
this._setCamera(Vec.From({ ...this.getCamera() }))
}
}
}
this._tickCameraState()
@ -8269,6 +8270,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*/
focus(): this {
this.focusManager.focus()
this.updateInstanceState({ isFocused: true }, { history: 'ignore' })
return this
}

View file

@ -1,6 +1,6 @@
import { StoreSnapshot } from '@tldraw/store'
import { TLRecord } from '@tldraw/tlschema'
import { TLStoreSnapshot } from '@tldraw/tlschema'
import { useEffect, useState } from 'react'
import { TLEditorSnapshot } from '../config/TLEditorSnapshot'
import { TLStoreOptions } from '../config/createTLStore'
import { TLStoreWithStatus } from '../utils/sync/StoreWithStatus'
import { TLLocalSyncClient } from '../utils/sync/TLLocalSyncClient'
@ -15,7 +15,7 @@ export function useLocalStore({
}: {
persistenceKey?: string
sessionId?: string
snapshot?: StoreSnapshot<TLRecord>
snapshot?: TLEditorSnapshot | TLStoreSnapshot
} & TLStoreOptions): TLStoreWithStatus {
const [state, setState] = useState<{ id: string; storeWithStatus: TLStoreWithStatus } | null>(
null

View file

@ -1,24 +1,27 @@
import { StoreSnapshot } from '@tldraw/store'
import { TLRecord } from '@tldraw/tlschema'
import { TLStoreSnapshot } from '@tldraw/tlschema'
import { areObjectsShallowEqual } from '@tldraw/utils'
import { useState } from 'react'
import { TLEditorSnapshot } from '../..'
import { loadSnapshot } from '../config/TLEditorSnapshot'
import { TLStoreOptions, createTLStore } from '../config/createTLStore'
/** @public */
type UseTLStoreOptions = TLStoreOptions & {
snapshot?: StoreSnapshot<TLRecord>
snapshot?: TLEditorSnapshot | TLStoreSnapshot
}
function createStore(opts: UseTLStoreOptions) {
const store = createTLStore(opts)
if (opts.snapshot) {
store.loadSnapshot(opts.snapshot)
loadSnapshot(store, opts.snapshot)
}
return { store, opts }
}
/** @public */
export function useTLStore(opts: TLStoreOptions & { snapshot?: StoreSnapshot<TLRecord> }) {
export function useTLStore(
opts: TLStoreOptions & { snapshot?: TLEditorSnapshot | TLStoreSnapshot }
) {
const [current, setCurrent] = useState(() => createStore(opts))
if (!areObjectsShallowEqual(current.opts, opts)) {

View file

@ -318,14 +318,18 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
// (undocumented)
_flushHistory(): void;
get: <K extends IdOf<R>>(id: K) => RecFromId<K> | undefined;
// @deprecated (undocumented)
getSnapshot(scope?: 'all' | RecordScope): StoreSnapshot<R>;
getStoreSnapshot(scope?: 'all' | RecordScope): StoreSnapshot<R>;
has: <K extends IdOf<R>>(id: K) => boolean;
readonly history: Atom<number, RecordsDiff<R>>;
readonly id: string;
// @internal (undocumented)
isPossiblyCorrupted(): boolean;
listen: (onHistory: StoreListener<R>, filters?: Partial<StoreListenerFilters>) => () => void;
// @deprecated (undocumented)
loadSnapshot(snapshot: StoreSnapshot<R>): void;
loadStoreSnapshot(snapshot: StoreSnapshot<R>): void;
// @internal (undocumented)
markAsPossiblyCorrupted(): void;
mergeRemoteChanges: (fn: () => void) => void;

View file

@ -485,21 +485,31 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
* Get a serialized snapshot of the store and its schema.
*
* ```ts
* const snapshot = store.getSnapshot()
* store.loadSnapshot(snapshot)
* const snapshot = store.getStoreSnapshot()
* store.loadStoreSnapshot(snapshot)
* ```
*
* @param scope - The scope of records to serialize. Defaults to 'document'.
*
* @public
*/
getSnapshot(scope: RecordScope | 'all' = 'document'): StoreSnapshot<R> {
getStoreSnapshot(scope: RecordScope | 'all' = 'document'): StoreSnapshot<R> {
return {
store: this.serialize(scope),
schema: this.schema.serialize(),
}
}
/**
* @deprecated use `getSnapshot` from the 'tldraw' package instead.
*/
getSnapshot(scope: RecordScope | 'all' = 'document') {
console.warn(
'[tldraw] `Store.getSnapshot` is deprecated and will be removed in a future release. Use `getSnapshot` from the `tldraw` package instead.'
)
return this.getStoreSnapshot(scope)
}
/**
* Migrate a serialized snapshot of the store and its schema.
*
@ -528,14 +538,14 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
* Load a serialized snapshot.
*
* ```ts
* const snapshot = store.getSnapshot()
* store.loadSnapshot(snapshot)
* const snapshot = store.getStoreSnapshot()
* store.loadStoreSnapshot(snapshot)
* ```
*
* @param snapshot - The snapshot to load.
* @public
*/
loadSnapshot(snapshot: StoreSnapshot<R>): void {
loadStoreSnapshot(snapshot: StoreSnapshot<R>): void {
const migrationResult = this.schema.migrateStoreSnapshot(snapshot)
if (migrationResult.type === 'error') {
@ -555,6 +565,17 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
}
}
/**
* @public
* @deprecated use `loadSnapshot` from the 'tldraw' package instead.
*/
loadSnapshot(snapshot: StoreSnapshot<R>) {
console.warn(
"[tldraw] `Store.loadSnapshot` is deprecated and will be removed in a future release. Use `loadSnapshot` from the 'tldraw' package instead."
)
this.loadStoreSnapshot(snapshot)
}
/**
* Get an array of all values in the store.
*

View file

@ -789,7 +789,7 @@ describe('snapshots', () => {
const serializedStore1 = store.serialize('all')
const serializedSchema1 = store.schema.serialize()
const snapshot1 = store.getSnapshot()
const snapshot1 = store.getStoreSnapshot()
const store2 = new Store({
props: {},
@ -799,11 +799,11 @@ describe('snapshots', () => {
}),
})
store2.loadSnapshot(snapshot1)
store2.loadStoreSnapshot(snapshot1)
const serializedStore2 = store2.serialize('all')
const serializedSchema2 = store2.schema.serialize()
const snapshot2 = store2.getSnapshot()
const snapshot2 = store2.getStoreSnapshot()
expect(serializedStore1).toEqual(serializedStore2)
expect(serializedSchema1).toEqual(serializedSchema2)
@ -811,7 +811,7 @@ describe('snapshots', () => {
})
it('throws errors when loading a snapshot with a different schema', () => {
const snapshot1 = store.getSnapshot()
const snapshot1 = store.getStoreSnapshot()
const store2 = new Store({
props: {},
@ -823,12 +823,12 @@ describe('snapshots', () => {
expect(() => {
// @ts-expect-error
store2.loadSnapshot(snapshot1)
store2.loadStoreSnapshot(snapshot1)
}).toThrowErrorMatchingInlineSnapshot(`"Missing definition for record type author"`)
})
it('throws errors when loading a snapshot with a different schema', () => {
const snapshot1 = store.getSnapshot()
const snapshot1 = store.getStoreSnapshot()
const store2 = new Store({
props: {},
@ -838,12 +838,12 @@ describe('snapshots', () => {
})
expect(() => {
store2.loadSnapshot(snapshot1 as any)
store2.loadStoreSnapshot(snapshot1 as any)
}).toThrowErrorMatchingInlineSnapshot(`"Missing definition for record type author"`)
})
it('migrates the snapshot', () => {
const snapshot1 = store.getSnapshot()
const snapshot1 = store.getStoreSnapshot()
const up = jest.fn((s: any) => {
s['book:lotr'].numPages = 42
})
@ -876,7 +876,7 @@ describe('snapshots', () => {
})
expect(() => {
store2.loadSnapshot(snapshot1)
store2.loadStoreSnapshot(snapshot1)
}).not.toThrow()
expect(up).toHaveBeenCalledTimes(1)
@ -1059,20 +1059,20 @@ describe('diffs', () => {
Book.create({ title: 'The Hobbit', id: bookId, author: authorId, numPages: 300 }),
])
const checkpoint1 = store.getSnapshot()
const checkpoint1 = store.getStoreSnapshot()
const forwardsDiff = store.extractingChanges(() => {
store.remove([authorId])
store.update(bookId, (book) => ({ ...book, title: 'The Hobbit: There and Back Again' }))
})
const checkpoint2 = store.getSnapshot()
const checkpoint2 = store.getStoreSnapshot()
store.applyDiff(reverseRecordsDiff(forwardsDiff))
expect(store.getSnapshot()).toEqual(checkpoint1)
expect(store.getStoreSnapshot()).toEqual(checkpoint1)
store.applyDiff(forwardsDiff)
expect(store.getSnapshot()).toEqual(checkpoint2)
expect(store.getStoreSnapshot()).toEqual(checkpoint2)
})
})

View file

@ -49,7 +49,6 @@ import { SerializedSchema } from '@tldraw/editor';
import { ShapeUtil } from '@tldraw/editor';
import { SharedStyle } from '@tldraw/editor';
import { StateNode } from '@tldraw/editor';
import { StoreSnapshot } from '@tldraw/editor';
import { StyleProp } from '@tldraw/editor';
import { SvgExportContext } from '@tldraw/editor';
import { T } from '@tldraw/editor';
@ -76,6 +75,7 @@ import { TldrawEditorBaseProps } from '@tldraw/editor';
import { TLDrawShape } from '@tldraw/editor';
import { TLDrawShapeSegment } from '@tldraw/editor';
import { TLEditorComponents } from '@tldraw/editor';
import { TLEditorSnapshot } from '@tldraw/editor';
import { TLEmbedShape } from '@tldraw/editor';
import { TLEnterEventHandler } from '@tldraw/editor';
import { TLEventHandlers } from '@tldraw/editor';
@ -105,7 +105,6 @@ import { TLPointerEvent } from '@tldraw/editor';
import { TLPointerEventInfo } from '@tldraw/editor';
import { TLPointerEventName } from '@tldraw/editor';
import { TLPropsMigrations } from '@tldraw/editor';
import { TLRecord } from '@tldraw/editor';
import { TLRotationSnapshot } from '@tldraw/editor';
import { TLSchema } from '@tldraw/editor';
import { TLScribbleProps } from '@tldraw/editor';
@ -119,6 +118,7 @@ import { TLShapeUtilCanBindOpts } from '@tldraw/editor';
import { TLShapeUtilCanvasSvgDef } from '@tldraw/editor';
import { TLShapeUtilFlag } from '@tldraw/editor';
import { TLStore } from '@tldraw/editor';
import { TLStoreSnapshot } from '@tldraw/editor';
import { TLStoreWithStatus } from '@tldraw/editor';
import { TLSvgOptions } from '@tldraw/editor';
import { TLTextShape } from '@tldraw/editor';
@ -1545,7 +1545,7 @@ pageId?: TLPageId | undefined;
preserveAspectRatio?: string | undefined;
scale?: number | undefined;
shapeUtils?: readonly TLAnyShapeUtilConstructor[] | undefined;
snapshot: StoreSnapshot<TLRecord>;
snapshot: TLEditorSnapshot | TLStoreSnapshot;
}>;
// @public
@ -1554,14 +1554,14 @@ export type TldrawImageProps = Expand<{
shapeUtils?: readonly TLAnyShapeUtilConstructor[];
format?: 'png' | 'svg';
pageId?: TLPageId;
snapshot: StoreSnapshot<TLRecord>;
snapshot: TLEditorSnapshot | TLStoreSnapshot;
} & Partial<TLSvgOptions>>;
// @public (undocumented)
export type TldrawProps = Expand<(Omit<TldrawUiProps, 'components'> & Omit<TldrawEditorBaseProps, 'components'> & {
components?: TLComponents;
}) & Partial<TLExternalContentProps> & ({
snapshot?: StoreSnapshot<TLRecord>;
snapshot?: TLEditorSnapshot | TLStoreSnapshot;
defaultName?: string;
migrations?: readonly MigrationSequence[];
persistenceKey?: string;

View file

@ -6,11 +6,11 @@ import {
Expand,
LoadingScreen,
MigrationSequence,
StoreSnapshot,
TLEditorComponents,
TLEditorSnapshot,
TLOnMountHandler,
TLRecord,
TLStore,
TLStoreSnapshot,
TLStoreWithStatus,
TldrawEditor,
TldrawEditorBaseProps,
@ -66,7 +66,7 @@ export type TldrawProps = Expand<
/**
* A snapshot to load for the store's initial data / schema.
*/
snapshot?: StoreSnapshot<TLRecord>
snapshot?: TLEditorSnapshot | TLStoreSnapshot
}
)
>

View file

@ -3,11 +3,11 @@ import {
ErrorScreen,
Expand,
LoadingScreen,
StoreSnapshot,
TLAnyBindingUtilConstructor,
TLAnyShapeUtilConstructor,
TLEditorSnapshot,
TLPageId,
TLRecord,
TLStoreSnapshot,
TLSvgOptions,
useShallowArrayIdentity,
useTLStore,
@ -29,7 +29,7 @@ export type TldrawImageProps = Expand<
/**
* The snapshot to display.
*/
snapshot: StoreSnapshot<TLRecord>
snapshot: TLEditorSnapshot | TLStoreSnapshot
/**
* The image format to use. Defaults to 'svg'.

View file

@ -5,6 +5,8 @@ import {
TLShape,
createShapeId,
debounce,
getSnapshot,
loadSnapshot,
} from '@tldraw/editor'
import { TestEditor } from './TestEditor'
import { TL } from './test-jsx'
@ -583,11 +585,11 @@ describe('snapshots', () => {
// now serialize
const snapshot = editor.store.getSnapshot()
const snapshot = getSnapshot(editor.store)
const newEditor = new TestEditor()
newEditor.store.loadSnapshot(snapshot)
loadSnapshot(newEditor.store, snapshot)
expect(editor.store.serialize()).toEqual(newEditor.store.serialize())
})

View file

@ -769,6 +769,9 @@ export const PageRecordType: RecordType<TLPage, "index" | "name">;
// @public (undocumented)
export const parentIdValidator: T.Validator<TLParentId>;
// @internal (undocumented)
export const pluckPreservingValues: (val?: null | TLInstance) => null | Partial<TLInstance>;
// @public (undocumented)
export const PointerRecordType: RecordType<TLPointer, never>;

View file

@ -1,4 +1,4 @@
import { Migration, MigrationId, Store, UnknownRecord } from '@tldraw/store'
import { Migration, MigrationId } from '@tldraw/store'
import { structuredClone } from '@tldraw/utils'
import { createTLSchema } from '../createTLSchema'
@ -24,29 +24,6 @@ for (const migration of testSchema.sortedMigrations) {
}
}
function getEmptySnapshot() {
const store = new Store({
schema: testSchema,
props: null as any,
})
store.ensureStoreIsUsable()
return store.getSnapshot()
}
export function snapshotUp(migrationId: MigrationId, ...records: UnknownRecord[]) {
const migration = testSchema.sortedMigrations.find((m) => m.id === migrationId) as Migration
if (!migration) {
throw new Error(`Migration ${migrationId} not found`)
}
const snapshot = getEmptySnapshot()
for (const record of records) {
snapshot.store[record.id as any] = structuredClone(record as any)
}
const result = migration.up(snapshot.store as any)
return result ?? snapshot.store
}
export function getTestMigration(migrationId: MigrationId) {
const migration = testSchema.sortedMigrations.find((m) => m.id === migrationId) as Migration
if (!migration) {

View file

@ -68,7 +68,12 @@ export {
} from './records/TLBinding'
export { CameraRecordType, type TLCamera, type TLCameraId } from './records/TLCamera'
export { DocumentRecordType, TLDOCUMENT_ID, type TLDocument } from './records/TLDocument'
export { TLINSTANCE_ID, type TLInstance, type TLInstanceId } from './records/TLInstance'
export {
TLINSTANCE_ID,
pluckPreservingValues,
type TLInstance,
type TLInstanceId,
} from './records/TLInstance'
export {
PageRecordType,
isPageId,

View file

@ -5,7 +5,7 @@ import {
createRecordType,
RecordId,
} from '@tldraw/store'
import { JsonObject } from '@tldraw/utils'
import { filterEntries, JsonObject } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { BoxModel, boxModelValidator } from '../misc/geometry-types'
import { idValidator } from '../misc/id-validator'
@ -69,6 +69,54 @@ export interface TLInstance extends BaseRecord<'instance', TLInstanceId> {
} | null
}
/** @internal */
export const shouldKeyBePreservedBetweenSessions = {
// This object defines keys that should be preserved across calls to loadSnapshot()
id: false, // meta
typeName: false, // meta
currentPageId: false, // does not preserve because who knows if the page still exists
opacityForNextShape: false, // does not preserve because it's a temporary state
stylesForNextShape: false, // does not preserve because it's a temporary state
followingUserId: false, // does not preserve because it's a temporary state
highlightedUserIds: false, // does not preserve because it's a temporary state
brush: false, // does not preserve because it's a temporary state
cursor: false, // does not preserve because it's a temporary state
scribbles: false, // does not preserve because it's a temporary state
isFocusMode: true, // preserves because it's a user preference
isDebugMode: true, // preserves because it's a user preference
isToolLocked: true, // preserves because it's a user preference
exportBackground: true, // preserves because it's a user preference
screenBounds: true, // preserves because it's capturing the user's screen state
insets: true, // preserves because it's capturing the user's screen state
zoomBrush: false, // does not preserve because it's a temporary state
chatMessage: false, // does not preserve because it's a temporary state
isChatting: false, // does not preserve because it's a temporary state
isPenMode: false, // does not preserve because it's a temporary state
isGridMode: true, // preserves because it's a user preference
isFocused: true, // preserves because obviously
devicePixelRatio: true, // preserves because it captures the user's screen state
isCoarsePointer: true, // preserves because it captures the user's screen state
isHoveringCanvas: false, // does not preserve because it's a temporary state
openMenus: false, // does not preserve because it's a temporary state
isChangingStyle: false, // does not preserve because it's a temporary state
isReadonly: true, // preserves because it's a config option
meta: false, // does not preserve because who knows what's in there, leave it up to sdk users to save and reinstate
duplicateProps: false, //
} as const satisfies { [K in keyof TLInstance]: boolean }
/** @internal */
export const pluckPreservingValues = (val?: TLInstance | null): null | Partial<TLInstance> =>
val
? (filterEntries(val, (key) => {
return shouldKeyBePreservedBetweenSessions[key as keyof TLInstance]
}) as Partial<TLInstance>)
: null
/** @public */
export type TLInstanceId = RecordId<TLInstance>