@symbo.ls/sdk 3.2.3 → 3.2.7

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 (183) hide show
  1. package/README.md +141 -0
  2. package/dist/cjs/config/environment.js +94 -10
  3. package/dist/cjs/index.js +152 -12
  4. package/dist/cjs/services/AdminService.js +351 -0
  5. package/dist/cjs/services/AuthService.js +738 -305
  6. package/dist/cjs/services/BaseService.js +158 -6
  7. package/dist/cjs/services/BranchService.js +484 -0
  8. package/dist/cjs/services/CollabService.js +439 -116
  9. package/dist/cjs/services/DnsService.js +340 -0
  10. package/dist/cjs/services/FeatureFlagService.js +175 -0
  11. package/dist/cjs/services/FileService.js +201 -0
  12. package/dist/cjs/services/IntegrationService.js +538 -0
  13. package/dist/cjs/services/MetricsService.js +62 -0
  14. package/dist/cjs/services/PaymentService.js +271 -0
  15. package/dist/cjs/services/PlanService.js +426 -0
  16. package/dist/cjs/services/ProjectService.js +1207 -0
  17. package/dist/cjs/services/PullRequestService.js +503 -0
  18. package/dist/cjs/services/ScreenshotService.js +304 -0
  19. package/dist/cjs/services/SubscriptionService.js +396 -0
  20. package/dist/cjs/services/TrackingService.js +661 -0
  21. package/dist/cjs/services/WaitlistService.js +148 -0
  22. package/dist/cjs/services/index.js +60 -4
  23. package/dist/cjs/state/RootStateManager.js +2 -23
  24. package/dist/cjs/state/rootEventBus.js +9 -0
  25. package/dist/cjs/utils/CollabClient.js +78 -12
  26. package/dist/cjs/utils/TokenManager.js +16 -3
  27. package/dist/cjs/utils/changePreprocessor.js +199 -0
  28. package/dist/cjs/utils/jsonDiff.js +46 -4
  29. package/dist/cjs/utils/ordering.js +309 -0
  30. package/dist/cjs/utils/services.js +285 -128
  31. package/dist/cjs/utils/validation.js +0 -3
  32. package/dist/esm/config/environment.js +94 -10
  33. package/dist/esm/index.js +47862 -18248
  34. package/dist/esm/services/AdminService.js +1132 -0
  35. package/dist/esm/services/AuthService.js +1493 -386
  36. package/dist/esm/services/BaseService.js +757 -6
  37. package/dist/esm/services/BranchService.js +1265 -0
  38. package/dist/esm/services/CollabService.js +24956 -16089
  39. package/dist/esm/services/DnsService.js +1121 -0
  40. package/dist/esm/services/FeatureFlagService.js +956 -0
  41. package/dist/esm/services/FileService.js +982 -0
  42. package/dist/esm/services/IntegrationService.js +1319 -0
  43. package/dist/esm/services/MetricsService.js +843 -0
  44. package/dist/esm/services/PaymentService.js +1052 -0
  45. package/dist/esm/services/PlanService.js +1207 -0
  46. package/dist/esm/services/ProjectService.js +2526 -0
  47. package/dist/esm/services/PullRequestService.js +1284 -0
  48. package/dist/esm/services/ScreenshotService.js +1085 -0
  49. package/dist/esm/services/SubscriptionService.js +1177 -0
  50. package/dist/esm/services/TrackingService.js +18454 -0
  51. package/dist/esm/services/WaitlistService.js +929 -0
  52. package/dist/esm/services/index.js +47373 -18027
  53. package/dist/esm/state/RootStateManager.js +11 -23
  54. package/dist/esm/state/rootEventBus.js +9 -0
  55. package/dist/esm/utils/CollabClient.js +17526 -16120
  56. package/dist/esm/utils/TokenManager.js +16 -3
  57. package/dist/esm/utils/changePreprocessor.js +542 -0
  58. package/dist/esm/utils/jsonDiff.js +958 -43
  59. package/dist/esm/utils/ordering.js +291 -0
  60. package/dist/esm/utils/services.js +285 -128
  61. package/dist/esm/utils/validation.js +116 -50
  62. package/dist/node/config/environment.js +94 -10
  63. package/dist/node/index.js +183 -16
  64. package/dist/node/services/AdminService.js +332 -0
  65. package/dist/node/services/AuthService.js +742 -310
  66. package/dist/node/services/BaseService.js +148 -6
  67. package/dist/node/services/BranchService.js +465 -0
  68. package/dist/node/services/CollabService.js +439 -116
  69. package/dist/node/services/DnsService.js +321 -0
  70. package/dist/node/services/FeatureFlagService.js +156 -0
  71. package/dist/node/services/FileService.js +182 -0
  72. package/dist/node/services/IntegrationService.js +519 -0
  73. package/dist/node/services/MetricsService.js +43 -0
  74. package/dist/node/services/PaymentService.js +252 -0
  75. package/dist/node/services/PlanService.js +407 -0
  76. package/dist/node/services/ProjectService.js +1188 -0
  77. package/dist/node/services/PullRequestService.js +484 -0
  78. package/dist/node/services/ScreenshotService.js +285 -0
  79. package/dist/node/services/SubscriptionService.js +377 -0
  80. package/dist/node/services/TrackingService.js +632 -0
  81. package/dist/node/services/WaitlistService.js +129 -0
  82. package/dist/node/services/index.js +60 -4
  83. package/dist/node/state/RootStateManager.js +2 -23
  84. package/dist/node/state/rootEventBus.js +9 -0
  85. package/dist/node/utils/CollabClient.js +77 -11
  86. package/dist/node/utils/TokenManager.js +16 -3
  87. package/dist/node/utils/changePreprocessor.js +180 -0
  88. package/dist/node/utils/jsonDiff.js +46 -4
  89. package/dist/node/utils/ordering.js +290 -0
  90. package/dist/node/utils/services.js +285 -128
  91. package/dist/node/utils/validation.js +0 -3
  92. package/package.json +30 -18
  93. package/src/config/environment.js +95 -10
  94. package/src/index.js +190 -23
  95. package/src/services/AdminService.js +374 -0
  96. package/src/services/AuthService.js +874 -328
  97. package/src/services/BaseService.js +166 -6
  98. package/src/services/BranchService.js +536 -0
  99. package/src/services/CollabService.js +557 -148
  100. package/src/services/DnsService.js +366 -0
  101. package/src/services/FeatureFlagService.js +174 -0
  102. package/src/services/FileService.js +213 -0
  103. package/src/services/IntegrationService.js +548 -0
  104. package/src/services/MetricsService.js +40 -0
  105. package/src/services/PaymentService.js +287 -0
  106. package/src/services/PlanService.js +468 -0
  107. package/src/services/ProjectService.js +1366 -0
  108. package/src/services/PullRequestService.js +537 -0
  109. package/src/services/ScreenshotService.js +258 -0
  110. package/src/services/SubscriptionService.js +425 -0
  111. package/src/services/TrackingService.js +853 -0
  112. package/src/services/WaitlistService.js +130 -0
  113. package/src/services/index.js +79 -5
  114. package/src/services/tests/BranchService/createBranch.test.js +153 -0
  115. package/src/services/tests/BranchService/deleteBranch.test.js +173 -0
  116. package/src/services/tests/BranchService/getBranchChanges.test.js +146 -0
  117. package/src/services/tests/BranchService/listBranches.test.js +87 -0
  118. package/src/services/tests/BranchService/mergeBranch.test.js +210 -0
  119. package/src/services/tests/BranchService/publishVersion.test.js +183 -0
  120. package/src/services/tests/BranchService/renameBranch.test.js +240 -0
  121. package/src/services/tests/BranchService/resetBranch.test.js +152 -0
  122. package/src/services/tests/FeatureFlagService/adminFeatureFlags.test.js +67 -0
  123. package/src/services/tests/FeatureFlagService/getFeatureFlags.test.js +75 -0
  124. package/src/services/tests/FileService/createFileFormData.test.js +74 -0
  125. package/src/services/tests/FileService/getFileUrl.test.js +69 -0
  126. package/src/services/tests/FileService/updateProjectIcon.test.js +109 -0
  127. package/src/services/tests/FileService/uploadDocument.test.js +36 -0
  128. package/src/services/tests/FileService/uploadFile.test.js +78 -0
  129. package/src/services/tests/FileService/uploadFileWithValidation.test.js +114 -0
  130. package/src/services/tests/FileService/uploadImage.test.js +36 -0
  131. package/src/services/tests/FileService/uploadMultipleFiles.test.js +111 -0
  132. package/src/services/tests/FileService/validateFile.test.js +63 -0
  133. package/src/services/tests/PlanService/createPlan.test.js +104 -0
  134. package/src/services/tests/PlanService/createPlanWithValidation.test.js +523 -0
  135. package/src/services/tests/PlanService/deletePlan.test.js +92 -0
  136. package/src/services/tests/PlanService/getActivePlans.test.js +123 -0
  137. package/src/services/tests/PlanService/getAdminPlans.test.js +84 -0
  138. package/src/services/tests/PlanService/getPlan.test.js +50 -0
  139. package/src/services/tests/PlanService/getPlanByKey.test.js +109 -0
  140. package/src/services/tests/PlanService/getPlanWithValidation.test.js +85 -0
  141. package/src/services/tests/PlanService/getPlans.test.js +53 -0
  142. package/src/services/tests/PlanService/getPlansByPriceRange.test.js +109 -0
  143. package/src/services/tests/PlanService/getPlansWithValidation.test.js +48 -0
  144. package/src/services/tests/PlanService/initializePlans.test.js +75 -0
  145. package/src/services/tests/PlanService/updatePlan.test.js +111 -0
  146. package/src/services/tests/PlanService/updatePlanWithValidation.test.js +556 -0
  147. package/src/state/RootStateManager.js +37 -32
  148. package/src/state/rootEventBus.js +19 -0
  149. package/src/utils/CollabClient.js +99 -12
  150. package/src/utils/TokenManager.js +20 -3
  151. package/src/utils/changePreprocessor.js +239 -0
  152. package/src/utils/jsonDiff.js +40 -5
  153. package/src/utils/ordering.js +271 -0
  154. package/src/utils/services.js +306 -139
  155. package/src/utils/validation.js +0 -3
  156. package/dist/cjs/services/AIService.js +0 -155
  157. package/dist/cjs/services/BasedService.js +0 -1185
  158. package/dist/cjs/services/CoreService.js +0 -2295
  159. package/dist/cjs/services/SocketService.js +0 -309
  160. package/dist/cjs/services/SymstoryService.js +0 -571
  161. package/dist/cjs/utils/basedQuerys.js +0 -181
  162. package/dist/cjs/utils/symstoryClient.js +0 -259
  163. package/dist/esm/services/AIService.js +0 -185
  164. package/dist/esm/services/BasedService.js +0 -5262
  165. package/dist/esm/services/CoreService.js +0 -2827
  166. package/dist/esm/services/SocketService.js +0 -456
  167. package/dist/esm/services/SymstoryService.js +0 -7025
  168. package/dist/esm/utils/basedQuerys.js +0 -163
  169. package/dist/esm/utils/symstoryClient.js +0 -354
  170. package/dist/node/services/AIService.js +0 -136
  171. package/dist/node/services/BasedService.js +0 -1156
  172. package/dist/node/services/CoreService.js +0 -2266
  173. package/dist/node/services/SocketService.js +0 -280
  174. package/dist/node/services/SymstoryService.js +0 -542
  175. package/dist/node/utils/basedQuerys.js +0 -162
  176. package/dist/node/utils/symstoryClient.js +0 -230
  177. package/src/services/AIService.js +0 -150
  178. package/src/services/BasedService.js +0 -1302
  179. package/src/services/CoreService.js +0 -2548
  180. package/src/services/SocketService.js +0 -336
  181. package/src/services/SymstoryService.js +0 -649
  182. package/src/utils/basedQuerys.js +0 -164
  183. package/src/utils/symstoryClient.js +0 -252
