@tldraw/sync-core 4.2.2 → 4.2.3

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.
Files changed (84) hide show
  1. package/dist-cjs/index.d.ts +58 -483
  2. package/dist-cjs/index.js +3 -13
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/RoomSession.js.map +1 -1
  5. package/dist-cjs/lib/TLSocketRoom.js +69 -117
  6. package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
  7. package/dist-cjs/lib/TLSyncClient.js +0 -7
  8. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  9. package/dist-cjs/lib/TLSyncRoom.js +688 -357
  10. package/dist-cjs/lib/TLSyncRoom.js.map +3 -3
  11. package/dist-cjs/lib/chunk.js +2 -2
  12. package/dist-cjs/lib/chunk.js.map +1 -1
  13. package/dist-esm/index.d.mts +58 -483
  14. package/dist-esm/index.mjs +5 -20
  15. package/dist-esm/index.mjs.map +2 -2
  16. package/dist-esm/lib/RoomSession.mjs.map +1 -1
  17. package/dist-esm/lib/TLSocketRoom.mjs +70 -121
  18. package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
  19. package/dist-esm/lib/TLSyncClient.mjs +0 -7
  20. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  21. package/dist-esm/lib/TLSyncRoom.mjs +702 -370
  22. package/dist-esm/lib/TLSyncRoom.mjs.map +3 -3
  23. package/dist-esm/lib/chunk.mjs +2 -2
  24. package/dist-esm/lib/chunk.mjs.map +1 -1
  25. package/package.json +11 -12
  26. package/src/index.ts +3 -32
  27. package/src/lib/ClientWebSocketAdapter.test.ts +0 -3
  28. package/src/lib/RoomSession.test.ts +0 -1
  29. package/src/lib/RoomSession.ts +0 -2
  30. package/src/lib/TLSocketRoom.ts +114 -228
  31. package/src/lib/TLSyncClient.ts +0 -12
  32. package/src/lib/TLSyncRoom.ts +913 -473
  33. package/src/lib/chunk.ts +2 -2
  34. package/src/test/FuzzEditor.ts +5 -4
  35. package/src/test/TLSocketRoom.test.ts +49 -255
  36. package/src/test/TLSyncRoom.test.ts +534 -1024
  37. package/src/test/TestServer.ts +1 -12
  38. package/src/test/customMessages.test.ts +1 -1
  39. package/src/test/presenceMode.test.ts +6 -6
  40. package/src/test/pruneTombstones.test.ts +178 -0
  41. package/src/test/syncFuzz.test.ts +4 -2
  42. package/src/test/upgradeDowngrade.test.ts +8 -290
  43. package/src/test/validation.test.ts +10 -15
  44. package/dist-cjs/lib/DurableObjectSqliteSyncWrapper.js +0 -55
  45. package/dist-cjs/lib/DurableObjectSqliteSyncWrapper.js.map +0 -7
  46. package/dist-cjs/lib/InMemorySyncStorage.js +0 -287
  47. package/dist-cjs/lib/InMemorySyncStorage.js.map +0 -7
  48. package/dist-cjs/lib/MicrotaskNotifier.js +0 -50
  49. package/dist-cjs/lib/MicrotaskNotifier.js.map +0 -7
  50. package/dist-cjs/lib/NodeSqliteWrapper.js +0 -48
  51. package/dist-cjs/lib/NodeSqliteWrapper.js.map +0 -7
  52. package/dist-cjs/lib/SQLiteSyncStorage.js +0 -428
  53. package/dist-cjs/lib/SQLiteSyncStorage.js.map +0 -7
  54. package/dist-cjs/lib/TLSyncStorage.js +0 -76
  55. package/dist-cjs/lib/TLSyncStorage.js.map +0 -7
  56. package/dist-cjs/lib/recordDiff.js +0 -52
  57. package/dist-cjs/lib/recordDiff.js.map +0 -7
  58. package/dist-esm/lib/DurableObjectSqliteSyncWrapper.mjs +0 -35
  59. package/dist-esm/lib/DurableObjectSqliteSyncWrapper.mjs.map +0 -7
  60. package/dist-esm/lib/InMemorySyncStorage.mjs +0 -272
  61. package/dist-esm/lib/InMemorySyncStorage.mjs.map +0 -7
  62. package/dist-esm/lib/MicrotaskNotifier.mjs +0 -30
  63. package/dist-esm/lib/MicrotaskNotifier.mjs.map +0 -7
  64. package/dist-esm/lib/NodeSqliteWrapper.mjs +0 -28
  65. package/dist-esm/lib/NodeSqliteWrapper.mjs.map +0 -7
  66. package/dist-esm/lib/SQLiteSyncStorage.mjs +0 -414
  67. package/dist-esm/lib/SQLiteSyncStorage.mjs.map +0 -7
  68. package/dist-esm/lib/TLSyncStorage.mjs +0 -56
  69. package/dist-esm/lib/TLSyncStorage.mjs.map +0 -7
  70. package/dist-esm/lib/recordDiff.mjs +0 -32
  71. package/dist-esm/lib/recordDiff.mjs.map +0 -7
  72. package/src/lib/DurableObjectSqliteSyncWrapper.ts +0 -95
  73. package/src/lib/InMemorySyncStorage.ts +0 -387
  74. package/src/lib/MicrotaskNotifier.test.ts +0 -429
  75. package/src/lib/MicrotaskNotifier.ts +0 -38
  76. package/src/lib/NodeSqliteSyncWrapper.integration.test.ts +0 -270
  77. package/src/lib/NodeSqliteSyncWrapper.test.ts +0 -272
  78. package/src/lib/NodeSqliteWrapper.ts +0 -99
  79. package/src/lib/SQLiteSyncStorage.ts +0 -627
  80. package/src/lib/TLSyncStorage.ts +0 -216
  81. package/src/lib/computeTombstonePruning.test.ts +0 -352
  82. package/src/lib/recordDiff.ts +0 -73
  83. package/src/test/InMemorySyncStorage.test.ts +0 -1684
  84. package/src/test/SQLiteSyncStorage.test.ts +0 -1378
