kopia lustrzana https://github.com/Tldraw/Tldraw
169 wiersze
4.8 KiB
TypeScript
169 wiersze
4.8 KiB
TypeScript
import { InstanceRecordType, PageRecordType, TLInstanceId } from '@tldraw/tlschema'
|
|
import { promiseWithResolve } from '@tldraw/utils'
|
|
import { createTLStore } from '../../config/createTLStore'
|
|
import { TLLocalSyncClient } from './TLLocalSyncClient'
|
|
import * as idb from './indexedDb'
|
|
|
|
jest.mock('./indexedDb', () => ({
|
|
...jest.requireActual('./indexedDb'),
|
|
storeSnapshotInIndexedDb: jest.fn(() => Promise.resolve()),
|
|
storeChangesInIndexedDb: jest.fn(() => Promise.resolve()),
|
|
}))
|
|
|
|
class BroadcastChannelMock {
|
|
onmessage?: (e: MessageEvent) => void
|
|
constructor(_name: string) {
|
|
// noop
|
|
}
|
|
postMessage = jest.fn((_msg: any) => {
|
|
// noop
|
|
})
|
|
close = jest.fn(() => {
|
|
// noop
|
|
})
|
|
}
|
|
|
|
function testClient(
|
|
instanceId: TLInstanceId = InstanceRecordType.createCustomId('test'),
|
|
channel = new BroadcastChannelMock('test')
|
|
) {
|
|
const store = createTLStore({
|
|
instanceId,
|
|
})
|
|
const onLoad = jest.fn(() => {
|
|
return
|
|
})
|
|
const onLoadError = jest.fn(() => {
|
|
return
|
|
})
|
|
const client = new TLLocalSyncClient(
|
|
store,
|
|
{
|
|
onLoad,
|
|
onLoadError,
|
|
universalPersistenceKey: 'test',
|
|
},
|
|
channel
|
|
)
|
|
return { client, store, onLoad, onLoadError, channel }
|
|
}
|
|
|
|
const reloadMock = jest.fn()
|
|
|
|
beforeAll(() => {
|
|
Object.defineProperty(window, 'location', {
|
|
configurable: true,
|
|
value: { reload: reloadMock },
|
|
})
|
|
})
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
|
|
jest.useFakeTimers()
|
|
|
|
const tick = async () => {
|
|
jest.advanceTimersByTime(1000)
|
|
await Promise.resolve()
|
|
}
|
|
|
|
test('the client connects on instantiation, announcing its schema', async () => {
|
|
const { channel } = testClient()
|
|
await tick()
|
|
expect(channel.postMessage).toHaveBeenCalledTimes(1)
|
|
const [msg] = channel.postMessage.mock.calls[0]
|
|
|
|
expect(msg).toMatchObject({ type: 'announce', schema: { recordVersions: {} } })
|
|
})
|
|
|
|
test('when a client receives an announce with a newer schema version it reloads itself', async () => {
|
|
const { client, channel, onLoadError } = testClient()
|
|
await tick()
|
|
jest.advanceTimersByTime(10000)
|
|
expect(reloadMock).not.toHaveBeenCalled()
|
|
channel.onmessage?.({
|
|
data: {
|
|
type: 'announce',
|
|
schema: {
|
|
...client.serializedSchema,
|
|
schemaVersion: client.serializedSchema.schemaVersion + 1,
|
|
},
|
|
},
|
|
} as any)
|
|
expect(reloadMock).toHaveBeenCalled()
|
|
expect(onLoadError).not.toHaveBeenCalled()
|
|
})
|
|
|
|
test('when a client receives an announce with a newer schema version shortly after loading it does not reload but instead reports a loadError', async () => {
|
|
const { client, channel, onLoadError } = testClient()
|
|
await tick()
|
|
jest.advanceTimersByTime(1000)
|
|
expect(reloadMock).not.toHaveBeenCalled()
|
|
channel.onmessage?.({
|
|
data: {
|
|
type: 'announce',
|
|
schema: {
|
|
...client.serializedSchema,
|
|
schemaVersion: client.serializedSchema.schemaVersion + 1,
|
|
},
|
|
},
|
|
} as any)
|
|
expect(reloadMock).not.toHaveBeenCalled()
|
|
expect(onLoadError).toHaveBeenCalled()
|
|
})
|
|
|
|
test('the first db write after a client connects is a full db overwrite', async () => {
|
|
const { client } = testClient()
|
|
await tick()
|
|
client.store.put([PageRecordType.create({ name: 'test', index: 'a0' })])
|
|
await tick()
|
|
expect(idb.storeSnapshotInIndexedDb).toHaveBeenCalledTimes(1)
|
|
expect(idb.storeChangesInIndexedDb).not.toHaveBeenCalled()
|
|
|
|
client.store.put([PageRecordType.create({ name: 'test2', index: 'a1' })])
|
|
await tick()
|
|
expect(idb.storeSnapshotInIndexedDb).toHaveBeenCalledTimes(1)
|
|
expect(idb.storeChangesInIndexedDb).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
test('it clears the diff queue after every write', async () => {
|
|
const { client } = testClient()
|
|
await tick()
|
|
client.store.put([PageRecordType.create({ name: 'test', index: 'a0' })])
|
|
await tick()
|
|
// @ts-expect-error
|
|
expect(client.diffQueue.length).toBe(0)
|
|
|
|
client.store.put([PageRecordType.create({ name: 'test2', index: 'a1' })])
|
|
await tick()
|
|
// @ts-expect-error
|
|
expect(client.diffQueue.length).toBe(0)
|
|
})
|
|
|
|
test('writes that come in during a persist operation will get persisted afterward', async () => {
|
|
const idbOperationResult = promiseWithResolve<void>()
|
|
;(idb.storeSnapshotInIndexedDb as jest.Mock).mockImplementationOnce(() => idbOperationResult)
|
|
|
|
const { client } = testClient()
|
|
await tick()
|
|
client.store.put([PageRecordType.create({ name: 'test', index: 'a0' })])
|
|
await tick()
|
|
|
|
// we should have called into idb but not resolved the promise yet
|
|
expect(idb.storeSnapshotInIndexedDb).toHaveBeenCalledTimes(1)
|
|
expect(idb.storeChangesInIndexedDb).toHaveBeenCalledTimes(0)
|
|
|
|
// if another change comes in, loads of time can pass, but nothing else should get called
|
|
client.store.put([PageRecordType.create({ name: 'test', index: 'a2' })])
|
|
await tick()
|
|
expect(idb.storeSnapshotInIndexedDb).toHaveBeenCalledTimes(1)
|
|
expect(idb.storeChangesInIndexedDb).toHaveBeenCalledTimes(0)
|
|
|
|
// if we resolve the idb operation, the next change should get persisted
|
|
idbOperationResult.resolve()
|
|
await tick()
|
|
await tick()
|
|
expect(idb.storeChangesInIndexedDb).toHaveBeenCalledTimes(1)
|
|
})
|