@tldraw/sync-core 4.3.0-canary.c7096a59bf3b → 4.3.0-canary.d039f3a1ab8f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +239 -57
- package/dist-cjs/index.js +7 -3
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/InMemorySyncStorage.js +289 -0
- package/dist-cjs/lib/InMemorySyncStorage.js.map +7 -0
- package/dist-cjs/lib/RoomSession.js.map +1 -1
- package/dist-cjs/lib/TLSocketRoom.js +117 -69
- package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
- package/dist-cjs/lib/TLSyncClient.js +7 -0
- package/dist-cjs/lib/TLSyncClient.js.map +2 -2
- package/dist-cjs/lib/TLSyncRoom.js +357 -688
- package/dist-cjs/lib/TLSyncRoom.js.map +3 -3
- package/dist-cjs/lib/TLSyncStorage.js +76 -0
- package/dist-cjs/lib/TLSyncStorage.js.map +7 -0
- package/dist-cjs/lib/recordDiff.js +52 -0
- package/dist-cjs/lib/recordDiff.js.map +7 -0
- package/dist-esm/index.d.mts +239 -57
- package/dist-esm/index.mjs +12 -5
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/InMemorySyncStorage.mjs +274 -0
- package/dist-esm/lib/InMemorySyncStorage.mjs.map +7 -0
- package/dist-esm/lib/RoomSession.mjs.map +1 -1
- package/dist-esm/lib/TLSocketRoom.mjs +121 -70
- package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
- package/dist-esm/lib/TLSyncClient.mjs +7 -0
- package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
- package/dist-esm/lib/TLSyncRoom.mjs +370 -702
- package/dist-esm/lib/TLSyncRoom.mjs.map +3 -3
- package/dist-esm/lib/TLSyncStorage.mjs +56 -0
- package/dist-esm/lib/TLSyncStorage.mjs.map +7 -0
- package/dist-esm/lib/recordDiff.mjs +32 -0
- package/dist-esm/lib/recordDiff.mjs.map +7 -0
- package/package.json +6 -6
- package/src/index.ts +21 -3
- package/src/lib/InMemorySyncStorage.ts +357 -0
- package/src/lib/RoomSession.test.ts +1 -0
- package/src/lib/RoomSession.ts +2 -0
- package/src/lib/TLSocketRoom.ts +228 -114
- package/src/lib/TLSyncClient.ts +12 -0
- package/src/lib/TLSyncRoom.ts +473 -913
- package/src/lib/TLSyncStorage.ts +216 -0
- package/src/lib/recordDiff.ts +73 -0
- package/src/test/InMemorySyncStorage.test.ts +1674 -0
- package/src/test/TLSocketRoom.test.ts +255 -49
- package/src/test/TLSyncRoom.test.ts +1021 -533
- package/src/test/TestServer.ts +12 -1
- package/src/test/customMessages.test.ts +1 -1
- package/src/test/presenceMode.test.ts +6 -6
- package/src/test/upgradeDowngrade.test.ts +282 -8
- package/src/test/validation.test.ts +10 -10
- package/src/test/pruneTombstones.test.ts +0 -178
|
@@ -1,11 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-deprecated */
|
|
2
|
+
import {
|
|
3
|
+
InstancePresenceRecordType,
|
|
4
|
+
PageRecordType,
|
|
5
|
+
TLDocument,
|
|
6
|
+
TLPage,
|
|
7
|
+
TLRecord,
|
|
8
|
+
} from '@tldraw/tlschema'
|
|
9
|
+
import {
|
|
10
|
+
createTLSchema,
|
|
11
|
+
createTLStore,
|
|
12
|
+
IndexKey,
|
|
13
|
+
promiseWithResolve,
|
|
14
|
+
sortById,
|
|
15
|
+
ZERO_INDEX_KEY,
|
|
16
|
+
} from 'tldraw'
|
|
3
17
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
18
|
import { RecordOpType } from '../lib/diff'
|
|
19
|
+
import { DEFAULT_INITIAL_SNAPSHOT, InMemorySyncStorage } from '../lib/InMemorySyncStorage'
|
|
5
20
|
import { getTlsyncProtocolVersion } from '../lib/protocol'
|
|
6
21
|
import { WebSocketMinimal } from '../lib/ServerSocketAdapter'
|
|
7
22
|
import { TLSocketRoom, TLSyncLog } from '../lib/TLSocketRoom'
|
|
8
23
|
import { TLSyncErrorCloseEventReason } from '../lib/TLSyncClient'
|
|
24
|
+
import { RoomSnapshot } from '../lib/TLSyncRoom'
|
|
9
25
|
|
|
10
26
|
function getStore() {
|
|
11
27
|
const schema = createTLSchema()
|
|
@@ -41,7 +57,7 @@ describe(TLSocketRoom, () => {
|
|
|
41
57
|
initialSnapshot: snapshot,
|
|
42
58
|
})
|
|
43
59
|
expect(room.getCurrentSnapshot()).not.toMatchObject({ clock: 0, documents: [] })
|
|
44
|
-
expect(room.getCurrentSnapshot().
|
|
60
|
+
expect(room.getCurrentSnapshot().documentClock).toBe(0)
|
|
45
61
|
expect(room.getCurrentSnapshot().documents.sort((a, b) => a.state.id.localeCompare(b.state.id)))
|
|
46
62
|
.toMatchInlineSnapshot(`
|
|
47
63
|
[
|
|
@@ -75,7 +91,7 @@ describe(TLSocketRoom, () => {
|
|
|
75
91
|
initialSnapshot: store.getStoreSnapshot(),
|
|
76
92
|
})
|
|
77
93
|
|
|
78
|
-
expect(room.getCurrentSnapshot()).toMatchObject({
|
|
94
|
+
expect(room.getCurrentSnapshot()).toMatchObject({ documentClock: 0, documents: [] })
|
|
79
95
|
|
|
80
96
|
// populate with an empty document (document:document and page:page records)
|
|
81
97
|
store.ensureStoreIsUsable()
|
|
@@ -83,7 +99,7 @@ describe(TLSocketRoom, () => {
|
|
|
83
99
|
const snapshot = store.getStoreSnapshot()
|
|
84
100
|
room.loadSnapshot(snapshot)
|
|
85
101
|
|
|
86
|
-
expect(room.getCurrentSnapshot().
|
|
102
|
+
expect(room.getCurrentSnapshot().documentClock).toBe(1)
|
|
87
103
|
expect(room.getCurrentSnapshot().documents.sort((a, b) => a.state.id.localeCompare(b.state.id)))
|
|
88
104
|
.toMatchInlineSnapshot(`
|
|
89
105
|
[
|
|
@@ -287,50 +303,38 @@ describe(TLSocketRoom, () => {
|
|
|
287
303
|
})
|
|
288
304
|
|
|
289
305
|
describe('Room state resetting behavior', () => {
|
|
290
|
-
it('
|
|
306
|
+
it('increments documentClock when loading snapshot with different data', () => {
|
|
291
307
|
const store = getStore()
|
|
292
308
|
store.ensureStoreIsUsable()
|
|
293
309
|
const room = new TLSocketRoom({
|
|
294
310
|
initialSnapshot: store.getStoreSnapshot(),
|
|
295
311
|
})
|
|
296
312
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
room.loadSnapshot(snapshot)
|
|
313
|
+
const oldClock = room.getCurrentDocumentClock()
|
|
314
|
+
expect(oldClock).toBe(0)
|
|
300
315
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
// Reset with a new snapshot
|
|
316
|
+
// Add a new page to make the snapshot different
|
|
317
|
+
store.put([PageRecordType.create({ name: 'New Page', index: 'a2' as IndexKey })])
|
|
305
318
|
const newSnapshot = store.getStoreSnapshot()
|
|
306
319
|
room.loadSnapshot(newSnapshot)
|
|
307
320
|
|
|
308
|
-
|
|
309
|
-
expect(newSnapshotResult.documentClock).toBe(oldClock + 1)
|
|
310
|
-
expect(newSnapshotResult.clock).toBe(oldClock + 1)
|
|
321
|
+
expect(room.getCurrentDocumentClock()).toBe(oldClock + 1)
|
|
311
322
|
})
|
|
312
323
|
|
|
313
|
-
it('
|
|
324
|
+
it('does not increment documentClock when loading identical snapshot', () => {
|
|
314
325
|
const store = getStore()
|
|
315
326
|
store.ensureStoreIsUsable()
|
|
316
327
|
const room = new TLSocketRoom({
|
|
317
328
|
initialSnapshot: store.getStoreSnapshot(),
|
|
318
329
|
})
|
|
319
330
|
|
|
320
|
-
|
|
321
|
-
const initialClock = room.getCurrentSnapshot().clock
|
|
322
|
-
|
|
323
|
-
// Reset with a new snapshot
|
|
324
|
-
const newSnapshot = store.getStoreSnapshot()
|
|
325
|
-
room.loadSnapshot(newSnapshot)
|
|
331
|
+
const oldClock = room.getCurrentDocumentClock()
|
|
326
332
|
|
|
327
|
-
|
|
328
|
-
|
|
333
|
+
// Load the same snapshot again
|
|
334
|
+
room.loadSnapshot(store.getStoreSnapshot())
|
|
329
335
|
|
|
330
|
-
//
|
|
331
|
-
|
|
332
|
-
expect(doc.lastChangedClock).toBe(initialClock + 1)
|
|
333
|
-
}
|
|
336
|
+
// Clock should not change since data is identical
|
|
337
|
+
expect(room.getCurrentDocumentClock()).toBe(oldClock)
|
|
334
338
|
})
|
|
335
339
|
|
|
336
340
|
it('preserves existing tombstones with original clock values', async () => {
|
|
@@ -356,30 +360,13 @@ describe(TLSocketRoom, () => {
|
|
|
356
360
|
|
|
357
361
|
room.loadSnapshot(room.getCurrentSnapshot())
|
|
358
362
|
|
|
363
|
+
// Tombstones should be preserved
|
|
359
364
|
expect(room.getCurrentSnapshot().tombstones).toEqual({
|
|
360
365
|
[testPageId]: deletionClock,
|
|
361
366
|
})
|
|
362
367
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
it('handles empty snapshot reset correctly', () => {
|
|
367
|
-
const store = getStore()
|
|
368
|
-
// Don't call ensureStoreIsUsable to get an empty snapshot
|
|
369
|
-
const room = new TLSocketRoom({
|
|
370
|
-
initialSnapshot: store.getStoreSnapshot(),
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
const oldClock = room.getCurrentSnapshot().clock
|
|
374
|
-
|
|
375
|
-
// Reset with empty snapshot
|
|
376
|
-
const emptySnapshot = store.getStoreSnapshot()
|
|
377
|
-
room.loadSnapshot(emptySnapshot)
|
|
378
|
-
|
|
379
|
-
const result = room.getCurrentSnapshot()
|
|
380
|
-
expect(result.documentClock).toBe(oldClock + 1)
|
|
381
|
-
expect(result.clock).toBe(oldClock + 1)
|
|
382
|
-
expect(result.documents).toHaveLength(0)
|
|
368
|
+
// Clock should not change since we loaded the same snapshot
|
|
369
|
+
expect(room.getCurrentSnapshot().documentClock).toBe(deletionClock)
|
|
383
370
|
})
|
|
384
371
|
|
|
385
372
|
it('preserves schema when resetting room state', () => {
|
|
@@ -817,3 +804,222 @@ describe(TLSocketRoom, () => {
|
|
|
817
804
|
})
|
|
818
805
|
})
|
|
819
806
|
})
|
|
807
|
+
|
|
808
|
+
describe('TLSocketRoom.updateStore', () => {
|
|
809
|
+
let storage = new InMemorySyncStorage<TLRecord>({ snapshot: DEFAULT_INITIAL_SNAPSHOT })
|
|
810
|
+
|
|
811
|
+
let room = new TLSocketRoom<TLRecord, undefined>({ storage })
|
|
812
|
+
function init(snapshot?: RoomSnapshot) {
|
|
813
|
+
storage = new InMemorySyncStorage<TLRecord>({ snapshot: snapshot ?? DEFAULT_INITIAL_SNAPSHOT })
|
|
814
|
+
room = new TLSocketRoom<TLRecord, undefined>({
|
|
815
|
+
storage,
|
|
816
|
+
})
|
|
817
|
+
}
|
|
818
|
+
beforeEach(() => {
|
|
819
|
+
init()
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
test('it allows updating records', async () => {
|
|
823
|
+
const clock = storage.getClock()
|
|
824
|
+
await room.updateStore((store) => {
|
|
825
|
+
const document = store.get('document:document') as TLDocument
|
|
826
|
+
document.name = 'My lovely document'
|
|
827
|
+
store.put(document)
|
|
828
|
+
})
|
|
829
|
+
expect(
|
|
830
|
+
(
|
|
831
|
+
storage.getSnapshot().documents.find((r) => r.state.id === 'document:document')
|
|
832
|
+
?.state as any
|
|
833
|
+
).name
|
|
834
|
+
).toBe('My lovely document')
|
|
835
|
+
expect(clock).toBeLessThan(storage.getClock())
|
|
836
|
+
})
|
|
837
|
+
|
|
838
|
+
test('it does not update unless you call .set', () => {
|
|
839
|
+
const clock = storage.getClock()
|
|
840
|
+
room.updateStore((store) => {
|
|
841
|
+
const document = store.get('document:document') as TLDocument
|
|
842
|
+
document.name = 'My lovely document'
|
|
843
|
+
})
|
|
844
|
+
expect(
|
|
845
|
+
(
|
|
846
|
+
storage.getSnapshot().documents.find((r) => r.state.id === 'document:document')
|
|
847
|
+
?.state as any
|
|
848
|
+
).name
|
|
849
|
+
).toBe('')
|
|
850
|
+
expect(clock).toBe(storage.getClock())
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
test('triggers onChange events on the storage if something changed', async () => {
|
|
854
|
+
const onChange = vi.fn()
|
|
855
|
+
storage.onChange(onChange)
|
|
856
|
+
await room.updateStore((store) => {
|
|
857
|
+
const document = store.get('document:document') as TLDocument
|
|
858
|
+
document.name = 'My lovely document'
|
|
859
|
+
store.put(document)
|
|
860
|
+
})
|
|
861
|
+
expect(onChange).toHaveBeenCalled()
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
test('does not trigger onChange events if the change is not committed', async () => {
|
|
865
|
+
const onChange = vi.fn()
|
|
866
|
+
storage.onChange(onChange)
|
|
867
|
+
room.updateStore((store) => {
|
|
868
|
+
const document = store.get('document:document') as TLDocument
|
|
869
|
+
document.name = 'My lovely document'
|
|
870
|
+
})
|
|
871
|
+
expect(onChange).not.toHaveBeenCalled()
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
test('it allows adding new records', async () => {
|
|
875
|
+
const id = PageRecordType.createId('page_3')
|
|
876
|
+
await room.updateStore((store) => {
|
|
877
|
+
const page = PageRecordType.create({ id, name: 'page 3', index: 'a0' as IndexKey })
|
|
878
|
+
store.put(page)
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
expect(storage.getSnapshot().documents.find((r) => r.state.id === id)?.state).toBeTruthy()
|
|
882
|
+
})
|
|
883
|
+
|
|
884
|
+
test('it allows deleting records', async () => {
|
|
885
|
+
await room.updateStore((store) => {
|
|
886
|
+
store.delete('page:page_2')
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
expect(storage.getSnapshot().documents.find((r) => r.state.id === 'page:page_2')).toBeFalsy()
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
test('it returns all records if you ask for them', async () => {
|
|
893
|
+
let allRecords
|
|
894
|
+
await room.updateStore((store) => {
|
|
895
|
+
allRecords = store.getAll()
|
|
896
|
+
})
|
|
897
|
+
expect(allRecords!.sort(sortById)).toEqual(
|
|
898
|
+
storage
|
|
899
|
+
.getSnapshot()
|
|
900
|
+
.documents.map((r) => r.state)
|
|
901
|
+
.sort(sortById)
|
|
902
|
+
)
|
|
903
|
+
await room.updateStore((store) => {
|
|
904
|
+
const page3 = PageRecordType.create({ name: 'page 3', index: 'a0' as IndexKey })
|
|
905
|
+
store.put(page3)
|
|
906
|
+
allRecords = store.getAll()
|
|
907
|
+
expect(allRecords.sort(sortById)).toEqual(
|
|
908
|
+
[...storage.getSnapshot().documents.map((r) => r.state), page3].sort(sortById)
|
|
909
|
+
)
|
|
910
|
+
store.delete(page3)
|
|
911
|
+
allRecords = store.getAll()
|
|
912
|
+
})
|
|
913
|
+
expect(allRecords!.sort(sortById)).toEqual(
|
|
914
|
+
storage
|
|
915
|
+
.getSnapshot()
|
|
916
|
+
.documents.map((r) => r.state)
|
|
917
|
+
.sort(sortById)
|
|
918
|
+
)
|
|
919
|
+
})
|
|
920
|
+
|
|
921
|
+
test('all operations fail after the store is closed', async () => {
|
|
922
|
+
let store
|
|
923
|
+
await room.updateStore((s) => {
|
|
924
|
+
store = s
|
|
925
|
+
})
|
|
926
|
+
expect(() => {
|
|
927
|
+
store!.put(PageRecordType.create({ name: 'page 3', index: 'a0' as IndexKey }))
|
|
928
|
+
}).toThrowErrorMatchingInlineSnapshot(`[Error: StoreUpdateContext is closed]`)
|
|
929
|
+
expect(() => {
|
|
930
|
+
store!.delete('page:page_2')
|
|
931
|
+
}).toThrowErrorMatchingInlineSnapshot(`[Error: StoreUpdateContext is closed]`)
|
|
932
|
+
expect(() => {
|
|
933
|
+
store!.getAll()
|
|
934
|
+
}).toThrowErrorMatchingInlineSnapshot(`[Error: StoreUpdateContext is closed]`)
|
|
935
|
+
expect(() => {
|
|
936
|
+
store!.get('page:page_2')
|
|
937
|
+
}).toThrowErrorMatchingInlineSnapshot(`[Error: StoreUpdateContext is closed]`)
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
test('it fails if the room is closed', async () => {
|
|
941
|
+
room.close()
|
|
942
|
+
await expect(
|
|
943
|
+
room.updateStore(() => {
|
|
944
|
+
// noop
|
|
945
|
+
})
|
|
946
|
+
).rejects.toMatchInlineSnapshot(`[Error: Cannot update store on a closed room]`)
|
|
947
|
+
})
|
|
948
|
+
|
|
949
|
+
test('it fails if you try to add bad data', async () => {
|
|
950
|
+
await expect(
|
|
951
|
+
room.updateStore((store) => {
|
|
952
|
+
const page = store.get('page:page') as TLPage
|
|
953
|
+
page.index = 34 as any
|
|
954
|
+
store.put(page)
|
|
955
|
+
})
|
|
956
|
+
).rejects.toMatchInlineSnapshot(
|
|
957
|
+
`[ValidationError: At page.index: Expected string, got a number]`
|
|
958
|
+
)
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
test('changes in multiple transaction are isolated from one another', async () => {
|
|
962
|
+
const page3 = PageRecordType.create({ name: 'page 3', index: 'a0' as IndexKey })
|
|
963
|
+
const didDelete = promiseWithResolve()
|
|
964
|
+
const didPut = promiseWithResolve()
|
|
965
|
+
const doneA = room.updateStore(async (store) => {
|
|
966
|
+
store.put(page3)
|
|
967
|
+
didPut.resolve(null)
|
|
968
|
+
await didDelete
|
|
969
|
+
expect(store.get(page3.id)).toBeTruthy()
|
|
970
|
+
})
|
|
971
|
+
const doneB = room.updateStore(async (store) => {
|
|
972
|
+
await didPut
|
|
973
|
+
expect(store.get(page3.id)).toBeFalsy()
|
|
974
|
+
store.delete(page3.id)
|
|
975
|
+
didDelete.resolve(null)
|
|
976
|
+
})
|
|
977
|
+
await Promise.all([doneA, doneB])
|
|
978
|
+
})
|
|
979
|
+
|
|
980
|
+
test('getting something that was deleted in the same transaction returns null', async () => {
|
|
981
|
+
await room.updateStore((store) => {
|
|
982
|
+
expect(store.get('page:page')).toBeTruthy()
|
|
983
|
+
store.delete('page:page')
|
|
984
|
+
expect(store.get('page:page')).toBe(null)
|
|
985
|
+
})
|
|
986
|
+
})
|
|
987
|
+
|
|
988
|
+
test('getting something that never existed in the first place returns null', async () => {
|
|
989
|
+
await room.updateStore((store) => {
|
|
990
|
+
expect(store.get('page:page_3')).toBe(null)
|
|
991
|
+
})
|
|
992
|
+
})
|
|
993
|
+
|
|
994
|
+
test('mutations to shapes gotten via .get are not committed unless you .put', async () => {
|
|
995
|
+
const page3 = PageRecordType.create({ name: 'page 3', index: 'a0' as IndexKey })
|
|
996
|
+
let page4 = PageRecordType.create({ name: 'page 4', index: 'a1' as IndexKey })
|
|
997
|
+
let page1
|
|
998
|
+
await room.updateStore((store) => {
|
|
999
|
+
page1 = store.get('page:page') as TLPage
|
|
1000
|
+
page1.name = 'my lovely page 1'
|
|
1001
|
+
store.put(page3)
|
|
1002
|
+
page3.name = 'my lovely page 3'
|
|
1003
|
+
store.put(page4)
|
|
1004
|
+
page4 = store.get(page4.id) as TLPage
|
|
1005
|
+
page4.name = 'my lovely page 4'
|
|
1006
|
+
})
|
|
1007
|
+
|
|
1008
|
+
const getPageNames = () =>
|
|
1009
|
+
room
|
|
1010
|
+
.getCurrentSnapshot()
|
|
1011
|
+
.documents.filter((r) => r.state.typeName === 'page')
|
|
1012
|
+
.map((r) => (r.state as any).name)
|
|
1013
|
+
.sort()
|
|
1014
|
+
|
|
1015
|
+
expect(getPageNames()).toEqual(['Page 1', 'page 3', 'page 4'])
|
|
1016
|
+
|
|
1017
|
+
await room.updateStore((store) => {
|
|
1018
|
+
store.put(page1!)
|
|
1019
|
+
store.put(page3)
|
|
1020
|
+
store.put(page4)
|
|
1021
|
+
})
|
|
1022
|
+
|
|
1023
|
+
expect(getPageNames()).toEqual(['my lovely page 1', 'my lovely page 3', 'my lovely page 4'])
|
|
1024
|
+
})
|
|
1025
|
+
})
|