@symbo.ls/sdk 2.32.6 → 2.32.8

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 (34) hide show
  1. package/dist/cjs/config/environment.js +19 -0
  2. package/dist/cjs/services/AuthService.js +4 -2
  3. package/dist/cjs/services/CollabService.js +214 -64
  4. package/dist/cjs/services/ProjectService.js +19 -10
  5. package/dist/cjs/utils/CollabClient.js +46 -9
  6. package/dist/esm/config/environment.js +19 -0
  7. package/dist/esm/index.js +302 -85
  8. package/dist/esm/services/AdminService.js +19 -0
  9. package/dist/esm/services/AuthService.js +23 -2
  10. package/dist/esm/services/BaseService.js +19 -0
  11. package/dist/esm/services/BranchService.js +19 -0
  12. package/dist/esm/services/CollabService.js +279 -73
  13. package/dist/esm/services/CoreService.js +19 -0
  14. package/dist/esm/services/DnsService.js +19 -0
  15. package/dist/esm/services/FileService.js +19 -0
  16. package/dist/esm/services/PaymentService.js +19 -0
  17. package/dist/esm/services/PlanService.js +19 -0
  18. package/dist/esm/services/ProjectService.js +38 -10
  19. package/dist/esm/services/PullRequestService.js +19 -0
  20. package/dist/esm/services/ScreenshotService.js +19 -0
  21. package/dist/esm/services/SubscriptionService.js +19 -0
  22. package/dist/esm/services/index.js +302 -85
  23. package/dist/esm/utils/CollabClient.js +65 -9
  24. package/dist/node/config/environment.js +19 -0
  25. package/dist/node/services/AuthService.js +4 -2
  26. package/dist/node/services/CollabService.js +214 -64
  27. package/dist/node/services/ProjectService.js +19 -10
  28. package/dist/node/utils/CollabClient.js +46 -9
  29. package/package.json +6 -6
  30. package/src/config/environment.js +20 -1
  31. package/src/services/AuthService.js +7 -2
  32. package/src/services/CollabService.js +258 -77
  33. package/src/services/ProjectService.js +19 -10
  34. package/src/utils/CollabClient.js +51 -8
@@ -15,8 +15,8 @@ class CollabClient {
15
15
  _buffer = [];
16
16
  _flushTimer = null;
17
17
  _clientId = nanoid();
18
- _outboxStore = null;
19
- // Dexie table
18
+ _outboxStore = createMemoryOutbox();
19
+ // Dexie table fallback
20
20
  _readyResolve;
21
21
  ready = new Promise((res) => this._readyResolve = res);
22
22
  constructor({ jwt, projectId, branch = "main", live = false }) {
@@ -32,14 +32,11 @@ class CollabClient {
32
32
  console.warn("[CollabClient] Failed to load IndexeddbPersistence:", err);
33
33
  });
34
34
  }
