@symbo.ls/sdk 2.34.35 → 3.1.2

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 (187) hide show
  1. package/README.md +2 -143
  2. package/dist/cjs/config/environment.js +30 -98
  3. package/dist/cjs/index.js +24 -144
  4. package/dist/cjs/services/AIService.js +155 -0
  5. package/dist/cjs/services/AuthService.js +305 -738
  6. package/dist/cjs/services/BaseService.js +6 -158
  7. package/dist/cjs/services/BasedService.js +1185 -0
  8. package/dist/cjs/services/CoreService.js +1751 -0
  9. package/dist/cjs/services/SocketIOService.js +307 -0
  10. package/dist/cjs/services/SocketService.js +161 -0
  11. package/dist/cjs/services/SymstoryService.js +571 -0
  12. package/dist/cjs/services/index.js +16 -64
  13. package/dist/cjs/utils/TokenManager.js +30 -78
  14. package/dist/cjs/utils/basedQuerys.js +181 -0
  15. package/dist/cjs/utils/services.js +103 -301
  16. package/dist/cjs/utils/symstoryClient.js +259 -0
  17. package/dist/cjs/utils/validation.js +3 -0
  18. package/dist/esm/config/environment.js +30 -98
  19. package/dist/esm/index.js +8797 -49416
  20. package/dist/esm/services/AIService.js +185 -0
  21. package/dist/esm/services/AuthService.js +386 -1493
  22. package/dist/esm/services/BaseService.js +6 -757
  23. package/dist/esm/services/BasedService.js +5278 -0
  24. package/dist/esm/services/CoreService.js +2264 -0
  25. package/dist/esm/services/SocketIOService.js +470 -0
  26. package/dist/esm/services/SocketService.js +191 -0
  27. package/dist/esm/services/SymstoryService.js +7041 -0
  28. package/dist/esm/services/index.js +8690 -49015
  29. package/dist/esm/utils/TokenManager.js +30 -78
  30. package/dist/esm/utils/basedQuerys.js +163 -0
  31. package/dist/esm/utils/services.js +103 -301
  32. package/dist/esm/utils/symstoryClient.js +370 -0
  33. package/dist/esm/utils/validation.js +7 -4
  34. package/dist/node/config/environment.js +30 -98
  35. package/dist/node/index.js +32 -175
  36. package/dist/node/services/AIService.js +136 -0
  37. package/dist/node/services/AuthService.js +310 -742
  38. package/dist/node/services/BaseService.js +6 -148
  39. package/dist/node/services/BasedService.js +1156 -0
  40. package/dist/node/services/CoreService.js +1722 -0
  41. package/dist/node/services/SocketIOService.js +278 -0
  42. package/dist/node/services/SocketService.js +142 -0
  43. package/dist/node/services/SymstoryService.js +542 -0
  44. package/dist/node/services/index.js +16 -64
  45. package/dist/node/utils/TokenManager.js +30 -78
  46. package/dist/node/utils/basedQuerys.js +162 -0
  47. package/dist/node/utils/services.js +103 -301
  48. package/dist/node/utils/symstoryClient.js +230 -0
  49. package/dist/node/utils/validation.js +3 -0
  50. package/package.json +16 -35
  51. package/src/config/environment.js +28 -99
  52. package/src/index.js +36 -181
  53. package/src/services/AIService.js +150 -0
  54. package/src/services/AuthService.js +328 -874
  55. package/src/services/BaseService.js +6 -166
  56. package/src/services/BasedService.js +1301 -0
  57. package/src/services/CoreService.js +1943 -0
  58. package/src/services/SocketIOService.js +334 -0
  59. package/src/services/SocketService.js +168 -0
  60. package/src/services/SymstoryService.js +649 -0
  61. package/src/services/index.js +13 -80
  62. package/src/utils/TokenManager.js +33 -88
  63. package/src/utils/basedQuerys.js +164 -0
  64. package/src/utils/services.js +107 -326
  65. package/src/utils/symstoryClient.js +252 -0
  66. package/src/utils/validation.js +3 -0
  67. package/dist/cjs/services/AdminService.js +0 -351
  68. package/dist/cjs/services/BranchService.js +0 -484
  69. package/dist/cjs/services/CollabService.js +0 -743
  70. package/dist/cjs/services/DnsService.js +0 -340
  71. package/dist/cjs/services/FeatureFlagService.js +0 -175
  72. package/dist/cjs/services/FileService.js +0 -201
  73. package/dist/cjs/services/IntegrationService.js +0 -538
  74. package/dist/cjs/services/MetricsService.js +0 -62
  75. package/dist/cjs/services/PaymentService.js +0 -271
  76. package/dist/cjs/services/PlanService.js +0 -426
  77. package/dist/cjs/services/ProjectService.js +0 -1207
  78. package/dist/cjs/services/PullRequestService.js +0 -503
  79. package/dist/cjs/services/ScreenshotService.js +0 -304
  80. package/dist/cjs/services/SubscriptionService.js +0 -396
  81. package/dist/cjs/services/TrackingService.js +0 -661
  82. package/dist/cjs/services/WaitlistService.js +0 -148
  83. package/dist/cjs/state/RootStateManager.js +0 -65
  84. package/dist/cjs/state/rootEventBus.js +0 -74
  85. package/dist/cjs/utils/CollabClient.js +0 -223
  86. package/dist/cjs/utils/changePreprocessor.js +0 -199
  87. package/dist/cjs/utils/jsonDiff.js +0 -145
  88. package/dist/cjs/utils/ordering.js +0 -309
  89. package/dist/esm/services/AdminService.js +0 -1132
  90. package/dist/esm/services/BranchService.js +0 -1265
  91. package/dist/esm/services/CollabService.js +0 -26838
  92. package/dist/esm/services/DnsService.js +0 -1121
  93. package/dist/esm/services/FeatureFlagService.js +0 -956
  94. package/dist/esm/services/FileService.js +0 -982
  95. package/dist/esm/services/IntegrationService.js +0 -1319
  96. package/dist/esm/services/MetricsService.js +0 -843
  97. package/dist/esm/services/PaymentService.js +0 -1052
  98. package/dist/esm/services/PlanService.js +0 -1207
  99. package/dist/esm/services/ProjectService.js +0 -2526
  100. package/dist/esm/services/PullRequestService.js +0 -1284
  101. package/dist/esm/services/ScreenshotService.js +0 -1085
  102. package/dist/esm/services/SubscriptionService.js +0 -1177
  103. package/dist/esm/services/TrackingService.js +0 -18343
  104. package/dist/esm/services/WaitlistService.js +0 -929
  105. package/dist/esm/state/RootStateManager.js +0 -90
  106. package/dist/esm/state/rootEventBus.js +0 -56
  107. package/dist/esm/utils/CollabClient.js +0 -18901
  108. package/dist/esm/utils/changePreprocessor.js +0 -542
  109. package/dist/esm/utils/jsonDiff.js +0 -7011
  110. package/dist/esm/utils/ordering.js +0 -291
  111. package/dist/node/services/AdminService.js +0 -332
  112. package/dist/node/services/BranchService.js +0 -465
  113. package/dist/node/services/CollabService.js +0 -724
  114. package/dist/node/services/DnsService.js +0 -321
  115. package/dist/node/services/FeatureFlagService.js +0 -156
  116. package/dist/node/services/FileService.js +0 -182
  117. package/dist/node/services/IntegrationService.js +0 -519
  118. package/dist/node/services/MetricsService.js +0 -43
  119. package/dist/node/services/PaymentService.js +0 -252
  120. package/dist/node/services/PlanService.js +0 -407
  121. package/dist/node/services/ProjectService.js +0 -1188
  122. package/dist/node/services/PullRequestService.js +0 -484
  123. package/dist/node/services/ScreenshotService.js +0 -285
  124. package/dist/node/services/SubscriptionService.js +0 -377
  125. package/dist/node/services/TrackingService.js +0 -632
  126. package/dist/node/services/WaitlistService.js +0 -129
  127. package/dist/node/state/RootStateManager.js +0 -36
  128. package/dist/node/state/rootEventBus.js +0 -55
  129. package/dist/node/utils/CollabClient.js +0 -194
  130. package/dist/node/utils/changePreprocessor.js +0 -180
  131. package/dist/node/utils/jsonDiff.js +0 -116
  132. package/dist/node/utils/ordering.js +0 -290
  133. package/src/services/AdminService.js +0 -374
  134. package/src/services/BranchService.js +0 -536
  135. package/src/services/CollabService.js +0 -900
  136. package/src/services/DnsService.js +0 -366
  137. package/src/services/FeatureFlagService.js +0 -174
  138. package/src/services/FileService.js +0 -213
  139. package/src/services/IntegrationService.js +0 -548
  140. package/src/services/MetricsService.js +0 -40
  141. package/src/services/PaymentService.js +0 -287
  142. package/src/services/PlanService.js +0 -468
  143. package/src/services/ProjectService.js +0 -1366
  144. package/src/services/PullRequestService.js +0 -537
  145. package/src/services/ScreenshotService.js +0 -258
  146. package/src/services/SubscriptionService.js +0 -425
  147. package/src/services/TrackingService.js +0 -853
  148. package/src/services/WaitlistService.js +0 -130
  149. package/src/services/tests/BranchService/createBranch.test.js +0 -153
  150. package/src/services/tests/BranchService/deleteBranch.test.js +0 -173
  151. package/src/services/tests/BranchService/getBranchChanges.test.js +0 -146
  152. package/src/services/tests/BranchService/listBranches.test.js +0 -87
  153. package/src/services/tests/BranchService/mergeBranch.test.js +0 -210
  154. package/src/services/tests/BranchService/publishVersion.test.js +0 -183
  155. package/src/services/tests/BranchService/renameBranch.test.js +0 -240
  156. package/src/services/tests/BranchService/resetBranch.test.js +0 -152
  157. package/src/services/tests/FeatureFlagService/adminFeatureFlags.test.js +0 -67
  158. package/src/services/tests/FeatureFlagService/getFeatureFlags.test.js +0 -75
  159. package/src/services/tests/FileService/createFileFormData.test.js +0 -74
  160. package/src/services/tests/FileService/getFileUrl.test.js +0 -69
  161. package/src/services/tests/FileService/updateProjectIcon.test.js +0 -109
  162. package/src/services/tests/FileService/uploadDocument.test.js +0 -36
  163. package/src/services/tests/FileService/uploadFile.test.js +0 -78
  164. package/src/services/tests/FileService/uploadFileWithValidation.test.js +0 -114
  165. package/src/services/tests/FileService/uploadImage.test.js +0 -36
  166. package/src/services/tests/FileService/uploadMultipleFiles.test.js +0 -111
  167. package/src/services/tests/FileService/validateFile.test.js +0 -63
  168. package/src/services/tests/PlanService/createPlan.test.js +0 -104
  169. package/src/services/tests/PlanService/createPlanWithValidation.test.js +0 -523
  170. package/src/services/tests/PlanService/deletePlan.test.js +0 -92
  171. package/src/services/tests/PlanService/getActivePlans.test.js +0 -123
  172. package/src/services/tests/PlanService/getAdminPlans.test.js +0 -84
  173. package/src/services/tests/PlanService/getPlan.test.js +0 -50
  174. package/src/services/tests/PlanService/getPlanByKey.test.js +0 -109
  175. package/src/services/tests/PlanService/getPlanWithValidation.test.js +0 -85
  176. package/src/services/tests/PlanService/getPlans.test.js +0 -53
  177. package/src/services/tests/PlanService/getPlansByPriceRange.test.js +0 -109
  178. package/src/services/tests/PlanService/getPlansWithValidation.test.js +0 -48
  179. package/src/services/tests/PlanService/initializePlans.test.js +0 -75
  180. package/src/services/tests/PlanService/updatePlan.test.js +0 -111
  181. package/src/services/tests/PlanService/updatePlanWithValidation.test.js +0 -556
  182. package/src/state/RootStateManager.js +0 -76
  183. package/src/state/rootEventBus.js +0 -67
  184. package/src/utils/CollabClient.js +0 -248
  185. package/src/utils/changePreprocessor.js +0 -239
  186. package/src/utils/jsonDiff.js +0 -144
  187. package/src/utils/ordering.js +0 -271