package/src/lib/chunk.ts CHANGED
@@ -88,8 +88,8 @@ export class JsonChunkAssembler {
88
88
  *
89
89
  * @param msg - The message to process, either JSON or chunk format
90
90
  * @returns Result object with data/stringified on success, error object on failure, or null for incomplete chunks
91
- * - `\{ data: object, stringified: string \}` - Successfully parsed complete message
92
- * - `\{ error: Error \}` - Parse error or invalid chunk sequence
91
+ * - `\{ data: object, stringified: string \}` - Successfully parsed complete message
92
+ * - `\{ error: Error \}` - Parse error or invalid chunk sequence
93
93
  * - `null` - Chunk received but more chunks expected
94
94
  *
95
95
  * @example
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  Editor,
3
3
  PageRecordType,
4
+ TLArrowBinding,
4
5
  TLPage,
5
6
  TLPageId,
6
7
  TLShape,
@@ -40,8 +41,8 @@ export type Op =
40
41
  }
41
42
  | {
42
43
  type: 'create-arrow'
43
- start: VecModel
44
- end: VecModel
44
+ start: TLArrowBinding | VecModel
45
+ end: TLArrowBinding | VecModel
45
46
  }
46
47
  | {
47
48
  type: 'delete-shape'
@@ -314,8 +315,8 @@ export class FuzzEditor extends RandomSource {
314
315
  x: 0,
315
316
  y: 0,
316
317
  props: {
317
- start: op.start as any,
318
- end: op.end as any,
318
+ start: op.start,
319
+ end: op.end,
319
320
  },
320
321
  })
321
322
  break
@@ -1,27 +1,11 @@
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'
1
+ import { InstancePresenceRecordType, PageRecordType } from '@tldraw/tlschema'
2
+ import { createTLSchema, createTLStore, ZERO_INDEX_KEY } from 'tldraw'
17
3
  import { beforeEach, describe, expect, it, vi } from 'vitest'
18
4
  import { RecordOpType } from '../lib/diff'
19
- import { DEFAULT_INITIAL_SNAPSHOT, InMemorySyncStorage } from '../lib/InMemorySyncStorage'
20
5
  import { getTlsyncProtocolVersion } from '../lib/protocol'
21
6
  import { WebSocketMinimal } from '../lib/ServerSocketAdapter'
22
7
  import { TLSocketRoom, TLSyncLog } from '../lib/TLSocketRoom'
23
8
  import { TLSyncErrorCloseEventReason } from '../lib/TLSyncClient'
24
- import { RoomSnapshot } from '../lib/TLSyncRoom'
25
9
 
