@playcraft/common 0.0.4 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/auth/jwt.test.d.ts +2 -0
  2. package/dist/auth/jwt.test.d.ts.map +1 -0
  3. package/dist/auth/jwt.test.js +13 -0
  4. package/dist/auth/jwt.test.js.map +1 -0
  5. package/dist/auth/signature.spec.d.ts +2 -0
  6. package/dist/auth/signature.spec.d.ts.map +1 -0
  7. package/dist/auth/signature.spec.js +195 -0
  8. package/dist/auth/signature.spec.js.map +1 -0
  9. package/dist/database/schema.d.ts +19 -0
  10. package/dist/database/schema.d.ts.map +1 -1
  11. package/dist/database/schema.js +1 -0
  12. package/dist/database/schema.js.map +1 -1
  13. package/dist/mcp/tools.d.ts +2 -2
  14. package/dist/mcp/tools.d.ts.map +1 -1
  15. package/dist/mcp/tools.js +573 -61
  16. package/dist/mcp/tools.js.map +1 -1
  17. package/dist/messenger/server.test.d.ts +2 -0
  18. package/dist/messenger/server.test.d.ts.map +1 -0
  19. package/dist/messenger/server.test.js +66 -0
  20. package/dist/messenger/server.test.js.map +1 -0
  21. package/dist/models/converters.d.ts +11 -0
  22. package/dist/models/converters.d.ts.map +1 -0
  23. package/dist/models/converters.js +25 -0
  24. package/dist/models/converters.js.map +1 -0
  25. package/dist/models/converters.test.d.ts +2 -0
  26. package/dist/models/converters.test.d.ts.map +1 -0
  27. package/dist/models/converters.test.js +18 -0
  28. package/dist/models/converters.test.js.map +1 -0
  29. package/dist/models/playcanvas-compat.d.ts +1 -0
  30. package/dist/models/playcanvas-compat.d.ts.map +1 -1
  31. package/dist/models/playcanvas-compat.js +1 -0
  32. package/dist/models/playcanvas-compat.js.map +1 -1
  33. package/dist/models/scene.d.ts +10 -1
  34. package/dist/models/scene.d.ts.map +1 -1
  35. package/dist/models/scene.js +10 -1
  36. package/dist/models/scene.js.map +1 -1
  37. package/dist/models/scene.test.d.ts +2 -0
  38. package/dist/models/scene.test.d.ts.map +1 -0
  39. package/dist/models/scene.test.js +12 -0
  40. package/dist/models/scene.test.js.map +1 -0
  41. package/dist/sharedb/server.d.ts +21 -3
  42. package/dist/sharedb/server.d.ts.map +1 -1
  43. package/dist/sharedb/server.js +273 -91
  44. package/dist/sharedb/server.js.map +1 -1
  45. package/dist/sharedb/server.test.d.ts +2 -0
  46. package/dist/sharedb/server.test.d.ts.map +1 -0
  47. package/dist/sharedb/server.test.js +59 -0
  48. package/dist/sharedb/server.test.js.map +1 -0
  49. package/dist/storage/cos.d.ts +6 -6
  50. package/dist/storage/cos.d.ts.map +1 -1
  51. package/dist/storage/cos.js +70 -80
  52. package/dist/storage/cos.js.map +1 -1
  53. package/dist/storage/index.d.ts +1 -0
  54. package/dist/storage/index.d.ts.map +1 -1
  55. package/dist/storage/index.js +2 -0
  56. package/dist/storage/index.js.map +1 -1
  57. package/dist/storage/interface.d.ts +1 -0
  58. package/dist/storage/interface.d.ts.map +1 -1
  59. package/dist/storage/upload-build-artifacts.d.ts +38 -0
  60. package/dist/storage/upload-build-artifacts.d.ts.map +1 -0
  61. package/dist/storage/upload-build-artifacts.js +60 -0
  62. package/dist/storage/upload-build-artifacts.js.map +1 -0
  63. package/package.json +3 -2
@@ -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
- * 🔧 性能优化:批量加载项目的所有 assets 到缓存
176
- * 避免每个 asset 都单独请求后端 API
205
+ * 🔧 已禁用:批量加载项目的所有 assets 到缓存
206
+ * 原因:加载所有资产比按需加载更慢,特别是对于大量资产的项目
207
+ * 保留代码结构,以备后续实现更智能的预加载策略
177
208
  */
178
209
  async loadProjectAssetsToCache(projectId) {
179
- // 如果已经有加载中的 Promise,等待它完成
180
- const existingPromise = this.cacheLoadPromises.get(projectId);
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
- url = `${this.options.backendUrl}/api/${collection}/internal/${docId}`;
331
- path = `/api/${collection}/internal/${docId}`;
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
- console.log(`[sharedb] Document created: ${collection}/${docId}, type=${doc.type}, version=${doc.version}, hasData=${!!doc.data}`);
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
- console.log(`[sharedb] Returning doc ${collection}/${docId}, type=${doc.type}, version=${doc.version}, hasData=${!!doc.data}`);
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,19 +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
- const collection = msg.c;
554
- const ids = msg.b || [];
555
- const results = {};
556
- await Promise.all(ids.map(async (id) => {
557
- const doc = await this.ensureDocExists(collection, id, 'bulkSubscribe');
558
- if (doc.type)
559
- results[id] = { v: doc.version, data: doc.data };
560
- }));
561
- ws.send(JSON.stringify({ a: 'bs', c: collection, data: results }));
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
+ });
562
741
  return;
563
742
  }
564
743
  if (msg?.a === 'pp') {
@@ -567,7 +746,7 @@ export class ShareDBRealtimeServer {
567
746
  }
568
747
  // 🔧 修复:添加 documents、settings、user_data collection 支持
569
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) {
570
- 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})`);
571
750
  try {
572
751
  await this.ensureDocExists(msg.c, msg.d, `ws:${msg.a}`);
573
752
  }
@@ -576,10 +755,12 @@ export class ShareDBRealtimeServer {
576
755
  }
577
756
  }
578
757
  // 🔧 调试:打印转发逻辑
579
- 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}`);
580
759
  if (messageHandler && (msg?.a === 'hs' || authed)) {
581
- console.log(`[sharedb] Forwarding message to ShareDB: ${msg?.a}/${msg?.c}/${msg?.d || 'N/A'}`);
582
- messageHandler({ data: text });
760
+ // console.log(`[sharedb] Forwarding message to ShareDB: ${msg?.a}/${msg?.c}/${msg?.d || 'N/A'}`);
761
+ runWithCtx(() => {
762
+ messageHandler({ data: text });
763
+ });
583
764
  }
584
765
  else {
585
766
  console.log(`[sharedb] ⚠️ Message NOT forwarded: authed=${authed}, hasMessageHandler=${!!messageHandler}`);
@@ -590,6 +771,7 @@ export class ShareDBRealtimeServer {
590
771
  }
591
772
  });
592
773
  ws.on('close', () => {
774
+ this.connectionContextMap.delete(ws);
593
775
  try {
594
776
  stream.end();
595
777
  }