@symbo.ls/sdk 3.1.2 → 3.2.6

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 +143 -2
  2. package/dist/cjs/config/environment.js +98 -30
  3. package/dist/cjs/index.js +144 -24
  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 +743 -0
  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 +64 -16
  23. package/dist/cjs/state/RootStateManager.js +65 -0
  24. package/dist/cjs/state/rootEventBus.js +74 -0
  25. package/dist/cjs/utils/CollabClient.js +223 -0
  26. package/dist/cjs/utils/TokenManager.js +78 -30
  27. package/dist/cjs/utils/changePreprocessor.js +199 -0
  28. package/dist/cjs/utils/jsonDiff.js +145 -0
  29. package/dist/cjs/utils/ordering.js +309 -0
  30. package/dist/cjs/utils/services.js +301 -103
  31. package/dist/cjs/utils/validation.js +0 -3
  32. package/dist/esm/config/environment.js +98 -30
  33. package/dist/esm/index.js +49505 -8718
  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 +26895 -0
  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 +49062 -8569
  53. package/dist/esm/state/RootStateManager.js +90 -0
  54. package/dist/esm/state/rootEventBus.js +56 -0
  55. package/dist/esm/utils/CollabClient.js +18889 -0
  56. package/dist/esm/utils/TokenManager.js +78 -30
  57. package/dist/esm/utils/changePreprocessor.js +542 -0
  58. package/dist/esm/utils/jsonDiff.js +7011 -0
  59. package/dist/esm/utils/ordering.js +291 -0
  60. package/dist/esm/utils/services.js +301 -103
  61. package/dist/esm/utils/validation.js +116 -50
  62. package/dist/node/config/environment.js +98 -30
  63. package/dist/node/index.js +175 -32
  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 +724 -0
  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 +64 -16
  83. package/dist/node/state/RootStateManager.js +36 -0
  84. package/dist/node/state/rootEventBus.js +55 -0
  85. package/dist/node/utils/CollabClient.js +194 -0
  86. package/dist/node/utils/TokenManager.js +78 -30
  87. package/dist/node/utils/changePreprocessor.js +180 -0
  88. package/dist/node/utils/jsonDiff.js +116 -0
  89. package/dist/node/utils/ordering.js +290 -0
  90. package/dist/node/utils/services.js +301 -103
  91. package/dist/node/utils/validation.js +0 -3
  92. package/package.json +39 -21
  93. package/src/config/environment.js +99 -28
  94. package/src/index.js +181 -36
  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 +900 -0
  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 +80 -13
  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 +76 -0
  148. package/src/state/rootEventBus.js +67 -0
  149. package/src/utils/CollabClient.js +248 -0
  150. package/src/utils/TokenManager.js +88 -33
  151. package/src/utils/changePreprocessor.js +239 -0
  152. package/src/utils/jsonDiff.js +144 -0
  153. package/src/utils/ordering.js +271 -0
  154. package/src/utils/services.js +326 -107
  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 -1751
  159. package/dist/cjs/services/SocketIOService.js +0 -307
  160. package/dist/cjs/services/SocketService.js +0 -161
  161. package/dist/cjs/services/SymstoryService.js +0 -571
  162. package/dist/cjs/utils/basedQuerys.js +0 -181
  163. package/dist/cjs/utils/symstoryClient.js +0 -259
  164. package/dist/esm/services/AIService.js +0 -185
  165. package/dist/esm/services/BasedService.js +0 -5278
  166. package/dist/esm/services/CoreService.js +0 -2264
  167. package/dist/esm/services/SocketIOService.js +0 -470
  168. package/dist/esm/services/SocketService.js +0 -191
  169. package/dist/esm/services/SymstoryService.js +0 -7041
  170. package/dist/esm/utils/basedQuerys.js +0 -163
  171. package/dist/esm/utils/symstoryClient.js +0 -370
  172. package/dist/node/services/AIService.js +0 -136
  173. package/dist/node/services/BasedService.js +0 -1156
  174. package/dist/node/services/CoreService.js +0 -1722
  175. package/dist/node/services/SocketIOService.js +0 -278
  176. package/dist/node/services/SocketService.js +0 -142
  177. package/dist/node/services/SymstoryService.js +0 -542
  178. package/dist/node/utils/basedQuerys.js +0 -162
  179. package/dist/node/utils/symstoryClient.js +0 -230
  180. package/src/services/AIService.js +0 -150
  181. package/src/services/BasedService.js +0 -1301
  182. package/src/services/CoreService.js +0 -1943
  183. package/src/services/SocketIOService.js +0 -334
  184. package/src/services/SocketService.js +0 -168
  185. package/src/services/SymstoryService.js +0 -649
  186. package/src/utils/basedQuerys.js +0 -164
  187. package/src/utils/symstoryClient.js +0 -252
