@tldraw/sync-core 5.3.0-internal.fba91ed55f6c → 5.3.0-next.4123e97459a9
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 +3 -154
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/InMemorySyncStorage.js +20 -55
- package/dist-cjs/lib/InMemorySyncStorage.js.map +2 -2
- package/dist-cjs/lib/RoomSession.js.map +1 -1
- package/dist-cjs/lib/SQLiteSyncStorage.js +52 -115
- package/dist-cjs/lib/SQLiteSyncStorage.js.map +2 -2
- package/dist-cjs/lib/TLSocketRoom.js +2 -27
- package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
- package/dist-cjs/lib/TLSyncClient.js +1 -6
- package/dist-cjs/lib/TLSyncClient.js.map +2 -2
- package/dist-cjs/lib/TLSyncRoom.js +10 -160
- package/dist-cjs/lib/TLSyncRoom.js.map +3 -3
- package/dist-cjs/lib/TLSyncStorage.js.map +2 -2
- package/dist-cjs/lib/protocol.js.map +2 -2
- package/dist-esm/index.d.mts +3 -154
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/InMemorySyncStorage.mjs +20 -55
- package/dist-esm/lib/InMemorySyncStorage.mjs.map +2 -2
- package/dist-esm/lib/RoomSession.mjs.map +1 -1
- package/dist-esm/lib/SQLiteSyncStorage.mjs +52 -115
- package/dist-esm/lib/SQLiteSyncStorage.mjs.map +2 -2
- package/dist-esm/lib/TLSocketRoom.mjs +2 -27
- package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
- package/dist-esm/lib/TLSyncClient.mjs +1 -6
- package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
- package/dist-esm/lib/TLSyncRoom.mjs +10 -160
- package/dist-esm/lib/TLSyncRoom.mjs.map +3 -3
- package/dist-esm/lib/TLSyncStorage.mjs.map +2 -2
- package/dist-esm/lib/protocol.mjs.map +2 -2
- package/package.json +6 -6
- package/src/index.ts +0 -3
- package/src/lib/InMemorySyncStorage.ts +21 -74
- package/src/lib/RoomSession.ts +2 -7
- package/src/lib/SQLiteSyncStorage.test.ts +2 -29
- package/src/lib/SQLiteSyncStorage.ts +59 -158
- package/src/lib/TLSocketRoom.ts +3 -61
- package/src/lib/TLSyncClient.test.ts +2 -9
- package/src/lib/TLSyncClient.ts +3 -15
- package/src/lib/TLSyncRoom.ts +10 -294
- package/src/lib/TLSyncStorage.ts +0 -8
- package/src/lib/protocol.ts +0 -17
- package/src/test/TLSocketRoom.test.ts +2 -37
- package/src/test/TLSyncRoom.test.ts +0 -25
- package/src/test/TestServer.ts +4 -14
- package/src/test/storageContractSuite.ts +0 -79
- package/src/test/upgradeDowngrade.test.ts +2 -144
- package/src/test/objectStore.test.ts +0 -630
|
@@ -41,7 +41,6 @@ export function makePage(id: string, name = id, index = 'a2') {
|
|
|
41
41
|
export interface StorageContractFactory {
|
|
42
42
|
create(opts?: {
|
|
43
43
|
snapshot?: RoomSnapshot
|
|
44
|
-
objectTypes?: readonly string[]
|
|
45
44
|
onChange?(arg: TLSyncStorageOnChangeCallbackProps): void
|
|
46
45
|
}): TLSyncStorage<TLRecord>
|
|
47
46
|
}
|
|
@@ -62,84 +61,6 @@ export function registerStorageContractTests(factory: StorageContractFactory) {
|
|
|
62
61
|
expect(snapshot.documents.map((d) => d.lastChangedClock)).toEqual([0, 0])
|
|
63
62
|
})
|
|
64
63
|
|
|
65
|
-
describe('object-store lane partition', () => {
|
|
66
|
-
// the storage doesn't care about record semantics, so 'page' stands in as a
|
|
67
|
-
// generic object-lane type here
|
|
68
|
-
const createPartitioned = (snapshot?: RoomSnapshot) =>
|
|
69
|
-
factory.create({ snapshot, objectTypes: ['page'] })
|
|
70
|
-
|
|
71
|
-
it('partitions object-lane records out of getSnapshot into getObjectsSnapshot', () => {
|
|
72
|
-
const storage = createPartitioned(
|
|
73
|
-
makeContractSnapshot(contractRecords, { documentClock: 5 })
|
|
74
|
-
)
|
|
75
|
-
const snapshot = storage.getSnapshot!()
|
|
76
|
-
expect(snapshot.documents.map((d) => d.state.typeName)).toEqual(['document'])
|
|
77
|
-
// clock + schema are unaffected by the partition
|
|
78
|
-
expect(snapshot.documentClock).toBe(5)
|
|
79
|
-
|
|
80
|
-
expect(storage.getObjectsSnapshot!().map((d) => d.state.typeName)).toEqual(['page'])
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('returns an empty objects snapshot when no objectTypes are configured', () => {
|
|
84
|
-
const storage = create(makeContractSnapshot(contractRecords))
|
|
85
|
-
expect(storage.getSnapshot!().documents.length).toBe(2)
|
|
86
|
-
expect(storage.getObjectsSnapshot!()).toEqual([])
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('routes writes by type and shares the clock, tombstones, and change feed', () => {
|
|
90
|
-
const storage = createPartitioned(makeContractSnapshot(contractRecords))
|
|
91
|
-
const newPage = makePage('lane')
|
|
92
|
-
storage.transaction((txn) => txn.set(newPage.id, newPage))
|
|
93
|
-
|
|
94
|
-
// the write landed in the object partition, not the document snapshot
|
|
95
|
-
expect(storage.getSnapshot!().documents.map((d) => d.state.id)).toEqual([TLDOCUMENT_ID])
|
|
96
|
-
expect(storage.getObjectsSnapshot!().map((d) => d.state.id)).toContain(newPage.id)
|
|
97
|
-
|
|
98
|
-
// reads and the change feed span both partitions
|
|
99
|
-
storage.transaction((txn) => {
|
|
100
|
-
expect(txn.get(newPage.id)).toEqual(newPage)
|
|
101
|
-
expect(txn.getChangesSince(0)!.diff.puts[newPage.id]).toEqual(newPage)
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
// deleting an object-lane record writes a shared tombstone
|
|
105
|
-
const clockBeforeDelete = storage.getClock()
|
|
106
|
-
storage.transaction((txn) => txn.delete(newPage.id))
|
|
107
|
-
expect(storage.getObjectsSnapshot!().map((d) => d.state.id)).not.toContain(newPage.id)
|
|
108
|
-
storage.transaction((txn) => {
|
|
109
|
-
expect(txn.getChangesSince(clockBeforeDelete)!.diff.deletes).toContain(newPage.id)
|
|
110
|
-
})
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
it('iterates both partitions so schema migrations cover object-lane records', () => {
|
|
114
|
-
const storage = createPartitioned(makeContractSnapshot(contractRecords))
|
|
115
|
-
storage.transaction((txn) => {
|
|
116
|
-
const ids = Array.from(txn.keys()).sort()
|
|
117
|
-
expect(ids).toEqual(contractRecords.map((r) => r.id).sort())
|
|
118
|
-
expect(
|
|
119
|
-
Array.from(txn.values())
|
|
120
|
-
.map((r) => r.id)
|
|
121
|
-
.sort()
|
|
122
|
-
).toEqual(ids)
|
|
123
|
-
expect(
|
|
124
|
-
Array.from(txn.entries())
|
|
125
|
-
.map(([id]) => id)
|
|
126
|
-
.sort()
|
|
127
|
-
).toEqual(ids)
|
|
128
|
-
})
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
it('round-trips through a merged snapshot (persist lanes separately, re-seed together)', () => {
|
|
132
|
-
const storage = createPartitioned(makeContractSnapshot(contractRecords))
|
|
133
|
-
const merged = {
|
|
134
|
-
...storage.getSnapshot!(),
|
|
135
|
-
documents: [...storage.getSnapshot!().documents, ...storage.getObjectsSnapshot!()],
|
|
136
|
-
}
|
|
137
|
-
const reseeded = createPartitioned(merged)
|
|
138
|
-
expect(reseeded.getSnapshot!().documents.map((d) => d.state.typeName)).toEqual(['document'])
|
|
139
|
-
expect(reseeded.getObjectsSnapshot!().map((d) => d.state.typeName)).toEqual(['page'])
|
|
140
|
-
})
|
|
141
|
-
})
|
|
142
|
-
|
|
143
64
|
describe('transactions', () => {
|
|
144
65
|
it('[SS2] returns the callback result along with clock metadata', () => {
|
|
145
66
|
const storage = create(makeContractSnapshot(contractRecords, { documentClock: 7 }))
|
|
@@ -2,7 +2,6 @@ import { computed } from '@tldraw/state'
|
|
|
2
2
|
import {
|
|
3
3
|
BaseRecord,
|
|
4
4
|
RecordId,
|
|
5
|
-
SerializedSchema,
|
|
6
5
|
Store,
|
|
7
6
|
StoreSchema,
|
|
8
7
|
UnknownRecord,
|
|
@@ -11,7 +10,6 @@ import {
|
|
|
11
10
|
createRecordMigrationSequence,
|
|
12
11
|
createRecordType,
|
|
13
12
|
} from '@tldraw/store'
|
|
14
|
-
import { commentSchemaRecords, createTLSchema } from '@tldraw/tlschema'
|
|
15
13
|
import { vi, type Mock } from 'vitest'
|
|
16
14
|
import { RecordOpType, ValueOpType } from '../lib/diff'
|
|
17
15
|
import { TLSocketServerSentEvent, getTlsyncProtocolVersion } from '../lib/protocol'
|
|
@@ -173,13 +171,8 @@ class TestInstance {
|
|
|
173
171
|
|
|
174
172
|
hasLoaded = false
|
|
175
173
|
|
|
176
|
-
constructor(
|
|
177
|
-
|
|
178
|
-
oldSchema = schemaV1,
|
|
179
|
-
newSchema = schemaV2,
|
|
180
|
-
roomOpts?: ConstructorParameters<typeof TestServer<RV2>>[2]
|
|
181
|
-
) {
|
|
182
|
-
this.server = new TestServer(newSchema, snapshot, roomOpts)
|
|
174
|
+
constructor(snapshot?: RoomSnapshot, oldSchema = schemaV1, newSchema = schemaV2) {
|
|
175
|
+
this.server = new TestServer(newSchema, snapshot)
|
|
183
176
|
this.oldSocketPair = new TestSocketPair('test_upgrade_old', this.server)
|
|
184
177
|
this.newSocketPair = new TestSocketPair('test_upgrade_new', this.server)
|
|
185
178
|
|
|
@@ -225,47 +218,6 @@ class TestInstance {
|
|
|
225
218
|
}
|
|
226
219
|
}
|
|
227
220
|
|
|
228
|
-
describe('record types unknown to older clients', () => {
|
|
229
|
-
// Mirrors a deploy that adds the comment record types: stale tabs still run a schema
|
|
230
|
-
// without them, while the server registers them and serves rooms that may contain them. The
|
|
231
|
-
// comment types ship guard migrations (retroactive, no `down`), so the server must reject
|
|
232
|
-
// stale sessions with CLIENT_TOO_OLD (prompting a refresh) rather than sending them record
|
|
233
|
-
// types their store cannot represent.
|
|
234
|
-
const schemaWithComments = createTLSchema({ records: commentSchemaRecords })
|
|
235
|
-
const schemaWithoutComments = createTLSchema()
|
|
236
|
-
|
|
237
|
-
function connectWith(server: TestServer<any>, schema: SerializedSchema) {
|
|
238
|
-
const id = 'stale-tab'
|
|
239
|
-
const socket = mockSocket()
|
|
240
|
-
server.room.handleNewSession({ sessionId: id, socket, meta: undefined, isReadonly: false })
|
|
241
|
-
server.room.handleMessage(id, {
|
|
242
|
-
type: 'connect',
|
|
243
|
-
connectRequestId: 'test',
|
|
244
|
-
lastServerClock: 0,
|
|
245
|
-
protocolVersion: getTlsyncProtocolVersion(),
|
|
246
|
-
schema,
|
|
247
|
-
})
|
|
248
|
-
return socket
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
it('[HS3] rejects sessions whose schema predates the comment types', () => {
|
|
252
|
-
const server = new TestServer(schemaWithComments as any, undefined, {
|
|
253
|
-
objectTypes: ['comment', 'comment-thread'],
|
|
254
|
-
})
|
|
255
|
-
const socket = connectWith(server, schemaWithoutComments.serialize())
|
|
256
|
-
expect(socket.close).toHaveBeenCalledWith(4099, TLSyncErrorCloseEventReason.CLIENT_TOO_OLD)
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
it('[HS3] accepts sessions whose schema registers the comment types', () => {
|
|
260
|
-
const server = new TestServer(schemaWithComments as any, undefined, {
|
|
261
|
-
objectTypes: ['comment', 'comment-thread'],
|
|
262
|
-
})
|
|
263
|
-
const socket = connectWith(server, schemaWithComments.serialize())
|
|
264
|
-
expect(socket.close).not.toHaveBeenCalled()
|
|
265
|
-
expect((socket.sendMessage as Mock).mock.calls[0][0]).toMatchObject({ type: 'connect' })
|
|
266
|
-
})
|
|
267
|
-
})
|
|
268
|
-
|
|
269
221
|
it('[MG2] the server can handle receiving v1 stuff from the client', () => {
|
|
270
222
|
const t = new TestInstance()
|
|
271
223
|
t.oldSocketPair.connect()
|
|
@@ -616,7 +568,6 @@ describe('when the client is too old', () => {
|
|
|
616
568
|
schema: schemaV2.serialize(),
|
|
617
569
|
serverClock: 10,
|
|
618
570
|
isReadonly: false,
|
|
619
|
-
objectAccess: 'write',
|
|
620
571
|
} satisfies TLSocketServerSentEvent<RV2>)
|
|
621
572
|
|
|
622
573
|
expect(v1SendMessage).toHaveBeenCalledWith({
|
|
@@ -628,7 +579,6 @@ describe('when the client is too old', () => {
|
|
|
628
579
|
schema: schemaV2.serialize(),
|
|
629
580
|
serverClock: 10,
|
|
630
581
|
isReadonly: false,
|
|
631
|
-
objectAccess: 'write',
|
|
632
582
|
} satisfies TLSocketServerSentEvent<RV2>)
|
|
633
583
|
|
|
634
584
|
v2SendMessage.mockClear()
|
|
@@ -1150,7 +1100,6 @@ describe('when the client is the same version', () => {
|
|
|
1150
1100
|
schema: schemaV2.serialize(),
|
|
1151
1101
|
serverClock: 10,
|
|
1152
1102
|
isReadonly: false,
|
|
1153
|
-
objectAccess: 'write',
|
|
1154
1103
|
} satisfies TLSocketServerSentEvent<RV2>)
|
|
1155
1104
|
|
|
1156
1105
|
expect(bSocket.sendMessage).toHaveBeenCalledWith({
|
|
@@ -1162,7 +1111,6 @@ describe('when the client is the same version', () => {
|
|
|
1162
1111
|
schema: schemaV2.serialize(),
|
|
1163
1112
|
serverClock: 10,
|
|
1164
1113
|
isReadonly: false,
|
|
1165
|
-
objectAccess: 'write',
|
|
1166
1114
|
} satisfies TLSocketServerSentEvent<RV2>)
|
|
1167
1115
|
;(aSocket.sendMessage as Mock).mockClear()
|
|
1168
1116
|
;(bSocket.sendMessage as Mock).mockClear()
|
|
@@ -1237,93 +1185,3 @@ describe('when the client is the same version', () => {
|
|
|
1237
1185
|
} satisfies TLSocketServerSentEvent<RV2>)
|
|
1238
1186
|
})
|
|
1239
1187
|
})
|
|
1240
|
-
|
|
1241
|
-
describe('record authorizers see server-schema records', () => {
|
|
1242
|
-
function makeInstance(authorizeUser: (args: any) => any) {
|
|
1243
|
-
return new TestInstance(undefined, schemaV1, schemaV2, {
|
|
1244
|
-
authorizeRecord: { user: authorizeUser },
|
|
1245
|
-
})
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
it('a put from an old client reaches the authorizer up-migrated', () => {
|
|
1249
|
-
const seen: any[] = []
|
|
1250
|
-
const t = makeInstance((args: any) => {
|
|
1251
|
-
seen.push(structuredClone({ type: args.type, prev: args.prev, next: args.next }))
|
|
1252
|
-
return args.next
|
|
1253
|
-
})
|
|
1254
|
-
t.oldSocketPair.connect()
|
|
1255
|
-
t.flush()
|
|
1256
|
-
|
|
1257
|
-
const user = UserV1.create({ name: 'bob', age: 10 })
|
|
1258
|
-
t.oldClient.store.put([user])
|
|
1259
|
-
t.flush()
|
|
1260
|
-
|
|
1261
|
-
expect(seen).toHaveLength(1)
|
|
1262
|
-
expect(seen[0].type).toBe('create')
|
|
1263
|
-
expect(seen[0].next).toMatchObject({ name: 'bob', birthdate: null })
|
|
1264
|
-
expect(seen[0].next).not.toHaveProperty('age')
|
|
1265
|
-
})
|
|
1266
|
-
|
|
1267
|
-
it('a stamp applied on create survives — no migration runs after the authorizer', () => {
|
|
1268
|
-
const t = makeInstance((args: any) =>
|
|
1269
|
-
args.type === 'create' ? { ...args.next, birthdate: '2001-02-03' } : args.next
|
|
1270
|
-
)
|
|
1271
|
-
t.oldSocketPair.connect()
|
|
1272
|
-
t.flush()
|
|
1273
|
-
|
|
1274
|
-
const user = UserV1.create({ name: 'bob', age: 10 })
|
|
1275
|
-
t.oldClient.store.put([user])
|
|
1276
|
-
t.flush()
|
|
1277
|
-
|
|
1278
|
-
expect(t.server.storage.documents.get(user.id)?.state).toMatchObject({
|
|
1279
|
-
name: 'bob',
|
|
1280
|
-
birthdate: '2001-02-03',
|
|
1281
|
-
})
|
|
1282
|
-
})
|
|
1283
|
-
|
|
1284
|
-
it('a patch from an old client reaches the authorizer as the upgraded committed candidate', () => {
|
|
1285
|
-
const seen: any[] = []
|
|
1286
|
-
const t = makeInstance((args: any) => {
|
|
1287
|
-
seen.push(structuredClone({ type: args.type, prev: args.prev, next: args.next }))
|
|
1288
|
-
return args.next
|
|
1289
|
-
})
|
|
1290
|
-
t.oldSocketPair.connect()
|
|
1291
|
-
t.newSocketPair.connect()
|
|
1292
|
-
t.flush()
|
|
1293
|
-
|
|
1294
|
-
const user = UserV2.create({ name: 'bob', birthdate: '2022-01-09' })
|
|
1295
|
-
t.newClient.store.put([user])
|
|
1296
|
-
t.flush()
|
|
1297
|
-
seen.length = 0
|
|
1298
|
-
|
|
1299
|
-
t.oldClient.store.update(user.id as any, (u: any) => ({ ...u, name: 'bobby' }))
|
|
1300
|
-
t.flush()
|
|
1301
|
-
|
|
1302
|
-
expect(seen).toHaveLength(1)
|
|
1303
|
-
expect(seen[0].type).toBe('update')
|
|
1304
|
-
expect(seen[0].prev).toMatchObject({ name: 'bob', birthdate: '2022-01-09' })
|
|
1305
|
-
// The down→patch→up round-trip through the lossy v1 schema resets birthdate to null.
|
|
1306
|
-
// The authorizer must see exactly what will be committed — not a preview mixing the
|
|
1307
|
-
// server record with the raw v1 diff (which would still show the old birthdate).
|
|
1308
|
-
expect(seen[0].next).toMatchObject({ name: 'bobby', birthdate: null })
|
|
1309
|
-
expect(seen[0].next).not.toHaveProperty('age')
|
|
1310
|
-
})
|
|
1311
|
-
|
|
1312
|
-
it('vetoes based on the server-schema record', () => {
|
|
1313
|
-
const t = makeInstance((args: any) =>
|
|
1314
|
-
args.type === 'update' && args.next.name === 'forged' ? null : args.next
|
|
1315
|
-
)
|
|
1316
|
-
t.oldSocketPair.connect()
|
|
1317
|
-
t.newSocketPair.connect()
|
|
1318
|
-
t.flush()
|
|
1319
|
-
|
|
1320
|
-
const user = UserV2.create({ name: 'bob', birthdate: '2022-01-09' })
|
|
1321
|
-
t.newClient.store.put([user])
|
|
1322
|
-
t.flush()
|
|
1323
|
-
|
|
1324
|
-
t.oldClient.store.update(user.id as any, (u: any) => ({ ...u, name: 'forged' }))
|
|
1325
|
-
t.flush()
|
|
1326
|
-
|
|
1327
|
-
expect(t.server.storage.documents.get(user.id)?.state).toMatchObject({ name: 'bob' })
|
|
1328
|
-
})
|
|
1329
|
-
})
|