@bhooai/nexus-core 2.0.9 → 2.0.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bhooai/nexus-core",
3
- "version": "2.0.9",
3
+ "version": "2.0.11",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -109,6 +109,15 @@ export async function createNexusApp(opts: CreateNexusAppOptions = {}): Promise<
109
109
  // ------------------------------------------------------------------
110
110
  const discovery = await discoverBackend(srcRoot);
111
111
 
112
+ // ------------------------------------------------------------------
113
+ // DB must be connected before importing routes/graphql that define models
114
+ // (blog-crud Post/Author/Comment call model() at import time — previously
115
+ // connect was only inside `if (admin.enabled)` after routes were mounted,
116
+ // causing "Not connected — call connect(uri) first." and missing /api/posts)
117
+ // ------------------------------------------------------------------
118
+ console.log(`[db] early connect ${config.db.uri} autoIndex=${config.db.autoIndex}`);
119
+ try { connect(config.db.uri, { autoIndex: config.db.autoIndex }); console.log(`[db] early connect ok`); } catch (e) { console.error(`[db] early connect failed`, e); }
120
+
112
121
  // ------------------------------------------------------------------
113
122
  // Storage facade + queue + mail — always configured, even if minimal
114
123
  // ------------------------------------------------------------------
@@ -320,6 +329,111 @@ export async function createNexusApp(opts: CreateNexusAppOptions = {}): Promise<
320
329
  onError: (err, ctx) => errorHandler.toApiError(err),
321
330
  });
322
331
 