35
- if (typeof window === "undefined" || !hasIndexedDB) {
36
- this._outboxStore = createMemoryOutbox();
37
- } else {
35
+ if (typeof window !== "undefined" && hasIndexedDB) {
38
36
  createDexieOutbox(`${projectId}:${branch}`).then((outboxStore) => {
39
37
  this._outboxStore = outboxStore;
40
38
  }).catch((err) => {
41
39
  console.warn("[CollabClient] Failed to load Dexie:", err);
42
- this._outboxStore = createMemoryOutbox();
43
40
  });
44
41
  }
45
42
  this.socket = io(environment.socketUrl, {
@@ -49,9 +46,7 @@ class CollabClient {
49
46
  reconnectionAttempts: Infinity,
50
47
  reconnectionDelayMax: 4e3
51
48
  });
52
- this.socket.on("snapshot", this._onSnapshot).on("ops", this._onOps).on("commit", this._onCommit).on("liveMode", (flag) => {
53
- this.live = flag;
54
- }).on("connect", this._onConnect).on("error", (e) => console.warn("[collab] socket error", e));
49
+ this.socket.on("snapshot", this._onSnapshot).on("ops", this._onOps).on("commit", this._onCommit).on("liveMode", this._onLiveMode).on("connect", this._onConnect).on("error", this._onError);
55
50
  this._prevJson = this.ydoc.getMap("root").toJSON();
56
51
  this.ydoc.on("afterTransaction", (tr) => {
57
52
  if (tr.origin === "remote") {
@@ -137,6 +132,48 @@ class CollabClient {
137
132
  });
138
133
  this._buffer.length = 0;
139
134
  }
135
+ dispose() {
136
+ var _a;
137
+ clearTimeout(this._flushTimer);
138
+ this._flushTimer = null;
139
+ this._buffer.length = 0;
140
+ if ((_a = this._outboxStore) == null ? void 0 : _a.clear) {
141
+ try {
142
+ const result = this._outboxStore.clear();
143
+ if (result && typeof result.catch === "function") {
144
+ result.catch(() => {
145
+ });
146
+ }
147
+ } catch (error) {
148
+ console.warn("[CollabClient] Failed to clear outbox store during dispose:", error);
149
+ }
150
+ }
151
+ if (this.socket) {
152
+ this.socket.off("snapshot", this._onSnapshot);
153
+ this.socket.off("ops", this._onOps);
154
+ this.socket.off("commit", this._onCommit);
155
+ this.socket.off("liveMode", this._onLiveMode);
156
+ this.socket.off("connect", this._onConnect);
157
+ this.socket.off("error", this._onError);
158
+ this.socket.removeAllListeners();
159
+ this.socket.disconnect();
160
+ this.socket = null;
161
+ }
162
+ if (this.ydoc) {
163
+ this.ydoc.destroy();
164
+ this.ydoc = null;
165
+ }
166
+ if (typeof this._readyResolve === "function") {
167
+ this._readyResolve();
168
+ this._readyResolve = null;
169
+ }
170
+ }
171
+ _onLiveMode = (flag) => {
172
+ this.live = flag;
173
+ };
174
+ _onError = (e) => {
175
+ console.warn("[collab] socket error", e);
176
+ };
140
177
  }
141
178
  function createMemoryOutbox() {
142
179
  const store = /* @__PURE__ */ new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbo.ls/sdk",
3
- "version": "2.32.6",
3
+ "version": "2.32.8",
4
4
  "type": "module",
5
5
  "main": "dist/esm/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -46,10 +46,10 @@
46
46
  "test:user": "cross-env NODE_ENV=$NODE_ENV npx tape integration-tests/index.js integration-tests/user/*.test.js | tap-spec"
47
47
  },
48
48
  "dependencies": {
49
- "@domql/element": "^2.32.6",
50
- "@domql/utils": "^2.32.6",
51
- "@symbo.ls/router": "^2.32.6",
52
- "@symbo.ls/socket": "^2.32.6",
49
+ "@domql/element": "^2.32.8",
50
+ "@domql/utils": "^2.32.8",
51
+ "@symbo.ls/router": "^2.32.8",
52
+ "@symbo.ls/socket": "^2.32.8",
53
53
  "acorn": "^8.14.0",
54
54
  "acorn-walk": "^8.3.4",
55
55
  "dexie": "^4.0.11",
@@ -71,5 +71,5 @@
71
71
  "tap-spec": "^5.0.0",
72
72
  "tape": "^5.9.0"
73
73
  },
74
- "gitHead": "8bffb49783c4aa5bd7d18933ccee296b92810f7e"
74
+ "gitHead": "f7531cb41b391a1357d31a03fbdd33ad93e0a29d"
75
75
  }
@@ -24,6 +24,9 @@ const CONFIG = {
24
24
  basedProject: 'platform-v2-sm', // For based api
25
25
  basedOrg: 'symbols', // For based api
26
26
  githubClientId: 'Ov23liAFrsR0StbAO6PO', // For github api
27
+ grafanaUrl:
28
+ 'https://faro-collector-prod-us-east-0.grafana.net/collect/aef64330db80bdfeaac084317bf72f99', // For grafana tracing
29
+ grafanaAppName: 'Localhost Symbols',
27
30
  // Environment-specific feature toggles (override common)
28
31
  features: {
29
32
  betaFeatures: true // Enable beta features in local dev
@@ -38,6 +41,9 @@ const CONFIG = {
38
41
  socketUrl: 'https://dev.api.symbols.app',
39
42
  apiUrl: 'https://dev.api.symbols.app',
40
43
  githubClientId: 'Ov23liHxyWFBxS8f1gnF',
44
+ grafanaUrl:
45
+ 'https://faro-collector-prod-us-east-0.grafana.net/collect/7a3ba473cee2025c68513667024316b8', // For grafana tracing
46
+ grafanaAppName: 'Symbols Dev',
41
47
  typesenseCollectionName: 'docs',
42
48
  typesenseApiKey: 'awmcVpbWqZi9IUgmvslp1C5LKDU8tMjA',
43
49
  typesenseHost: 'tl2qpnwxev4cjm36p-1.a1.typesense.net',
@@ -51,6 +57,9 @@ const CONFIG = {
51
57
  basedProject: 'platform-v2-sm',
52
58
  basedOrg: 'symbols',
53
59
  githubClientId: 'Ov23liHxyWFBxS8f1gnF',
60
+ grafanaUrl:
61
+ 'https://faro-collector-prod-us-east-0.grafana.net/collect/7a3ba473cee2025c68513667024316b8', // For grafana tracing
62
+ grafanaAppName: 'Symbols Test',
54
63
  typesenseCollectionName: 'docs',
55
64
  typesenseApiKey: 'awmcVpbWqZi9IUgmvslp1C5LKDU8tMjA',
56
65
  typesenseHost: 'tl2qpnwxev4cjm36p-1.a1.typesense.net',
@@ -61,6 +70,9 @@ const CONFIG = {
61
70
  socketUrl: 'https://upcoming.api.symbols.app',
62
71
  apiUrl: 'https://upcoming.api.symbols.app',
63
72
  githubClientId: 'Ov23liWF7NvdZ056RV5J',
73
+ grafanaUrl:
74
+ 'https://faro-collector-prod-us-east-0.grafana.net/collect/7a3ba473cee2025c68513667024316b8', // For grafana tracing
75
+ grafanaAppName: 'Symbols Upcoming',
64
76
  typesenseCollectionName: 'docs',
65
77
  typesenseApiKey: 'awmcVpbWqZi9IUgmvslp1C5LKDU8tMjA',
66
78
  typesenseHost: 'tl2qpnwxev4cjm36p-1.a1.typesense.net',
@@ -74,6 +86,9 @@ const CONFIG = {
74
86
  basedProject: 'platform-v2-sm',
75
87
  basedOrg: 'symbols',
76
88
  githubClientId: 'Ov23ligwZDQVD0VfuWNa',
89
+ grafanaUrl:
90
+ 'https://faro-collector-prod-us-east-0.grafana.net/collect/7a3ba473cee2025c68513667024316b8', // For grafana tracing
91
+ grafanaAppName: 'Symbols Staging',
77
92
  typesenseCollectionName: 'docs',
78
93
  typesenseApiKey: 'awmcVpbWqZi9IUgmvslp1C5LKDU8tMjA',
79
94
  typesenseHost: 'tl2qpnwxev4cjm36p-1.a1.typesense.net',
@@ -87,6 +102,9 @@ const CONFIG = {
87
102
  basedProject: 'platform-v2-sm',
88
103
  basedOrg: 'symbols',
89
104
  githubClientId: 'Ov23liFAlOEIXtX3dBtR',
105
+ grafanaUrl:
106
+ 'https://faro-collector-prod-us-east-0.grafana.net/collect/5c1089f3c3eea4ec5658e05c3f53baae', // For grafana tracing
107
+ grafanaAppName: 'Symbols',
90
108
  typesenseCollectionName: 'docs',
91
109
  typesenseApiKey: 'awmcVpbWqZi9IUgmvslp1C5LKDU8tMjA',
92
110
  typesenseHost: 'tl2qpnwxev4cjm36p-1.a1.typesense.net',
@@ -125,6 +143,7 @@ export const getConfig = () => {
125
143
  basedOrg: process.env.SYMBOLS_APP_BASED_ORG || envConfig.basedOrg,
126
144
  githubClientId:
127
145
  process.env.SYMBOLS_APP_GITHUB_CLIENT_ID || envConfig.githubClientId,
146
+ grafanaUrl: process.env.SYMBOLS_APP_GRAFANA_URL || envConfig.grafanaUrl,
128
147
  typesenseCollectionName:
129
148
  process.env.TYPESENSE_COLLECTION_NAME ||
130
149
  envConfig.typesenseCollectionName,
@@ -149,7 +168,7 @@ export const getConfig = () => {
149
168
  'googleClientId'
150
169
  ]
151
170
 
152
- const missingFields = requiredFields.filter((field) => !finalConfig[field])
171
+ const missingFields = requiredFields.filter(field => !finalConfig[field])
153
172
 
154
173
  if (missingFields.length > 0) {
155
174
  console.error(
@@ -349,10 +349,15 @@ export class AuthService extends BaseService {
349
349
  }
350
350
  }
351
351
 
352
- async getMe() {
352
+ async getMe(options = {}) {
353
353
  this._requireReady('getMe')
354
354
  try {
355
- const response = await this._request('/auth/me', {
355
+ const session = this._resolvePluginSession(options.session)
356
+ const endpoint = session
357
+ ? `/auth/me?session=${encodeURIComponent(session)}`
358
+ : '/auth/me'
359
+
360
+ const response = await this._request(endpoint, {
356
361
  method: 'GET',
357
362
  methodName: 'getMe'
358
363
  })
@@ -12,15 +12,24 @@ export class CollabService extends BaseService {
12
12
  this._client = null
13
13
  this._stateManager = null
14
14
  this._connected = false
15
+ this._connecting = false
16
+ this._connectPromise = null
17
+ this._connectionMeta = null
18
+ this._pendingConnectReject = null
15
19
  this._undoStack = []
16
20
  this._redoStack = []
17
21
  this._isUndoRedo = false
18
22
  // Store operations made while offline so they can be flushed once the
19
23
  // socket reconnects.
20
24
  this._pendingOps = []
25
+
26
+ this._onSocketConnect = this._onSocketConnect.bind(this)
27
+ this._onSocketDisconnect = this._onSocketDisconnect.bind(this)
28
+ this._onSocketError = this._onSocketError.bind(this)
21
29
  }
22
30
 
23
31
  init({ context }) {
32
+ super.init({ context })
24
33
  // console.log('CollabService init')
25
34
  // console.log(context)
26
35
 
@@ -105,109 +114,243 @@ export class CollabService extends BaseService {
105
114
 
106
115
  /* ---------- Connection Management ---------- */
107
116
  async connect(options = {}) {
108
- // Make sure we have the state manager ready now that the context should
109
- // contain the root state (after updateSDKContext()).
110
- this._ensureStateManager()
111
-
112
- const {
113
- authToken: jwt,
114
- projectId,
115
- branch = 'main',
116
- pro
117
- } = {
118
- ...this._context,
119
- ...options
117
+ if (this._connectPromise) {
118
+ return this._connectPromise
120
119
  }
121
- // console.log(jwt, projectId, branch, pro)
122
120
 
123
- if (!projectId) {
124
- const state = this._stateManager?.root
125
- const el = state.__element
126
- el.call('openNotification', {
127
- type: 'error',
128
- title: 'projectId is required',
129
- message: 'projectId is required for CollabService connection'
130
- })
131
- throw new Error('projectId is required for CollabService connection')
132
- }
121
+ this._connectPromise = (async () => {
122
+ this._connecting = true
123
+ this._connected = false
133
124
 
134
- // Disconnect existing connection if any
135
- if (this._client) {
136
- await this.disconnect()
137
- }
125
+ // Make sure we have the state manager ready now that the context should
126
+ // contain the root state (after updateSDKContext()).
127
+ this._ensureStateManager()
138
128
 
139
- this._client = new CollabClient({
140
- jwt,
141
- projectId,
142
- branch,
143
- live: Boolean(pro)
144
- })
129
+ const mergedOptions = {
130
+ ...this._context,
131
+ ...options
132
+ }
145
133
 
146
- // Mark as connected once the socket establishes a connection. This prevents
147
- // the SDK from being stuck waiting for an initial snapshot that may never
148
- // arrive (e.g. for new/empty documents).
149
- await new Promise((resolve) => {
150
- if (this._client.socket?.connected) {
151
- resolve()
152
- } else {
153
- this._client.socket?.once('connect', resolve)
134
+ let { authToken: jwt } = mergedOptions
135
+ const {
136
+ projectId,
137
+ branch = 'main',
138
+ pro
139
+ } = mergedOptions
140
+
141
+ if (!jwt && this._tokenManager) {
142
+ try {
143
+ jwt = await this._tokenManager.ensureValidToken()
144
+ } catch (error) {
145
+ console.warn('[CollabService] Failed to obtain auth token from token manager', error)
146
+ }
147
+
148
+ if (!jwt && typeof this._tokenManager.getAccessToken === 'function') {
149
+ jwt = this._tokenManager.getAccessToken()
150
+ }
154
151
  }
155
- })
156
152
 
157
- // Set up event listeners
158
- this._client.socket?.on('ops', ({ changes }) => {
159
- console.log(`ops event`)
160
- this._stateManager.applyChanges(changes, { fromSocket: true })
161
- })
153
+ if (!jwt) {
154
+ throw new Error('[CollabService] Cannot connect without auth token')
155
+ }
162
156
 
163
- this._client.socket?.on('commit', ({ version }) => {
164
- if (version) {
165
- this._stateManager.setVersion(version)
157
+ this._context = {
158
+ ...this._context,
159
+ authToken: jwt,
160
+ projectId,
161
+ branch,
162
+ pro
166
163
  }
167
164
 
168
- // Inform UI about automatic commit
169
- rootBus.emit('checkpoint:done', { version, origin: 'auto' })
170
- })
165
+ if (!projectId) {
166
+ const state = this._stateManager?.root
167
+ const el = state.__element
168
+ el.call('openNotification', {
169
+ type: 'error',
170
+ title: 'projectId is required',
171
+ message: 'projectId is required for CollabService connection'
172
+ })
173
+ throw new Error('projectId is required for CollabService connection')
174
+ }
175
+
176
+ // Disconnect existing connection if any
177
+ if (this._client) {
178
+ await this.disconnect()
179
+ }
171
180
 
172
- // 🔄 Presence / members / cursor updates
173
- this._client.socket?.on('clients', this._handleClientsEvent.bind(this))
181
+ this._client = new CollabClient({
182
+ jwt,
183
+ projectId,
184
+ branch,
185
+ live: Boolean(pro)
186
+ })
174
187
 
175
- // 🗜️ Bundle events emitted by the dependency bundler service
176
- this._client.socket?.on(
177
- 'bundle:done',
178
- this._handleBundleDoneEvent.bind(this)
179
- )
180
- this._client.socket?.on(
181
- 'bundle:error',
182
- this._handleBundleErrorEvent.bind(this)
183
- )
188
+ // Mark as connected once the socket establishes a connection. This prevents
189
+ // the SDK from being stuck waiting for an initial snapshot that may never
190
+ // arrive (e.g. for new/empty documents).
191
+ const {socket} = this._client
184
192
 
185
- // Flush any operations that were queued while we were offline.
186
- if (this._pendingOps.length) {
187
- console.log(
188
- `[CollabService] Flushing ${this._pendingOps.length} offline operation batch(es)`
189
- )
190
- this._pendingOps.forEach(({ changes, granularChanges, orders }) => {
191
- this.socket.emit('ops', { changes, granularChanges, orders, ts: Date.now() })
193
+ try {
194
+ await new Promise((resolve, reject) => {
195
+ if (!socket) {
196
+ reject(new Error('[CollabService] Socket instance missing'))
197
+ return
198
+ }
199
+
200
+ if (socket.connected) {
201
+ resolve()
202
+ return
203
+ }
204
+
205
+ /* eslint-disable no-use-before-define */
206
+ const cleanup = () => {
207
+ socket.off('connect', handleConnect)
208
+ socket.off('connect_error', handleError)
209
+ socket.off('error', handleError)
210
+ socket.off('disconnect', handleDisconnect)
211
+ if (this._pendingConnectReject === handleError) {
212
+ this._pendingConnectReject = null
213
+ }
214
+ }
215
+
216
+ const handleConnect = () => {
217
+ cleanup()
218
+ resolve()
219
+ }
220
+
221
+ const handleError = (error) => {
222
+ cleanup()
223
+ reject(
224
+ error instanceof Error
225
+ ? error
226
+ : new Error(String(error || 'Unknown connection error'))
227
+ )
228
+ }
229
+
230
+ const handleDisconnect = (reason) => {
231
+ handleError(
232
+ reason instanceof Error
233
+ ? reason
234
+ : new Error(
235
+ `[CollabService] Socket disconnected before connect: ${reason || 'unknown'}`
236
+ )
237
+ )
238
+ }
239
+
240
+ this._pendingConnectReject = handleError
241
+
242
+ socket.once('connect', handleConnect)
243
+ socket.once('connect_error', handleError)
244
+ socket.once('error', handleError)
245
+ socket.once('disconnect', handleDisconnect)
246
+ /* eslint-enable no-use-before-define */
247
+ })
248
+ } catch (error) {
249
+ socket?.disconnect()
250
+ this._client = null
251
+ this._connectionMeta = null
252
+ throw error
253
+ }
254
+
255
+ this._attachSocketLifecycleListeners()
256
+ if (socket?.connected) {
257
+ this._onSocketConnect()
258
+ }
259
+
260
+ // Set up event listeners
261
+ socket?.on('ops', ({ changes }) => {
262
+ console.log(`ops event`)
263
+ this._stateManager.applyChanges(changes, { fromSocket: true })
192
264
  })
193
- this._pendingOps.length = 0
194
- }
195
265
 
196
- this._connected = true
197
- // console.log('[CollabService] Connected to project:', projectId)
266
+ socket?.on('commit', ({ version }) => {
267
+ if (version) {
268
+ this._stateManager.setVersion(version)
269
+ }
270
+
271
+ // Inform UI about automatic commit
272
+ rootBus.emit('checkpoint:done', { version, origin: 'auto' })
273
+ })
274
+
275
+ // 🔄 Presence / members / cursor updates
276
+ socket?.on('clients', this._handleClientsEvent.bind(this))
277
+
278
+ // 🗜️ Bundle events – emitted by the dependency bundler service
279
+ socket?.on(
280
+ 'bundle:done',
281
+ this._handleBundleDoneEvent.bind(this)
282
+ )
283
+ socket?.on(
284
+ 'bundle:error',
285
+ this._handleBundleErrorEvent.bind(this)
286
+ )
287
+
288
+ // Flush any operations that were queued while we were offline.
289
+ if (this._pendingOps.length) {
290
+ console.log(
291
+ `[CollabService] Flushing ${this._pendingOps.length} offline operation batch(es)`
292
+ )
293
+ this._pendingOps.forEach(({ changes, granularChanges, orders }) => {
294
+ this.socket.emit('ops', { changes, granularChanges, orders, ts: Date.now() })
295
+ })
296
+ this._pendingOps.length = 0
297
+ }
298
+
299
+ await this._client.ready
300
+
301
+ this._connectionMeta = {
302
+ projectId,
303
+ branch,
304
+ live: Boolean(pro)
305
+ }
306
+
307
+ return this.getConnectionInfo()
308
+ })()
309
+
310
+ try {
311
+ return await this._connectPromise
312
+ } finally {
313
+ this._connecting = false
314
+ this._connectPromise = null
315
+ }
198
316
  }
199
317
 
200
318
  disconnect() {
201
319
  if (this._client?.socket) {
202
- this._client.socket.disconnect()
320
+ if (this._pendingConnectReject) {
321
+ this._pendingConnectReject(new Error('[CollabService] Connection attempt aborted'))
322
+ this._pendingConnectReject = null
323
+ }
324
+ this._detachSocketLifecycleListeners()
325
+ if (typeof this._client.dispose === 'function') {
326
+ this._client.dispose()
327
+ } else {
328
+ this._client.socket.disconnect()
329
+ }
203
330
  }
204
331
  this._client = null
205
332
  this._connected = false
333
+ this._connecting = false
334
+ this._connectionMeta = null
335
+ this._pendingConnectReject = null
206
336
  console.log('[CollabService] Disconnected')
207
337
  }
208
338
 
209
339
  isConnected() {
210
- return this._connected && this._client?.socket?.connected
340
+ return Boolean(this._connected && this._client?.socket?.connected)
341
+ }
342
+
343
+ getConnectionInfo() {
344
+ return {
345
+ connected: this.isConnected(),
346
+ connecting: this._connecting,
347
+ projectId: this._connectionMeta?.projectId ?? null,
348
+ branch: this._connectionMeta?.branch ?? null,
349
+ live: this._connectionMeta?.live ?? null,
350
+ pendingOps: this._pendingOps.length,
351
+ undoStackSize: this.getUndoStackSize(),
352
+ redoStackSize: this.getRedoStackSize()
353
+ }
211
354
  }
212
355
 
213
356
  /* convenient shortcuts */
@@ -660,6 +803,44 @@ export class CollabService extends BaseService {
660
803
  }
661
804
 
662
805
  /* ---------- Manual checkpoint ---------- */
806
+ _attachSocketLifecycleListeners() {
807
+ const socket = this._client?.socket
808
+ if (!socket) {
809
+ return
810
+ }
811
+
812
+ socket.on('connect', this._onSocketConnect)
813
+ socket.on('disconnect', this._onSocketDisconnect)
814
+ socket.on('connect_error', this._onSocketError)
815
+ }
816
+
817
+ _detachSocketLifecycleListeners() {
818
+ const socket = this._client?.socket
819
+ if (!socket) {
820
+ return
821
+ }
822
+
823
+ socket.off('connect', this._onSocketConnect)
824
+ socket.off('disconnect', this._onSocketDisconnect)
825
+ socket.off('connect_error', this._onSocketError)
826
+ }
827
+
828
+ _onSocketConnect() {
829
+ this._connected = true
830
+ }
831
+
832
+ _onSocketDisconnect(reason) {
833
+ this._connected = false
834
+
835
+ if (reason && reason !== 'io client disconnect') {
836
+ console.warn('[CollabService] Socket disconnected', reason)
837
+ }
838
+ }
839
+
840
+ _onSocketError(error) {
841
+ console.warn('[CollabService] Socket connection error', error)
842
+ }
843
+
663
844
  /**
664
845
  * Manually request a checkpoint / commit of buffered operations on the server.
665
846
  * Resolves with the new version number once the backend confirms via the