@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.
- package/README.md +141 -0
- package/dist/cjs/config/environment.js +94 -10
- package/dist/cjs/index.js +152 -12
- package/dist/cjs/services/AdminService.js +351 -0
- package/dist/cjs/services/AuthService.js +738 -305
- package/dist/cjs/services/BaseService.js +158 -6
- package/dist/cjs/services/BranchService.js +484 -0
- package/dist/cjs/services/CollabService.js +439 -116
- package/dist/cjs/services/DnsService.js +340 -0
- package/dist/cjs/services/FeatureFlagService.js +175 -0
- package/dist/cjs/services/FileService.js +201 -0
- package/dist/cjs/services/IntegrationService.js +538 -0
- package/dist/cjs/services/MetricsService.js +62 -0
- package/dist/cjs/services/PaymentService.js +271 -0
- package/dist/cjs/services/PlanService.js +426 -0
- package/dist/cjs/services/ProjectService.js +1207 -0
- package/dist/cjs/services/PullRequestService.js +503 -0
- package/dist/cjs/services/ScreenshotService.js +304 -0
- package/dist/cjs/services/SubscriptionService.js +396 -0
- package/dist/cjs/services/TrackingService.js +661 -0
- package/dist/cjs/services/WaitlistService.js +148 -0
- package/dist/cjs/services/index.js +60 -4
- package/dist/cjs/state/RootStateManager.js +2 -23
- package/dist/cjs/state/rootEventBus.js +9 -0
- package/dist/cjs/utils/CollabClient.js +78 -12
- package/dist/cjs/utils/TokenManager.js +16 -3
- package/dist/cjs/utils/changePreprocessor.js +199 -0
- package/dist/cjs/utils/jsonDiff.js +46 -4
- package/dist/cjs/utils/ordering.js +309 -0
- package/dist/cjs/utils/services.js +285 -128
- package/dist/cjs/utils/validation.js +0 -3
- package/dist/esm/config/environment.js +94 -10
- package/dist/esm/index.js +47862 -18248
- package/dist/esm/services/AdminService.js +1132 -0
- package/dist/esm/services/AuthService.js +1493 -386
- package/dist/esm/services/BaseService.js +757 -6
- package/dist/esm/services/BranchService.js +1265 -0
- package/dist/esm/services/CollabService.js +24956 -16089
- package/dist/esm/services/DnsService.js +1121 -0
- package/dist/esm/services/FeatureFlagService.js +956 -0
- package/dist/esm/services/FileService.js +982 -0
- package/dist/esm/services/IntegrationService.js +1319 -0
- package/dist/esm/services/MetricsService.js +843 -0
- package/dist/esm/services/PaymentService.js +1052 -0
- package/dist/esm/services/PlanService.js +1207 -0
- package/dist/esm/services/ProjectService.js +2526 -0
- package/dist/esm/services/PullRequestService.js +1284 -0
- package/dist/esm/services/ScreenshotService.js +1085 -0
- package/dist/esm/services/SubscriptionService.js +1177 -0
- package/dist/esm/services/TrackingService.js +18454 -0
- package/dist/esm/services/WaitlistService.js +929 -0
- package/dist/esm/services/index.js +47373 -18027
- package/dist/esm/state/RootStateManager.js +11 -23
- package/dist/esm/state/rootEventBus.js +9 -0
- package/dist/esm/utils/CollabClient.js +17526 -16120
- package/dist/esm/utils/TokenManager.js +16 -3
- package/dist/esm/utils/changePreprocessor.js +542 -0
- package/dist/esm/utils/jsonDiff.js +958 -43
- package/dist/esm/utils/ordering.js +291 -0
- package/dist/esm/utils/services.js +285 -128
- package/dist/esm/utils/validation.js +116 -50
- package/dist/node/config/environment.js +94 -10
- package/dist/node/index.js +183 -16
- package/dist/node/services/AdminService.js +332 -0
- package/dist/node/services/AuthService.js +742 -310
- package/dist/node/services/BaseService.js +148 -6
- package/dist/node/services/BranchService.js +465 -0
- package/dist/node/services/CollabService.js +439 -116
- package/dist/node/services/DnsService.js +321 -0
- package/dist/node/services/FeatureFlagService.js +156 -0
- package/dist/node/services/FileService.js +182 -0
- package/dist/node/services/IntegrationService.js +519 -0
- package/dist/node/services/MetricsService.js +43 -0
- package/dist/node/services/PaymentService.js +252 -0
- package/dist/node/services/PlanService.js +407 -0
- package/dist/node/services/ProjectService.js +1188 -0
- package/dist/node/services/PullRequestService.js +484 -0
- package/dist/node/services/ScreenshotService.js +285 -0
- package/dist/node/services/SubscriptionService.js +377 -0
- package/dist/node/services/TrackingService.js +632 -0
- package/dist/node/services/WaitlistService.js +129 -0
- package/dist/node/services/index.js +60 -4
- package/dist/node/state/RootStateManager.js +2 -23
- package/dist/node/state/rootEventBus.js +9 -0
- package/dist/node/utils/CollabClient.js +77 -11
- package/dist/node/utils/TokenManager.js +16 -3
- package/dist/node/utils/changePreprocessor.js +180 -0
- package/dist/node/utils/jsonDiff.js +46 -4
- package/dist/node/utils/ordering.js +290 -0
- package/dist/node/utils/services.js +285 -128
- package/dist/node/utils/validation.js +0 -3
- package/package.json +30 -18
- package/src/config/environment.js +95 -10
- package/src/index.js +190 -23
- package/src/services/AdminService.js +374 -0
- package/src/services/AuthService.js +874 -328
- package/src/services/BaseService.js +166 -6
- package/src/services/BranchService.js +536 -0
- package/src/services/CollabService.js +557 -148
- package/src/services/DnsService.js +366 -0
- package/src/services/FeatureFlagService.js +174 -0
- package/src/services/FileService.js +213 -0
- package/src/services/IntegrationService.js +548 -0
- package/src/services/MetricsService.js +40 -0
- package/src/services/PaymentService.js +287 -0
- package/src/services/PlanService.js +468 -0
- package/src/services/ProjectService.js +1366 -0
- package/src/services/PullRequestService.js +537 -0
- package/src/services/ScreenshotService.js +258 -0
- package/src/services/SubscriptionService.js +425 -0
- package/src/services/TrackingService.js +853 -0
- package/src/services/WaitlistService.js +130 -0
- package/src/services/index.js +79 -5
- package/src/services/tests/BranchService/createBranch.test.js +153 -0
- package/src/services/tests/BranchService/deleteBranch.test.js +173 -0
- package/src/services/tests/BranchService/getBranchChanges.test.js +146 -0
- package/src/services/tests/BranchService/listBranches.test.js +87 -0
- package/src/services/tests/BranchService/mergeBranch.test.js +210 -0
- package/src/services/tests/BranchService/publishVersion.test.js +183 -0
- package/src/services/tests/BranchService/renameBranch.test.js +240 -0
- package/src/services/tests/BranchService/resetBranch.test.js +152 -0
- package/src/services/tests/FeatureFlagService/adminFeatureFlags.test.js +67 -0
- package/src/services/tests/FeatureFlagService/getFeatureFlags.test.js +75 -0
- package/src/services/tests/FileService/createFileFormData.test.js +74 -0
- package/src/services/tests/FileService/getFileUrl.test.js +69 -0
- package/src/services/tests/FileService/updateProjectIcon.test.js +109 -0
- package/src/services/tests/FileService/uploadDocument.test.js +36 -0
- package/src/services/tests/FileService/uploadFile.test.js +78 -0
- package/src/services/tests/FileService/uploadFileWithValidation.test.js +114 -0
- package/src/services/tests/FileService/uploadImage.test.js +36 -0
- package/src/services/tests/FileService/uploadMultipleFiles.test.js +111 -0
- package/src/services/tests/FileService/validateFile.test.js +63 -0
- package/src/services/tests/PlanService/createPlan.test.js +104 -0
- package/src/services/tests/PlanService/createPlanWithValidation.test.js +523 -0
- package/src/services/tests/PlanService/deletePlan.test.js +92 -0
- package/src/services/tests/PlanService/getActivePlans.test.js +123 -0
- package/src/services/tests/PlanService/getAdminPlans.test.js +84 -0
- package/src/services/tests/PlanService/getPlan.test.js +50 -0
- package/src/services/tests/PlanService/getPlanByKey.test.js +109 -0
- package/src/services/tests/PlanService/getPlanWithValidation.test.js +85 -0
- package/src/services/tests/PlanService/getPlans.test.js +53 -0
- package/src/services/tests/PlanService/getPlansByPriceRange.test.js +109 -0
- package/src/services/tests/PlanService/getPlansWithValidation.test.js +48 -0
- package/src/services/tests/PlanService/initializePlans.test.js +75 -0
- package/src/services/tests/PlanService/updatePlan.test.js +111 -0
- package/src/services/tests/PlanService/updatePlanWithValidation.test.js +556 -0
- package/src/state/RootStateManager.js +37 -32
- package/src/state/rootEventBus.js +19 -0
- package/src/utils/CollabClient.js +99 -12
- package/src/utils/TokenManager.js +20 -3
- package/src/utils/changePreprocessor.js +239 -0
- package/src/utils/jsonDiff.js +40 -5
- package/src/utils/ordering.js +271 -0
- package/src/utils/services.js +306 -139
- package/src/utils/validation.js +0 -3
- package/dist/cjs/services/AIService.js +0 -155
- package/dist/cjs/services/BasedService.js +0 -1185
- package/dist/cjs/services/CoreService.js +0 -2295
- package/dist/cjs/services/SocketService.js +0 -309
- package/dist/cjs/services/SymstoryService.js +0 -571
- package/dist/cjs/utils/basedQuerys.js +0 -181
- package/dist/cjs/utils/symstoryClient.js +0 -259
- package/dist/esm/services/AIService.js +0 -185
- package/dist/esm/services/BasedService.js +0 -5262
- package/dist/esm/services/CoreService.js +0 -2827
- package/dist/esm/services/SocketService.js +0 -456
- package/dist/esm/services/SymstoryService.js +0 -7025
- package/dist/esm/utils/basedQuerys.js +0 -163
- package/dist/esm/utils/symstoryClient.js +0 -354
- package/dist/node/services/AIService.js +0 -136
- package/dist/node/services/BasedService.js +0 -1156
- package/dist/node/services/CoreService.js +0 -2266
- package/dist/node/services/SocketService.js +0 -280
- package/dist/node/services/SymstoryService.js +0 -542
- package/dist/node/utils/basedQuerys.js +0 -162
- package/dist/node/utils/symstoryClient.js +0 -230
- package/src/services/AIService.js +0 -150
- package/src/services/BasedService.js +0 -1302
- package/src/services/CoreService.js +0 -2548
- package/src/services/SocketService.js +0 -336
- package/src/services/SymstoryService.js +0 -649
- package/src/utils/basedQuerys.js +0 -164
- 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 =
|
|
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
|
-
|
|
39
|
-
|
|
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',
|
|
80
|
+
.on('liveMode', this._onLiveMode)
|
|
56
81
|
.on('connect', this._onConnect)
|
|
57
|
-
.on('error',
|
|
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
|
-
|
|
88
|
-
|
|
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
|
|
74
|
+
return sessionStorageInstance || this._memoryStorage
|
|
58
75
|
case 'memory':
|
|
59
76
|
return this._memoryStorage
|
|
60
77
|
default:
|
|
61
|
-
return
|
|
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
|
+
}
|
package/src/utils/jsonDiff.js
CHANGED
|
@@ -7,12 +7,47 @@ function isPlainObject (o) {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
function deepEqual (a, b) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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'
|