@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.
- package/README.md +143 -2
- package/dist/cjs/config/environment.js +98 -30
- package/dist/cjs/index.js +144 -24
- 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 +743 -0
- 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 +64 -16
- package/dist/cjs/state/RootStateManager.js +65 -0
- package/dist/cjs/state/rootEventBus.js +74 -0
- package/dist/cjs/utils/CollabClient.js +223 -0
- package/dist/cjs/utils/TokenManager.js +78 -30
- package/dist/cjs/utils/changePreprocessor.js +199 -0
- package/dist/cjs/utils/jsonDiff.js +145 -0
- package/dist/cjs/utils/ordering.js +309 -0
- package/dist/cjs/utils/services.js +301 -103
- package/dist/cjs/utils/validation.js +0 -3
- package/dist/esm/config/environment.js +98 -30
- package/dist/esm/index.js +49505 -8718
- 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 +26895 -0
- 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 +49062 -8569
- package/dist/esm/state/RootStateManager.js +90 -0
- package/dist/esm/state/rootEventBus.js +56 -0
- package/dist/esm/utils/CollabClient.js +18889 -0
- package/dist/esm/utils/TokenManager.js +78 -30
- package/dist/esm/utils/changePreprocessor.js +542 -0
- package/dist/esm/utils/jsonDiff.js +7011 -0
- package/dist/esm/utils/ordering.js +291 -0
- package/dist/esm/utils/services.js +301 -103
- package/dist/esm/utils/validation.js +116 -50
- package/dist/node/config/environment.js +98 -30
- package/dist/node/index.js +175 -32
- 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 +724 -0
- 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 +64 -16
- package/dist/node/state/RootStateManager.js +36 -0
- package/dist/node/state/rootEventBus.js +55 -0
- package/dist/node/utils/CollabClient.js +194 -0
- package/dist/node/utils/TokenManager.js +78 -30
- package/dist/node/utils/changePreprocessor.js +180 -0
- package/dist/node/utils/jsonDiff.js +116 -0
- package/dist/node/utils/ordering.js +290 -0
- package/dist/node/utils/services.js +301 -103
- package/dist/node/utils/validation.js +0 -3
- package/package.json +39 -21
- package/src/config/environment.js +99 -28
- package/src/index.js +181 -36
- 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 +900 -0
- 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 +80 -13
- 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 +76 -0
- package/src/state/rootEventBus.js +67 -0
- package/src/utils/CollabClient.js +248 -0
- package/src/utils/TokenManager.js +88 -33
- package/src/utils/changePreprocessor.js +239 -0
- package/src/utils/jsonDiff.js +144 -0
- package/src/utils/ordering.js +271 -0
- package/src/utils/services.js +326 -107
- 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 -1751
- package/dist/cjs/services/SocketIOService.js +0 -307
- package/dist/cjs/services/SocketService.js +0 -161
- 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 -5278
- package/dist/esm/services/CoreService.js +0 -2264
- package/dist/esm/services/SocketIOService.js +0 -470
- package/dist/esm/services/SocketService.js +0 -191
- package/dist/esm/services/SymstoryService.js +0 -7041
- package/dist/esm/utils/basedQuerys.js +0 -163
- package/dist/esm/utils/symstoryClient.js +0 -370
- package/dist/node/services/AIService.js +0 -136
- package/dist/node/services/BasedService.js +0 -1156
- package/dist/node/services/CoreService.js +0 -1722
- package/dist/node/services/SocketIOService.js +0 -278
- package/dist/node/services/SocketService.js +0 -142
- 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 -1301
- package/src/services/CoreService.js +0 -1943
- package/src/services/SocketIOService.js +0 -334
- package/src/services/SocketService.js +0 -168
- package/src/services/SymstoryService.js +0 -649
- package/src/utils/basedQuerys.js +0 -164
- 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
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
307
|
-
|
|
344
|
+
try {
|
|
345
|
+
const {storage} = this
|
|
346
|
+
const keys = this.storageKeys
|
|
308
347
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
348
|
+
if (this.tokens.accessToken) {
|
|
349
|
+
storage.setItem(keys.accessToken, this.tokens.accessToken)
|
|
350
|
+
}
|
|
312
351
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
352
|
+
if (this.tokens.refreshToken) {
|
|
353
|
+
storage.setItem(keys.refreshToken, this.tokens.refreshToken)
|
|
354
|
+
}
|
|
316
355
|
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
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:
|
|
343
|
-
expiresIn:
|
|
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
|
+
}
|