From ed8d4d9e05fd30a1f36b8ca398c4d784470d7990 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Tue, 27 Jun 2023 13:25:55 +0100 Subject: [PATCH] [improvement] store snapshot types (#1657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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` --- packages/editor/api-report.md | 6 ++--- packages/editor/src/lib/TldrawEditor.tsx | 4 +-- .../editor/src/lib/config/createTLStore.ts | 4 +-- .../editor/src/lib/utils/sync/indexedDb.ts | 4 +-- packages/file-format/src/lib/file.ts | 4 +-- packages/store/api-report.md | 26 +++++++++---------- packages/store/src/index.ts | 1 + packages/store/src/lib/Store.ts | 18 ++++++++----- packages/store/src/lib/StoreSchema.ts | 8 +++--- packages/store/src/lib/test/migrate.test.ts | 4 +-- packages/store/src/lib/test/testSchema.v1.ts | 6 ++--- packages/store/src/lib/test/validate.test.ts | 4 +-- packages/tlschema/api-report.md | 9 ++++--- packages/tlschema/src/TLStore.ts | 7 +++-- packages/tlschema/src/fixup.ts | 4 +-- packages/tlschema/src/index.ts | 1 + packages/tlschema/src/store-migrations.ts | 18 ++++++------- 17 files changed, 71 insertions(+), 57 deletions(-) diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 818de3c12..4f267aa61 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -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; + initialData?: SerializedStore; persistenceKey?: string; sessionId?: string; defaultName?: string; @@ -2661,7 +2661,7 @@ export type TLStoreEventInfo = HistoryEntry; // @public (undocumented) export type TLStoreOptions = { - initialData?: StoreSnapshot; + initialData?: SerializedStore; defaultName?: string; } & ({ schema: StoreSchema; diff --git a/packages/editor/src/lib/TldrawEditor.tsx b/packages/editor/src/lib/TldrawEditor.tsx index 5fa4bbe1a..abd5d5067 100644 --- a/packages/editor/src/lib/TldrawEditor.tsx +++ b/packages/editor/src/lib/TldrawEditor.tsx @@ -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 + initialData?: SerializedStore /** * 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. diff --git a/packages/editor/src/lib/config/createTLStore.ts b/packages/editor/src/lib/config/createTLStore.ts index 3fba6ed7a..e5589ab4d 100644 --- a/packages/editor/src/lib/config/createTLStore.ts +++ b/packages/editor/src/lib/config/createTLStore.ts @@ -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 + initialData?: SerializedStore defaultName?: string } & ({ shapes: readonly AnyTLShapeInfo[] } | { schema: StoreSchema }) diff --git a/packages/editor/src/lib/utils/sync/indexedDb.ts b/packages/editor/src/lib/utils/sync/indexedDb.ts index b0e8c0b63..39c215d54 100644 --- a/packages/editor/src/lib/utils/sync/indexedDb.ts +++ b/packages/editor/src/lib/utils/sync/indexedDb.ts @@ -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 + snapshot: SerializedStore sessionId?: string | null sessionStateSnapshot?: TLSessionStateSnapshot | null didCancel?: () => boolean diff --git a/packages/file-format/src/lib/file.ts b/packages/file-format/src/lib/file.ts index 9ff98e789..57d5b0e29 100644 --- a/packages/file-format/src/lib/file.ts +++ b/packages/file-format/src/lib/file.ts @@ -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> + let migrationResult: MigrationResult> try { const storeSnapshot = Object.fromEntries(data.records.map((r) => [r.id, r as TLRecord])) migrationResult = schema.migrateStoreSnapshot(storeSnapshot, data.schema) diff --git a/packages/store/api-report.md b/packages/store/api-report.md index a446f58f5..329cab88c 100644 --- a/packages/store/api-report.md +++ b/packages/store/api-report.md @@ -213,13 +213,16 @@ export interface SerializedSchema { storeVersion: number; } +// @public +export type SerializedStore = Record, R>; + // @public export function squashRecordDiffs(diffs: RecordsDiff[]): RecordsDiff; // @public export class Store { constructor(config: { - initialData?: StoreSnapshot; + initialData?: SerializedStore; schema: StoreSchema; props: Props; }); @@ -241,20 +244,14 @@ export class Store { // (undocumented) _flushHistory(): void; get: >(id: K) => RecFromId | undefined; - getSnapshot(scope?: 'all' | RecordScope): { - store: StoreSnapshot; - schema: SerializedSchema; - }; + getSnapshot(scope?: 'all' | RecordScope): StoreSnapshot; has: >(id: K) => boolean; readonly history: Atom>; readonly id: string; // @internal (undocumented) isPossiblyCorrupted(): boolean; listen: (onHistory: StoreListener, filters?: Partial) => () => void; - loadSnapshot(snapshot: { - store: StoreSnapshot; - schema: SerializedSchema; - }): void; + loadSnapshot(snapshot: StoreSnapshot): void; // @internal (undocumented) markAsPossiblyCorrupted(): void; mergeRemoteChanges: (fn: () => void) => void; @@ -273,7 +270,7 @@ export class Store { readonly scopedTypes: { readonly [K in RecordScope]: ReadonlySet; }; - serialize: (scope?: 'all' | RecordScope) => StoreSnapshot; + serialize: (scope?: 'all' | RecordScope) => SerializedStore; unsafeGetWithoutCapture: >(id: K) => RecFromId | undefined; update: >(id: K, updater: (record: RecFromId) => RecFromId) => void; // (undocumented) @@ -307,7 +304,7 @@ export class StoreSchema { // (undocumented) migratePersistedRecord(record: R, persistedSchema: SerializedSchema, direction?: 'down' | 'up'): MigrationResult; // (undocumented) - migrateStoreSnapshot(storeSnapshot: StoreSnapshot, persistedSchema: SerializedSchema): MigrationResult>; + migrateStoreSnapshot(storeSnapshot: SerializedStore, persistedSchema: SerializedSchema): MigrationResult>; // (undocumented) serialize(): SerializedSchema; // (undocumented) @@ -333,8 +330,11 @@ export type StoreSchemaOptions = { createIntegrityChecker?: (store: Store) => void; }; -// @public -export type StoreSnapshot = Record, R>; +// @public (undocumented) +export type StoreSnapshot = { + store: SerializedStore; + schema: SerializedSchema; +}; // @public (undocumented) export type StoreValidator = { diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index a5f6494cf..692172fde 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -7,6 +7,7 @@ export type { ComputedCache, HistoryEntry, RecordsDiff, + SerializedStore, StoreError, StoreListener, StoreSnapshot, diff --git a/packages/store/src/lib/Store.ts b/packages/store/src/lib/Store.ts index 21a61d024..4ed3f427a 100644 --- a/packages/store/src/lib/Store.ts +++ b/packages/store/src/lib/Store.ts @@ -73,7 +73,13 @@ export type ComputedCache = { * * @public */ -export type StoreSnapshot = Record, R> +export type SerializedStore = Record, R> + +/** @public */ +export type StoreSnapshot = { + store: SerializedStore + schema: SerializedSchema +} /** @public */ export type StoreValidator = { @@ -163,7 +169,7 @@ export class Store { constructor(config: { /** The store's initial data. */ - initialData?: StoreSnapshot + initialData?: SerializedStore /** * 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 { * @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 => { - const result = {} as StoreSnapshot + serialize = (scope: RecordScope | 'all' = 'document'): SerializedStore => { + const result = {} as SerializedStore 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 { * @param scope - The scope of records to serialize. Defaults to 'document'. * @public */ - getSnapshot(scope: RecordScope | 'all' = 'document') { + getSnapshot(scope: RecordScope | 'all' = 'document'): StoreSnapshot { return { store: this.serialize(scope), schema: this.schema.serialize(), @@ -544,7 +550,7 @@ export class Store { * * @public */ - loadSnapshot(snapshot: { store: StoreSnapshot; schema: SerializedSchema }): void { + loadSnapshot(snapshot: StoreSnapshot): void { const migrationResult = this.schema.migrateStoreSnapshot(snapshot.store, snapshot.schema) if (migrationResult.type === 'error') { diff --git a/packages/store/src/lib/StoreSchema.ts b/packages/store/src/lib/StoreSchema.ts index 8ff3caf34..390a61274 100644 --- a/packages/store/src/lib/StoreSchema.ts +++ b/packages/store/src/lib/StoreSchema.ts @@ -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 { } migrateStoreSnapshot( - storeSnapshot: StoreSnapshot, + storeSnapshot: SerializedStore, persistedSchema: SerializedSchema - ): MigrationResult> { + ): MigrationResult> { const migrations = this.options.snapshotMigrations if (!migrations) { return { type: 'success', value: storeSnapshot } @@ -205,7 +205,7 @@ export class StoreSchema { } if (ourStoreVersion > persistedStoreVersion) { - const result = migrate>({ + const result = migrate>({ value: storeSnapshot, migrations, fromVersion: persistedStoreVersion, diff --git a/packages/store/src/lib/test/migrate.test.ts b/packages/store/src/lib/test/migrate.test.ts index 7ae11d368..500ef34fe 100644 --- a/packages/store/src/lib/test/migrate.test.ts +++ b/packages/store/src/lib/test/migrate.test.ts @@ -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 = { + const snapshot: SerializedStore = { 'user-1': { id: 'user-1', typeName: 'user', diff --git a/packages/store/src/lib/test/testSchema.v1.ts b/packages/store/src/lib/test/testSchema.v1.ts index b4e11d89d..110090c1b 100644 --- a/packages/store/src/lib/test/testSchema.v1.ts +++ b/packages/store/src/lib/test/testSchema.v1.ts @@ -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) => { + up: (store: SerializedStore) => { return Object.fromEntries(Object.entries(store).filter(([_, r]) => r.typeName !== 'org')) }, - down: (store: StoreSnapshot) => { + down: (store: SerializedStore) => { // noop return store }, diff --git a/packages/store/src/lib/test/validate.test.ts b/packages/store/src/lib/test/validate.test.ts index c0fdd8653..062d88555 100644 --- a/packages/store/src/lib/test/validate.test.ts +++ b/packages/store/src/lib/test/validate.test.ts @@ -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> { @@ -90,7 +90,7 @@ describe('Store with validation', () => { }) describe('Validating initial data', () => { - let snapshot: StoreSnapshot + let snapshot: SerializedStore beforeEach(() => { const authorId = Author.createId('tolkein') diff --git a/packages/tlschema/api-report.md b/packages/tlschema/api-report.md index 8f6ccf979..495c6cf5b 100644 --- a/packages/tlschema/api-report.md +++ b/packages/tlschema/api-report.md @@ -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; export const canvasUiColorTypeValidator: T.Validator<"accent" | "black" | "laser" | "muted-1" | "selection-fill" | "selection-stroke" | "white">; // @internal (undocumented) -export function CLIENT_FIXUP_SCRIPT(persistedStore: StoreSnapshot): StoreSnapshot; +export function CLIENT_FIXUP_SCRIPT(persistedStore: SerializedStore): SerializedStore; // @public export function createAssetValidator(type: Type, props: T.Validator): T.ObjectValidator<{ @@ -1131,6 +1131,9 @@ export type TLScribble = { delay: number; }; +// @public (undocumented) +export type TLSerializedStore = SerializedStore; + // @public export type TLShape = TLDefaultShape | TLUnknownShape; @@ -1162,7 +1165,7 @@ export type TLStoreProps = { export type TLStoreSchema = StoreSchema; // @public (undocumented) -export type TLStoreSnapshot = StoreSnapshot; +export type TLStoreSnapshot = SerializedStore; // @public (undocumented) export type TLTextShape = TLBaseShape<'text', TLTextShapeProps>; diff --git a/packages/tlschema/src/TLStore.ts b/packages/tlschema/src/TLStore.ts index 9ac444d85..f1ee38379 100644 --- a/packages/tlschema/src/TLStore.ts +++ b/packages/tlschema/src/TLStore.ts @@ -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 /** @public */ -export type TLStoreSnapshot = StoreSnapshot +export type TLSerializedStore = SerializedStore + +/** @public */ +export type TLStoreSnapshot = SerializedStore /** @public */ export type TLStoreProps = { diff --git a/packages/tlschema/src/fixup.ts b/packages/tlschema/src/fixup.ts index b93f62b1d..3ea9628ec 100644 --- a/packages/tlschema/src/fixup.ts +++ b/packages/tlschema/src/fixup.ts @@ -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) { +export function CLIENT_FIXUP_SCRIPT(persistedStore: SerializedStore) { const records = Object.values(persistedStore) for (let i = 0; i < records.length; i++) { diff --git a/packages/tlschema/src/index.ts b/packages/tlschema/src/index.ts index 720412703..f8ac3b2cc 100644 --- a/packages/tlschema/src/index.ts +++ b/packages/tlschema/src/index.ts @@ -1,4 +1,5 @@ export { + type TLSerializedStore, type TLStore, type TLStoreProps, type TLStoreSchema, diff --git a/packages/tlschema/src/store-migrations.ts b/packages/tlschema/src/store-migrations.ts index 11a580dba..39f06d74d 100644 --- a/packages/tlschema/src/store-migrations.ts +++ b/packages/tlschema/src/store-migrations.ts @@ -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) => { + up: (store: SerializedStore) => { return Object.fromEntries( Object.entries(store).filter( ([_, v]) => v.typeName !== 'shape' || (v.type !== 'icon' && v.type !== 'code') ) ) }, - down: (store: StoreSnapshot) => { + down: (store: SerializedStore) => { // noop return store }, }, [Versions.AddInstancePresenceType]: { - up: (store: StoreSnapshot) => { + up: (store: SerializedStore) => { return store }, - down: (store: StoreSnapshot) => { + down: (store: SerializedStore) => { return Object.fromEntries( Object.entries(store).filter(([_, v]) => v.typeName !== 'instance_presence') ) }, }, [Versions.RemoveTLUserAndPresenceAndAddPointer]: { - up: (store: StoreSnapshot) => { + up: (store: SerializedStore) => { return Object.fromEntries( Object.entries(store).filter(([_, v]) => !v.typeName.match(/^(user|user_presence)$/)) ) }, - down: (store: StoreSnapshot) => { + down: (store: SerializedStore) => { return Object.fromEntries( Object.entries(store).filter(([_, v]) => v.typeName !== 'pointer') ) }, }, [Versions.RemoveUserDocument]: { - up: (store: StoreSnapshot) => { + up: (store: SerializedStore) => { return Object.fromEntries( Object.entries(store).filter(([_, v]) => !v.typeName.match('user_document')) ) }, - down: (store: StoreSnapshot) => { + down: (store: SerializedStore) => { return store }, },