332
+ // ------------------------------------------------------------------
333
+ // Realtime WS server — mount discovered ws/*.room.ts handlers (chat lobby)
334
+ // ------------------------------------------------------------------
335
+ let realtime: any = null;
336
+ try {
337
+ const { RealtimeServer } = await import('@bhooai/nexus-realtime');
338
+ realtime = new RealtimeServer({ httpServer: server.httpServer, path: '/ws' });
339
+ // Load custom ws rooms (e.g. chat) for persistence + presence bridging
340
+ const roomHandlers = new Map<string, any>();
341
+ for (const file of discovery.rooms) {
342
+ try {
343
+ const mod: any = await importDefault(file);
344
+ const def = mod?.default ?? mod;
345
+ if (def?.name) roomHandlers.set(def.name, def);
346
+ } catch (e) {
347
+ console.warn(`[realtime] failed to load ws room ${file.path}:`, e);
348
+ }
349
+ }
350
+ // Bridge custom chat protocol (roomId/msg/typing) to generic realtime (room/broadcast)
351
+ // so lobby works with both old clients (roomId) and new (room/broadcast).
352
+ const chatRoom = roomHandlers.get('chat');
353
+ if (chatRoom) {
354
+ const origOnMessage = (realtime as any).onMessage.bind(realtime);
355
+ (realtime as any).onMessage = async (conn: any, raw: string) => {
356
+ let msg: any;
357
+ try { msg = JSON.parse(raw); } catch { return origOnMessage(conn, raw); }
358
+ // Normalize join with roomId/username (chat) → generic join with room
359
+ if (msg.type === 'join' && (msg.roomId || msg.room)) {
360
+ const room = msg.room ?? msg.roomId;
361
+ (conn as any).data = (conn as any).data ?? {};
362
+ if (msg.username) (conn as any).data.username = msg.username;
363
+ // Persist room for later msg attribution
364
+ (conn as any).username = msg.username ?? (conn as any).username;
365
+ // Only call custom onJoin for side-effects, but suppress its broadcast (generic will handle presence)
366
+ try {
367
+ const sockLike: any = { data: (conn as any).data, join: () => {}, to: () => ({ emit: () => {} }) };
368
+ const ctxNoop: any = { server: { to: () => ({ emit: () => {} }), broadcast: () => {} } };
369
+ await chatRoom.onJoin?.(sockLike, { roomId: room, room, username: msg.username }, ctxNoop);
370
+ } catch {}
371
+ msg.room = room;
372
+ return origOnMessage(conn, JSON.stringify(msg));
373
+ }
374
+ // Custom msg → persist + single broadcast as 'msg' (legacy) — also visible to generic broadcast listeners
375
+ if (msg.type === 'msg' && (msg.roomId || msg.room) && msg.text) {
376
+ const room = msg.roomId ?? msg.room;
377
+ const from = (conn as any).data?.username ?? msg.username ?? (conn as any).username ?? 'anon';
378
+ const at = new Date().toISOString();
379
+ // Persist via chat handler (Message.create) without double-broadcast
380
+ if (chatRoom?.onMessage?.['msg']) {
381
+ try {
382
+ const sockLike: any = { data: (conn as any).data ?? { username: from } };
383
+ const ctxNoop: any = { server: { to: () => ({ emit: () => {} }), broadcast: () => {} } };
384
+ await chatRoom.onMessage['msg'](sockLike, { roomId: room, room, text: msg.text }, ctxNoop);
385
+ } catch {}
386
+ }
387
+ // Single publish as 'msg' — include room so deliverRoom can route to all members (including sender, since origin undefined)
388
+ const payload: any = { type: 'msg', room, from, text: msg.text, at, payload: { from, text: msg.text, at } };
389
+ try { (realtime as any).adapter.publish(`room:${room}`, JSON.stringify(payload)); } catch {}
390
+ try { (realtime as any).adapter.publish(`room:chat:${room}`, JSON.stringify(payload)); } catch {}
391
+ return;
392
+ }
393
+ if (msg.type === 'typing' && (msg.roomId || msg.room)) {
394
+ const room = msg.roomId ?? msg.room;
395
+ const username = (conn as any).data?.username ?? msg.username ?? 'anon';
396
+ const payload: any = { type: 'typing', room, username, payload: { username } };
397
+ try { (realtime as any).adapter.publish(`room:${room}`, JSON.stringify(payload)); } catch {}
398
+ try { (realtime as any).adapter.publish(`room:chat:${room}`, JSON.stringify(payload)); } catch {}
399
+ return;
400
+ }
401
+ // Handle generic broadcast with event:msg (from new clients) → persist + re-broadcast as msg
402
+ if (msg.type === 'broadcast' && msg.event === 'msg' && msg.data?.text) {
403
+ const room = msg.room;
404
+ const from = msg.data.from ?? (conn as any).data?.username ?? 'anon';
405
+ const text = msg.data.text;
406
+ const at = msg.data.at ?? new Date().toISOString();
407
+ if (chatRoom?.onMessage?.['msg']) {
408
+ try {
409
+ const sockLike: any = { data: (conn as any).data ?? { username: from } };
410
+ const ctxNoop: any = { server: { to: () => ({ emit: () => {} }), broadcast: () => {} } };
411
+ await chatRoom.onMessage['msg'](sockLike, { roomId: room, room, text }, ctxNoop);
412
+ } catch {}
413
+ }
414
+ const payload: any = { type: 'msg', room, from, text, at, payload: { from, text, at } };
415
+ try { (realtime as any).adapter.publish(`room:${room}`, JSON.stringify(payload)); } catch {}
416
+ try { (realtime as any).adapter.publish(`room:chat:${room}`, JSON.stringify(payload)); } catch {}
417
+ return;
418
+ }
419
+ if (msg.type === 'broadcast' && msg.event === 'typing') {
420
+ const room = msg.room;
421
+ const username = msg.data?.username ?? (conn as any).data?.username ?? 'anon';
422
+ const payload: any = { type: 'typing', room, username, payload: { username } };
423
+ try { (realtime as any).adapter.publish(`room:${room}`, JSON.stringify(payload)); } catch {}
424
+ try { (realtime as any).adapter.publish(`room:chat:${room}`, JSON.stringify(payload)); } catch {}
425
+ return;
426
+ }
427
+ // Normalize roomId → room for generic handling
428
+ if (msg.roomId && !msg.room) msg.room = msg.roomId;
429
+ return origOnMessage(conn, JSON.stringify(msg));
430
+ };
431
+ }
432
+ } catch (e) {
433
+ // realtime optional — if not installed, ws lobby falls back to no-op
434
+ if ((e as any)?.code !== 'ERR_MODULE_NOT_FOUND') console.warn('[realtime] ws init failed:', e);
435
+ }
436
+
323
437
  // Body parsing (JSON / urlencoded / multipart) — before everything else so
324
438
  // POST/PUT/PATCH handlers see ctx.body.
325
439
  server.use(bodyParser(config.server.bodyLimit));