@@ -0,0 +1,248 @@
1
+ import { io } from 'socket.io-client'
2
+ import * as Y from 'yjs'
3
+ import { nanoid } from 'nanoid'
4
+ import environment from '../config/environment.js'
5
+ // import gzip from 'gzip-js' // reserved for future compression features
6
+
7
+ // diff / patch helpers
8
+ import { diffJson, applyOpsToJson } from './jsonDiff.js'
9
+
10
+ /* eslint-disable no-use-before-define, no-new, no-promise-executor-return */
11
+
12
+ // Dexie and IndexeddbPersistence will be conditionally imported in browser environments
13
+
14
+ export class CollabClient {
15
+ /* public fields */
16
+ socket = null
17
+ ydoc = null
18
+ branch = 'main'
19
+ live = false
20
+ projectId = null
21
+ jwt = null
22
+
23
+ /* private state */
24
+ _buffer = []
25
+ _flushTimer = null
26
+ _clientId = nanoid()
27
+ _outboxStore = createMemoryOutbox() // Dexie table fallback
28
+ _readyResolve
29
+ ready = new Promise(res => (this._readyResolve = res))
30
+
31
+ constructor ({ jwt, projectId, branch = 'main', live = false }) {
32
+ Object.assign(this, { jwt, projectId, branch, live })
33
+
34
+ /* 1️⃣ create Yjs doc + offline persistence */
35
+ this.ydoc = new Y.Doc()
36
+
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
+ }
65
+
66
+ /* 3️⃣ WebSocket transport */
67
+ this.socket = io(environment.socketUrl, {
68
+ path: '/collab-socket',
69
+ transports: ['websocket'],
70
+ auth: { token: jwt, projectId, branch, live },
71
+ reconnectionAttempts: Infinity,
72
+ reconnectionDelayMax: 4000
73
+ })
74
+
75
+ /* socket events */
76
+ this.socket
77
+ .on('snapshot', this._onSnapshot)
78
+ .on('ops', this._onOps)
79
+ .on('commit', this._onCommit)
80
+ .on('liveMode', this._onLiveMode)
81
+ .on('connect', this._onConnect)
82
+ .on('error', this._onError)
83
+
84
+ /* Track last known JSON representation so we can compute granular diffs. */
85
+ this._prevJson = this.ydoc.getMap('root').toJSON()
86
+
87
+ /* 4️⃣ hook Yjs change listener */
88
+ this.ydoc.on('afterTransaction', tr => {
89
+ // Ignore changes that originated from remote patches.
90
+ if (tr.origin === 'remote') {return}
91
+
92
+ const currentJson = this.ydoc.getMap('root').toJSON()
93
+
94
+ // Compute minimal diff between previous and current state.
95
+ const ops = diffJson(this._prevJson, currentJson)
96
+
97
+ // Cache new snapshot for next diff calculation.
98
+ this._prevJson = currentJson
99
+
100
+ if (!ops.length) {return}
101
+ this._queueOps(ops)
102
+ })
103
+ }
104
+
105
+ /* ---------- public helpers ---------- */
106
+ toggleLive (flag) { this.socket.emit('toggleLive', Boolean(flag)) }
107
+ sendCursor (data) { this.socket.emit('cursor', data) }
108
+ sendPresence (d) { this.socket.emit('presence', d) }
109
+
110
+ /* ---------- private handlers ---------- */
111
+ _onSnapshot = ({ data /* Uint8Array */ }) => {
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
+ }
118
+
119
+ // Store current state as baseline for future diffs.
120
+ this._prevJson = this.ydoc.getMap('root').toJSON()
121
+ if (typeof this._readyResolve === 'function') {
122
+ this._readyResolve()
123
+ this._readyResolve = null
124
+ }
125
+ }
126
+
127
+ _onOps = ({ changes }) => {
128
+ // Apply remote ops
129
+ applyOpsToJson(changes, this.ydoc)
130
+
131
+ // Refresh baseline snapshot so we don't generate redundant diffs for the
132
+ // just-applied remote changes.
133
+ this._prevJson = this.ydoc.getMap('root').toJSON()
134
+ }
135
+
136
+ _onCommit = async ({ version }) => {
137
+ await this._outboxStore.clear()
138
+ console.info('[collab] committed', version)
139
+ }
140
+
141
+ _onConnect = async () => {
142
+ // Mark client as ready if we haven't received a snapshot yet
143
+ if (typeof this._readyResolve === 'function') {
144
+ this._readyResolve()
145
+ // Prevent multiple resolutions
146
+ this._readyResolve = null
147
+ }
148
+
149
+ // flush locally stored ops
150
+ const queued = await this._outboxStore.toArray()
151
+ if (queued.length) {
152
+ this.socket.emit('ops', {
153
+ changes: queued.flatMap(e => e.ops),
154
+ ts: Date.now(),
155
+ clientId: this._clientId
156
+ })
157
+ await this._outboxStore.clear()
158
+ }
159
+ }
160
+
161
+ /* ---------- buffering & debounce ---------- */
162
+ _queueOps (ops) {
163
+ this._buffer.push(...ops)
164
+ this._outboxStore.put({ id: nanoid(), ops })
165
+
166
+ if (this.live && this.socket.connected) {
167
+ this._flushNow()
168
+ } else {
169
+ clearTimeout(this._flushTimer)
170
+ this._flushTimer = setTimeout(() => this._flushNow(), 40)
171
+ }
172
+ }
173
+
174
+ _flushNow () {
175
+ if (!this._buffer.length || !this.socket.connected) {return}
176
+ this.socket.emit('ops', {
177
+ changes: this._buffer,
178
+ ts: Date.now(),
179
+ clientId: this._clientId
180
+ })
181
+ this._buffer.length = 0
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
+ }
240
+ }
241
+
242
+ /* ---------- Dexie helper for browser ---------- */
243
+ async function createDexieOutbox (name) {
244
+ const { default: Dexie } = await import('dexie')
245
+ const db = new Dexie(`collab-${name}`)
246
+ db.version(1).stores({ outbox: 'id, ops' })
247
+ return db.table('outbox')
248
+ }
@@ -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
 
