[improvement] store snapshot types (#1657)

This PR improves the types for the Store.

- renames `StoreSnapshot` to `SerializedStore`, which is the return type
of `Store.serialize`
- creates `StoreSnapshot` as a type for the return type of
`Store.getSnapshot` / the argument type for `Store.loadSnapshot`
- creates `TLStoreSnapshot` as the type used for the `TLStore`.

This came out of a session I had with a user. This should prevent
needing to import types from `@tldraw/store` directly.

### Change Type

- [x] `major` — Breaking change

### Test Plan

- [x] Unit Tests

### Release Notes

- [dev] Rename `StoreSnapshot` to `SerializedStore`
- [dev] Create new `StoreSnapshot` as type related to
`getSnapshot`/`loadSnapshot`
This commit is contained in:
Steve Ruiz 2023-06-27 13:25:55 +01:00 committed by GitHub
parent faecd88220
commit ed8d4d9e05
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 71 additions and 57 deletions

View file

@ -34,10 +34,10 @@ import { SelectionCorner } from '@tldraw/primitives';
import { SelectionEdge } from '@tldraw/primitives';
import { SelectionHandle } from '@tldraw/primitives';
import { SerializedSchema } from '@tldraw/store';
import { SerializedStore } from '@tldraw/store';
import { ShapeProps } from '@tldraw/tlschema';
import { Signal } from '@tldraw/state';
import { StoreSchema } from '@tldraw/store';
import { StoreSnapshot } from '@tldraw/store';
import { StrokePoint } from '@tldraw/primitives';
import { StyleProp } from '@tldraw/tlschema';
import { TLArrowShape } from '@tldraw/tlschema';
@ -2228,7 +2228,7 @@ export type TldrawEditorProps = {
store: TLStore | TLStoreWithStatus;
} | {
store?: undefined;
initialData?: StoreSnapshot<TLRecord>;
initialData?: SerializedStore<TLRecord>;
persistenceKey?: string;
sessionId?: string;
defaultName?: string;
@ -2661,7 +2661,7 @@ export type TLStoreEventInfo = HistoryEntry<TLRecord>;
// @public (undocumented)
export type TLStoreOptions = {
initialData?: StoreSnapshot<TLRecord>;
initialData?: SerializedStore<TLRecord>;
defaultName?: string;
} & ({
schema: StoreSchema<TLRecord, TLStoreProps>;

View file

@ -1,4 +1,4 @@
import { Store, StoreSnapshot } from '@tldraw/store'
import { SerializedStore, Store } from '@tldraw/store'
import { TLRecord, TLStore } from '@tldraw/tlschema'
import { RecursivePartial, Required, annotateError } from '@tldraw/utils'
import React, {
@ -70,7 +70,7 @@ export type TldrawEditorProps = {
/**
* The editor's initial data.
*/
initialData?: StoreSnapshot<TLRecord>
initialData?: SerializedStore<TLRecord>
/**
* The id under which to sync and persist the editor's data. If none is given tldraw will not sync or persist
* the editor's data.

View file

@ -1,11 +1,11 @@
import { HistoryEntry, Store, StoreSchema, StoreSnapshot } from '@tldraw/store'
import { HistoryEntry, SerializedStore, Store, StoreSchema } from '@tldraw/store'
import { TLRecord, TLStore, TLStoreProps, createTLSchema } from '@tldraw/tlschema'
import { checkShapesAndAddCore } from './defaultShapes'
import { AnyTLShapeInfo, TLShapeInfo } from './defineShape'
/** @public */
export type TLStoreOptions = {
initialData?: StoreSnapshot<TLRecord>
initialData?: SerializedStore<TLRecord>
defaultName?: string
} & ({ shapes: readonly AnyTLShapeInfo[] } | { schema: StoreSchema<TLRecord, TLStoreProps> })

View file

@ -1,4 +1,4 @@
import { RecordsDiff, SerializedSchema, StoreSnapshot } from '@tldraw/store'
import { RecordsDiff, SerializedSchema, SerializedStore } from '@tldraw/store'
import { TLRecord, TLStoreSchema } from '@tldraw/tlschema'
import { IDBPDatabase, openDB } from 'idb'
import { TLSessionStateSnapshot } from '../../config/TLSessionStateSnapshot'
@ -156,7 +156,7 @@ export async function storeSnapshotInIndexedDb({
}: {
persistenceKey: string
schema: TLStoreSchema
snapshot: StoreSnapshot<any>
snapshot: SerializedStore<any>
sessionId?: string | null
sessionStateSnapshot?: TLSessionStateSnapshot | null
didCancel?: () => boolean

View file

@ -13,7 +13,7 @@ import {
MigrationResult,
RecordId,
SerializedSchema,
StoreSnapshot,
SerializedStore,
UnknownRecord,
} from '@tldraw/store'
import { TLUiToastsContextType, TLUiTranslationKey } from '@tldraw/ui'
@ -119,7 +119,7 @@ export function parseTldrawJsonFile({
// even if the file version is up to date, it might contain old-format
// records. lets create a store with the records and migrate it to the
// latest version
let migrationResult: MigrationResult<StoreSnapshot<TLRecord>>
let migrationResult: MigrationResult<SerializedStore<TLRecord>>
try {
const storeSnapshot = Object.fromEntries(data.records.map((r) => [r.id, r as TLRecord]))
migrationResult = schema.migrateStoreSnapshot(storeSnapshot, data.schema)

View file

@ -213,13 +213,16 @@ export interface SerializedSchema {
storeVersion: number;
}
// @public
export type SerializedStore<R extends UnknownRecord> = Record<IdOf<R>, R>;
// @public
export function squashRecordDiffs<T extends UnknownRecord>(diffs: RecordsDiff<T>[]): RecordsDiff<T>;
// @public
export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
constructor(config: {
initialData?: StoreSnapshot<R>;
initialData?: SerializedStore<R>;
schema: StoreSchema<R, Props>;
props: Props;
});
@ -241,20 +244,14 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
// (undocumented)
_flushHistory(): void;
get: <K extends IdOf<R>>(id: K) => RecFromId<K> | undefined;
getSnapshot(scope?: 'all' | RecordScope): {
store: StoreSnapshot<R>;
schema: SerializedSchema;
};
getSnapshot(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;
loadSnapshot(snapshot: {
store: StoreSnapshot<R>;
schema: SerializedSchema;
}): void;
loadSnapshot(snapshot: StoreSnapshot<R>): void;
// @internal (undocumented)
markAsPossiblyCorrupted(): void;
mergeRemoteChanges: (fn: () => void) => void;
@ -273,7 +270,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
readonly scopedTypes: {
readonly [K in RecordScope]: ReadonlySet<R['typeName']>;
};
serialize: (scope?: 'all' | RecordScope) => StoreSnapshot<R>;
serialize: (scope?: 'all' | RecordScope) => SerializedStore<R>;
unsafeGetWithoutCapture: <K extends IdOf<R>>(id: K) => RecFromId<K> | undefined;
update: <K extends IdOf<R>>(id: K, updater: (record: RecFromId<K>) => RecFromId<K>) => void;
// (undocumented)
@ -307,7 +304,7 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
// (undocumented)
migratePersistedRecord(record: R, persistedSchema: SerializedSchema, direction?: 'down' | 'up'): MigrationResult<R>;
// (undocumented)
migrateStoreSnapshot(storeSnapshot: StoreSnapshot<R>, persistedSchema: SerializedSchema): MigrationResult<StoreSnapshot<R>>;
migrateStoreSnapshot(storeSnapshot: SerializedStore<R>, persistedSchema: SerializedSchema): MigrationResult<SerializedStore<R>>;
// (undocumented)
serialize(): SerializedSchema;
// (undocumented)
@ -333,8 +330,11 @@ export type StoreSchemaOptions<R extends UnknownRecord, P> = {
createIntegrityChecker?: (store: Store<R, P>) => void;
};
// @public
export type StoreSnapshot<R extends UnknownRecord> = Record<IdOf<R>, R>;
// @public (undocumented)
export type StoreSnapshot<R extends UnknownRecord> = {
store: SerializedStore<R>;
schema: SerializedSchema;
};
// @public (undocumented)
export type StoreValidator<R extends UnknownRecord> = {

View file

@ -7,6 +7,7 @@ export type {
ComputedCache,
HistoryEntry,
RecordsDiff,
SerializedStore,
StoreError,
StoreListener,
StoreSnapshot,

View file

@ -73,7 +73,13 @@ export type ComputedCache<Data, R extends UnknownRecord> = {
*
* @public
*/
export type StoreSnapshot<R extends UnknownRecord> = Record<IdOf<R>, R>
export type SerializedStore<R extends UnknownRecord> = Record<IdOf<R>, R>
/** @public */
export type StoreSnapshot<R extends UnknownRecord> = {
store: SerializedStore<R>
schema: SerializedSchema
}
/** @public */
export type StoreValidator<R extends UnknownRecord> = {
@ -163,7 +169,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
constructor(config: {
/** The store's initial data. */
initialData?: StoreSnapshot<R>
initialData?: SerializedStore<R>
/**
* A map of validators for each record type. A record's validator will be called when the record
* is created or updated. It should throw an error if the record is invalid.
@ -503,8 +509,8 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
* @param scope - The scope of records to serialize. Defaults to 'document'.
* @returns The record store snapshot as a JSON payload.
*/
serialize = (scope: RecordScope | 'all' = 'document'): StoreSnapshot<R> => {
const result = {} as StoreSnapshot<R>
serialize = (scope: RecordScope | 'all' = 'document'): SerializedStore<R> => {
const result = {} as SerializedStore<R>
for (const [id, atom] of objectMapEntries(this.atoms.value)) {
const record = atom.value
if (scope === 'all' || this.scopedTypes[scope].has(record.typeName)) {
@ -525,7 +531,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
* @param scope - The scope of records to serialize. Defaults to 'document'.
* @public
*/
getSnapshot(scope: RecordScope | 'all' = 'document') {
getSnapshot(scope: RecordScope | 'all' = 'document'): StoreSnapshot<R> {
return {
store: this.serialize(scope),
schema: this.schema.serialize(),
@ -544,7 +550,7 @@ export class Store<R extends UnknownRecord = UnknownRecord, Props = unknown> {
*
* @public
*/
loadSnapshot(snapshot: { store: StoreSnapshot<R>; schema: SerializedSchema }): void {
loadSnapshot(snapshot: StoreSnapshot<R>): void {
const migrationResult = this.schema.migrateStoreSnapshot(snapshot.store, snapshot.schema)
if (migrationResult.type === 'error') {

View file

@ -1,7 +1,7 @@
import { getOwnProperty, objectMapValues } from '@tldraw/utils'
import { IdOf, UnknownRecord } from './BaseRecord'
import { RecordType } from './RecordType'
import { Store, StoreSnapshot } from './Store'
import { SerializedStore, Store } from './Store'
import {
MigrationFailureReason,
MigrationResult,
@ -189,9 +189,9 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
}
migrateStoreSnapshot(
storeSnapshot: StoreSnapshot<R>,
storeSnapshot: SerializedStore<R>,
persistedSchema: SerializedSchema
): MigrationResult<StoreSnapshot<R>> {
): MigrationResult<SerializedStore<R>> {
const migrations = this.options.snapshotMigrations
if (!migrations) {
return { type: 'success', value: storeSnapshot }
@ -205,7 +205,7 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
}
if (ourStoreVersion > persistedStoreVersion) {
const result = migrate<StoreSnapshot<R>>({
const result = migrate<SerializedStore<R>>({
value: storeSnapshot,
migrations,
fromVersion: persistedStoreVersion,

View file

@ -1,5 +1,5 @@
import { MigrationFailureReason } from '../migrate'
import { StoreSnapshot } from '../Store'
import { SerializedStore } from '../Store'
import { testSchemaV0 } from './testSchema.v0'
import { testSchemaV1 } from './testSchema.v1'
@ -305,7 +305,7 @@ test('subtype versions in the future fail', () => {
})
test('migrating a whole store snapshot works', () => {
const snapshot: StoreSnapshot<any> = {
const snapshot: SerializedStore<any> = {
'user-1': {
id: 'user-1',
typeName: 'user',

View file

@ -1,7 +1,7 @@
import { assert } from '@tldraw/utils'
import { BaseRecord, RecordId } from '../BaseRecord'
import { createRecordType } from '../RecordType'
import { StoreSnapshot } from '../Store'
import { SerializedStore } from '../Store'
import { StoreSchema } from '../StoreSchema'
import { defineMigrations } from '../migrate'
@ -203,10 +203,10 @@ const snapshotMigrations = defineMigrations({
currentVersion: StoreVersions.RemoveOrg,
migrators: {
[StoreVersions.RemoveOrg]: {
up: (store: StoreSnapshot<any>) => {
up: (store: SerializedStore<any>) => {
return Object.fromEntries(Object.entries(store).filter(([_, r]) => r.typeName !== 'org'))
},
down: (store: StoreSnapshot<any>) => {
down: (store: SerializedStore<any>) => {
// noop
return store
},

View file

@ -1,6 +1,6 @@
import { BaseRecord, IdOf, RecordId } from '../BaseRecord'
import { createRecordType } from '../RecordType'
import { Store, StoreSnapshot } from '../Store'
import { SerializedStore, Store } from '../Store'
import { StoreSchema } from '../StoreSchema'
interface Book extends BaseRecord<'book', RecordId<Book>> {
@ -90,7 +90,7 @@ describe('Store with validation', () => {
})
describe('Validating initial data', () => {
let snapshot: StoreSnapshot<Book | Author>
let snapshot: SerializedStore<Book | Author>
beforeEach(() => {
const authorId = Author.createId('tolkein')

View file

@ -9,10 +9,10 @@ import { Expand } from '@tldraw/utils';
import { Migrations } from '@tldraw/store';
import { RecordId } from '@tldraw/store';
import { RecordType } from '@tldraw/store';
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';
@ -110,7 +110,7 @@ export const CameraRecordType: RecordType<TLCamera, never>;
export const canvasUiColorTypeValidator: T.Validator<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">;
// @internal (undocumented)
export function CLIENT_FIXUP_SCRIPT(persistedStore: StoreSnapshot<TLRecord>): StoreSnapshot<TLRecord>;
export function CLIENT_FIXUP_SCRIPT(persistedStore: SerializedStore<TLRecord>): SerializedStore<TLRecord>;
// @public
export function createAssetValidator<Type extends string, Props extends object>(type: Type, props: T.Validator<Props>): T.ObjectValidator<{
@ -1131,6 +1131,9 @@ export type TLScribble = {
delay: number;
};
// @public (undocumented)
export type TLSerializedStore = SerializedStore<TLRecord>;
// @public
export type TLShape = TLDefaultShape | TLUnknownShape;
@ -1162,7 +1165,7 @@ export type TLStoreProps = {
export type TLStoreSchema = StoreSchema<TLRecord, TLStoreProps>;
// @public (undocumented)
export type TLStoreSnapshot = StoreSnapshot<TLRecord>;
export type TLStoreSnapshot = SerializedStore<TLRecord>;
// @public (undocumented)
export type TLTextShape = TLBaseShape<'text', TLTextShapeProps>;

View file

@ -1,4 +1,4 @@
import { Store, StoreSchema, StoreSchemaOptions, StoreSnapshot } from '@tldraw/store'
import { SerializedStore, Store, StoreSchema, StoreSchemaOptions } from '@tldraw/store'
import { annotateError, structuredClone } from '@tldraw/utils'
import { CameraRecordType, TLCameraId } from './records/TLCamera'
import { DocumentRecordType, TLDOCUMENT_ID } from './records/TLDocument'
@ -33,7 +33,10 @@ function redactRecordForErrorReporting(record: any) {
export type TLStoreSchema = StoreSchema<TLRecord, TLStoreProps>
/** @public */
export type TLStoreSnapshot = StoreSnapshot<TLRecord>
export type TLSerializedStore = SerializedStore<TLRecord>
/** @public */
export type TLStoreSnapshot = SerializedStore<TLRecord>
/** @public */
export type TLStoreProps = {

View file

@ -1,9 +1,9 @@
import { StoreSnapshot } from '@tldraw/store'
import { SerializedStore } from '@tldraw/store'
import { Vec2dModel } from './misc/geometry-types'
import { TLRecord } from './records/TLRecord'
/** @internal */
export function CLIENT_FIXUP_SCRIPT(persistedStore: StoreSnapshot<TLRecord>) {
export function CLIENT_FIXUP_SCRIPT(persistedStore: SerializedStore<TLRecord>) {
const records = Object.values(persistedStore)
for (let i = 0; i < records.length; i++) {

View file

@ -1,4 +1,5 @@
export {
type TLSerializedStore,
type TLStore,
type TLStoreProps,
type TLStoreSchema,

View file

@ -1,4 +1,4 @@
import { defineMigrations, StoreSnapshot } from '@tldraw/store'
import { defineMigrations, SerializedStore } from '@tldraw/store'
import { TLRecord } from './records/TLRecord'
const Versions = {
@ -15,47 +15,47 @@ export const storeMigrations = defineMigrations({
currentVersion: Versions.RemoveUserDocument,
migrators: {
[Versions.RemoveCodeAndIconShapeTypes]: {
up: (store: StoreSnapshot<TLRecord>) => {
up: (store: SerializedStore<TLRecord>) => {
return Object.fromEntries(
Object.entries(store).filter(
([_, v]) => v.typeName !== 'shape' || (v.type !== 'icon' && v.type !== 'code')
)
)
},
down: (store: StoreSnapshot<TLRecord>) => {
down: (store: SerializedStore<TLRecord>) => {
// noop
return store
},
},
[Versions.AddInstancePresenceType]: {
up: (store: StoreSnapshot<TLRecord>) => {
up: (store: SerializedStore<TLRecord>) => {
return store
},
down: (store: StoreSnapshot<TLRecord>) => {
down: (store: SerializedStore<TLRecord>) => {
return Object.fromEntries(
Object.entries(store).filter(([_, v]) => v.typeName !== 'instance_presence')
)
},
},
[Versions.RemoveTLUserAndPresenceAndAddPointer]: {
up: (store: StoreSnapshot<TLRecord>) => {
up: (store: SerializedStore<TLRecord>) => {
return Object.fromEntries(
Object.entries(store).filter(([_, v]) => !v.typeName.match(/^(user|user_presence)$/))
)
},
down: (store: StoreSnapshot<TLRecord>) => {
down: (store: SerializedStore<TLRecord>) => {
return Object.fromEntries(
Object.entries(store).filter(([_, v]) => v.typeName !== 'pointer')
)
},
},
[Versions.RemoveUserDocument]: {
up: (store: StoreSnapshot<TLRecord>) => {
up: (store: SerializedStore<TLRecord>) => {
return Object.fromEntries(
Object.entries(store).filter(([_, v]) => !v.typeName.match('user_document'))
)
},
down: (store: StoreSnapshot<TLRecord>) => {
down: (store: SerializedStore<TLRecord>) => {
return store
},
},