@tldraw/sync-core 4.3.0-canary.d8da2a99f394 → 4.3.0-canary.e1766dd4eab3
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/FuzzEditor.ts +4 -5
- package/src/test/InMemorySyncStorage.test.ts +1674 -0
- package/src/test/TLSocketRoom.test.ts +255 -49
- package/src/test/TLSyncRoom.test.ts +1022 -534
- 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/syncFuzz.test.ts +2 -4
- package/src/test/upgradeDowngrade.test.ts +282 -8
- package/src/test/validation.test.ts +10 -10
- package/src/test/pruneTombstones.test.ts +0 -178
package/src/lib/TLSocketRoom.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { StoreSchema, UnknownRecord } from '@tldraw/store'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { createTLSchema, TLStoreSnapshot } from '@tldraw/tlschema'
|
|
3
|
+
import { getOwnProperty, hasOwnProperty, isEqual, structuredClone } from '@tldraw/utils'
|
|
4
|
+
import { DEFAULT_INITIAL_SNAPSHOT, InMemorySyncStorage } from './InMemorySyncStorage'
|
|
4
5
|
import { RoomSessionState } from './RoomSession'
|
|
5
6
|
import { ServerSocketAdapter, WebSocketMinimal } from './ServerSocketAdapter'
|
|
6
7
|
import { TLSyncErrorCloseEventReason } from './TLSyncClient'
|
|
7
|
-
import { RoomSnapshot,
|
|
8
|
+
import { RoomSnapshot, TLSyncRoom } from './TLSyncRoom'
|
|
9
|
+
import {
|
|
10
|
+
convertStoreSnapshotToRoomSnapshot,
|
|
11
|
+
loadSnapshotIntoStorage,
|
|
12
|
+
TLSyncStorage,
|
|
13
|
+
} from './TLSyncStorage'
|
|
8
14
|
import { JsonChunkAssembler } from './chunk'
|
|
9
15
|
import { TLSocketServerSentEvent } from './protocol'
|
|
10
16
|
|
|
@@ -37,6 +43,51 @@ export interface TLSyncLog {
|
|
|
37
43
|
error?(...args: any[]): void
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Base options for TLSocketRoom.
|
|
48
|
+
* @public
|
|
49
|
+
*/
|
|
50
|
+
export interface TLSocketRoomOptions<R extends UnknownRecord, SessionMeta> {
|
|
51
|
+
storage?: TLSyncStorage<R>
|
|
52
|
+
/**
|
|
53
|
+
* @deprecated use the storage option instead
|
|
54
|
+
*/
|
|
55
|
+
initialSnapshot?: RoomSnapshot | TLStoreSnapshot
|
|
56
|
+
/**
|
|
57
|
+
* @deprecated use the storage option with an onChange callback instead
|
|
58
|
+
*/
|
|
59
|
+
onDataChange?(): void
|
|
60
|
+
schema?: StoreSchema<R, any>
|
|
61
|
+
// how long to wait for a client to communicate before disconnecting them
|
|
62
|
+
clientTimeout?: number
|
|
63
|
+
log?: TLSyncLog
|
|
64
|
+
// a callback that is called when a client is disconnected
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/method-signature-style
|
|
66
|
+
onSessionRemoved?: (
|
|
67
|
+
room: TLSocketRoom<R, SessionMeta>,
|
|
68
|
+
args: { sessionId: string; numSessionsRemaining: number; meta: SessionMeta }
|
|
69
|
+
) => void
|
|
70
|
+
// a callback that is called whenever a message is sent
|
|
71
|
+
// eslint-disable-next-line @typescript-eslint/method-signature-style
|
|
72
|
+
onBeforeSendMessage?: (args: {
|
|
73
|
+
sessionId: string
|
|
74
|
+
/** @internal keep the protocol private for now */
|
|
75
|
+
message: TLSocketServerSentEvent<R>
|
|
76
|
+
stringified: string
|
|
77
|
+
meta: SessionMeta
|
|
78
|
+
}) => void
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/method-signature-style
|
|
80
|
+
onAfterReceiveMessage?: (args: {
|
|
81
|
+
sessionId: string
|
|
82
|
+
/** @internal keep the protocol private for now */
|
|
83
|
+
message: TLSocketServerSentEvent<R>
|
|
84
|
+
stringified: string
|
|
85
|
+
meta: SessionMeta
|
|
86
|
+
}) => void
|
|
87
|
+
/** @internal */
|
|
88
|
+
onPresenceChange?(): void
|
|
89
|
+
}
|
|
90
|
+
|
|
40
91
|
/**
|
|
41
92
|
* A server-side room that manages WebSocket connections and synchronizes tldraw document state
|
|
42
93
|
* between multiple clients in real-time. Each room represents a collaborative document space
|
|
@@ -105,10 +156,10 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
105
156
|
{ assembler: JsonChunkAssembler; socket: WebSocketMinimal; unlisten: () => void }
|
|
106
157
|
>()
|
|
107
158
|
readonly log?: TLSyncLog
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
159
|
+
|
|
160
|
+
public storage: TLSyncStorage<R>
|
|
161
|
+
|
|
162
|
+
private disposables = new Set<() => void>()
|
|
112
163
|
|
|
113
164
|
/**
|
|
114
165
|
* Creates a new TLSocketRoom instance for managing collaborative document synchronization.
|
|
@@ -124,56 +175,36 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
124
175
|
* - onDataChange - Called when document data changes
|
|
125
176
|
* - onPresenceChange - Called when presence data changes
|
|
126
177
|
*/
|
|
127
|
-
constructor(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// how long to wait for a client to communicate before disconnecting them
|
|
132
|
-
clientTimeout?: number
|
|
133
|
-
log?: TLSyncLog
|
|
134
|
-
// a callback that is called when a client is disconnected
|
|
135
|
-
// eslint-disable-next-line @typescript-eslint/method-signature-style
|
|
136
|
-
onSessionRemoved?: (
|
|
137
|
-
room: TLSocketRoom<R, SessionMeta>,
|
|
138
|
-
args: { sessionId: string; numSessionsRemaining: number; meta: SessionMeta }
|
|
139
|
-
) => void
|
|
140
|
-
// a callback that is called whenever a message is sent
|
|
141
|
-
// eslint-disable-next-line @typescript-eslint/method-signature-style
|
|
142
|
-
onBeforeSendMessage?: (args: {
|
|
143
|
-
sessionId: string
|
|
144
|
-
/** @internal keep the protocol private for now */
|
|
145
|
-
message: TLSocketServerSentEvent<R>
|
|
146
|
-
stringified: string
|
|
147
|
-
meta: SessionMeta
|
|
148
|
-
}) => void
|
|
149
|
-
// eslint-disable-next-line @typescript-eslint/method-signature-style
|
|
150
|
-
onAfterReceiveMessage?: (args: {
|
|
151
|
-
sessionId: string
|
|
152
|
-
/** @internal keep the protocol private for now */
|
|
153
|
-
message: TLSocketServerSentEvent<R>
|
|
154
|
-
stringified: string
|
|
155
|
-
meta: SessionMeta
|
|
156
|
-
}) => void
|
|
157
|
-
onDataChange?(): void
|
|
158
|
-
/** @internal */
|
|
159
|
-
onPresenceChange?(): void
|
|
178
|
+
constructor(public readonly opts: TLSocketRoomOptions<R, SessionMeta>) {
|
|
179
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
180
|
+
if (opts.storage && opts.initialSnapshot) {
|
|
181
|
+
throw new Error('Cannot provide both storage and initialSnapshot options')
|
|
160
182
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
183
|
+
const storage = opts.storage
|
|
184
|
+
? opts.storage
|
|
185
|
+
: new InMemorySyncStorage<R>({
|
|
186
|
+
snapshot: convertStoreSnapshotToRoomSnapshot(
|
|
187
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
188
|
+
opts.initialSnapshot ?? DEFAULT_INITIAL_SNAPSHOT
|
|
189
|
+
),
|
|
190
|
+
})
|
|
166
191
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
192
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
193
|
+
if ('onDataChange' in opts && opts.onDataChange) {
|
|
194
|
+
this.disposables.add(
|
|
195
|
+
storage.onChange(() => {
|
|
196
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
197
|
+
opts.onDataChange?.()
|
|
198
|
+
})
|
|
199
|
+
)
|
|
170
200
|
}
|
|
171
201
|
this.room = new TLSyncRoom<R, SessionMeta>({
|
|
172
|
-
|
|
202
|
+
onPresenceChange: opts.onPresenceChange,
|
|
173
203
|
schema: opts.schema ?? (createTLSchema() as any),
|
|
174
|
-
snapshot: initialSnapshot,
|
|
175
204
|
log: opts.log,
|
|
205
|
+
storage,
|
|
176
206
|
})
|
|
207
|
+
this.storage = storage
|
|
177
208
|
this.room.events.on('session_removed', (args) => {
|
|
178
209
|
this.sessions.delete(args.sessionId)
|
|
179
210
|
if (this.opts.onSessionRemoved) {
|
|
@@ -396,7 +427,7 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
396
427
|
* ```
|
|
397
428
|
*/
|
|
398
429
|
getCurrentDocumentClock() {
|
|
399
|
-
return this.
|
|
430
|
+
return this.storage.getClock()
|
|
400
431
|
}
|
|
401
432
|
|
|
402
433
|
/**
|
|
@@ -418,7 +449,9 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
418
449
|
* ```
|
|
419
450
|
*/
|
|
420
451
|
getRecord(id: string) {
|
|
421
|
-
return
|
|
452
|
+
return this.storage.transaction((txn) => {
|
|
453
|
+
return structuredClone(txn.get(id)) as any
|
|
454
|
+
}).result as R
|
|
422
455
|
}
|
|
423
456
|
|
|
424
457
|
/**
|
|
@@ -466,6 +499,7 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
466
499
|
* to restore the room state later or revert to a previous version.
|
|
467
500
|
*
|
|
468
501
|
* @returns Complete room snapshot including documents, clock values, and tombstones
|
|
502
|
+
* @deprecated if you need to do this use
|
|
469
503
|
*
|
|
470
504
|
* @example
|
|
471
505
|
* ```ts
|
|
@@ -479,7 +513,10 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
479
513
|
* ```
|
|
480
514
|
*/
|
|
481
515
|
getCurrentSnapshot() {
|
|
482
|
-
|
|
516
|
+
if (this.storage.getSnapshot) {
|
|
517
|
+
return this.storage.getSnapshot()
|
|
518
|
+
}
|
|
519
|
+
throw new Error('getCurrentSnapshot is not supported for this storage type')
|
|
483
520
|
}
|
|
484
521
|
|
|
485
522
|
/**
|
|
@@ -491,25 +528,12 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
491
528
|
*/
|
|
492
529
|
getPresenceRecords() {
|
|
493
530
|
const result = {} as Record<string, UnknownRecord>
|
|
494
|
-
for (const
|
|
495
|
-
|
|
496
|
-
result[document.state.id] = document.state
|
|
497
|
-
}
|
|
531
|
+
for (const presence of this.room.presenceStore.values()) {
|
|
532
|
+
result[presence.id] = presence
|
|
498
533
|
}
|
|
499
534
|
return result
|
|
500
535
|
}
|
|
501
536
|
|
|
502
|
-
/**
|
|
503
|
-
* Returns a JSON-serialized snapshot of the current document state. This is
|
|
504
|
-
* equivalent to JSON.stringify(getCurrentSnapshot()) but provided as a convenience.
|
|
505
|
-
*
|
|
506
|
-
* @returns JSON string representation of the room snapshot
|
|
507
|
-
* @internal
|
|
508
|
-
*/
|
|
509
|
-
getCurrentSerializedSnapshot() {
|
|
510
|
-
return JSON.stringify(this.room.getSnapshot())
|
|
511
|
-
}
|
|
512
|
-
|
|
513
537
|
/**
|
|
514
538
|
* Loads a document snapshot, completely replacing the current room state.
|
|
515
539
|
* This will disconnect all current clients and update the document to match
|
|
@@ -529,42 +553,9 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
529
553
|
* ```
|
|
530
554
|
*/
|
|
531
555
|
loadSnapshot(snapshot: RoomSnapshot | TLStoreSnapshot) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
}
|
|
535
|
-
const oldRoom = this.room
|
|
536
|
-
const oldRoomSnapshot = oldRoom.getSnapshot()
|
|
537
|
-
const oldIds = oldRoomSnapshot.documents.map((d) => d.state.id)
|
|
538
|
-
const newIds = new Set(snapshot.documents.map((d) => d.state.id))
|
|
539
|
-
const removedIds = oldIds.filter((id) => !newIds.has(id))
|
|
540
|
-
|
|
541
|
-
const tombstones: RoomSnapshot['tombstones'] = { ...oldRoomSnapshot.tombstones }
|
|
542
|
-
removedIds.forEach((id) => {
|
|
543
|
-
tombstones[id] = oldRoom.clock + 1
|
|
556
|
+
this.storage.transaction((txn) => {
|
|
557
|
+
loadSnapshotIntoStorage(txn, this.room.schema, snapshot)
|
|
544
558
|
})
|
|
545
|
-
newIds.forEach((id) => {
|
|
546
|
-
delete tombstones[id]
|
|
547
|
-
})
|
|
548
|
-
|
|
549
|
-
const newRoom = new TLSyncRoom<R, SessionMeta>({
|
|
550
|
-
...this.syncCallbacks,
|
|
551
|
-
schema: oldRoom.schema,
|
|
552
|
-
snapshot: {
|
|
553
|
-
clock: oldRoom.clock + 1,
|
|
554
|
-
documentClock: oldRoom.clock + 1,
|
|
555
|
-
documents: snapshot.documents.map((d) => ({
|
|
556
|
-
lastChangedClock: oldRoom.clock + 1,
|
|
557
|
-
state: d.state,
|
|
558
|
-
})),
|
|
559
|
-
schema: snapshot.schema,
|
|
560
|
-
tombstones,
|
|
561
|
-
tombstoneHistoryStartsAtClock: oldRoomSnapshot.tombstoneHistoryStartsAtClock,
|
|
562
|
-
},
|
|
563
|
-
log: this.log,
|
|
564
|
-
})
|
|
565
|
-
// replace room with new one and kick out all the clients
|
|
566
|
-
this.room = newRoom
|
|
567
|
-
oldRoom.close()
|
|
568
559
|
}
|
|
569
560
|
|
|
570
561
|
/**
|
|
@@ -609,9 +600,32 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
609
600
|
* }
|
|
610
601
|
* })
|
|
611
602
|
* ```
|
|
603
|
+
* @deprecated use the storage.transaction method instead
|
|
612
604
|
*/
|
|
605
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
613
606
|
async updateStore(updater: (store: RoomStoreMethods<R>) => void | Promise<void>) {
|
|
614
|
-
|
|
607
|
+
if (this.isClosed()) {
|
|
608
|
+
throw new Error('Cannot update store on a closed room')
|
|
609
|
+
}
|
|
610
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
611
|
+
const ctx = new StoreUpdateContext<R>(
|
|
612
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
613
|
+
Object.fromEntries(this.getCurrentSnapshot().documents.map((d) => [d.state.id, d.state])),
|
|
614
|
+
this.room.schema
|
|
615
|
+
)
|
|
616
|
+
try {
|
|
617
|
+
await updater(ctx)
|
|
618
|
+
} finally {
|
|
619
|
+
ctx.close()
|
|
620
|
+
}
|
|
621
|
+
this.storage.transaction((txn) => {
|
|
622
|
+
for (const [id, record] of Object.entries(ctx.updates.puts)) {
|
|
623
|
+
txn.set(id, record as R)
|
|
624
|
+
}
|
|
625
|
+
for (const id of ctx.updates.deletes) {
|
|
626
|
+
txn.delete(id)
|
|
627
|
+
}
|
|
628
|
+
})
|
|
615
629
|
}
|
|
616
630
|
|
|
617
631
|
/**
|
|
@@ -688,6 +702,8 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
|
|
|
688
702
|
*/
|
|
689
703
|
close() {
|
|
690
704
|
this.room.close()
|
|
705
|
+
this.disposables.forEach((d) => d())
|
|
706
|
+
this.disposables.clear()
|
|
691
707
|
}
|
|
692
708
|
|
|
693
709
|
/**
|
|
@@ -729,15 +745,113 @@ export type OmitVoid<T, KS extends keyof T = keyof T> = {
|
|
|
729
745
|
[K in KS extends any ? (void extends T[KS] ? never : KS) : never]: T[K]
|
|
730
746
|
}
|
|
731
747
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
748
|
+
/**
|
|
749
|
+
* Interface for making transactional changes to room store data. Used within
|
|
750
|
+
* updateStore transactions to modify documents atomically.
|
|
751
|
+
*
|
|
752
|
+
* @example
|
|
753
|
+
* ```ts
|
|
754
|
+
* await room.updateStore((store) => {
|
|
755
|
+
* const shape = store.get('shape:123')
|
|
756
|
+
* if (shape) {
|
|
757
|
+
* store.put({ ...shape, x: shape.x + 10 })
|
|
758
|
+
* }
|
|
759
|
+
* store.delete('shape:456')
|
|
760
|
+
* })
|
|
761
|
+
* ```
|
|
762
|
+
*
|
|
763
|
+
* @public
|
|
764
|
+
* @deprecated use the storage.transaction method instead
|
|
765
|
+
*/
|
|
766
|
+
export interface RoomStoreMethods<R extends UnknownRecord = UnknownRecord> {
|
|
767
|
+
/**
|
|
768
|
+
* Add or update a record in the store.
|
|
769
|
+
*
|
|
770
|
+
* @param record - The record to store
|
|
771
|
+
*/
|
|
772
|
+
put(record: R): void
|
|
773
|
+
/**
|
|
774
|
+
* Delete a record from the store.
|
|
775
|
+
*
|
|
776
|
+
* @param recordOrId - The record or record ID to delete
|
|
777
|
+
*/
|
|
778
|
+
delete(recordOrId: R | string): void
|
|
779
|
+
/**
|
|
780
|
+
* Get a record by its ID.
|
|
781
|
+
*
|
|
782
|
+
* @param id - The record ID
|
|
783
|
+
* @returns The record or null if not found
|
|
784
|
+
*/
|
|
785
|
+
get(id: string): R | null
|
|
786
|
+
/**
|
|
787
|
+
* Get all records in the store.
|
|
788
|
+
*
|
|
789
|
+
* @returns Array of all records
|
|
790
|
+
*/
|
|
791
|
+
getAll(): R[]
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* @deprecated use the storage.transaction method instead
|
|
796
|
+
*/
|
|
797
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
798
|
+
class StoreUpdateContext<R extends UnknownRecord> implements RoomStoreMethods<R> {
|
|
799
|
+
constructor(
|
|
800
|
+
private readonly snapshot: Record<string, UnknownRecord>,
|
|
801
|
+
private readonly schema: StoreSchema<R, any>
|
|
802
|
+
) {}
|
|
803
|
+
readonly updates = {
|
|
804
|
+
puts: {} as Record<string, UnknownRecord>,
|
|
805
|
+
deletes: new Set<string>(),
|
|
806
|
+
}
|
|
807
|
+
put(record: R): void {
|
|
808
|
+
if (this._isClosed) throw new Error('StoreUpdateContext is closed')
|
|
809
|
+
const recordType = getOwnProperty(this.schema.types, record.typeName)
|
|
810
|
+
if (!recordType) {
|
|
811
|
+
throw new Error(`Missing definition for record type ${record.typeName}`)
|
|
812
|
+
}
|
|
813
|
+
const recordBefore = this.snapshot[record.id] ?? undefined
|
|
814
|
+
recordType.validate(record, recordBefore as R)
|
|
815
|
+
|
|
816
|
+
if (record.id in this.snapshot && isEqual(this.snapshot[record.id], record)) {
|
|
817
|
+
delete this.updates.puts[record.id]
|
|
818
|
+
} else {
|
|
819
|
+
this.updates.puts[record.id] = structuredClone(record)
|
|
820
|
+
}
|
|
821
|
+
this.updates.deletes.delete(record.id)
|
|
822
|
+
}
|
|
823
|
+
delete(recordOrId: R | string): void {
|
|
824
|
+
if (this._isClosed) throw new Error('StoreUpdateContext is closed')
|
|
825
|
+
const id = typeof recordOrId === 'string' ? recordOrId : recordOrId.id
|
|
826
|
+
delete this.updates.puts[id]
|
|
827
|
+
if (this.snapshot[id]) {
|
|
828
|
+
this.updates.deletes.add(id)
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
get(id: string): R | null {
|
|
832
|
+
if (this._isClosed) throw new Error('StoreUpdateContext is closed')
|
|
833
|
+
if (hasOwnProperty(this.updates.puts, id)) {
|
|
834
|
+
return structuredClone(this.updates.puts[id]) as R
|
|
835
|
+
}
|
|
836
|
+
if (this.updates.deletes.has(id)) {
|
|
837
|
+
return null
|
|
838
|
+
}
|
|
839
|
+
return structuredClone(this.snapshot[id] ?? null) as R
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
getAll(): R[] {
|
|
843
|
+
if (this._isClosed) throw new Error('StoreUpdateContext is closed')
|
|
844
|
+
const result = Object.values(this.updates.puts)
|
|
845
|
+
for (const [id, record] of Object.entries(this.snapshot)) {
|
|
846
|
+
if (!this.updates.deletes.has(id) && !hasOwnProperty(this.updates.puts, id)) {
|
|
847
|
+
result.push(record)
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
return structuredClone(result) as R[]
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
private _isClosed = false
|
|
854
|
+
close() {
|
|
855
|
+
this._isClosed = true
|
|
742
856
|
}
|
|
743
857
|
}
|
package/src/lib/TLSyncClient.ts
CHANGED
|
@@ -109,6 +109,18 @@ export const TLSyncErrorCloseEventReason = {
|
|
|
109
109
|
/** Room has reached maximum capacity */
|
|
110
110
|
ROOM_FULL: 'ROOM_FULL',
|
|
111
111
|
} as const
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
export class TLSyncError extends Error {
|
|
117
|
+
constructor(
|
|
118
|
+
message: string,
|
|
119
|
+
public reason: TLSyncErrorCloseEventReason
|
|
120
|
+
) {
|
|
121
|
+
super(message)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
112
124
|
/**
|
|
113
125
|
* Union type of all possible server connection close reasons.
|
|
114
126
|
* Represents the string values that can be passed when a server closes
|