@@ -147,7 +164,28 @@ export class TokenManager {
147
164
  if (!this.tokens.expiresAt) {return true} // No expiry info, assume valid
148
165
 
149
166
  const now = Date.now()
150
- return now < (this.tokens.expiresAt - this.config.refreshBuffer)
167
+ const isValid = now < (this.tokens.expiresAt - this.config.refreshBuffer)
168
+
169
+ if (!isValid) {
170
+ console.log('[TokenManager] Access token is expired or near expiry:', {
171
+ now: new Date(now).toISOString(),
172
+ expiresAt: new Date(this.tokens.expiresAt).toISOString(),
173
+ refreshBuffer: this.config.refreshBuffer
174
+ })
175
+ }
176
+
177
+ return isValid
178
+ }
179
+
180
+ /**
181
+ * Check if access token exists and is not expired (without refresh buffer)
182
+ */
183
+ isAccessTokenActuallyValid () {
184
+ if (!this.tokens.accessToken) {return false}
185
+ if (!this.tokens.expiresAt) {return true} // No expiry info, assume valid
186
+
187
+ const now = Date.now()
188
+ return now < this.tokens.expiresAt
151
189
  }
152
190
 
153
191
  /**
@@ -303,23 +341,30 @@ export class TokenManager {
303
341
  * Save tokens to storage
304
342
  */