@@ -1,900 +0,0 @@
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
- import { validateParams } from '../utils/validation.js'
6
- import { deepStringifyFunctions } from '@domql/utils'
7
- import { preprocessChanges } from '../utils/changePreprocessor.js'
8
-
9
- // Helper: clone a value while converting all functions to strings. This is
10
- // tailored for collab payloads (tuples / granularChanges) and is more robust
11
- // for nested array shapes than the generic DOMQL helper.
12
- const FUNCTION_META_KEYS = ['node', '__ref', '__element', 'parent', 'parse']
13
-
14
- function stringifyFunctionsForTransport(value, seen = new WeakMap()) {
15
- if (value === null || typeof value !== 'object') {
16
- return typeof value === 'function' ? value.toString() : value
17
- }
18
-
19
- if (seen.has(value)) {
20
- return seen.get(value)
21
- }
22
-
23
- const clone = Array.isArray(value) ? [] : {}
24
- seen.set(value, clone)
25
-
26
- if (Array.isArray(value)) {
27
- for (let i = 0; i < value.length; i++) {
28
- clone[i] = stringifyFunctionsForTransport(value[i], seen)
29
- }
30
- return clone
31
- }
32
-
33
- const keys = Object.keys(value)
34
- for (let i = 0; i < keys.length; i++) {
35
- const key = keys[i]
36
- if (!FUNCTION_META_KEYS.includes(key)) {
37
- clone[key] = stringifyFunctionsForTransport(value[key], seen)
38
- }
39
- }
40
-
41
- return clone
42
- }
43
-
44
- export class CollabService extends BaseService {
45
- constructor(config) {
46
- super(config)
47
- this._client = null
48
- this._stateManager = null
49
- this._connected = false
50
- this._connecting = false
51
- this._connectPromise = null
52
- this._connectionMeta = null
53
- this._pendingConnectReject = null
54
- this._undoStack = []
55
- this._redoStack = []
56
- this._isUndoRedo = false
57
- // Store operations made while offline so they can be flushed once the
58
- // socket reconnects.
59
- this._pendingOps = []
60
-
61
- this._onSocketConnect = this._onSocketConnect.bind(this)
62
- this._onSocketDisconnect = this._onSocketDisconnect.bind(this)
63
- this._onSocketError = this._onSocketError.bind(this)
64
- }
65
-
66
- init({ context }) {
67
- super.init({ context })
68
- // console.log('CollabService init')
69
- // console.log(context)
70
-
71
- // Defer state manager creation until a valid root state is present.
72
- // The root state may not be set yet when the SDK is first initialised
73
- // (e.g. inside initializeSDK()). We therefore create the manager lazily
74
- // either when the SDK context is later updated or right before the first
75
- // connection attempt.
76
- if (context?.state) {
77
- try {
78
- this._stateManager = new RootStateManager(context.state)
79
- } catch (err) {
80
- this._setError(err)
81
- throw err
82
- }
83
- }
84
-
85
- this._setReady()
86
- }
87
-
88
- /**
89
- * Overridden to re-initialise the state manager once the root state becomes
90
- * available via a subsequent SDK `updateContext()` call.
91
- */
92
- updateContext(context = {}) {
93
- // Preserve base behaviour
94
- super.updateContext(context)
95
-
96
- // Lazily (re)create state manager if a state tree is available
97
- if (context.state) {
98
- this._stateManager = new RootStateManager(context.state)
99
- }
100
- }
101
-
102
- /**
103
- * Ensure that the state manager exists. This is called right before any
104
- * operation that requires access to the root state (e.g. `connect()`).
105
- * Throws an explicit error if the root state is still missing so that the
106
- * caller can react accordingly.
107
- */
108
- _ensureStateManager() {
109
- if (!this._stateManager) {
110
- if (!this._context?.state) {
111
- throw new Error('[CollabService] Cannot operate without root state')
112
- }
113
- this._stateManager = new RootStateManager(this._context.state)
114
- }
115
-
116
- // 🌐 Ensure we always have a usable `__element` stub so that calls like
117
- // `el.call('openNotification', …)` or `el.call('deepStringifyFunctions', …)` do not
118
- // crash in headless / Node.js environments (e.g. integration tests).
119
- const root = this._stateManager?.root
120
-
121
- if (root && !root.__element) {
122
- // Minimal no-op implementation of the DOMQL element API used here
123
- root.__element = {
124
- /**
125
- * Very small subset of the DOMQL `call` API that we rely on inside the
126
- * CollabService for browser notifications and data helpers.
127
- * In a Node.js test context we simply log or return fallbacks.
128
- */
129
- call: (method, ...args) => {
130
- switch (method) {
131
- case 'openNotification': {
132
- const [payload = {}] = args
133
- const { type = 'info', title = '', message = '' } = payload
134
- const logger = type === 'error' ? console.error : console.log
135
- logger(`[Notification] ${title}${message ? ` – ${message}` : ''}`)
136
- return
137
- }
138
- case 'deepStringifyFunctions': {
139
- // Pass-through to the shared utility from `smbls`
140
- return deepStringifyFunctions(...args)
141
- }
142
- default:
143
- return {}
144
- }
145
- }
146
- }
147
- }
148
- }
149
-
150
- /* ---------- Connection Management ---------- */
151
- async connect(options = {}) {
152
- if (this._connectPromise) {
153
- return this._connectPromise
154
- }
155
-
156
- this._connectPromise = (async () => {
157
- this._connecting = true
158
- this._connected = false
159
-
160
- // Make sure we have the state manager ready now that the context should
161
- // contain the root state (after updateSDKContext()).
162
- this._ensureStateManager()
163
-
164
- const mergedOptions = {
165
- ...this._context,
166
- ...options
167
- }
168
-
169
- let { authToken: jwt } = mergedOptions
170
- const { projectId, branch = 'main', pro } = mergedOptions
171
-
172
- if (!jwt && this._tokenManager) {
173
- try {
174
- jwt = await this._tokenManager.ensureValidToken()
175
- } catch (error) {
176
- console.warn(
177
- '[CollabService] Failed to obtain auth token from token manager',
178
- error
179
- )
180
- }
181
-
182
- if (!jwt && typeof this._tokenManager.getAccessToken === 'function') {
183
- jwt = this._tokenManager.getAccessToken()
184
- }
185
- }
186
-
187
- if (!jwt) {
188
- throw new Error('[CollabService] Cannot connect without auth token')
189
- }
190
-
191
- this._context = {
192
- ...this._context,
193
- authToken: jwt,
194
- projectId,
195
- branch,
196
- pro
197
- }
198
-
199
- if (!projectId) {
200
- const state = this._stateManager?.root
201
- const el = state.__element
202
- el.call('openNotification', {
203
- type: 'error',
204
- title: 'projectId is required',
205
- message: 'projectId is required for CollabService connection'
206
- })
207
- throw new Error('projectId is required for CollabService connection')
208
- }
209
-
210
- // Disconnect existing connection if any
211
- if (this._client) {
212
- await this.disconnect()
213
- }
214
-
215
- this._client = new CollabClient({
216
- jwt,
217
- projectId,
218
- branch,
219
- live: Boolean(pro)
220
- })
221
-
222
- // Mark as connected once the socket establishes a connection. This prevents
223
- // the SDK from being stuck waiting for an initial snapshot that may never
224
- // arrive (e.g. for new/empty documents).
225
- const { socket } = this._client
226
-
227
- try {
228
- await new Promise((resolve, reject) => {
229
- if (!socket) {
230
- reject(new Error('[CollabService] Socket instance missing'))
231
- return
232
- }
233
-
234
- if (socket.connected) {
235
- resolve()
236
- return
237
- }
238
-
239
- /* eslint-disable no-use-before-define */
240
- const cleanup = () => {
241
- socket.off('connect', handleConnect)
242
- socket.off('connect_error', handleError)
243
- socket.off('error', handleError)
244
- socket.off('disconnect', handleDisconnect)
245
- if (this._pendingConnectReject === handleError) {
246
- this._pendingConnectReject = null
247
- }
248
- }
249
-
250
- const handleConnect = () => {
251
- cleanup()
252
- resolve()
253
- }
254
-
255
- const handleError = (error) => {
256
- cleanup()
257
- reject(
258
- error instanceof Error
259
- ? error
260
- : new Error(String(error || 'Unknown connection error'))
261
- )
262
- }
263
-
264
- const handleDisconnect = (reason) => {
265
- handleError(
266
- reason instanceof Error
267
- ? reason
268
- : new Error(
269
- `[CollabService] Socket disconnected before connect: ${
270
- reason || 'unknown'
271
- }`
272
- )
273
- )
274
- }
275
-
276
- this._pendingConnectReject = handleError
277
-
278
- socket.once('connect', handleConnect)
279
- socket.once('connect_error', handleError)
280
- socket.once('error', handleError)
281
- socket.once('disconnect', handleDisconnect)
282
- /* eslint-enable no-use-before-define */
283
- })
284
- } catch (error) {
285
- socket?.disconnect()
286
- this._client = null
287
- this._connectionMeta = null
288
- throw error
289
- }
290
-
291
- this._attachSocketLifecycleListeners()
292
- if (socket?.connected) {
293
- this._onSocketConnect()
294
- }
295
-
296
- // Set up event listeners
297
- socket?.on('ops', ({ changes }) => {
298
- console.log(`ops event`)
299
- this._stateManager.applyChanges(changes, { fromSocket: true })
300
- })
301
-
302
- socket?.on('commit', ({ version }) => {
303
- if (version) {
304
- this._stateManager.setVersion(version)
305
- }
306
-
307
- // Inform UI about automatic commit
308
- rootBus.emit('checkpoint:done', { version, origin: 'auto' })
309
- })
310
-
311
- // 🔄 Presence / members / cursor updates
312
- socket?.on('clients', this._handleClientsEvent.bind(this))
313
-
314
- // 🗜️ Bundle events – emitted by the dependency bundler service
315
- socket?.on('bundle:done', this._handleBundleDoneEvent.bind(this))
316
- socket?.on('bundle:error', this._handleBundleErrorEvent.bind(this))
317
-
318
- // Flush any operations that were queued while we were offline.
319
- if (this._pendingOps.length) {
320
- console.log(
321
- `[CollabService] Flushing ${this._pendingOps.length} offline operation batch(es)`
322
- )
323
- this._pendingOps.forEach(
324
- ({ changes, granularChanges, orders, options: opOptions }) => {
325
- const { message } = opOptions || {}
326
- const ts = Date.now()
327
- const payload = {
328
- changes,
329
- granularChanges,
330
- orders,
331
- ts
332
- }
333
- if (message) {
334
- payload.message = message
335
- }
336
- this.socket.emit('ops', payload)
337
- }
338
- )
339
- this._pendingOps.length = 0
340
- }
341
-
342
- await this._client.ready
343
-
344
- this._connectionMeta = {
345
- projectId,
346
- branch,
347
- live: Boolean(pro)
348
- }
349
-
350
- return this.getConnectionInfo()
351
- })()
352
-
353
- try {
354
- return await this._connectPromise
355
- } finally {
356
- this._connecting = false
357
- this._connectPromise = null
358
- }
359
- }
360
-
361
- disconnect() {
362
- if (this._client?.socket) {
363
- if (this._pendingConnectReject) {
364
- this._pendingConnectReject(
365
- new Error('[CollabService] Connection attempt aborted')
366
- )
367
- this._pendingConnectReject = null
368
- }
369
- this._detachSocketLifecycleListeners()
370
- if (typeof this._client.dispose === 'function') {
371
- this._client.dispose()
372
- } else {
373
- this._client.socket.disconnect()
374
- }
375
- }
376
- this._client = null
377
- this._connected = false
378
- this._connecting = false
379
- this._connectionMeta = null
380
- this._pendingConnectReject = null
381
- console.log('[CollabService] Disconnected')
382
- }
383
-
384
- isConnected() {
385
- return Boolean(this._connected && this._client?.socket?.connected)
386
- }
387
-
388
- getConnectionInfo() {
389
- return {
390
- connected: this.isConnected(),
391
- connecting: this._connecting,
392
- projectId: this._connectionMeta?.projectId ?? null,
393
- branch: this._connectionMeta?.branch ?? null,
394
- live: this._connectionMeta?.live ?? null,
395
- pendingOps: this._pendingOps.length,
396
- undoStackSize: this.getUndoStackSize(),
397
- redoStackSize: this.getRedoStackSize()
398
- }
399
- }
400
-
401
- /* convenient shortcuts */
402
- get ydoc() {
403
- return this._client?.ydoc
404
- }
405
- get socket() {
406
- return this._client?.socket
407
- }
408
-
409
- toggleLive(f) {
410
- this._client?.toggleLive(f)
411
- }
412
- sendCursor(d) {
413
- this._client?.sendCursor(d)
414
- }
415
- sendPresence(d) {
416
- this._client?.sendPresence(d)
417
- }
418
-
419
- /* ---------- data helpers ---------- */
420
- updateData(tuples, options = {}) {
421
- // Always ensure we have a state manager so local changes are applied.
422
- this._ensureStateManager()
423
-
424
- const { isUndo = false, isRedo = false } = options
425
-
426
- // Track operations for undo/redo (but not when performing undo/redo)
427
- if (!isUndo && !isRedo && !this._isUndoRedo) {
428
- this._trackForUndo(tuples, options)
429
- }
430
-
431
- // Preprocess into granular changes and derive orders
432
- const root = this._stateManager?.root
433
- const { granularChanges: processedTuples, orders } = preprocessChanges(
434
- root,
435
- tuples,
436
- options
437
- )
438
-
439
- // Include any additional changes passed via opts
440
- if (options.append && options.append.length) {
441
- processedTuples.push(...options.append)
442
- }
443
-
444
- // Apply changes to local state tree immediately.
445
- this._stateManager.applyChanges(tuples, { ...options })
446
-
447
- // Use a dedicated helper that correctly handles nested array structures
448
- // such as granular tuples while also avoiding DOM / state metadata keys.
449
- const stringifiedGranularTuples =
450
- stringifyFunctionsForTransport(processedTuples)
451
-
452
- const stringifiedTuples = stringifyFunctionsForTransport(tuples)
453
-
454
- const { message } = options
455
-
456
- // If not connected yet, queue the operations for later synchronisation.
457
- if (!this.isConnected()) {
458
- console.warn('[CollabService] Not connected, queuing real-time update')
459
- this._pendingOps.push({
460
- changes: stringifiedTuples,
461
- granularChanges: stringifiedGranularTuples,
462
- orders,
463
- options
464
- })
465
- return
466
- }
467
-
468
- // When connected, send the operations to the backend.
469
- if (this.socket?.connected) {
470
- const ts = Date.now()
471
- // console.log('[CollabService] Sending operations to the backend', {
472
- // changes: stringifiedTuples,
473
- // granularChanges: stringifiedGranularTuples,
474
- // orders,
475
- // ts,
476
- // message
477
- // })
478
- const payload = {
479
- changes: stringifiedTuples,
480
- granularChanges: stringifiedGranularTuples,
481
- orders,
482
- ts
483
- }
484
-
485
- if (message) {
486
- payload.message = message
487
- }
488
-
489
- this.socket.emit('ops', payload)
490
- }
491
-
492
- return { success: true }
493
- }
494
-
495
- _trackForUndo(tuples, options) {
496
- // Get current state before changes for undo
497
- const undoOperations = tuples.map((tuple) => {
498
- const [action, path] = tuple
499
- const currentValue = this._getValueAtPath(path)
500
-
501
- if (action === 'delete') {
502
- // For delete operations, store the current value to restore
503
- return ['update', path, currentValue]
504
- }
505
- if (typeof currentValue !== 'undefined') {
506
- return ['update', path, currentValue]
507
- }
508
- return ['delete', path]
509
- })
510
-
511
- this._undoStack.push({
512
- operations: undoOperations,
513
- originalOperations: tuples,
514
- options,
515
- timestamp: Date.now()
516
- })
517
-
518
- // Clear redo stack when new operation is performed
519
- this._redoStack.length = 0
520
-
521
- // Limit undo stack size (configurable)
522
- const maxUndoSteps = this._options?.maxUndoSteps || 50
523
- if (this._undoStack.length > maxUndoSteps) {
524
- this._undoStack.shift()
525
- }
526
- }
527
-
528
- _getValueAtPath(path) {
529
- // Get value from root state at given path
530
- const state = this._stateManager?.root
531
- if (!state || !state.getByPath) {
532
- return null
533
- }
534
-
535
- try {
536
- return state.getByPath(path)
537
- } catch (error) {
538
- console.warn('[CollabService] Could not get value at path:', path, error)
539
- return null
540
- }
541
- }
542
-
543
- undo() {
544
- if (!this._undoStack.length) {
545
- const state = this._stateManager?.root
546
- const el = state.__element
547
- el.call('openNotification', {
548
- type: 'error',
549
- title: 'Nothing to undo'
550
- })
551
- throw new Error('Nothing to undo')
552
- }
553
-
554
- if (!this.isConnected()) {
555
- console.warn('[CollabService] Not connected, cannot undo')
556
- return
557
- }
558
-
559
- const undoItem = this._undoStack.pop()
560
- const { operations, originalOperations, options } = undoItem
561
-
562
- // Move to redo stack
563
- this._redoStack.push({
564
- operations: originalOperations,
565
- originalOperations: operations,
566
- options,
567
- timestamp: Date.now()
568
- })
569
-
570
- // Apply undo operations
571
- this._isUndoRedo = true
572
- try {
573
- this.updateData(operations, {
574
- ...options,
575
- isUndo: true,
576
- message: `Undo: ${options.message || 'operation'}`
577
- })
578
- } finally {
579
- this._isUndoRedo = false
580
- }
581
-
582
- return operations
583
- }
584
-
585
- redo() {
586
- if (!this._redoStack.length) {
587
- const state = this._stateManager?.root
588
- const el = state.__element
589
- el.call('openNotification', {
590
- type: 'error',
591
- title: 'Nothing to redo'
592
- })
593
- throw new Error('Nothing to redo')
594
- }
595
-
596
- if (!this.isConnected()) {
597
- console.warn('[CollabService] Not connected, cannot redo')
598
- return
599
- }
600
-
601
- const redoItem = this._redoStack.pop()
602
- const { operations, originalOperations, options } = redoItem
603
-
604
- // Move back to undo stack
605
- this._undoStack.push({
606
- operations: originalOperations,
607
- originalOperations: operations,
608
- options,
609
- timestamp: Date.now()
610
- })
611
-
612
- // Apply redo operations
613
- this._isUndoRedo = true
614
- try {
615
- this.updateData(operations, {
616
- ...options,
617
- isRedo: true,
618
- message: `Redo: ${options.message || 'operation'}`
619
- })
620
- } finally {
621
- this._isUndoRedo = false
622
- }
623
-
624
- return operations
625
- }
626
-
627
- /* ---------- Undo/Redo State ---------- */
628
- canUndo() {
629
- return this._undoStack.length > 0
630
- }
631
-
632
- canRedo() {
633
- return this._redoStack.length > 0
634
- }
635
-
636
- getUndoStackSize() {
637
- return this._undoStack.length
638
- }
639
-
640
- getRedoStackSize() {
641
- return this._redoStack.length
642
- }
643
-
644
- clearUndoHistory() {
645
- this._undoStack.length = 0
646
- this._redoStack.length = 0
647
- }
648
-
649
- addItem(type, data, opts = {}) {
650
- try {
651
- validateParams.type(type)
652
- validateParams.data(data, type)
653
-
654
- const { value, ...schema } = data
655
-
656
- // Base tuple for the actual value update
657
- const tuples = [
658
- ['update', [type, data.key], value],
659
- ['update', ['schema', type, data.key], schema || {}]
660
- ]
661
-
662
- // Prevent components:changed event emission when updateData is invoked via addItem
663
- const updatedOpts = { ...opts, skipComponentsChangedEvent: true }
664
-
665
- return this.updateData(tuples, updatedOpts)
666
- } catch (error) {
667
- throw new Error(`Failed to add item: ${error.message}`, { cause: error })
668
- }
669
- }
670
-
671
- addMultipleItems(items, opts = {}) {
672
- const tuples = []
673
-
674
- try {
675
- items.forEach(([type, data]) => {
676
- validateParams.type(type)
677
- validateParams.data(data, type)
678
-
679
- const { value, ...schema } = data
680
-
681
- tuples.push(
682
- ['update', [type, data.key], value],
683
- ['update', ['schema', type, data.key], schema]
684
- )
685
- })
686
-
687
- this.updateData([...tuples, ...(opts.append || [])], {
688
- message: `Created ${tuples.length} items`,
689
- ...opts
690
- })
691
- return tuples
692
- } catch (error) {
693
- const state = this._stateManager?.root
694
- const el = state.__element
695
- el.call('openNotification', {
696
- type: 'error',
697
- title: 'Failed to add item',
698
- message: error.message
699
- })
700
- throw new Error(`Failed to add item: ${error.message}`, { cause: error })
701
- }
702
- }
703
-
704
- updateItem(type, data, opts = {}) {
705
- try {
706
- validateParams.type(type)
707
- validateParams.data(data, type)
708
-
709
- const { value, ...schema } = data
710
- const tuples = [
711
- ['update', [type, data.key], value],
712
- ['update', ['schema', type, data.key], schema]
713
- ]
714
- return this.updateData(tuples, opts)
715
- } catch (error) {
716
- const state = this._stateManager?.root
717
- const el = state.__element
718
- el.call('openNotification', {
719
- type: 'error',
720
- title: 'Failed to update item',
721
- message: error.message
722
- })
723
- throw new Error(`Failed to update item: ${error.message}`, {
724
- cause: error
725
- })
726
- }
727
- }
728
-
729
- deleteItem(type, key, opts = {}) {
730
- try {
731
- validateParams.type(type)
732
- validateParams.key(key, type)
733
-
734
- const tuples = [
735
- ['delete', [type, key]],
736
- ['delete', ['schema', type, key]],
737
- ...(opts.append || [])
738
- ]
739
- return this.updateData(tuples, {
740
- message: `Deleted ${key} from ${type}`,
741
- ...opts
742
- })
743
- } catch (error) {
744
- const state = this._stateManager?.root
745
- const el = state.__element
746
- el.call('openNotification', {
747
- type: 'error',
748
- title: 'Failed to delete item',
749
- message: error.message
750
- })
751
- throw new Error(`Failed to delete item: ${error.message}`, {
752
- cause: error
753
- })
754
- }
755
- }
756
-
757
- /* ---------- socket event helpers ---------- */
758
- /**
759
- * Handle "clients" or "presence" events coming from the collab socket.
760
- * The backend sends the full `clients` object which we directly patch to
761
- * the root state, mimicking the legacy SocketService behaviour so that
762
- * existing UI components keep working unmodified.
763
- */
764
- _handleClientsEvent(data = {}) {
765
- const root = this._stateManager?.root
766
- if (root && typeof root.replace === 'function') {
767
- root.clients = data
768
- }
769
- rootBus.emit('clients:updated', data)
770
- }
771
-
772
- /* ---------- Dependency bundling events ---------- */
773
- _handleBundleDoneEvent({
774
- project,
775
- ticket,
776
- dependencies = {},
777
- schema = {}
778
- } = {}) {
779
- console.info('[CollabService] Bundle done', { project, ticket })
780
-
781
- // Update local state with latest dependency information
782
- try {
783
- this._ensureStateManager()
784
-
785
- const { dependencies: schemaDependencies = {} } = schema || {}
786
-
787
- const tuples = [
788
- ['update', ['dependencies'], dependencies],
789
- ['update', ['schema', 'dependencies'], schemaDependencies]
790
- ]
791
-
792
- this._stateManager.applyChanges(tuples, {
793
- fromSocket: true,
794
- preventFetchDeps: true,
795
- preventUpdate: ['Iframe']
796
- })
797
- } catch (err) {
798
- console.error('[CollabService] Failed to update deps after bundle', err)
799
- }
800
-
801
- // Notify UI via rootBus and toast/notification helper if available
802
- const root = this._stateManager?.root
803
- const el = root?.__element
804
-
805
- if (el?.call) {
806
- el.call('openNotification', {
807
- type: 'success',
808
- title: 'Dependencies ready',
809
- message: `Project ${project} dependencies have been bundled successfully.`
810
- })
811
- }
812
-
813
- rootBus.emit('bundle:done', { project, ticket })
814
- }
815
-
816
- _handleBundleErrorEvent({ project, ticket, error } = {}) {
817
- console.error('[CollabService] Bundle error', { project, ticket, error })
818
-
819
- const root = this._stateManager?.root
820
- const el = root?.__element
821
-
822
- if (el?.call) {
823
- el.call('openNotification', {
824
- type: 'error',
825
- title: 'Dependency bundle failed',
826
- message:
827
- error ||
828
- `An error occurred while bundling dependencies for project ${project}.`
829
- })
830
- }
831
-
832
- rootBus.emit('bundle:error', { project, ticket, error })
833
- }
834
-
835
- /* ---------- Manual checkpoint ---------- */
836
- _attachSocketLifecycleListeners() {
837
- const socket = this._client?.socket
838
- if (!socket) {
839
- return
840
- }
841
-
842
- socket.on('connect', this._onSocketConnect)
843
- socket.on('disconnect', this._onSocketDisconnect)
844
- socket.on('connect_error', this._onSocketError)
845
- }
846
-
847
- _detachSocketLifecycleListeners() {
848
- const socket = this._client?.socket
849
- if (!socket) {
850
- return
851
- }
852
-
853
- socket.off('connect', this._onSocketConnect)
854
- socket.off('disconnect', this._onSocketDisconnect)
855
- socket.off('connect_error', this._onSocketError)
856
- }
857
-
858
- _onSocketConnect() {
859
- this._connected = true
860
- }
861
-
862
- _onSocketDisconnect(reason) {
863
- this._connected = false
864
-
865
- if (reason && reason !== 'io client disconnect') {
866
- console.warn('[CollabService] Socket disconnected', reason)
867
- }
868
- }
869
-
870
- _onSocketError(error) {
871
- console.warn('[CollabService] Socket connection error', error)
872
- }
873
-
874
- /**
875
- * Manually request a checkpoint / commit of buffered operations on the server.
876
- * Resolves with the new version number once the backend confirms via the
877
- * regular "commit" event.
878
- */
879
- checkpoint() {
880
- if (!this.isConnected()) {
881
- console.warn('[CollabService] Not connected, cannot request checkpoint')
882
- return Promise.reject(new Error('Not connected'))
883
- }
884
-
885
- return new Promise((resolve) => {
886
- const handler = ({ version }) => {
887
- // Ensure we clean up the listener after the first commit event.
888
- this.socket?.off('commit', handler)
889
- rootBus.emit('checkpoint:done', { version, origin: 'manual' })
890
- resolve(version)
891
- }
892
-
893
- // Listen for the next commit that the server will emit after checkpoint.
894
- this.socket?.once('commit', handler)
895
-
896
- // Trigger server-side checkpoint.
897
- this.socket?.emit('checkpoint')
898
- })
899
- }
900
- }