26
10
  function getStore() {
27
11
  const schema = createTLSchema()
@@ -57,7 +41,7 @@ describe(TLSocketRoom, () => {
57
41
  initialSnapshot: snapshot,
58
42
  })
59
43
  expect(room.getCurrentSnapshot()).not.toMatchObject({ clock: 0, documents: [] })
60
- expect(room.getCurrentSnapshot().documentClock).toBe(0)
44
+ expect(room.getCurrentSnapshot().clock).toBe(0)
61
45
  expect(room.getCurrentSnapshot().documents.sort((a, b) => a.state.id.localeCompare(b.state.id)))
62
46
  .toMatchInlineSnapshot(`
63
47
  [
@@ -91,7 +75,7 @@ describe(TLSocketRoom, () => {
91
75
  initialSnapshot: store.getStoreSnapshot(),
92
76
  })
93
77
 
94
- expect(room.getCurrentSnapshot()).toMatchObject({ documentClock: 0, documents: [] })
78
+ expect(room.getCurrentSnapshot()).toMatchObject({ clock: 0, documents: [] })
95
79
 
96
80
  // populate with an empty document (document:document and page:page records)
97
81
  store.ensureStoreIsUsable()
@@ -99,7 +83,7 @@ describe(TLSocketRoom, () => {
99
83
  const snapshot = store.getStoreSnapshot()
100
84
  room.loadSnapshot(snapshot)
101
85
 
102
- expect(room.getCurrentSnapshot().documentClock).toBe(1)
86
+ expect(room.getCurrentSnapshot().clock).toBe(1)
103
87
  expect(room.getCurrentSnapshot().documents.sort((a, b) => a.state.id.localeCompare(b.state.id)))
104
88
  .toMatchInlineSnapshot(`
105
89
  [
@@ -303,38 +287,50 @@ describe(TLSocketRoom, () => {
303
287
  })
304
288
 
305
289
  describe('Room state resetting behavior', () => {
306
- it('increments documentClock when loading snapshot with different data', () => {
290
+ it('sets documentClock to oldRoom.clock + 1 when resetting room state', () => {
307
291
  const store = getStore()
308
292
  store.ensureStoreIsUsable()
309
293
  const room = new TLSocketRoom({
310
294
  initialSnapshot: store.getStoreSnapshot(),
311
295
  })
312
296
 
313
- const oldClock = room.getCurrentDocumentClock()
314
- expect(oldClock).toBe(0)
297
+ // Load a snapshot to increment the clock
298
+ const snapshot = store.getStoreSnapshot()
299
+ room.loadSnapshot(snapshot)
315
300
 
316
- // Add a new page to make the snapshot different
317
- store.put([PageRecordType.create({ name: 'New Page', index: 'a2' as IndexKey })])
301
+ const oldClock = room.getCurrentSnapshot().clock
302
+ expect(oldClock).toBe(1)
303
+
304
+ // Reset with a new snapshot
318
305
  const newSnapshot = store.getStoreSnapshot()
319
306
  room.loadSnapshot(newSnapshot)
320
307
 
321
- expect(room.getCurrentDocumentClock()).toBe(oldClock + 1)
308
+ const newSnapshotResult = room.getCurrentSnapshot()
309
+ expect(newSnapshotResult.documentClock).toBe(oldClock + 1)
310
+ expect(newSnapshotResult.clock).toBe(oldClock + 1)
322
311
  })
323
312
 
324
- it('does not increment documentClock when loading identical snapshot', () => {
313
+ it('updates all documents lastChangedClock when resetting', () => {
325
314
  const store = getStore()
326
315
  store.ensureStoreIsUsable()
327
316
  const room = new TLSocketRoom({
328
317
  initialSnapshot: store.getStoreSnapshot(),
329
318
  })
330
319
 
331
- const oldClock = room.getCurrentDocumentClock()
320
+ // Get initial clock
321
+ const initialClock = room.getCurrentSnapshot().clock
322
+
323
+ // Reset with a new snapshot
324
+ const newSnapshot = store.getStoreSnapshot()
325
+ room.loadSnapshot(newSnapshot)
332
326
 
333
- // Load the same snapshot again
334
- room.loadSnapshot(store.getStoreSnapshot())
327
+ const result = room.getCurrentSnapshot()
328
+ expect(result.clock).toBe(initialClock + 1)
335
329
 
336
- // Clock should not change since data is identical
337
- expect(room.getCurrentDocumentClock()).toBe(oldClock)
330
+ // All documents should have updated lastChangedClock
331
+ for (const doc of result.documents) {
332
+ expect(doc.lastChangedClock).toBe(initialClock + 1)
333
+ }
338
334
  })
339
335
 
340
336
  it('preserves existing tombstones with original clock values', async () => {
@@ -360,13 +356,30 @@ describe(TLSocketRoom, () => {
360
356
 
361
357
  room.loadSnapshot(room.getCurrentSnapshot())
362
358
 
363
- // Tombstones should be preserved
364
359
  expect(room.getCurrentSnapshot().tombstones).toEqual({
365
360
  [testPageId]: deletionClock,
366
361
  })
367
362
 
368
- // Clock should not change since we loaded the same snapshot
369
- expect(room.getCurrentSnapshot().documentClock).toBe(deletionClock)
363
+ expect(room.getCurrentSnapshot().documentClock).toBe(deletionClock + 1)
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)
370
383
  })
371
384
 
372
385
  it('preserves schema when resetting room state', () => {
@@ -804,222 +817,3 @@ describe(TLSocketRoom, () => {
804
817
  })
805
818
  })
806
819
  })
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
- })