@playcraft/common 0.0.6 → 0.0.8
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/dist/database/schema.d.ts +19 -0
- package/dist/database/schema.d.ts.map +1 -1
- package/dist/database/schema.js +1 -0
- package/dist/database/schema.js.map +1 -1
- package/dist/mcp/tools.d.ts +2 -2
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +573 -61
- package/dist/mcp/tools.js.map +1 -1
- package/dist/models/playcanvas-compat.d.ts +1 -0
- package/dist/models/playcanvas-compat.d.ts.map +1 -1
- package/dist/models/playcanvas-compat.js +1 -0
- package/dist/models/playcanvas-compat.js.map +1 -1
- package/dist/models/scene.d.ts +10 -1
- package/dist/models/scene.d.ts.map +1 -1
- package/dist/models/scene.js +10 -1
- package/dist/models/scene.js.map +1 -1
- package/dist/sharedb/server.d.ts +21 -3
- package/dist/sharedb/server.d.ts.map +1 -1
- package/dist/sharedb/server.js +273 -93
- package/dist/sharedb/server.js.map +1 -1
- package/dist/storage/cos.d.ts +6 -6
- package/dist/storage/cos.d.ts.map +1 -1
- package/dist/storage/cos.js +70 -80
- package/dist/storage/cos.js.map +1 -1
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +2 -0
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/interface.d.ts +1 -0
- package/dist/storage/interface.d.ts.map +1 -1
- package/dist/storage/upload-build-artifacts.d.ts +38 -0
- package/dist/storage/upload-build-artifacts.d.ts.map +1 -0
- package/dist/storage/upload-build-artifacts.js +60 -0
- package/dist/storage/upload-build-artifacts.js.map +1 -0
- package/package.json +3 -2
package/dist/sharedb/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
1
2
|
import ShareDB from 'sharedb';
|
|
2
3
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
3
4
|
import WebSocketJSONStream from '@teamwork/websocket-json-stream';
|
|
@@ -21,6 +22,9 @@ export class ShareDBRealtimeServer {
|
|
|
21
22
|
CACHE_TTL_MS = 30 * 60 * 1000; // 30 分钟
|
|
22
23
|
MAX_CACHE_SIZE = 10000;
|
|
23
24
|
cacheCleanupTimer = null;
|
|
25
|
+
/** 连接上下文(auth 时由 client 传 projectId/branchId),用于单条 get 与列表同分支 */
|
|
26
|
+
connectionContextMap = new Map();
|
|
27
|
+
connectionContextStorage = new AsyncLocalStorage();
|
|
24
28
|
constructor(options = {}) {
|
|
25
29
|
this.options = options;
|
|
26
30
|
this.backend = new ShareDB({ defaultType: 'json0' });
|
|
@@ -134,13 +138,13 @@ export class ShareDBRealtimeServer {
|
|
|
134
138
|
// 🔧 修复:添加 documents 和 settings 到 readSnapshots 中间件
|
|
135
139
|
// Ensure missing docs in readSnapshots (primarily for scenes/assets/documents/settings)
|
|
136
140
|
this.backend.use('readSnapshots', async (ctx, next) => {
|
|
137
|
-
console.log(`[sharedb] readSnapshots middleware called for collection=${ctx.collection}, snapshots=${ctx.snapshots?.length || 0}`);
|
|
141
|
+
// console.log(`[sharedb] readSnapshots middleware called for collection=${ctx.collection}, snapshots=${ctx.snapshots?.length || 0}`);
|
|
138
142
|
if (ctx.collection !== 'scenes' && ctx.collection !== 'assets' && ctx.collection !== 'documents' && ctx.collection !== 'settings' && ctx.collection !== 'user_data') {
|
|
139
|
-
console.log(`[sharedb] Skipping readSnapshots for collection=${ctx.collection}`);
|
|
143
|
+
// console.log(`[sharedb] Skipping readSnapshots for collection=${ctx.collection}`);
|
|
140
144
|
return next();
|
|
141
145
|
}
|
|
142
146
|
for (const snap of ctx.snapshots || []) {
|
|
143
|
-
console.log(`[sharedb] Processing snapshot: id=${snap?.id}, hasType=${!!snap?.type}`);
|
|
147
|
+
// console.log(`[sharedb] Processing snapshot: id=${snap?.id}, hasType=${!!snap?.type}`);
|
|
144
148
|
if (!snap || snap.type)
|
|
145
149
|
continue;
|
|
146
150
|
const id = String(snap.id);
|
|
@@ -155,11 +159,37 @@ export class ShareDBRealtimeServer {
|
|
|
155
159
|
if (ctx.collection === 'user_data') {
|
|
156
160
|
snap.data = doc.data ?? {};
|
|
157
161
|
}
|
|
162
|
+
else if (ctx.collection === 'assets') {
|
|
163
|
+
// 🔧 Redis 里可能是旧 fallback(无 name/type 或 name=unknown-xxx),一律按不完整从后端补拉
|
|
164
|
+
const existing = doc.data;
|
|
165
|
+
const isUnknownName = existing?.name && String(existing.name).startsWith('unknown-');
|
|
166
|
+
const isIncomplete = !existing || typeof existing !== 'object' || !existing.name || !existing.type || isUnknownName;
|
|
167
|
+
// 🔧 强制刷新:如果 name 是 unknown,总是从后端重新获取
|
|
168
|
+
if (isUnknownName) {
|
|
169
|
+
console.log(`[sharedb] Asset ${id} has unknown name, forcing refresh from backend`);
|
|
170
|
+
const fromBackend = await this.fetchAssetDataFromBackend(id);
|
|
171
|
+
if (fromBackend && fromBackend.name && !String(fromBackend.name).startsWith('unknown-')) {
|
|
172
|
+
console.log(`[sharedb] ✅ Asset ${id} refreshed with name: ${fromBackend.name}`);
|
|
173
|
+
snap.data = fromBackend;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
console.warn(`[sharedb] ⚠️ Asset ${id} backend returned:`, fromBackend?.name || 'null');
|
|
177
|
+
snap.data = fromBackend ?? existing ?? { id: Number(id) || id, name: `unknown-${id}`, type: 'folder', data: {} };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (isIncomplete) {
|
|
181
|
+
const fromBackend = await this.fetchAssetDataFromBackend(id);
|
|
182
|
+
snap.data = fromBackend ?? existing ?? { id: Number(id) || id, name: `unknown-${id}`, type: 'folder', data: {} };
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
snap.data = existing;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
158
188
|
else {
|
|
159
189
|
snap.data = doc.data;
|
|
160
190
|
}
|
|
161
191
|
snap.v = doc.version;
|
|
162
|
-
console.log(`[sharedb] ✅ Snapshot filled: id=${id}, type=${snap.type}, hasData=${!!snap.data}`);
|
|
192
|
+
// console.log(`[sharedb] ✅ Snapshot filled: id=${id}, type=${snap.type}, hasData=${!!snap.data}`);
|
|
163
193
|
}
|
|
164
194
|
next();
|
|
165
195
|
});
|
|
@@ -172,63 +202,13 @@ export class ShareDBRealtimeServer {
|
|
|
172
202
|
return this._commonConn;
|
|
173
203
|
}
|
|
174
204
|
/**
|
|
175
|
-
* 🔧
|
|
176
|
-
*
|
|
205
|
+
* 🔧 已禁用:批量加载项目的所有 assets 到缓存
|
|
206
|
+
* 原因:加载所有资产比按需加载更慢,特别是对于大量资产的项目
|
|
207
|
+
* 保留代码结构,以备后续实现更智能的预加载策略
|
|
177
208
|
*/
|
|
178
209
|
async loadProjectAssetsToCache(projectId) {
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
if (existingPromise) {
|
|
182
|
-
return existingPromise;
|
|
183
|
-
}
|
|
184
|
-
// 创建新的加载 Promise
|
|
185
|
-
const loadPromise = (async () => {
|
|
186
|
-
try {
|
|
187
|
-
if (!this.options.backendUrl) {
|
|
188
|
-
console.warn(`[sharedb] No backendUrl configured, cannot load assets for project ${projectId}`);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
const emitSecret = this.options.internalSecret || process.env.EMIT_SECRET || 'dev-emit-secret-change-me';
|
|
192
|
-
const url = `${this.options.backendUrl}/api/projects/${projectId}/assets?branchId=local-branch`;
|
|
193
|
-
const path = `/api/projects/${projectId}/assets`;
|
|
194
|
-
console.log(`[sharedb] 🚀 Batch loading assets for project ${projectId} from ${url}`);
|
|
195
|
-
const startTime = Date.now();
|
|
196
|
-
// 生成签名 headers
|
|
197
|
-
const signedHeaders = generateSignedHeaders('GET', path, emitSecret, 'realtime');
|
|
198
|
-
const res = await fetch(url, {
|
|
199
|
-
headers: signedHeaders,
|
|
200
|
-
});
|
|
201
|
-
if (!res.ok) {
|
|
202
|
-
console.error(`[sharedb] Failed to batch load assets: status=${res.status}`);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
const data = await res.json();
|
|
206
|
-
const assets = data.result || data; // 兼容不同的响应格式
|
|
207
|
-
if (!Array.isArray(assets)) {
|
|
208
|
-
console.error(`[sharedb] Invalid assets response format`);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
const now = Date.now();
|
|
212
|
-
const projectCache = new Map();
|
|
213
|
-
for (const asset of assets) {
|
|
214
|
-
if (asset.id != null) {
|
|
215
|
-
projectCache.set(String(asset.id), { data: asset, timestamp: now, accessCount: 0 });
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
this.assetCache.set(projectId, projectCache);
|
|
219
|
-
const duration = Date.now() - startTime;
|
|
220
|
-
console.log(`[sharedb] ✅ Cached ${assets.length} assets for project ${projectId} in ${duration}ms`);
|
|
221
|
-
}
|
|
222
|
-
catch (error) {
|
|
223
|
-
console.error(`[sharedb] Failed to batch load assets for project ${projectId}:`, error);
|
|
224
|
-
}
|
|
225
|
-
finally {
|
|
226
|
-
// 清除加载 Promise,允许重试
|
|
227
|
-
this.cacheLoadPromises.delete(projectId);
|
|
228
|
-
}
|
|
229
|
-
})();
|
|
230
|
-
this.cacheLoadPromises.set(projectId, loadPromise);
|
|
231
|
-
return loadPromise;
|
|
210
|
+
// 禁用批量预加载,改为按需加载
|
|
211
|
+
return Promise.resolve();
|
|
232
212
|
}
|
|
233
213
|
/**
|
|
234
214
|
* 🔧 从缓存中获取 asset(需已知 contentHash),并更新访问计数与时间戳
|
|
@@ -242,6 +222,31 @@ export class ShareDBRealtimeServer {
|
|
|
242
222
|
cached.timestamp = Date.now();
|
|
243
223
|
return cached.data;
|
|
244
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* 🔧 通过 assetId 从缓存中获取 asset(不需要 contentHash),并更新访问计数与时间戳
|
|
227
|
+
*/
|
|
228
|
+
getAssetFromCacheById(projectId, assetId) {
|
|
229
|
+
const projectCache = this.assetCache.get(projectId);
|
|
230
|
+
if (!projectCache)
|
|
231
|
+
return null;
|
|
232
|
+
// 首先尝试直接用 assetId 查找(批量加载使用的格式)
|
|
233
|
+
const cached = projectCache.get(assetId);
|
|
234
|
+
if (cached) {
|
|
235
|
+
cached.accessCount++;
|
|
236
|
+
cached.timestamp = Date.now();
|
|
237
|
+
return cached.data;
|
|
238
|
+
}
|
|
239
|
+
// 然后尝试查找以 assetId: 开头的 key(单个缓存使用的格式)
|
|
240
|
+
const prefix = `${assetId}:`;
|
|
241
|
+
for (const [key, c] of projectCache) {
|
|
242
|
+
if (key.startsWith(prefix)) {
|
|
243
|
+
c.accessCount++;
|
|
244
|
+
c.timestamp = Date.now();
|
|
245
|
+
return c.data;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
245
250
|
/**
|
|
246
251
|
* 🔧 缓存 asset 数据,使用 assetId:contentHash 作为 key;删除同一 assetId 的旧 hash 版本
|
|
247
252
|
*/
|
|
@@ -271,13 +276,118 @@ export class ShareDBRealtimeServer {
|
|
|
271
276
|
accessCount: 0,
|
|
272
277
|
};
|
|
273
278
|
projectCache.set(cacheKey, cached);
|
|
274
|
-
console.log(`[sharedb] Cached asset ${assetId} with hash ${contentHash.slice(0, 8)}`);
|
|
279
|
+
// console.log(`[sharedb] Cached asset ${assetId} with hash ${contentHash.slice(0, 8)}`);
|
|
275
280
|
return data;
|
|
276
281
|
}
|
|
277
|
-
/** 从当前连接上下文获取 projectId(暂无实现时返回 null,则不按项目缓存) */
|
|
278
282
|
getProjectIdFromContext() {
|
|
279
|
-
return null;
|
|
283
|
+
return this.connectionContextStorage.getStore()?.projectId ?? null;
|
|
280
284
|
}
|
|
285
|
+
getBranchIdFromContext() {
|
|
286
|
+
return this.connectionContextStorage.getStore()?.branchId;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* 从后端拉取单个 asset 的完整数据(用于「文档已存在但 data 不完整」时补全读取)
|
|
290
|
+
* 返回剥离 contentHash 后的对象,失败返回 null
|
|
291
|
+
*/
|
|
292
|
+
async fetchAssetDataFromBackend(docId) {
|
|
293
|
+
if (!this.options.backendUrl)
|
|
294
|
+
return null;
|
|
295
|
+
const branchId = this.getBranchIdFromContext();
|
|
296
|
+
const path = `/api/assets/internal/${docId}`;
|
|
297
|
+
const pathWithQuery = branchId ? `${path}?branchId=${encodeURIComponent(branchId)}` : path;
|
|
298
|
+
const url = `${this.options.backendUrl}${pathWithQuery}`;
|
|
299
|
+
const emitSecret = this.options.internalSecret || process.env.EMIT_SECRET || 'dev-emit-secret-change-me';
|
|
300
|
+
const signedHeaders = generateSignedHeaders('GET', path, emitSecret, 'realtime');
|
|
301
|
+
try {
|
|
302
|
+
const res = await fetch(url, { headers: signedHeaders });
|
|
303
|
+
if (!res.ok)
|
|
304
|
+
return null;
|
|
305
|
+
const data = await res.json();
|
|
306
|
+
if (!data || typeof data !== 'object')
|
|
307
|
+
return null;
|
|
308
|
+
const { contentHash: _h, ...rest } = data;
|
|
309
|
+
return rest;
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* 🔧 性能优化:批量从后端预取资产数据并写入缓存
|
|
317
|
+
* 尝试使用批量 API(POST /api/assets/internal/batch),如不可用则 fallback 到并行单请求
|
|
318
|
+
* 这样后续 ensureDocExists 可直接从缓存命中,避免逐个 HTTP 请求
|
|
319
|
+
*/
|
|
320
|
+
// private async batchPreloadAssets(assetIds: string[]): Promise<void> {
|
|
321
|
+
// if (!this.options.backendUrl || assetIds.length === 0) return;
|
|
322
|
+
// const projectId = this.getProjectIdFromContext();
|
|
323
|
+
// if (!projectId) return;
|
|
324
|
+
// // 过滤出缓存中不存在的 asset
|
|
325
|
+
// const missingIds = assetIds.filter(id => !this.getAssetFromCacheById(projectId, id));
|
|
326
|
+
// if (missingIds.length === 0) {
|
|
327
|
+
// console.log(`[sharedb] All ${assetIds.length} assets already cached, skipping batch preload`);
|
|
328
|
+
// return;
|
|
329
|
+
// }
|
|
330
|
+
// console.log(`[sharedb] Batch preloading ${missingIds.length}/${assetIds.length} assets from backend...`);
|
|
331
|
+
// const branchId = this.getBranchIdFromContext();
|
|
332
|
+
// const emitSecret = this.options.internalSecret || process.env.EMIT_SECRET || 'dev-emit-secret-change-me';
|
|
333
|
+
// // 尝试批量 API
|
|
334
|
+
// try {
|
|
335
|
+
// const path = '/api/assets/internal/batch';
|
|
336
|
+
// const pathWithQuery = branchId ? `${path}?branchId=${encodeURIComponent(branchId)}` : path;
|
|
337
|
+
// const url = `${this.options.backendUrl}${pathWithQuery}`;
|
|
338
|
+
// const signedHeaders = generateSignedHeaders('POST', path, emitSecret, 'realtime');
|
|
339
|
+
// const res = await fetch(url, {
|
|
340
|
+
// method: 'POST',
|
|
341
|
+
// headers: {
|
|
342
|
+
// ...signedHeaders,
|
|
343
|
+
// 'Content-Type': 'application/json',
|
|
344
|
+
// },
|
|
345
|
+
// body: JSON.stringify({ ids: missingIds }),
|
|
346
|
+
// });
|
|
347
|
+
// if (res.ok) {
|
|
348
|
+
// const batchData = await res.json();
|
|
349
|
+
// if (Array.isArray(batchData)) {
|
|
350
|
+
// for (const assetData of batchData) {
|
|
351
|
+
// if (assetData && assetData.id) {
|
|
352
|
+
// const id = String(assetData.id);
|
|
353
|
+
// if (assetData.contentHash) {
|
|
354
|
+
// this.cacheAssetData(projectId, id, assetData);
|
|
355
|
+
// }
|
|
356
|
+
// }
|
|
357
|
+
// }
|
|
358
|
+
// console.log(`[sharedb] ✅ Batch preloaded ${batchData.length} assets via batch API`);
|
|
359
|
+
// return;
|
|
360
|
+
// }
|
|
361
|
+
// }
|
|
362
|
+
// // batch API 返回非 200 或数据格式不符,fallback 到并行单请求
|
|
363
|
+
// console.log(`[sharedb] Batch API unavailable (status=${res.status}), falling back to parallel individual requests`);
|
|
364
|
+
// } catch (e) {
|
|
365
|
+
// console.log(`[sharedb] Batch API failed, falling back to parallel individual requests:`, e);
|
|
366
|
+
// }
|
|
367
|
+
// // Fallback: 并行发送单个请求
|
|
368
|
+
// const results = await Promise.allSettled(
|
|
369
|
+
// missingIds.map(async (id) => {
|
|
370
|
+
// const data = await this.fetchAssetDataFromBackend(id);
|
|
371
|
+
// if (data) {
|
|
372
|
+
// // 手动构造缓存(fetchAssetDataFromBackend 已剥离 contentHash)
|
|
373
|
+
// let projectCache = this.assetCache.get(projectId);
|
|
374
|
+
// if (!projectCache) {
|
|
375
|
+
// projectCache = new Map();
|
|
376
|
+
// this.assetCache.set(projectId, projectCache);
|
|
377
|
+
// }
|
|
378
|
+
// // 使用 assetId 作为 key(没有 contentHash 时的 fallback)
|
|
379
|
+
// projectCache.set(id, {
|
|
380
|
+
// data,
|
|
381
|
+
// timestamp: Date.now(),
|
|
382
|
+
// accessCount: 0,
|
|
383
|
+
// });
|
|
384
|
+
// }
|
|
385
|
+
// return { id, data };
|
|
386
|
+
// })
|
|
387
|
+
// );
|
|
388
|
+
// const successCount = results.filter(r => r.status === 'fulfilled' && (r as any).value?.data).length;
|
|
389
|
+
// console.log(`[sharedb] ✅ Parallel preloaded ${successCount}/${missingIds.length} assets`);
|
|
390
|
+
// }
|
|
281
391
|
async ensureDocExists(collection, id, reason) {
|
|
282
392
|
const docId = String(id);
|
|
283
393
|
const doc = this.getCommonConn().get(collection, docId);
|
|
@@ -303,9 +413,9 @@ export class ShareDBRealtimeServer {
|
|
|
303
413
|
}
|
|
304
414
|
if (initial == null && this.options.onDocCreate) {
|
|
305
415
|
try {
|
|
306
|
-
console.log(`[sharedb] Fetching initial data for ${collection}/${docId} via onDocCreate`);
|
|
416
|
+
// console.log(`[sharedb] Fetching initial data for ${collection}/${docId} via onDocCreate`);
|
|
307
417
|
initial = await this.options.onDocCreate(collection, docId);
|
|
308
|
-
console.log(`[sharedb] onDocCreate returned:`, initial ? `data with keys [${Object.keys(initial).join(', ')}]` : 'null');
|
|
418
|
+
// console.log(`[sharedb] onDocCreate returned:`, initial ? `data with keys [${Object.keys(initial).join(', ')}]` : 'null');
|
|
309
419
|
}
|
|
310
420
|
catch (e) {
|
|
311
421
|
console.error(`[sharedb] onDocCreate failed for ${collection}/${docId}:`, e);
|
|
@@ -325,32 +435,39 @@ export class ShareDBRealtimeServer {
|
|
|
325
435
|
path = `/api/assets/internal/${docId}/file`;
|
|
326
436
|
}
|
|
327
437
|
else {
|
|
328
|
-
// assets 和 scenes 获取元数据(JSON
|
|
438
|
+
// assets 和 scenes 获取元数据(JSON),带 branchId 与列表同分支
|
|
329
439
|
if (collection === 'assets' || collection === 'scenes') {
|
|
330
|
-
|
|
331
|
-
path =
|
|
440
|
+
const pathBase = `/api/${collection}/internal/${docId}`;
|
|
441
|
+
path = pathBase;
|
|
442
|
+
const branchId = this.getBranchIdFromContext();
|
|
443
|
+
if (collection === 'assets' && !branchId) {
|
|
444
|
+
console.warn(`[sharedb] ensureDocExists assets/${docId} no branchId in context (auth may lack projectId/branchId)`);
|
|
445
|
+
}
|
|
446
|
+
url = branchId
|
|
447
|
+
? `${this.options.backendUrl}${pathBase}?branchId=${encodeURIComponent(branchId)}`
|
|
448
|
+
: `${this.options.backendUrl}${pathBase}`;
|
|
332
449
|
}
|
|
333
450
|
else {
|
|
334
451
|
url = `${this.options.backendUrl}/api/${collection}/${docId}`;
|
|
335
452
|
path = `/api/${collection}/${docId}`;
|
|
336
453
|
}
|
|
337
454
|
}
|
|
338
|
-
console.log(`[sharedb] Fetching initial data from ${url}`);
|
|
455
|
+
// console.log(`[sharedb] Fetching initial data from ${url}`);
|
|
339
456
|
// 生成签名 headers
|
|
340
457
|
const signedHeaders = generateSignedHeaders('GET', path, emitSecret, 'realtime');
|
|
341
458
|
const res = await fetch(url, {
|
|
342
459
|
headers: signedHeaders,
|
|
343
460
|
});
|
|
344
|
-
console.log(`[sharedb] Backend response: status=${res.status}, ok=${res.ok}`);
|
|
461
|
+
// console.log(`[sharedb] Backend response: status=${res.status}, ok=${res.ok}`);
|
|
345
462
|
if (res.ok) {
|
|
346
463
|
// 🔧 对于 documents,返回文本内容;其他返回 JSON
|
|
347
464
|
if (collection === 'documents') {
|
|
348
465
|
initial = await res.text();
|
|
349
|
-
console.log(`[sharedb] Backend returned text: ${initial ? initial.substring(0, 100) + '...' : 'empty'}`);
|
|
466
|
+
// console.log(`[sharedb] Backend returned text: ${initial ? initial.substring(0, 100) + '...' : 'empty'}`);
|
|
350
467
|
}
|
|
351
468
|
else {
|
|
352
469
|
const responseData = await res.json();
|
|
353
|
-
console.log(`[sharedb] Backend returned:`, responseData ? `data with keys [${Object.keys(responseData).join(', ')}]` : 'null');
|
|
470
|
+
// console.log(`[sharedb] Backend returned:`, responseData ? `data with keys [${Object.keys(responseData).join(', ')}]` : 'null');
|
|
354
471
|
// 🔧 assets:剥离 contentHash 后写入 doc;若有 projectId 则按 assetId:contentHash 缓存
|
|
355
472
|
if (collection === 'assets' && responseData && 'contentHash' in responseData) {
|
|
356
473
|
const projectId = this.getProjectIdFromContext();
|
|
@@ -360,6 +477,17 @@ export class ShareDBRealtimeServer {
|
|
|
360
477
|
const { contentHash: _h, ...rest } = responseData;
|
|
361
478
|
return rest;
|
|
362
479
|
})();
|
|
480
|
+
// 确保 assets 文档包含 item_id(编辑器 loadAndSubscribe 依赖此字段)
|
|
481
|
+
if (initial && initial.item_id == null && initial.id != null) {
|
|
482
|
+
initial.item_id = initial.id;
|
|
483
|
+
}
|
|
484
|
+
// 验证 assets 必需字段,缺失时从 responseData 补充
|
|
485
|
+
if (initial && (!initial.name || !initial.type)) {
|
|
486
|
+
if (!initial.name)
|
|
487
|
+
initial.name = responseData.name || `unknown-${docId}`;
|
|
488
|
+
if (!initial.type)
|
|
489
|
+
initial.type = responseData.type || 'folder';
|
|
490
|
+
}
|
|
363
491
|
}
|
|
364
492
|
else if (collection === 'scenes' && responseData && 'contentHash' in responseData) {
|
|
365
493
|
// 🔧 scenes:Backend 返回 contentHash,仅剥离不写入 doc
|
|
@@ -389,16 +517,25 @@ export class ShareDBRealtimeServer {
|
|
|
389
517
|
// 🔧 settings: 项目设置,默认空对象
|
|
390
518
|
initial = {};
|
|
391
519
|
}
|
|
520
|
+
else if (collection === 'assets') {
|
|
521
|
+
initial = {
|
|
522
|
+
item_id: Number(docId) || 1,
|
|
523
|
+
id: Number(docId) || 1,
|
|
524
|
+
name: `unknown-${docId}`,
|
|
525
|
+
type: 'folder',
|
|
526
|
+
data: {},
|
|
527
|
+
};
|
|
528
|
+
}
|
|
392
529
|
else {
|
|
393
530
|
initial =
|
|
394
531
|
collection === 'scenes'
|
|
395
|
-
? SceneModel.getDefaultData(docId)
|
|
532
|
+
? SceneModel.getDefaultData(docId, `Scene ${docId}`)
|
|
396
533
|
: { item_id: Number(docId) || 1, data: {} };
|
|
397
534
|
}
|
|
398
535
|
}
|
|
399
536
|
// 🔧 修复:documents 使用 text 类型(ot-text 包注册的类型名是 'text'),其他使用 json0 类型
|
|
400
537
|
const docType = collection === 'documents' ? otTextType.name || otTextType.uri || 'text' : 'json0';
|
|
401
|
-
console.log(`[sharedb] Creating document ${collection}/${docId} with type=${docType}, initial data type=${typeof initial}, length=${initial?.length || 'N/A'}`);
|
|
538
|
+
// console.log(`[sharedb] Creating document ${collection}/${docId} with type=${docType}, initial data type=${typeof initial}, length=${initial?.length || 'N/A'}`);
|
|
402
539
|
await new Promise((resolve, reject) => {
|
|
403
540
|
doc.create(initial, docType, (err) => {
|
|
404
541
|
// 🔧 忽略 "Document already exists" 错误(并发订阅时可能发生)
|
|
@@ -411,13 +548,15 @@ export class ShareDBRealtimeServer {
|
|
|
411
548
|
reject(err);
|
|
412
549
|
}
|
|
413
550
|
else {
|
|
414
|
-
|
|
551
|
+
const createdTypeLabel = typeof doc.type === 'string' ? doc.type : (doc.type?.name ?? doc.type?.uri ?? '?');
|
|
552
|
+
// console.log(`[sharedb] ✅ Document created: ${collection}/${docId}, type=${createdTypeLabel}, version=${doc.version}, hasData=${!!doc.data}`);
|
|
415
553
|
resolve();
|
|
416
554
|
}
|
|
417
555
|
});
|
|
418
556
|
});
|
|
419
557
|
}
|
|
420
|
-
|
|
558
|
+
const typeLabel = typeof doc.type === 'string' ? doc.type : (doc.type?.name ?? doc.type?.uri ?? '?');
|
|
559
|
+
// console.log(`[sharedb] Returning doc ${collection}/${docId}, type=${typeLabel}, version=${doc.version}, hasData=${!!doc.data}`);
|
|
421
560
|
return doc;
|
|
422
561
|
}
|
|
423
562
|
handleConnection(ws) {
|
|
@@ -431,8 +570,8 @@ export class ShareDBRealtimeServer {
|
|
|
431
570
|
// 使用 any 类型避免 TypeScript 重载签名冲突
|
|
432
571
|
ws.send = (data, optionsOrCallback, callback) => {
|
|
433
572
|
const dataStr = String(data);
|
|
434
|
-
// 🔧
|
|
435
|
-
console.log(`[sharedb] >>> Sending to client (length=${dataStr.length}): ${dataStr.substring(0, 200)}...`);
|
|
573
|
+
// 🔧 调试:打印所有发送的消息(在发送前)- 只在开发环境启用
|
|
574
|
+
// console.log(`[sharedb] >>> Sending to client (length=${dataStr.length}): ${dataStr.substring(0, 200)}...`);
|
|
436
575
|
// 处理可选的 options 和 callback 参数
|
|
437
576
|
const cb = typeof optionsOrCallback === 'function' ? optionsOrCallback : callback;
|
|
438
577
|
const opts = typeof optionsOrCallback === 'object' ? optionsOrCallback : undefined;
|
|
@@ -504,6 +643,10 @@ export class ShareDBRealtimeServer {
|
|
|
504
643
|
if (effectiveUser && !streamListenCalled) {
|
|
505
644
|
authed = true;
|
|
506
645
|
streamListenCalled = true;
|
|
646
|
+
this.connectionContextMap.set(ws, {
|
|
647
|
+
projectId: p?.projectId,
|
|
648
|
+
branchId: p?.branchId,
|
|
649
|
+
});
|
|
507
650
|
const currentUserId = effectiveUser?.id ?? 1;
|
|
508
651
|
const authPayload = {
|
|
509
652
|
user: {
|
|
@@ -546,21 +689,55 @@ export class ShareDBRealtimeServer {
|
|
|
546
689
|
}
|
|
547
690
|
return;
|
|
548
691
|
}
|
|
549
|
-
// JSON:兼容 bulk subscribe / ping-pong / 主动 ensureDocExists
|
|
692
|
+
// JSON:兼容 bulk subscribe / ping-pong / 主动 ensureDocExists(整段在 connection 上下文中执行,以便 fetch 单条 asset 时带 branchId)
|
|
550
693
|
try {
|
|
551
694
|
const msg = JSON.parse(text);
|
|
695
|
+
const ctx = this.connectionContextMap.get(ws);
|
|
696
|
+
const runWithCtx = (fn) => {
|
|
697
|
+
this.connectionContextStorage.run(ctx, fn);
|
|
698
|
+
};
|
|
552
699
|
if (msg?.a === 'bs') {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
700
|
+
await new Promise((resolve, reject) => {
|
|
701
|
+
runWithCtx(async () => {
|
|
702
|
+
try {
|
|
703
|
+
const collection = msg.c;
|
|
704
|
+
const ids = Array.isArray(msg.b) ? msg.b : [];
|
|
705
|
+
// 🔧 性能优化:先批量预取缺失的资产数据,减少逐个 HTTP 请求
|
|
706
|
+
// if (collection === 'assets' && this.options.backendUrl && ids.length > 0) {
|
|
707
|
+
// await this.batchPreloadAssets(ids);
|
|
708
|
+
// }
|
|
709
|
+
const results = {};
|
|
710
|
+
await Promise.all(ids.map(async (id) => {
|
|
711
|
+
const doc = await this.ensureDocExists(collection, id, 'bulkSubscribe');
|
|
712
|
+
if (!doc.type)
|
|
713
|
+
return;
|
|
714
|
+
let data = doc.data;
|
|
715
|
+
if (collection === 'assets') {
|
|
716
|
+
const existing = doc.data;
|
|
717
|
+
const isUnknownName = existing?.name && String(existing.name).startsWith('unknown-');
|
|
718
|
+
const isIncomplete = !existing || typeof existing !== 'object' || !existing.name || !existing.type || isUnknownName;
|
|
719
|
+
if (isIncomplete) {
|
|
720
|
+
// 🔧 先尝试从缓存获取(批量预加载后缓存中应该有)
|
|
721
|
+
const projectId = this.getProjectIdFromContext();
|
|
722
|
+
const fromCache = projectId ? this.getAssetFromCacheById(projectId, id) : null;
|
|
723
|
+
if (fromCache) {
|
|
724
|
+
data = fromCache;
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
const fromBackend = await this.fetchAssetDataFromBackend(id);
|
|
728
|
+
data = fromBackend ?? existing ?? { id: Number(id) || id, name: `unknown-${id}`, type: 'folder', data: {} };
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
results[id] = { v: doc.version, data };
|
|
733
|
+
}));
|
|
734
|
+
ws.send(JSON.stringify({ a: 'bs', c: collection, data: results }));
|
|
735
|
+
}
|
|
736
|
+
finally {
|
|
737
|
+
resolve();
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
});
|
|
564
741
|
return;
|
|
565
742
|
}
|
|
566
743
|
if (msg?.a === 'pp') {
|
|
@@ -569,7 +746,7 @@ export class ShareDBRealtimeServer {
|
|
|
569
746
|
}
|
|
570
747
|
// 🔧 修复:添加 documents、settings、user_data collection 支持
|
|
571
748
|
if ((msg?.a === 'f' || msg?.a === 's') && (msg?.c === 'scenes' || msg?.c === 'assets' || msg?.c === 'documents' || msg?.c === 'settings' || msg?.c === 'user_data') && msg?.d != null) {
|
|
572
|
-
console.log(`[sharedb] Pre-creating doc for ${msg.c}/${msg.d} (action=${msg.a})`);
|
|
749
|
+
// console.log(`[sharedb] Pre-creating doc for ${msg.c}/${msg.d} (action=${msg.a})`);
|
|
573
750
|
try {
|
|
574
751
|
await this.ensureDocExists(msg.c, msg.d, `ws:${msg.a}`);
|
|
575
752
|
}
|
|
@@ -578,10 +755,12 @@ export class ShareDBRealtimeServer {
|
|
|
578
755
|
}
|
|
579
756
|
}
|
|
580
757
|
// 🔧 调试:打印转发逻辑
|
|
581
|
-
console.log(`[sharedb] Message received: a=${msg?.a}, c=${msg?.c}, d=${msg?.d}, authed=${authed}, hasMessageHandler=${!!messageHandler}`);
|
|
758
|
+
// console.log(`[sharedb] Message received: a=${msg?.a}, c=${msg?.c}, d=${msg?.d}, authed=${authed}, hasMessageHandler=${!!messageHandler}`);
|
|
582
759
|
if (messageHandler && (msg?.a === 'hs' || authed)) {
|
|
583
|
-
console.log(`[sharedb] Forwarding message to ShareDB: ${msg?.a}/${msg?.c}/${msg?.d || 'N/A'}`);
|
|
584
|
-
|
|
760
|
+
// console.log(`[sharedb] Forwarding message to ShareDB: ${msg?.a}/${msg?.c}/${msg?.d || 'N/A'}`);
|
|
761
|
+
runWithCtx(() => {
|
|
762
|
+
messageHandler({ data: text });
|
|
763
|
+
});
|
|
585
764
|
}
|
|
586
765
|
else {
|
|
587
766
|
console.log(`[sharedb] ⚠️ Message NOT forwarded: authed=${authed}, hasMessageHandler=${!!messageHandler}`);
|
|
@@ -592,6 +771,7 @@ export class ShareDBRealtimeServer {
|
|
|
592
771
|
}
|
|
593
772
|
});
|
|
594
773
|
ws.on('close', () => {
|
|
774
|
+
this.connectionContextMap.delete(ws);
|
|
595
775
|
try {
|
|
596
776
|
stream.end();
|
|
597
777
|
}
|