@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 +1 -1
- package/src/app/createNexusApp.ts +114 -0
package/package.json
CHANGED
|
@@ -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));
|