@@ -1,7 +1,5 @@
1
1
  import { io } from 'socket.io-client'
2
2
  import * as Y from 'yjs'
3
- import { IndexeddbPersistence } from 'y-indexeddb'
4
- import Dexie from 'dexie'
5
3
  import { nanoid } from 'nanoid'
6
4
  import environment from '../config/environment.js'
7
5
  // import gzip from 'gzip-js' // reserved for future compression features
@@ -11,6 +9,8 @@ import { diffJson, applyOpsToJson } from './jsonDiff.js'
11
9
 
12
10
  /* eslint-disable no-use-before-define, no-new, no-promise-executor-return */
13
11
 
12
+ // Dexie and IndexeddbPersistence will be conditionally imported in browser environments
13
+
14
14
  export class CollabClient {
15
15
  /* public fields */
16
16
  socket = null
@@ -24,7 +24,7 @@ export class CollabClient {
24
24
  _buffer = []
25
25
  _flushTimer = null
26
26
  _clientId = nanoid()
27
- _outboxStore = null // Dexie table
27
+ _outboxStore = createMemoryOutbox() // Dexie table fallback
28
28
  _readyResolve
29
29
  ready = new Promise(res => (this._readyResolve = res))
30
30
 
@@ -33,10 +33,35 @@ export class CollabClient {
33
33
 
34
34
  /* 1️⃣ create Yjs doc + offline persistence */
35
35
  this.ydoc = new Y.Doc()
36
- new IndexeddbPersistence(`${projectId}:${branch}`, this.ydoc)
37
36
 
38
- /* 2️⃣ init Dexie for outbox */
39
- this._outboxStore = createDexieOutbox(`${projectId}:${branch}`)
37
+ // Only use IndexeddbPersistence in browser environments
38
+ const hasIndexedDB = typeof globalThis.indexedDB !== 'undefined'
39
+
40
+ if (typeof window === 'undefined' || !hasIndexedDB) {
41
+ // In Node.js (or when indexedDB is not available), skip persistence
42
+ console.log('[CollabClient] IndexedDB not available – skipping offline persistence')
43
+ } else {
44
+ // Dynamically import IndexeddbPersistence only when indexedDB exists
45
+ import('y-indexeddb')
46
+ .then(({ IndexeddbPersistence }) => {
47
+ new IndexeddbPersistence(`${projectId}:${branch}`, this.ydoc)
48
+ })
49
+ .catch(err => {
50
+ console.warn('[CollabClient] Failed to load IndexeddbPersistence:', err)
51
+ })
52
+ }
53
+
54
+ /* 2️⃣ init Dexie for outbox (browser only) */
55
+ if (typeof window !== 'undefined' && hasIndexedDB) {
56
+ // In browser environments, use Dexie
57
+ createDexieOutbox(`${projectId}:${branch}`)
58
+ .then(outboxStore => {
59
+ this._outboxStore = outboxStore
60
+ })
61
+ .catch(err => {
62
+ console.warn('[CollabClient] Failed to load Dexie:', err)
63
+ })
64
+ }
40
65
 
41
66
  /* 3️⃣ WebSocket transport */
42
67
  this.socket = io(environment.socketUrl, {
@@ -52,9 +77,9 @@ export class CollabClient {
52
77
  .on('snapshot', this._onSnapshot)
53
78
  .on('ops', this._onOps)
54
79
  .on('commit', this._onCommit)
55
- .on('liveMode', flag => { this.live = flag })
80
+ .on('liveMode', this._onLiveMode)
56
81
  .on('connect', this._onConnect)
57
- .on('error', e => console.warn('[collab] socket error', e))
82
+ .on('error', this._onError)
58
83
 
59
84
  /* Track last known JSON representation so we can compute granular diffs. */
60
85
  this._prevJson = this.ydoc.getMap('root').toJSON()
@@ -84,8 +109,12 @@ export class CollabClient {
84
109
 
85
110
  /* ---------- private handlers ---------- */
86
111
  _onSnapshot = ({ data /* Uint8Array */ }) => {
87
- // first paint; trust server compressed payload (≤256 kB)
88
- Y.applyUpdate(this.ydoc, Uint8Array.from(data))
112
+ if (Array.isArray(data) ? data.length : (data && data.byteLength)) {
113
+ // First paint; trust server compressed payload (≤256 kB)
114
+ Y.applyUpdate(this.ydoc, Uint8Array.from(data))
115
+ } else {
116
+ console.warn('[collab] Received empty snapshot – skipping applyUpdate')
117
+ }
89
118
 
90
119
  // Store current state as baseline for future diffs.
91
120
  this._prevJson = this.ydoc.getMap('root').toJSON()
@@ -151,10 +180,68 @@ export class CollabClient {
151
180
  })
152
181
  this._buffer.length = 0
153
182
  }
183
+
184
+ dispose () {
185
+ clearTimeout(this._flushTimer)
186
+ this._flushTimer = null
187
+ this._buffer.length = 0
188
+
189
+ if (this._outboxStore?.clear) {
190
+ try {
191
+ const result = this._outboxStore.clear()
192
+ if (result && typeof result.catch === 'function') {
193
+ result.catch(() => {})
194
+ }
195
+ } catch (error) {
196
+ console.warn('[CollabClient] Failed to clear outbox store during dispose:', error)
197
+ }
198
+ }
199
+
200
+ if (this.socket) {
201
+ this.socket.off('snapshot', this._onSnapshot)
202
+ this.socket.off('ops', this._onOps)
203
+ this.socket.off('commit', this._onCommit)
204
+ this.socket.off('liveMode', this._onLiveMode)
205
+ this.socket.off('connect', this._onConnect)
206
+ this.socket.off('error', this._onError)
207
+ this.socket.removeAllListeners()
208
+ this.socket.disconnect()
209
+ this.socket = null
210
+ }
211
+
212
+ if (this.ydoc) {
213
+ this.ydoc.destroy()
214
+ this.ydoc = null
215
+ }
216
+
217
+ if (typeof this._readyResolve === 'function') {
218
+ this._readyResolve()
219
+ this._readyResolve = null
220
+ }
221
+ }
222
+
223
+ _onLiveMode = (flag) => {
224
+ this.live = flag
225
+ }
226
+
227
+ _onError = (e) => {
228
+ console.warn('[collab] socket error', e)
229
+ }
230
+ }
231
+
232
+ /* ---------- Memory storage helper for Node.js ---------- */
233
+ function createMemoryOutbox () {
234
+ const store = new Map()
235
+ return {
236
+ put: (item) => store.set(item.id, item),
237
+ toArray: () => Array.from(store.values()),
238
+ clear: () => store.clear()
239
+ }
154
240
  }
155
241
 
156
- /* ---------- Dexie helper ---------- */
157
- function createDexieOutbox (name) {
242
+ /* ---------- Dexie helper for browser ---------- */
243
+ async function createDexieOutbox (name) {
244
+ const { default: Dexie } = await import('dexie')
158
245
  const db = new Dexie(`collab-${name}`)
159
246
  db.version(1).stores({ outbox: 'id, ops' })
160
247
  return db.table('outbox')
@@ -6,7 +6,7 @@ export class TokenManager {
6
6
  constructor (options = {}) {
7
7
  this.config = {
8
8
  storagePrefix: 'symbols_',
9
- storageType: 'localStorage', // 'localStorage' | 'sessionStorage' | 'memory'
9
+ storageType: (typeof window === 'undefined' || process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'testing') ? 'memory' : 'localStorage', // 'localStorage' | 'sessionStorage' | 'memory'
10
10
  refreshBuffer: 60 * 1000, // Refresh 1 minute before expiry
11
11
  maxRetries: 3,
12
12
  apiUrl: options.apiUrl || '/api',
@@ -52,13 +52,30 @@ export class TokenManager {
52
52
  return this._memoryStorage
53
53
  }
54
54
 
55
+ // Guard against environments where accessing storage throws (e.g., opaque origins)
56
+ const safeGetStorage = (provider) => {
57
+ try {
58
+ const storage = provider()
59
+ // Try a simple set/remove cycle to ensure it is usable
60
+ const testKey = `${this.config.storagePrefix}__tm_test__`
61
+ storage.setItem(testKey, '1')
62
+ storage.removeItem(testKey)
63
+ return storage
64
+ } catch {
65
+ return null
66
+ }
67
+ }
68
+
69
+ const localStorageInstance = safeGetStorage(() => window.localStorage)
70
+ const sessionStorageInstance = safeGetStorage(() => window.sessionStorage)
71
+
55
72
  switch (this.config.storageType) {
56
73
  case 'sessionStorage':
57
- return window.sessionStorage
74
+ return sessionStorageInstance || this._memoryStorage
58
75
  case 'memory':
59
76
  return this._memoryStorage
60
77
  default:
61
- return window.localStorage
78
+ return localStorageInstance || this._memoryStorage
62
79
  }
63
80
  }
64
81
 
@@ -0,0 +1,239 @@
1
+
2
+ import { diffJson } from './jsonDiff.js'
3
+ import { computeOrdersForTuples } from './ordering.js'
4
+
5
+ function isPlainObject (val) {
6
+ return val && typeof val === 'object' && !Array.isArray(val)
7
+ }
8
+
9
+ function getByPathSafe (root, path) {
10
+ if (!root || typeof root.getByPath !== 'function') { return null }
11
+ try { return root.getByPath(path) } catch { return null }
12
+ }
13
+
14
+ // Given the original high-level tuples and a fully qualified path, resolve the
15
+ // "next" value that the change set intends to write at that path.
16
+ // This walks tuples from last to first so that later changes win, and supports
17
+ // nested paths where the tuple only targets a parent container, e.g.:
18
+ // ['update', ['components', 'CanvasLogoDropdown'], { ... }]
19
+ // for a granular path like:
20
+ // ['components', 'CanvasLogoDropdown', 'ProjectNav', 'ListInDropdown', 'children']
21
+ function resolveNextValueFromTuples (tuples, path) {
22
+ if (!Array.isArray(tuples) || !Array.isArray(path)) { return null }
23
+
24
+ // Walk from the end to honour the latest change
25
+ for (let i = tuples.length - 1; i >= 0; i--) {
26
+ const t = tuples[i]
27
+ if (!Array.isArray(t) || t.length < 3) {
28
+ // eslint-disable-next-line no-continue
29
+ continue
30
+ }
31
+ const [action, tuplePath, tupleValue] = t
32
+ if ((action !== 'update' && action !== 'set') || !Array.isArray(tuplePath)) {
33
+ // eslint-disable-next-line no-continue
34
+ continue
35
+ }
36
+ if (tuplePath.length > path.length) {
37
+ // eslint-disable-next-line no-continue
38
+ continue
39
+ }
40
+
41
+ // Ensure tuplePath is a prefix of the requested path
42
+ let isPrefix = true
43
+ for (let j = 0; j < tuplePath.length; j++) {
44
+ if (tuplePath[j] !== path[j]) {
45
+ isPrefix = false
46
+ break
47
+ }
48
+ }
49
+ if (!isPrefix) {
50
+ // eslint-disable-next-line no-continue
51
+ continue
52
+ }
53
+
54
+ // Direct match: the tuple already targets the exact path
55
+ if (tuplePath.length === path.length) {
56
+ return tupleValue
57
+ }
58
+
59
+ // Nested match: drill into the tuple value using the remaining segments
60
+ let current = tupleValue
61
+ for (let j = tuplePath.length; j < path.length; j++) {
62
+ if (current == null) { return null }
63
+ current = current[path[j]]
64
+ }
65
+ if (current !== null) {
66
+ return current
67
+ }
68
+ }
69
+
70
+ return null
71
+ }
72
+
73
+ /**
74
+ * Preprocess broad project changes into granular changes and ordering metadata.
75
+ * - Expands top-level object updates (e.g. ['update', ['components'], {...}])
76
+ * into fine-grained ['update'|'delete', [...], value] tuples using a diff
77
+ * against the current state when available
78
+ * - Preserves schema paths as-is
79
+ * - Filters out explicit deletes targeting __order keys
80
+ * - Appends any extra tuples from options.append
81
+ * - Computes stable orders for impacted parent containers
82
+ * - IMPORTANT: When the tuple represents creation of a brand-new entity
83
+ * (e.g. ['update', ['components', key], {...}] where the path did not exist
84
+ * before, or the corresponding ['schema', ...] path is new), we DO NOT
85
+ * expand it into many granular changes. We keep the original change to avoid
86
+ * generating noisy diffs for creates.
87
+ */
88
+ export function preprocessChanges (root, tuples = [], options = {}) {
89
+ const expandTuple = (t) => {
90
+ const [action, path, value] = t || []
91
+ const isSchemaPath = Array.isArray(path) && path[0] === 'schema'
92
+ const isFilesPath = Array.isArray(path) && path[0] === 'files'
93
+ if (action === 'delete') { return [t] }
94
+
95
+ const canConsiderExpansion = (
96
+ action === 'update' &&
97
+ Array.isArray(path) &&
98
+ (
99
+ path.length === 1 ||
100
+ path.length === 2 ||
101
+ (isSchemaPath && path.length === 3)
102
+ ) &&
103
+ isPlainObject(value)
104
+ )
105
+ if (!canConsiderExpansion || isFilesPath || (value && value.type === 'files')) { return [t] }
106
+
107
+ // Detect brand-new entity creation:
108
+ // - Non-schema entities typically come as ['update', [type, key], {...}]
109
+ // - Schema entries as ['update', ['schema', type, key], {...}]
110
+ // If the exact path does not exist yet, treat it as a "create" operation and
111
+ // do NOT expand into granular leaf updates.
112
+ const prevRaw = getByPathSafe(root, path)
113
+ const isCreatePath = (
114
+ (Array.isArray(path)) &&
115
+ action === 'update' &&
116
+ (
117
+ // e.g. ['update', ['components', 'NewKey'], {...}]
118
+ (!isSchemaPath && path.length === 2) ||
119
+ // e.g. ['update', ['schema', 'components', 'NewKey'], {...}]
120
+ (isSchemaPath && path.length === 3)
121
+ ) &&
122
+ (prevRaw === null || typeof prevRaw === 'undefined')
123
+ )
124
+ if (isCreatePath) { return [t] }
125
+
126
+ const prev = prevRaw || {}
127
+ const next = value || {}
128
+ if (!isPlainObject(prev) || !isPlainObject(next)) { return [t] }
129
+
130
+ const ops = diffJson(prev, next, [])
131
+ // If diff yields no nested ops, preserve the original tuple as a fallback
132
+ // (e.g. when value equality or missing previous state prevents expansion).
133
+ if (!ops.length) { return [t] }
134
+
135
+ const out = []
136
+ for (let i = 0; i < ops.length; i++) {
137
+ const op = ops[i]
138
+ const fullPath = [...path, ...op.path]
139
+ const last = fullPath[fullPath.length - 1]
140
+ if (op.action === 'set') {
141
+ out.push(['update', fullPath, op.value])
142
+ } else if (op.action === 'del') {
143
+ if (last !== '__order') { out.push(['delete', fullPath]) }
144
+ }
145
+ }
146
+ // Prefer granular leaf operations only to minimize payload duplication.
147
+ return out
148
+ }
149
+
150
+ const minimizeTuples = (input) => {
151
+ const out = []
152
+ const seen = new Set()
153
+ for (let i = 0; i < input.length; i++) {
154
+ const expanded = expandTuple(input[i])
155
+ for (let k = 0; k < expanded.length; k++) {
156
+ const tuple = expanded[k]
157
+ const isDelete = Array.isArray(tuple) && tuple[0] === 'delete'
158
+ const isOrderKey = (
159
+ isDelete &&
160
+ Array.isArray(tuple[1]) &&
161
+ tuple[1][tuple[1].length - 1] === '__order'
162
+ )
163
+ if (!isOrderKey) {
164
+ const key = JSON.stringify(tuple)
165
+ if (!seen.has(key)) { seen.add(key); out.push(tuple) }
166
+ }
167
+ }
168
+ }
169
+ return out
170
+ }
171
+
172
+ const granularChanges = (() => {
173
+ try {
174
+ const res = minimizeTuples(tuples)
175
+ if (options.append && options.append.length) { res.push(...options.append) }
176
+ return res
177
+ } catch {
178
+ // Fallback to original tuples if anything goes wrong
179
+ return Array.isArray(tuples) ? tuples.slice() : []
180
+ }
181
+ })()
182
+
183
+ const hydratedGranularChanges = granularChanges.map(t => {
184
+ if (!Array.isArray(t) || t.length < 3) { return t }
185
+ const [action, path] = t
186
+ if ((action !== 'update' && action !== 'set') || !Array.isArray(path)) {
187
+ return t
188
+ }
189
+
190
+ const nextValue = resolveNextValueFromTuples(tuples, path)
191
+ if (nextValue === null) {
192
+ return t
193
+ }
194
+
195
+ return [action, path, nextValue]
196
+ })
197
+
198
+ // Base orders from granular changes/state
199
+ const baseOrders = computeOrdersForTuples(root, hydratedGranularChanges)
200
+
201
+ // Prefer explicit order for containers updated via ['update', [type], value] or ['update', [type, key], value]
202
+ const preferOrdersMap = new Map()
203
+ for (let i = 0; i < tuples.length; i++) {
204
+ const t = tuples[i]
205
+ if (!Array.isArray(t) || t.length < 3) {
206
+ // eslint-disable-next-line no-continue
207
+ continue
208
+ }
209
+ const [action, path, value] = t
210
+ const isFilesPath = Array.isArray(path) && path[0] === 'files'
211
+ if (
212
+ action !== 'update' ||
213
+ !Array.isArray(path) ||
214
+ (path.length !== 1 && path.length !== 2) ||
215
+ !isPlainObject(value) ||
216
+ isFilesPath ||
217
+ (value && value.type === 'files')
218
+ ) {
219
+ // eslint-disable-next-line no-continue
220
+ continue
221
+ }
222
+ const keys = Object.keys(value).filter(k => k !== '__order')
223
+ const key = JSON.stringify(path)
224
+ preferOrdersMap.set(key, { path, keys })
225
+ }
226
+
227
+ const mergedOrders = []
228
+ const seen = new Set()
229
+ // Add preferred top-level orders first
230
+ preferOrdersMap.forEach((v, k) => { seen.add(k); mergedOrders.push(v) })
231
+ // Add remaining base orders
232
+ for (let i = 0; i < baseOrders.length; i++) {
233
+ const v = baseOrders[i]
234
+ const k = JSON.stringify(v.path)
235
+ if (!seen.has(k)) { seen.add(k); mergedOrders.push(v) }
236
+ }
237
+
238
+ return { granularChanges: hydratedGranularChanges, orders: mergedOrders }
239
+ }
@@ -7,12 +7,47 @@ function isPlainObject (o) {
7
7
  }
8
8
 
9
9
  function deepEqual (a, b) {
10
- try {
11
- return JSON.stringify(a) === JSON.stringify(b)
12
- } catch (err) {
13
- console.warn('deepEqual error', err)
14
- return false
10
+ // Fast path for strict equality (handles primitives and same refs)
11
+ if (Object.is(a, b)) { return true }
12
+
13
+ // Functions: compare source text to detect semantic change
14
+ if (typeof a === 'function' && typeof b === 'function') {
15
+ try { return a.toString() === b.toString() } catch { return false }
15
16
  }
17
+
18
+ // One is function and the other is not
19
+ if (typeof a === 'function' || typeof b === 'function') { return false }
20
+
21
+ // Dates
22
+ if (a instanceof Date && b instanceof Date) { return a.getTime() === b.getTime() }
23
+
24
+ // RegExp
25
+ if (a instanceof RegExp && b instanceof RegExp) { return String(a) === String(b) }
26
+
27
+ // Arrays
28
+ if (Array.isArray(a) && Array.isArray(b)) {
29
+ if (a.length !== b.length) { return false }
30
+ for (let i = 0; i < a.length; i++) {
31
+ if (!deepEqual(a[i], b[i])) { return false }
32
+ }
33
+ return true
34
+ }
35
+
36
+ // Objects (including plain objects when we get here)
37
+ if (a && b && typeof a === 'object' && typeof b === 'object') {
38
+ const aKeys = Object.keys(a)
39
+ const bKeys = Object.keys(b)
40
+ if (aKeys.length !== bKeys.length) { return false }
41
+ for (let i = 0; i < aKeys.length; i++) {
42
+ const key = aKeys[i]
43
+ if (!Object.hasOwn(b, key)) { return false }
44
+ if (!deepEqual(a[key], b[key])) { return false }
45
+ }
46
+ return true
47
+ }
48
+
49
+ // Fallback for different types
50
+ return false
16
51
  }
17
52
 
18
53
  import * as Y from 'yjs'