@jskit-ai/realtime 0.1.4

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.
@@ -0,0 +1,743 @@
1
+ import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
2
+ import { normalizePositiveInteger, normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
+ import {
4
+ registerDomainEventListener,
5
+ resolveServiceRegistrations
6
+ } from "@jskit-ai/kernel/server/runtime";
7
+ import {
8
+ createSocketIoServer,
9
+ closeSocketIoServer,
10
+ resolveRealtimeRedisUrl,
11
+ configureSocketIoRedisAdapter,
12
+ closeSocketIoRedisConnections
13
+ } from "./runtime.js";
14
+ import {
15
+ REALTIME_RUNTIME_SERVER_TOKEN,
16
+ REALTIME_SOCKET_IO_SERVER_TOKEN
17
+ } from "./tokens.js";
18
+
19
+ const REALTIME_RUNTIME_SERVER_API = Object.freeze({
20
+ createSocketIoServer,
21
+ closeSocketIoServer
22
+ });
23
+ const REALTIME_DOMAIN_EVENT_BRIDGE_TOKEN = "runtime.realtime.domain-event-bridge";
24
+
25
+ const REALTIME_ROOM_ALL_CLIENTS = "clients";
26
+ const REALTIME_ROOM_ALL_USERS = "users";
27
+
28
+ function normalizeArray(value) {
29
+ const queue = Array.isArray(value) ? [...value] : [value];
30
+ const list = [];
31
+
32
+ while (queue.length > 0) {
33
+ const entry = queue.shift();
34
+ if (Array.isArray(entry)) {
35
+ queue.push(...entry);
36
+ continue;
37
+ }
38
+ if (entry == null) {
39
+ continue;
40
+ }
41
+ list.push(entry);
42
+ }
43
+
44
+ return list;
45
+ }
46
+
47
+ function roomForUser(userId) {
48
+ return `user:${Number(userId)}`;
49
+ }
50
+
51
+ function roomForWorkspace(workspaceId) {
52
+ return `workspace:${Number(workspaceId)}`;
53
+ }
54
+
55
+ function roomForWorkspaceUser(workspaceId, userId) {
56
+ return `workspace:${Number(workspaceId)}:user:${Number(userId)}`;
57
+ }
58
+
59
+ function parseCookieHeader(value = "") {
60
+ const source = String(value || "").trim();
61
+ if (!source) {
62
+ return {};
63
+ }
64
+
65
+ return source.split(";").reduce((cookies, entry) => {
66
+ const separator = entry.indexOf("=");
67
+ if (separator < 1) {
68
+ return cookies;
69
+ }
70
+ const key = entry.slice(0, separator).trim();
71
+ const rawValue = entry.slice(separator + 1).trim();
72
+ if (!key) {
73
+ return cookies;
74
+ }
75
+
76
+ try {
77
+ cookies[key] = decodeURIComponent(rawValue);
78
+ } catch {
79
+ cookies[key] = rawValue;
80
+ }
81
+ return cookies;
82
+ }, {});
83
+ }
84
+
85
+ function createProviderLogger(scope, { debugEnabled = false } = {}) {
86
+ const logger =
87
+ scope && typeof scope.has === "function" && scope.has(KERNEL_TOKENS.Logger) ? scope.make(KERNEL_TOKENS.Logger) : null;
88
+
89
+ return Object.freeze({
90
+ debug: (...args) => {
91
+ if (debugEnabled !== true) {
92
+ return;
93
+ }
94
+ if (logger && typeof logger.info === "function") {
95
+ logger.info(...args);
96
+ return;
97
+ }
98
+ console.info(...args);
99
+ },
100
+ info: (...args) => {
101
+ if (logger && typeof logger.info === "function") {
102
+ logger.info(...args);
103
+ return;
104
+ }
105
+ console.info(...args);
106
+ },
107
+ warn: (...args) => {
108
+ if (logger && typeof logger.warn === "function") {
109
+ logger.warn(...args);
110
+ return;
111
+ }
112
+ console.warn(...args);
113
+ },
114
+ error: (...args) => {
115
+ if (logger && typeof logger.error === "function") {
116
+ logger.error(...args);
117
+ return;
118
+ }
119
+ console.error(...args);
120
+ }
121
+ });
122
+ }
123
+
124
+ function parseDebugFlag(value, fallback = null) {
125
+ if (typeof value === "boolean") {
126
+ return value;
127
+ }
128
+
129
+ const normalized = normalizeText(value).toLowerCase();
130
+ if (!normalized) {
131
+ return fallback;
132
+ }
133
+ if (normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on") {
134
+ return true;
135
+ }
136
+ if (normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off") {
137
+ return false;
138
+ }
139
+
140
+ return fallback;
141
+ }
142
+
143
+ function resolveRealtimeServerDebugEnabled(scope) {
144
+ const appConfig = scope && typeof scope.has === "function" && scope.has("appConfig") ? scope.make("appConfig") : {};
145
+ const env = scope && typeof scope.has === "function" && scope.has(KERNEL_TOKENS.Env) ? scope.make(KERNEL_TOKENS.Env) : {};
146
+ const realtime = appConfig && typeof appConfig === "object" ? appConfig.realtime : null;
147
+
148
+ const envFlag = parseDebugFlag(env?.JSKIT_REALTIME_DEBUG);
149
+ if (envFlag !== null) {
150
+ return envFlag;
151
+ }
152
+
153
+ const configFlag = parseDebugFlag(realtime?.debug);
154
+ if (configFlag !== null) {
155
+ return configFlag;
156
+ }
157
+
158
+ return false;
159
+ }
160
+
161
+ function buildRealtimeDispatchIndex(registrations = []) {
162
+ const index = new Map();
163
+
164
+ for (const registration of registrations) {
165
+ const serviceToken = String(registration?.serviceToken || "").trim();
166
+ if (!serviceToken) {
167
+ continue;
168
+ }
169
+
170
+ const eventsByMethod = registration?.metadata?.events;
171
+ if (!eventsByMethod || typeof eventsByMethod !== "object" || Array.isArray(eventsByMethod)) {
172
+ continue;
173
+ }
174
+
175
+ for (const [methodName, specs] of Object.entries(eventsByMethod)) {
176
+ const normalizedMethodName = String(methodName || "").trim();
177
+ if (!normalizedMethodName) {
178
+ continue;
179
+ }
180
+ const key = `${serviceToken}:${normalizedMethodName}`;
181
+ const list = [];
182
+ const entries = Array.isArray(specs) ? specs : [];
183
+
184
+ for (const spec of entries) {
185
+ const realtime = spec?.realtime;
186
+ const event = normalizeText(realtime?.event);
187
+ if (!event) {
188
+ continue;
189
+ }
190
+ list.push(
191
+ Object.freeze({
192
+ event,
193
+ audience: realtime?.audience
194
+ })
195
+ );
196
+ }
197
+
198
+ if (list.length > 0) {
199
+ index.set(key, Object.freeze(list));
200
+ }
201
+ }
202
+ }
203
+
204
+ return index;
205
+ }
206
+
207
+ function mergeRealtimePayload(event, payloadPatch) {
208
+ const basePayload = event && typeof event === "object" && !Array.isArray(event) ? event : {};
209
+ if (payloadPatch == null) {
210
+ return basePayload;
211
+ }
212
+ if (!payloadPatch || typeof payloadPatch !== "object" || Array.isArray(payloadPatch)) {
213
+ throw new TypeError("Realtime payload callback must return an object.");
214
+ }
215
+
216
+ // Keep canonical domain-event fields authoritative while allowing additive metadata.
217
+ return {
218
+ ...payloadPatch,
219
+ ...basePayload
220
+ };
221
+ }
222
+
223
+ function resolveScopeWorkspaceId(scope = {}) {
224
+ const source = scope && typeof scope === "object" && !Array.isArray(scope) ? scope : {};
225
+ if (String(source.kind || "").trim().toLowerCase() === "workspace") {
226
+ return normalizePositiveInteger(source.id);
227
+ }
228
+ if (String(source.kind || "").trim().toLowerCase() === "workspace_user") {
229
+ return normalizePositiveInteger(source.workspaceId || source.id);
230
+ }
231
+ return normalizePositiveInteger(source.workspaceId);
232
+ }
233
+
234
+ function resolveScopeUserId(scope = {}) {
235
+ const source = scope && typeof scope === "object" && !Array.isArray(scope) ? scope : {};
236
+ if (String(source.kind || "").trim().toLowerCase() === "user") {
237
+ return normalizePositiveInteger(source.id);
238
+ }
239
+ if (String(source.kind || "").trim().toLowerCase() === "workspace_user") {
240
+ return normalizePositiveInteger(source.userId);
241
+ }
242
+ return normalizePositiveInteger(source.userId);
243
+ }
244
+
245
+ function applyAudiencePreset(preset, { event, rooms, flags, logger } = {}) {
246
+ const normalizedPreset = normalizeText(preset).toLowerCase();
247
+ if (!normalizedPreset || normalizedPreset === "none") {
248
+ return;
249
+ }
250
+
251
+ if (normalizedPreset === "all_clients") {
252
+ flags.broadcastAllClients = true;
253
+ return;
254
+ }
255
+
256
+ if (normalizedPreset === "all_users") {
257
+ rooms.add(REALTIME_ROOM_ALL_USERS);
258
+ return;
259
+ }
260
+
261
+ if (normalizedPreset === "actor_user") {
262
+ const actorId = normalizePositiveInteger(event?.actorId);
263
+ if (actorId > 0) {
264
+ rooms.add(roomForUser(actorId));
265
+ }
266
+ return;
267
+ }
268
+
269
+ if (normalizedPreset === "all_workspace_users") {
270
+ const workspaceId = resolveScopeWorkspaceId(event?.scope);
271
+ if (workspaceId > 0) {
272
+ rooms.add(roomForWorkspace(workspaceId));
273
+ return;
274
+ }
275
+ logger.warn(
276
+ {
277
+ audience: normalizedPreset,
278
+ scope: event?.scope || null
279
+ },
280
+ "Realtime audience preset requires a workspace scope."
281
+ );
282
+ return;
283
+ }
284
+
285
+ if (normalizedPreset === "event_scope") {
286
+ const scopeKind = normalizeText(event?.scope?.kind).toLowerCase();
287
+ if (scopeKind === "workspace") {
288
+ const workspaceId = resolveScopeWorkspaceId(event?.scope);
289
+ if (workspaceId > 0) {
290
+ rooms.add(roomForWorkspace(workspaceId));
291
+ }
292
+ return;
293
+ }
294
+ if (scopeKind === "workspace_user") {
295
+ const workspaceId = resolveScopeWorkspaceId(event?.scope);
296
+ const userId = resolveScopeUserId(event?.scope);
297
+ if (workspaceId > 0 && userId > 0) {
298
+ rooms.add(roomForWorkspaceUser(workspaceId, userId));
299
+ }
300
+ return;
301
+ }
302
+ if (scopeKind === "user") {
303
+ const userId = resolveScopeUserId(event?.scope);
304
+ if (userId > 0) {
305
+ rooms.add(roomForUser(userId));
306
+ }
307
+ return;
308
+ }
309
+ rooms.add(REALTIME_ROOM_ALL_USERS);
310
+ return;
311
+ }
312
+
313
+ logger.warn(
314
+ {
315
+ audience: normalizedPreset
316
+ },
317
+ "Realtime bridge ignored unknown audience preset."
318
+ );
319
+ }
320
+
321
+ function addAudienceRoomsFromObject(selection, { event, rooms, flags, logger } = {}) {
322
+ if (!selection || typeof selection !== "object" || Array.isArray(selection)) {
323
+ return;
324
+ }
325
+
326
+ if (Object.hasOwn(selection, "preset")) {
327
+ applyAudiencePreset(selection.preset, {
328
+ event,
329
+ rooms,
330
+ flags,
331
+ logger
332
+ });
333
+ }
334
+ if (selection.broadcast === true) {
335
+ flags.broadcastAllClients = true;
336
+ }
337
+
338
+ const room = normalizeText(selection.room);
339
+ if (room) {
340
+ rooms.add(room);
341
+ }
342
+ for (const entry of normalizeArray(selection.rooms)) {
343
+ const normalizedRoom = normalizeText(entry);
344
+ if (normalizedRoom) {
345
+ rooms.add(normalizedRoom);
346
+ }
347
+ }
348
+
349
+ const userId = normalizePositiveInteger(selection.userId);
350
+ if (userId > 0) {
351
+ rooms.add(roomForUser(userId));
352
+ }
353
+ for (const entry of normalizeArray(selection.userIds)) {
354
+ const normalizedUserId = normalizePositiveInteger(entry);
355
+ if (normalizedUserId > 0) {
356
+ rooms.add(roomForUser(normalizedUserId));
357
+ }
358
+ }
359
+
360
+ const workspaceId = normalizePositiveInteger(selection.workspaceId);
361
+ if (workspaceId > 0) {
362
+ rooms.add(roomForWorkspace(workspaceId));
363
+ }
364
+ for (const entry of normalizeArray(selection.workspaceIds)) {
365
+ const normalizedWorkspaceId = normalizePositiveInteger(entry);
366
+ if (normalizedWorkspaceId > 0) {
367
+ rooms.add(roomForWorkspace(normalizedWorkspaceId));
368
+ }
369
+ }
370
+
371
+ const workspaceUser = selection.workspaceUser;
372
+ if (workspaceUser && typeof workspaceUser === "object") {
373
+ const targetWorkspaceId = normalizePositiveInteger(workspaceUser.workspaceId);
374
+ const targetUserId = normalizePositiveInteger(workspaceUser.userId);
375
+ if (targetWorkspaceId > 0 && targetUserId > 0) {
376
+ rooms.add(roomForWorkspaceUser(targetWorkspaceId, targetUserId));
377
+ }
378
+ }
379
+
380
+ for (const entry of normalizeArray(selection.workspaceUsers)) {
381
+ if (!entry || typeof entry !== "object") {
382
+ continue;
383
+ }
384
+ const targetWorkspaceId = normalizePositiveInteger(entry.workspaceId);
385
+ const targetUserId = normalizePositiveInteger(entry.userId);
386
+ if (targetWorkspaceId > 0 && targetUserId > 0) {
387
+ rooms.add(roomForWorkspaceUser(targetWorkspaceId, targetUserId));
388
+ }
389
+ }
390
+ }
391
+
392
+ function collectUserIdsFromQueryRows(rows = []) {
393
+ const result = new Set();
394
+ for (const row of normalizeArray(rows)) {
395
+ if (typeof row === "number") {
396
+ const directId = normalizePositiveInteger(row);
397
+ if (directId > 0) {
398
+ result.add(directId);
399
+ }
400
+ continue;
401
+ }
402
+ if (!row || typeof row !== "object" || Array.isArray(row)) {
403
+ continue;
404
+ }
405
+ const userId = normalizePositiveInteger(row.userId || row.user_id || row.id);
406
+ if (userId > 0) {
407
+ result.add(userId);
408
+ }
409
+ }
410
+ return [...result];
411
+ }
412
+
413
+ async function resolveAudienceQueryRooms(userQuery, { scope, event, logger } = {}) {
414
+ if (typeof userQuery !== "function") {
415
+ return [];
416
+ }
417
+
418
+ if (!scope || typeof scope.has !== "function" || typeof scope.make !== "function" || !scope.has(KERNEL_TOKENS.Knex)) {
419
+ logger.warn("Realtime audience userQuery requires runtime database token.");
420
+ return [];
421
+ }
422
+
423
+ const knex = scope.make(KERNEL_TOKENS.Knex);
424
+ const queryResult = await userQuery({
425
+ knex,
426
+ event
427
+ });
428
+ const userIds = collectUserIdsFromQueryRows(await Promise.resolve(queryResult));
429
+ return userIds.map((userId) => roomForUser(userId));
430
+ }
431
+
432
+ async function resolveAudienceTargets(dispatcher, event, { scope, logger } = {}) {
433
+ const rooms = new Set();
434
+ const flags = {
435
+ broadcastAllClients: false
436
+ };
437
+
438
+ let selection = dispatcher?.audience;
439
+ if (typeof selection === "function") {
440
+ selection = await selection({
441
+ event
442
+ });
443
+ }
444
+
445
+ for (const entry of normalizeArray(selection)) {
446
+ if (typeof entry === "string") {
447
+ applyAudiencePreset(entry, {
448
+ event,
449
+ rooms,
450
+ flags,
451
+ logger
452
+ });
453
+ continue;
454
+ }
455
+
456
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
457
+ continue;
458
+ }
459
+
460
+ addAudienceRoomsFromObject(entry, {
461
+ event,
462
+ rooms,
463
+ flags,
464
+ logger
465
+ });
466
+
467
+ if (typeof entry.userQuery === "function") {
468
+ const queryRooms = await resolveAudienceQueryRooms(entry.userQuery, {
469
+ scope,
470
+ event,
471
+ logger
472
+ });
473
+ for (const room of queryRooms) {
474
+ rooms.add(room);
475
+ }
476
+ }
477
+ }
478
+
479
+ return Object.freeze({
480
+ broadcastAllClients: flags.broadcastAllClients,
481
+ rooms: Object.freeze([...rooms])
482
+ });
483
+ }
484
+
485
+ async function resolveSocketActorId(authService, socket) {
486
+ if (!authService || typeof authService.authenticateRequest !== "function") {
487
+ return 0;
488
+ }
489
+
490
+ const cookies = parseCookieHeader(socket?.request?.headers?.cookie);
491
+ const authResult = await authService.authenticateRequest({
492
+ cookies
493
+ });
494
+ if (!authResult || authResult.authenticated !== true) {
495
+ return 0;
496
+ }
497
+ return normalizePositiveInteger(authResult?.profile?.id);
498
+ }
499
+
500
+ async function resolveActorWorkspaceIds(workspaceMembershipsRepository, actorId) {
501
+ if (!workspaceMembershipsRepository || typeof workspaceMembershipsRepository.listActiveWorkspaceIdsByUserId !== "function") {
502
+ return [];
503
+ }
504
+
505
+ const workspaceIds = await workspaceMembershipsRepository.listActiveWorkspaceIdsByUserId(actorId);
506
+ return normalizeArray(workspaceIds)
507
+ .map((entry) => normalizePositiveInteger(entry))
508
+ .filter((entry) => entry > 0);
509
+ }
510
+
511
+ function registerRealtimeSocketAudienceBootstrap(scope, io, logger) {
512
+ if (!io || typeof io.on !== "function") {
513
+ return;
514
+ }
515
+
516
+ const authService =
517
+ scope && typeof scope.has === "function" && scope.has("authService") ? scope.make("authService") : null;
518
+ const workspaceMembershipsRepository =
519
+ scope && typeof scope.has === "function" && scope.has("workspaceMembershipsRepository")
520
+ ? scope.make("workspaceMembershipsRepository")
521
+ : null;
522
+
523
+ io.on("connection", async (socket) => {
524
+ try {
525
+ socket.join(REALTIME_ROOM_ALL_CLIENTS);
526
+ logger.debug(
527
+ {
528
+ listenerId: REALTIME_DOMAIN_EVENT_BRIDGE_TOKEN,
529
+ stage: "socket.connection",
530
+ room: REALTIME_ROOM_ALL_CLIENTS
531
+ },
532
+ "Realtime socket joined default clients room."
533
+ );
534
+
535
+ const actorId = await resolveSocketActorId(authService, socket);
536
+ if (actorId < 1) {
537
+ logger.debug(
538
+ {
539
+ listenerId: REALTIME_DOMAIN_EVENT_BRIDGE_TOKEN,
540
+ stage: "socket.connection",
541
+ authenticated: false
542
+ },
543
+ "Realtime socket connected without authenticated actor."
544
+ );
545
+ return;
546
+ }
547
+
548
+ socket.data = socket.data && typeof socket.data === "object" ? socket.data : {};
549
+ socket.data.actorId = actorId;
550
+
551
+ socket.join(REALTIME_ROOM_ALL_USERS);
552
+ socket.join(roomForUser(actorId));
553
+ logger.debug(
554
+ {
555
+ listenerId: REALTIME_DOMAIN_EVENT_BRIDGE_TOKEN,
556
+ stage: "socket.connection",
557
+ actorId,
558
+ joinedRooms: [REALTIME_ROOM_ALL_USERS, roomForUser(actorId)]
559
+ },
560
+ "Realtime socket joined actor/user rooms."
561
+ );
562
+
563
+ const workspaceIds = await resolveActorWorkspaceIds(workspaceMembershipsRepository, actorId);
564
+ for (const workspaceId of workspaceIds) {
565
+ socket.join(roomForWorkspace(workspaceId));
566
+ socket.join(roomForWorkspaceUser(workspaceId, actorId));
567
+ }
568
+ logger.debug(
569
+ {
570
+ listenerId: REALTIME_DOMAIN_EVENT_BRIDGE_TOKEN,
571
+ stage: "socket.connection",
572
+ actorId,
573
+ workspaceIds
574
+ },
575
+ "Realtime socket joined workspace rooms."
576
+ );
577
+ } catch (error) {
578
+ logger.warn(
579
+ {
580
+ listenerId: REALTIME_DOMAIN_EVENT_BRIDGE_TOKEN,
581
+ error: String(error?.message || error || "unknown error")
582
+ },
583
+ "Realtime socket audience bootstrap failed."
584
+ );
585
+ }
586
+ });
587
+ }
588
+
589
+ class RealtimeServiceProvider {
590
+ static id = REALTIME_RUNTIME_SERVER_TOKEN;
591
+
592
+ register(app) {
593
+ if (!app || typeof app.singleton !== "function" || typeof app.tag !== "function") {
594
+ throw new Error("RealtimeServiceProvider requires application singleton()/tag().");
595
+ }
596
+
597
+ app.singleton(REALTIME_RUNTIME_SERVER_TOKEN, () => REALTIME_RUNTIME_SERVER_API);
598
+ app.singleton(REALTIME_SOCKET_IO_SERVER_TOKEN, (scope) => {
599
+ const fastify = scope.make(KERNEL_TOKENS.Fastify);
600
+ return createSocketIoServer({
601
+ fastify
602
+ });
603
+ });
604
+
605
+ registerDomainEventListener(app, REALTIME_DOMAIN_EVENT_BRIDGE_TOKEN, (scope) => {
606
+ const io = scope.make(REALTIME_SOCKET_IO_SERVER_TOKEN);
607
+ const realtimeDispatchIndex = buildRealtimeDispatchIndex(resolveServiceRegistrations(scope));
608
+ const logger = createProviderLogger(scope, {
609
+ debugEnabled: resolveRealtimeServerDebugEnabled(scope)
610
+ });
611
+
612
+ return {
613
+ listenerId: REALTIME_DOMAIN_EVENT_BRIDGE_TOKEN,
614
+ async handle(event = {}) {
615
+ const serviceToken = normalizeText(event?.meta?.service?.token);
616
+ const methodName = normalizeText(event?.meta?.service?.method);
617
+ const emittedRealtimeEvent = normalizeText(event?.meta?.realtime?.event);
618
+ if (!serviceToken || !methodName) {
619
+ logger.warn(
620
+ {
621
+ listenerId: REALTIME_DOMAIN_EVENT_BRIDGE_TOKEN,
622
+ reason: "missing-service-meta",
623
+ meta: event?.meta || null
624
+ },
625
+ "Realtime bridge skipped domain event."
626
+ );
627
+ return;
628
+ }
629
+
630
+ const dispatchersForMethod = realtimeDispatchIndex.get(`${serviceToken}:${methodName}`) || [];
631
+ const dispatchers =
632
+ emittedRealtimeEvent.length > 0
633
+ ? dispatchersForMethod.filter((dispatcher) => normalizeText(dispatcher?.event) === emittedRealtimeEvent)
634
+ : dispatchersForMethod;
635
+ logger.debug(
636
+ {
637
+ listenerId: REALTIME_DOMAIN_EVENT_BRIDGE_TOKEN,
638
+ serviceToken,
639
+ methodName,
640
+ emittedRealtimeEvent: emittedRealtimeEvent || null,
641
+ dispatcherCount: dispatchers.length,
642
+ methodDispatcherCount: dispatchersForMethod.length,
643
+ eventType: event?.type || null,
644
+ scope: event?.scope || null,
645
+ entityId: event?.entityId || null
646
+ },
647
+ "Realtime bridge received service event."
648
+ );
649
+
650
+ if (dispatchers.length < 1) {
651
+ logger.warn(
652
+ {
653
+ listenerId: REALTIME_DOMAIN_EVENT_BRIDGE_TOKEN,
654
+ serviceToken,
655
+ methodName,
656
+ emittedRealtimeEvent: emittedRealtimeEvent || null
657
+ },
658
+ "Realtime bridge found no matching dispatcher for service event."
659
+ );
660
+ return;
661
+ }
662
+
663
+ for (const dispatcher of dispatchers) {
664
+ const payloadPatch =
665
+ event?.meta?.realtime && typeof event.meta.realtime === "object"
666
+ ? event.meta.realtime.payload
667
+ : null;
668
+ const payload = mergeRealtimePayload(event, payloadPatch);
669
+ const targets = await resolveAudienceTargets(dispatcher, event, {
670
+ scope,
671
+ logger
672
+ });
673
+
674
+ if (targets.broadcastAllClients === true) {
675
+ io.emit(dispatcher.event, payload);
676
+ }
677
+ for (const room of targets.rooms) {
678
+ io.to(room).emit(dispatcher.event, payload);
679
+ }
680
+
681
+ logger.debug(
682
+ {
683
+ listenerId: REALTIME_DOMAIN_EVENT_BRIDGE_TOKEN,
684
+ socketEvent: dispatcher.event,
685
+ serviceToken,
686
+ methodName,
687
+ rooms: targets.rooms,
688
+ broadcastAllClients: targets.broadcastAllClients,
689
+ connectedClients:
690
+ io && io.engine && Number.isInteger(Number(io.engine.clientsCount))
691
+ ? Number(io.engine.clientsCount)
692
+ : null,
693
+ payloadScope: payload?.scope || null,
694
+ payloadEntityId: payload?.entityId || null
695
+ },
696
+ "Realtime bridge emitted socket event."
697
+ );
698
+ }
699
+ }
700
+ };
701
+ });
702
+ }
703
+
704
+ async boot(app) {
705
+ if (!app || typeof app.make !== "function") {
706
+ throw new Error("RealtimeServiceProvider requires application make().");
707
+ }
708
+
709
+ this.socketIoServer = app.make(REALTIME_SOCKET_IO_SERVER_TOKEN);
710
+ const debugEnabled = resolveRealtimeServerDebugEnabled(app);
711
+ const logger = createProviderLogger(app, {
712
+ debugEnabled
713
+ });
714
+ logger.debug(
715
+ {
716
+ providerId: RealtimeServiceProvider.id,
717
+ debugEnabled
718
+ },
719
+ "Realtime server debug mode enabled."
720
+ );
721
+ registerRealtimeSocketAudienceBootstrap(app, this.socketIoServer, logger);
722
+
723
+ const env = typeof app.has === "function" && app.has(KERNEL_TOKENS.Env) ? app.make(KERNEL_TOKENS.Env) : {};
724
+ const redisUrl = resolveRealtimeRedisUrl(env);
725
+ this.redisConnection = await configureSocketIoRedisAdapter(this.socketIoServer, {
726
+ redisUrl
727
+ });
728
+ }
729
+
730
+ async shutdown() {
731
+ if (!this.socketIoServer) {
732
+ return;
733
+ }
734
+ await closeSocketIoServer(this.socketIoServer);
735
+ await closeSocketIoRedisConnections(this.redisConnection || {});
736
+ this.redisConnection = null;
737
+ this.socketIoServer = null;
738
+ }
739
+ }
740
+
741
+ export {
742
+ RealtimeServiceProvider
743
+ };