2023-04-25 11:01:25 +00:00
|
|
|
import { Store, StoreSchema, StoreSchemaOptions, StoreSnapshot } from '@tldraw/tlstore'
|
|
|
|
import { annotateError, structuredClone } from '@tldraw/utils'
|
|
|
|
import { TLRecord } from './TLRecord'
|
2023-05-26 13:37:59 +00:00
|
|
|
import { CameraRecordType } from './records/TLCamera'
|
|
|
|
import { DocumentRecordType, TLDOCUMENT_ID } from './records/TLDocument'
|
|
|
|
import { InstanceRecordType, TLInstanceId } from './records/TLInstance'
|
|
|
|
import { InstancePageStateRecordType } from './records/TLInstancePageState'
|
|
|
|
import { PageRecordType } from './records/TLPage'
|
|
|
|
import { PointerRecordType, TLPOINTER_ID } from './records/TLPointer'
|
|
|
|
import { UserDocumentRecordType } from './records/TLUserDocument'
|
2023-04-25 11:01:25 +00:00
|
|
|
|
|
|
|
function sortByIndex<T extends { index: string }>(a: T, b: T) {
|
|
|
|
if (a.index < b.index) {
|
|
|
|
return -1
|
|
|
|
} else if (a.index > b.index) {
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
return 0
|
|
|
|
}
|
|
|
|
|
|
|
|
function redactRecordForErrorReporting(record: any) {
|
|
|
|
if (record.typeName === 'asset') {
|
|
|
|
if ('src' in record) {
|
|
|
|
record.src = '<redacted>'
|
|
|
|
}
|
|
|
|
|
|
|
|
if ('src' in record.props) {
|
|
|
|
record.props.src = '<redacted>'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @public */
|
|
|
|
export type TLStoreSchema = StoreSchema<TLRecord, TLStoreProps>
|
|
|
|
|
|
|
|
/** @public */
|
|
|
|
export type TLStoreSnapshot = StoreSnapshot<TLRecord>
|
|
|
|
|
|
|
|
/** @public */
|
|
|
|
export type TLStoreProps = {
|
|
|
|
instanceId: TLInstanceId
|
|
|
|
documentId: typeof TLDOCUMENT_ID
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @public */
|
|
|
|
export type TLStore = Store<TLRecord, TLStoreProps>
|
|
|
|
|
|
|
|
/** @public */
|
|
|
|
export const onValidationFailure: StoreSchemaOptions<
|
|
|
|
TLRecord,
|
|
|
|
TLStoreProps
|
|
|
|
>['onValidationFailure'] = ({ error, phase, record, recordBefore }): TLRecord => {
|
|
|
|
const isExistingValidationIssue =
|
|
|
|
// if we're initializing the store for the first time, we should
|
|
|
|
// allow invalid records so people can load old buggy data:
|
|
|
|
phase === 'initialize'
|
|
|
|
|
|
|
|
annotateError(error, {
|
|
|
|
tags: {
|
|
|
|
origin: 'store.validateRecord',
|
|
|
|
storePhase: phase,
|
|
|
|
isExistingValidationIssue,
|
|
|
|
},
|
|
|
|
extras: {
|
|
|
|
recordBefore: recordBefore
|
|
|
|
? redactRecordForErrorReporting(structuredClone(recordBefore))
|
|
|
|
: undefined,
|
|
|
|
recordAfter: redactRecordForErrorReporting(structuredClone(record)),
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
throw error
|
|
|
|
}
|
|
|
|
|
|
|
|
function getDefaultPages() {
|
2023-05-26 13:37:59 +00:00
|
|
|
return [PageRecordType.create({ name: 'Page 1', index: 'a1' })]
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/** @internal */
|
2023-05-12 11:39:36 +00:00
|
|
|
export function createIntegrityChecker(store: TLStore): () => void {
|
|
|
|
const $pages = store.query.records('page')
|
2023-05-25 09:54:29 +00:00
|
|
|
const $userDocumentSettings = store.query.record('user_document')
|
2023-05-12 11:39:36 +00:00
|
|
|
|
|
|
|
const $instanceState = store.query.record('instance', () => ({
|
|
|
|
id: { eq: store.props.instanceId },
|
|
|
|
}))
|
|
|
|
|
|
|
|
const $instancePageStates = store.query.records('instance_page_state')
|
|
|
|
|
|
|
|
const ensureStoreIsUsable = (): void => {
|
2023-05-25 09:54:29 +00:00
|
|
|
const { instanceId: tabId } = store.props
|
2023-05-12 11:39:36 +00:00
|
|
|
// make sure we have exactly one document
|
|
|
|
if (!store.has(TLDOCUMENT_ID)) {
|
2023-05-26 13:37:59 +00:00
|
|
|
store.put([DocumentRecordType.create({ id: TLDOCUMENT_ID })])
|
2023-05-12 11:39:36 +00:00
|
|
|
return ensureStoreIsUsable()
|
|
|
|
}
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-05-25 09:54:29 +00:00
|
|
|
if (!store.has(TLPOINTER_ID)) {
|
2023-05-26 13:37:59 +00:00
|
|
|
store.put([PointerRecordType.create({ id: TLPOINTER_ID })])
|
2023-05-25 09:54:29 +00:00
|
|
|
return ensureStoreIsUsable()
|
|
|
|
}
|
|
|
|
|
2023-05-12 11:39:36 +00:00
|
|
|
// make sure we have document state for the current user
|
|
|
|
const userDocumentSettings = $userDocumentSettings.value
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-05-12 11:39:36 +00:00
|
|
|
if (!userDocumentSettings) {
|
2023-05-26 13:37:59 +00:00
|
|
|
store.put([UserDocumentRecordType.create({})])
|
2023-05-12 11:39:36 +00:00
|
|
|
return ensureStoreIsUsable()
|
|
|
|
}
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-05-12 11:39:36 +00:00
|
|
|
// make sure there is at least one page
|
|
|
|
const pages = $pages.value.sort(sortByIndex)
|
|
|
|
if (pages.length === 0) {
|
|
|
|
store.put(getDefaultPages())
|
|
|
|
return ensureStoreIsUsable()
|
|
|
|
}
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-05-12 11:39:36 +00:00
|
|
|
// make sure we have state for the current user's current tab
|
|
|
|
const instanceState = $instanceState.value
|
|
|
|
if (!instanceState) {
|
|
|
|
// The tab props are either the the last used tab's props or undefined
|
|
|
|
const propsForNextShape = userDocumentSettings.lastUsedTabId
|
|
|
|
? store.get(userDocumentSettings.lastUsedTabId)?.propsForNextShape
|
|
|
|
: undefined
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-05-12 11:39:36 +00:00
|
|
|
// The current page is either the last updated page or the first page
|
|
|
|
const currentPageId = userDocumentSettings?.lastUpdatedPageId ?? pages[0].id!
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-05-12 11:39:36 +00:00
|
|
|
store.put([
|
2023-05-26 13:37:59 +00:00
|
|
|
InstanceRecordType.create({
|
2023-05-12 11:39:36 +00:00
|
|
|
id: tabId,
|
|
|
|
currentPageId,
|
|
|
|
propsForNextShape,
|
|
|
|
exportBackground: true,
|
|
|
|
}),
|
|
|
|
])
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-05-12 11:39:36 +00:00
|
|
|
return ensureStoreIsUsable()
|
|
|
|
}
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-05-12 11:39:36 +00:00
|
|
|
// make sure the user's currentPageId is still valid
|
|
|
|
let currentPageId = instanceState.currentPageId
|
|
|
|
if (!pages.find((p) => p.id === currentPageId)) {
|
|
|
|
currentPageId = pages[0].id!
|
|
|
|
store.put([{ ...instanceState, currentPageId }])
|
|
|
|
return ensureStoreIsUsable()
|
|
|
|
}
|
2023-04-25 11:01:25 +00:00
|
|
|
|
2023-05-12 11:39:36 +00:00
|
|
|
for (const page of pages) {
|
|
|
|
const instancePageStates = $instancePageStates.value.filter(
|
|
|
|
(tps) => tps.pageId === page.id && tps.instanceId === tabId
|
|
|
|
)
|
|
|
|
if (instancePageStates.length > 1) {
|
|
|
|
// make sure we only have one instancePageState per instance per page
|
|
|
|
store.remove(instancePageStates.slice(1).map((ips) => ips.id))
|
|
|
|
} else if (instancePageStates.length === 0) {
|
2023-05-26 13:37:59 +00:00
|
|
|
const camera = CameraRecordType.create({})
|
2023-05-12 11:39:36 +00:00
|
|
|
store.put([
|
|
|
|
camera,
|
2023-05-26 13:37:59 +00:00
|
|
|
InstancePageStateRecordType.create({
|
|
|
|
pageId: page.id,
|
|
|
|
instanceId: tabId,
|
|
|
|
cameraId: camera.id,
|
|
|
|
}),
|
2023-05-12 11:39:36 +00:00
|
|
|
])
|
|
|
|
return ensureStoreIsUsable()
|
|
|
|
}
|
|
|
|
|
|
|
|
// make sure the camera exists
|
|
|
|
const camera = store.get(instancePageStates[0].cameraId)
|
|
|
|
if (!camera) {
|
2023-05-26 13:37:59 +00:00
|
|
|
store.put([CameraRecordType.create({ id: instancePageStates[0].cameraId })])
|
2023-05-12 11:39:36 +00:00
|
|
|
return ensureStoreIsUsable()
|
|
|
|
}
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|
|
|
|
}
|
2023-05-12 11:39:36 +00:00
|
|
|
|
|
|
|
return ensureStoreIsUsable
|
2023-04-25 11:01:25 +00:00
|
|
|
}
|