305
343
  saveTokens () {
306
- const {storage} = this
307
- const keys = this.storageKeys
344
+ try {
345
+ const {storage} = this
346
+ const keys = this.storageKeys
308
347
 
309
- if (this.tokens.accessToken) {
310
- storage.setItem(keys.accessToken, this.tokens.accessToken)
311
- }
348
+ if (this.tokens.accessToken) {
349
+ storage.setItem(keys.accessToken, this.tokens.accessToken)
350
+ }
312
351
 
313
- if (this.tokens.refreshToken) {
314
- storage.setItem(keys.refreshToken, this.tokens.refreshToken)
315
- }
352
+ if (this.tokens.refreshToken) {
353
+ storage.setItem(keys.refreshToken, this.tokens.refreshToken)
354
+ }
316
355
 
317
- if (this.tokens.expiresAt) {
318
- storage.setItem(keys.expiresAt, this.tokens.expiresAt.toString())
319
- }
356
+ if (this.tokens.expiresAt) {
357
+ storage.setItem(keys.expiresAt, this.tokens.expiresAt.toString())
358
+ }
359
+
360
+ if (this.tokens.expiresIn) {
361
+ storage.setItem(keys.expiresIn, this.tokens.expiresIn.toString())
362
+ }
320
363
 
321
- if (this.tokens.expiresIn) {
322
- storage.setItem(keys.expiresIn, this.tokens.expiresIn.toString())
364
+ } catch (error) {
365
+ console.error('[TokenManager] Error saving tokens to storage:', error)
366
+ // Don't throw here as it would break the token setting flow
367
+ // but log the error for debugging
323
368
  }
324
369
  }
325
370
 
@@ -327,25 +372,35 @@ export class TokenManager {
327
372
  * Load tokens from storage
328
373
  */
329
374
  loadTokens () {
330
- const {storage} = this
331
- const keys = this.storageKeys
332
-
333
- const accessToken = storage.getItem(keys.accessToken)
334
- const refreshToken = storage.getItem(keys.refreshToken)
335
- const expiresAt = storage.getItem(keys.expiresAt)
336
- const expiresIn = storage.getItem(keys.expiresIn)
375
+ try {
376
+ const {storage} = this
377
+ const keys = this.storageKeys
378
+
379
+ const accessToken = storage.getItem(keys.accessToken)
380
+ const refreshToken = storage.getItem(keys.refreshToken)
381
+ const expiresAt = storage.getItem(keys.expiresAt)
382
+ const expiresIn = storage.getItem(keys.expiresIn)
383
+
384
+ if (accessToken) {
385
+ this.tokens = {
386
+ accessToken,
387
+ refreshToken,
388
+ expiresAt: expiresAt ? parseInt(expiresAt, 10) : null,
389
+ expiresIn: expiresIn ? parseInt(expiresIn, 10) : null,
390
+ tokenType: 'Bearer'
391
+ }
337
392
 
338
- if (accessToken) {
393
+ // Schedule refresh for loaded tokens
394
+ this.scheduleRefresh()
395
+ }
396
+ } catch (error) {
397
+ console.error('[TokenManager] Error loading tokens from storage:', error)
339
398
  this.tokens = {
340
- accessToken,
341
- refreshToken,
342
- expiresAt: expiresAt ? parseInt(expiresAt, 10) : null,
343
- expiresIn: expiresIn ? parseInt(expiresIn, 10) : null,
344
- tokenType: 'Bearer'
399
+ accessToken: null,
400
+ refreshToken: null,
401
+ expiresAt: null,
402
+ expiresIn: null
345
403
  }
346
-
347
- // Schedule refresh for loaded tokens
348
- this.scheduleRefresh()
349
404
  }
350
405
  }
351
406
 
@@ -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
+ }