@symbo.ls/sdk 3.1.2 → 3.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.
- package/README.md +2 -2
- package/dist/cjs/config/environment.js +5 -21
- package/dist/cjs/index.js +6 -26
- package/dist/cjs/services/AIService.js +3 -3
- package/dist/cjs/services/CollabService.js +420 -0
- package/dist/cjs/services/CoreService.js +651 -107
- package/dist/cjs/services/SocketService.js +207 -59
- package/dist/cjs/services/index.js +5 -13
- package/dist/cjs/state/RootStateManager.js +86 -0
- package/dist/cjs/state/rootEventBus.js +65 -0
- package/dist/cjs/utils/CollabClient.js +157 -0
- package/dist/cjs/utils/TokenManager.js +62 -27
- package/dist/cjs/utils/jsonDiff.js +103 -0
- package/dist/cjs/utils/services.js +129 -88
- package/dist/cjs/utils/symstoryClient.js +5 -5
- package/dist/esm/config/environment.js +5 -21
- package/dist/esm/index.js +20459 -9286
- package/dist/esm/services/AIService.js +3 -3
- package/dist/esm/services/BasedService.js +5 -21
- package/dist/esm/services/CollabService.js +18028 -0
- package/dist/esm/services/CoreService.js +718 -155
- package/dist/esm/services/SocketService.js +323 -58
- package/dist/esm/services/SymstoryService.js +10 -26
- package/dist/esm/services/index.js +20305 -9158
- package/dist/esm/state/RootStateManager.js +102 -0
- package/dist/esm/state/rootEventBus.js +47 -0
- package/dist/esm/utils/CollabClient.js +17483 -0
- package/dist/esm/utils/TokenManager.js +62 -27
- package/dist/esm/utils/jsonDiff.js +6096 -0
- package/dist/esm/utils/services.js +129 -88
- package/dist/esm/utils/symstoryClient.js +10 -26
- package/dist/node/config/environment.js +5 -21
- package/dist/node/index.js +10 -34
- package/dist/node/services/AIService.js +3 -3
- package/dist/node/services/CollabService.js +401 -0
- package/dist/node/services/CoreService.js +651 -107
- package/dist/node/services/SocketService.js +197 -59
- package/dist/node/services/index.js +5 -13
- package/dist/node/state/RootStateManager.js +57 -0
- package/dist/node/state/rootEventBus.js +46 -0
- package/dist/node/utils/CollabClient.js +128 -0
- package/dist/node/utils/TokenManager.js +62 -27
- package/dist/node/utils/jsonDiff.js +74 -0
- package/dist/node/utils/services.js +129 -88
- package/dist/node/utils/symstoryClient.js +5 -5
- package/package.json +12 -6
- package/src/config/environment.js +5 -19
- package/src/index.js +9 -31
- package/src/services/AIService.js +3 -3
- package/src/services/BasedService.js +1 -0
- package/src/services/CollabService.js +491 -0
- package/src/services/CoreService.js +715 -110
- package/src/services/SocketService.js +227 -59
- package/src/services/index.js +6 -13
- package/src/state/RootStateManager.js +71 -0
- package/src/state/rootEventBus.js +48 -0
- package/src/utils/CollabClient.js +161 -0
- package/src/utils/TokenManager.js +68 -30
- package/src/utils/jsonDiff.js +109 -0
- package/src/utils/services.js +140 -88
- package/src/utils/symstoryClient.js +5 -5
- package/dist/cjs/services/SocketIOService.js +0 -307
- package/dist/esm/services/SocketIOService.js +0 -470
- package/dist/node/services/SocketIOService.js +0 -278
- package/src/services/SocketIOService.js +0 -334
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { BaseService } from './BaseService.js'
|
|
2
|
+
import { CollabClient } from '../utils/CollabClient.js'
|
|
3
|
+
import { RootStateManager } from '../state/RootStateManager.js'
|
|
4
|
+
import { rootBus } from '../state/rootEventBus.js'
|
|
5
|
+
|
|
6
|
+
// (helper conversions reserved for future features)
|
|
7
|
+
|
|
8
|
+
export class CollabService extends BaseService {
|
|
9
|
+
constructor (config) {
|
|
10
|
+
super(config)
|
|
11
|
+
this._client = null
|
|
12
|
+
this._stateManager = null
|
|
13
|
+
this._connected = false
|
|
14
|
+
this._undoStack = []
|
|
15
|
+
this._redoStack = []
|
|
16
|
+
this._isUndoRedo = false
|
|
17
|
+
// Store operations made while offline so they can be flushed once the
|
|
18
|
+
// socket reconnects.
|
|
19
|
+
this._pendingOps = []
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
init ({ context }) {
|
|
23
|
+
// Defer state manager creation until a valid root state is present.
|
|
24
|
+
// The root state may not be set yet when the SDK is first initialised
|
|
25
|
+
// (e.g. inside initializeSDK()). We therefore create the manager lazily
|
|
26
|
+
// either when the SDK context is later updated or right before the first
|
|
27
|
+
// connection attempt.
|
|
28
|
+
if (context?.state) {
|
|
29
|
+
try {
|
|
30
|
+
this._stateManager = new RootStateManager(context.state)
|
|
31
|
+
} catch (err) {
|
|
32
|
+
this._setError(err)
|
|
33
|
+
throw err
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this._setReady()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Overridden to re-initialise the state manager once the root state becomes
|
|
42
|
+
* available via a subsequent SDK `updateContext()` call.
|
|
43
|
+
*/
|
|
44
|
+
updateContext (context = {}) {
|
|
45
|
+
// Preserve base behaviour
|
|
46
|
+
super.updateContext(context)
|
|
47
|
+
|
|
48
|
+
// Lazily (re)create state manager if a state tree is available
|
|
49
|
+
if (context.state) {
|
|
50
|
+
this._stateManager = new RootStateManager(context.state)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Ensure that the state manager exists. This is called right before any
|
|
56
|
+
* operation that requires access to the root state (e.g. `connect()`).
|
|
57
|
+
* Throws an explicit error if the root state is still missing so that the
|
|
58
|
+
* caller can react accordingly.
|
|
59
|
+
*/
|
|
60
|
+
_ensureStateManager () {
|
|
61
|
+
if (!this._stateManager) {
|
|
62
|
+
if (!this._context?.state) {
|
|
63
|
+
throw new Error('[CollabService] Cannot operate without root state')
|
|
64
|
+
}
|
|
65
|
+
this._stateManager = new RootStateManager(this._context.state)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ---------- Connection Management ---------- */
|
|
70
|
+
async connect (options = {}) {
|
|
71
|
+
// Make sure we have the state manager ready now that the context should
|
|
72
|
+
// contain the root state (after updateSDKContext()).
|
|
73
|
+
this._ensureStateManager()
|
|
74
|
+
|
|
75
|
+
const {
|
|
76
|
+
authToken: jwt,
|
|
77
|
+
projectId,
|
|
78
|
+
branch = 'main',
|
|
79
|
+
pro
|
|
80
|
+
} = {
|
|
81
|
+
...this._context,
|
|
82
|
+
...options
|
|
83
|
+
}
|
|
84
|
+
console.log(jwt, projectId, branch, pro)
|
|
85
|
+
|
|
86
|
+
if (!projectId) {
|
|
87
|
+
throw new Error('projectId is required for CollabService connection')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Disconnect existing connection if any
|
|
91
|
+
if (this._client) {
|
|
92
|
+
await this.disconnect()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
this._client = new CollabClient({
|
|
97
|
+
jwt,
|
|
98
|
+
projectId,
|
|
99
|
+
branch,
|
|
100
|
+
live: Boolean(pro)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Mark as connected once the socket establishes a connection. This prevents
|
|
104
|
+
// the SDK from being stuck waiting for an initial snapshot that may never
|
|
105
|
+
// arrive (e.g. for new/empty documents).
|
|
106
|
+
await new Promise(resolve => {
|
|
107
|
+
if (this._client.socket?.connected) {
|
|
108
|
+
resolve()
|
|
109
|
+
} else {
|
|
110
|
+
this._client.socket?.once('connect', resolve)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
console.log('[CollabService] socket connected')
|
|
115
|
+
|
|
116
|
+
// Set up event listeners
|
|
117
|
+
this._client.socket?.on('ops', ({ changes }) => {
|
|
118
|
+
console.log(`ops event`)
|
|
119
|
+
console.log(changes)
|
|
120
|
+
this._stateManager.applyChanges(changes, { fromSocket: true })
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
this._client.socket?.on('commit', ({ version }) => {
|
|
124
|
+
this._stateManager.setVersion(version)
|
|
125
|
+
// Inform UI about automatic commit
|
|
126
|
+
rootBus.emit('checkpoint:done', { version, origin: 'auto' })
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// 🔄 Presence / members / cursor updates
|
|
130
|
+
this._client.socket?.on('clients', this._handleClientsEvent.bind(this))
|
|
131
|
+
this._client.socket?.on('presence', this._handleClientsEvent.bind(this))
|
|
132
|
+
this._client.socket?.on('cursor', this._handleCursorEvent.bind(this))
|
|
133
|
+
|
|
134
|
+
// Flush any operations that were queued while we were offline.
|
|
135
|
+
if (this._pendingOps.length) {
|
|
136
|
+
console.log(
|
|
137
|
+
`[CollabService] Flushing ${this._pendingOps.length} offline operation batch(es)`
|
|
138
|
+
)
|
|
139
|
+
this._pendingOps.forEach(({ tuples }) => {
|
|
140
|
+
this.socket.emit('ops', { changes: tuples, ts: Date.now() })
|
|
141
|
+
})
|
|
142
|
+
this._pendingOps.length = 0
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this._connected = true
|
|
146
|
+
console.log('[CollabService] Connected to project:', projectId)
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error('[CollabService] Connection failed:', err)
|
|
149
|
+
throw err
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
disconnect () {
|
|
154
|
+
if (this._client?.socket) {
|
|
155
|
+
this._client.socket.disconnect()
|
|
156
|
+
}
|
|
157
|
+
this._client = null
|
|
158
|
+
this._connected = false
|
|
159
|
+
console.log('[CollabService] Disconnected')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
isConnected () {
|
|
163
|
+
return this._connected && this._client?.socket?.connected
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* convenient shortcuts */
|
|
167
|
+
get ydoc () {
|
|
168
|
+
return this._client?.ydoc
|
|
169
|
+
}
|
|
170
|
+
get socket () {
|
|
171
|
+
return this._client?.socket
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
toggleLive (f) {
|
|
175
|
+
this._client?.toggleLive(f)
|
|
176
|
+
}
|
|
177
|
+
sendCursor (d) {
|
|
178
|
+
this._client?.sendCursor(d)
|
|
179
|
+
}
|
|
180
|
+
sendPresence (d) {
|
|
181
|
+
this._client?.sendPresence(d)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* ---------- data helpers ---------- */
|
|
185
|
+
updateData (tuples, options = {}) {
|
|
186
|
+
// Always ensure we have a state manager so local changes are applied.
|
|
187
|
+
this._ensureStateManager()
|
|
188
|
+
|
|
189
|
+
const { isUndo = false, isRedo = false } = options
|
|
190
|
+
|
|
191
|
+
// Track operations for undo/redo (but not when performing undo/redo)
|
|
192
|
+
if (!isUndo && !isRedo && !this._isUndoRedo) {
|
|
193
|
+
this._trackForUndo(tuples, options)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Apply changes to local state tree immediately.
|
|
197
|
+
this._stateManager.applyChanges(tuples, { ...options })
|
|
198
|
+
|
|
199
|
+
// If not connected yet, queue the operations for later synchronisation.
|
|
200
|
+
if (!this.isConnected()) {
|
|
201
|
+
console.warn('[CollabService] Not connected, queuing real-time update')
|
|
202
|
+
this._pendingOps.push({ tuples, options })
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// When connected, send the operations to the backend.
|
|
207
|
+
if (this.socket?.connected) {
|
|
208
|
+
this.socket.emit('ops', { changes: tuples, ts: Date.now() })
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return { success: true }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
_trackForUndo (tuples, options) {
|
|
215
|
+
// Get current state before changes for undo
|
|
216
|
+
const undoOperations = tuples.map(tuple => {
|
|
217
|
+
const [action, path] = tuple
|
|
218
|
+
const currentValue = this._getValueAtPath(path)
|
|
219
|
+
|
|
220
|
+
if (action === 'delete') {
|
|
221
|
+
// For delete operations, store the current value to restore
|
|
222
|
+
return ['update', path, currentValue]
|
|
223
|
+
}
|
|
224
|
+
if (typeof currentValue !== 'undefined') {
|
|
225
|
+
return ['update', path, currentValue]
|
|
226
|
+
}
|
|
227
|
+
return ['delete', path]
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
this._undoStack.push({
|
|
231
|
+
operations: undoOperations,
|
|
232
|
+
originalOperations: tuples,
|
|
233
|
+
options,
|
|
234
|
+
timestamp: Date.now()
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
// Clear redo stack when new operation is performed
|
|
238
|
+
this._redoStack.length = 0
|
|
239
|
+
|
|
240
|
+
// Limit undo stack size (configurable)
|
|
241
|
+
const maxUndoSteps = this._options?.maxUndoSteps || 50
|
|
242
|
+
if (this._undoStack.length > maxUndoSteps) {
|
|
243
|
+
this._undoStack.shift()
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
_getValueAtPath (path) {
|
|
248
|
+
// Get value from root state at given path
|
|
249
|
+
const state = this._stateManager?.root
|
|
250
|
+
if (!state || !state.getByPath) {
|
|
251
|
+
return null
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
return state.getByPath(path)
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.warn('[CollabService] Could not get value at path:', path, error)
|
|
258
|
+
return null
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
undo () {
|
|
263
|
+
if (!this._undoStack.length) {
|
|
264
|
+
throw new Error('Nothing to undo')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!this.isConnected()) {
|
|
268
|
+
console.warn('[CollabService] Not connected, cannot undo')
|
|
269
|
+
return
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const undoItem = this._undoStack.pop()
|
|
273
|
+
const { operations, originalOperations, options } = undoItem
|
|
274
|
+
|
|
275
|
+
// Move to redo stack
|
|
276
|
+
this._redoStack.push({
|
|
277
|
+
operations: originalOperations,
|
|
278
|
+
originalOperations: operations,
|
|
279
|
+
options,
|
|
280
|
+
timestamp: Date.now()
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
// Apply undo operations
|
|
284
|
+
this._isUndoRedo = true
|
|
285
|
+
try {
|
|
286
|
+
this.updateData(operations, {
|
|
287
|
+
...options,
|
|
288
|
+
isUndo: true,
|
|
289
|
+
message: `Undo: ${options.message || 'operation'}`
|
|
290
|
+
})
|
|
291
|
+
} finally {
|
|
292
|
+
this._isUndoRedo = false
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return operations
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
redo () {
|
|
299
|
+
if (!this._redoStack.length) {
|
|
300
|
+
throw new Error('Nothing to redo')
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!this.isConnected()) {
|
|
304
|
+
console.warn('[CollabService] Not connected, cannot redo')
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const redoItem = this._redoStack.pop()
|
|
309
|
+
const { operations, originalOperations, options } = redoItem
|
|
310
|
+
|
|
311
|
+
// Move back to undo stack
|
|
312
|
+
this._undoStack.push({
|
|
313
|
+
operations: originalOperations,
|
|
314
|
+
originalOperations: operations,
|
|
315
|
+
options,
|
|
316
|
+
timestamp: Date.now()
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
// Apply redo operations
|
|
320
|
+
this._isUndoRedo = true
|
|
321
|
+
try {
|
|
322
|
+
this.updateData(operations, {
|
|
323
|
+
...options,
|
|
324
|
+
isRedo: true,
|
|
325
|
+
message: `Redo: ${options.message || 'operation'}`
|
|
326
|
+
})
|
|
327
|
+
} finally {
|
|
328
|
+
this._isUndoRedo = false
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return operations
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/* ---------- Undo/Redo State ---------- */
|
|
335
|
+
canUndo () {
|
|
336
|
+
return this._undoStack.length > 0
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
canRedo () {
|
|
340
|
+
return this._redoStack.length > 0
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
getUndoStackSize () {
|
|
344
|
+
return this._undoStack.length
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
getRedoStackSize () {
|
|
348
|
+
return this._redoStack.length
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
clearUndoHistory () {
|
|
352
|
+
this._undoStack.length = 0
|
|
353
|
+
this._redoStack.length = 0
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
addItem (type, data, opts = {}) {
|
|
357
|
+
const { value, ...schema } = data
|
|
358
|
+
const tuples = [
|
|
359
|
+
['update', [type, data.key], value],
|
|
360
|
+
['update', ['schema', type, data.key], schema],
|
|
361
|
+
...(opts.additionalChanges || [])
|
|
362
|
+
]
|
|
363
|
+
return this.updateData(tuples, opts)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
addMultipleItems (items, opts = {}) {
|
|
367
|
+
const tuples = []
|
|
368
|
+
items.forEach(([type, data]) => {
|
|
369
|
+
const { value, ...schema } = data
|
|
370
|
+
tuples.push(
|
|
371
|
+
['update', [type, data.key], value],
|
|
372
|
+
['update', ['schema', type, data.key], schema]
|
|
373
|
+
)
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
this.updateData([...tuples, ...(opts.additionalChanges || [])], {
|
|
377
|
+
message: `Created ${tuples.length} items`,
|
|
378
|
+
...opts
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
return tuples
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
updateItem (type, data, opts = {}) {
|
|
385
|
+
const { value, ...schema } = data
|
|
386
|
+
const tuples = [
|
|
387
|
+
['update', [type, data.key], value],
|
|
388
|
+
['update', ['schema', type, data.key], schema]
|
|
389
|
+
]
|
|
390
|
+
return this.updateData(tuples, opts)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
deleteItem (type, key, opts = {}) {
|
|
394
|
+
const tuples = [
|
|
395
|
+
['delete', [type, key]],
|
|
396
|
+
['delete', ['schema', type, key]],
|
|
397
|
+
...(opts.additionalChanges || [])
|
|
398
|
+
]
|
|
399
|
+
return this.updateData(tuples, {
|
|
400
|
+
message: `Deleted ${key} from ${type}`,
|
|
401
|
+
...opts
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* ---------- socket event helpers ---------- */
|
|
406
|
+
/**
|
|
407
|
+
* Handle "clients" or "presence" events coming from the collab socket.
|
|
408
|
+
* The backend sends the full `clients` object which we directly patch to
|
|
409
|
+
* the root state, mimicking the legacy SocketService behaviour so that
|
|
410
|
+
* existing UI components keep working unmodified.
|
|
411
|
+
*/
|
|
412
|
+
_handleClientsEvent (data = {}) {
|
|
413
|
+
const root = this._stateManager?.root
|
|
414
|
+
if (root && typeof root.replace === 'function') {
|
|
415
|
+
root.replace(
|
|
416
|
+
{ clients: data },
|
|
417
|
+
{
|
|
418
|
+
fromSocket: true,
|
|
419
|
+
preventUpdate: true
|
|
420
|
+
}
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Handle granular cursor updates coming from the socket.
|
|
427
|
+
* Expected payload: { userId, positions?, chosenPos?, ... }
|
|
428
|
+
* Only the provided fields are patched into the state tree.
|
|
429
|
+
*/
|
|
430
|
+
_handleCursorEvent (payload = {}) {
|
|
431
|
+
const { userId, positions, chosenPos, ...rest } = payload || {}
|
|
432
|
+
if (!userId) {
|
|
433
|
+
return
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const tuples = []
|
|
437
|
+
|
|
438
|
+
if (positions) {
|
|
439
|
+
tuples.push([
|
|
440
|
+
'update',
|
|
441
|
+
['canvas', 'clients', userId, 'positions'],
|
|
442
|
+
positions
|
|
443
|
+
])
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (chosenPos) {
|
|
447
|
+
tuples.push([
|
|
448
|
+
'update',
|
|
449
|
+
['canvas', 'clients', userId, 'chosenPos'],
|
|
450
|
+
chosenPos
|
|
451
|
+
])
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// merge any additional cursor–related fields directly under the user node
|
|
455
|
+
if (Object.keys(rest).length) {
|
|
456
|
+
tuples.push(['update', ['canvas', 'clients', userId], rest])
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (tuples.length) {
|
|
460
|
+
this._stateManager.applyChanges(tuples, { fromSocket: true })
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/* ---------- Manual checkpoint ---------- */
|
|
465
|
+
/**
|
|
466
|
+
* Manually request a checkpoint / commit of buffered operations on the server.
|
|
467
|
+
* Resolves with the new version number once the backend confirms via the
|
|
468
|
+
* regular "commit" event.
|
|
469
|
+
*/
|
|
470
|
+
checkpoint () {
|
|
471
|
+
if (!this.isConnected()) {
|
|
472
|
+
console.warn('[CollabService] Not connected, cannot request checkpoint')
|
|
473
|
+
return Promise.reject(new Error('Not connected'))
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return new Promise(resolve => {
|
|
477
|
+
const handler = ({ version }) => {
|
|
478
|
+
// Ensure we clean up the listener after the first commit event.
|
|
479
|
+
this.socket?.off('commit', handler)
|
|
480
|
+
rootBus.emit('checkpoint:done', { version, origin: 'manual' })
|
|
481
|
+
resolve(version)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Listen for the next commit that the server will emit after checkpoint.
|
|
485
|
+
this.socket?.once('commit', handler)
|
|
486
|
+
|
|
487
|
+
// Trigger server-side checkpoint.
|
|
488
|
+
this.socket?.emit('checkpoint')
|
|
489
|
+
})
|
|
490
|
+
}
|
|
491
|
+
}
|