dialogue-ts 0.0.1

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,1389 @@
1
+ // src/create-dialogue.ts
2
+ import { Hono } from "hono";
3
+ import { Err as Err3 } from "slang-ts";
4
+
5
+ // src/history.ts
6
+ function createHistoryManager(config = {}) {
7
+ const store = /* @__PURE__ */ new Map();
8
+ function ensureRoomEvent(roomId, eventName) {
9
+ let roomHistory = store.get(roomId);
10
+ if (!roomHistory) {
11
+ roomHistory = /* @__PURE__ */ new Map();
12
+ store.set(roomId, roomHistory);
13
+ }
14
+ let eventHistory = roomHistory.get(eventName);
15
+ if (!eventHistory) {
16
+ eventHistory = [];
17
+ roomHistory.set(eventName, eventHistory);
18
+ }
19
+ return eventHistory;
20
+ }
21
+ return {
22
+ push(roomId, eventName, event, limit) {
23
+ const events = ensureRoomEvent(roomId, eventName);
24
+ events.push(event);
25
+ if (events.length > limit) {
26
+ const evictCount = events.length - limit;
27
+ const evicted = events.splice(0, evictCount);
28
+ if (config.onCleanup && evicted.length > 0) {
29
+ config.onCleanup(roomId, eventName, evicted);
30
+ }
31
+ }
32
+ },
33
+ get(roomId, eventName, start, end) {
34
+ const roomHistory = store.get(roomId);
35
+ if (!roomHistory) {
36
+ return [];
37
+ }
38
+ const events = roomHistory.get(eventName);
39
+ if (!events || events.length === 0) {
40
+ return [];
41
+ }
42
+ const len = events.length;
43
+ const actualStart = Math.max(0, len - end);
44
+ const actualEnd = len - start;
45
+ if (actualStart >= actualEnd) {
46
+ return [];
47
+ }
48
+ return events.slice(actualStart, actualEnd).reverse();
49
+ },
50
+ getAll(roomId, limit) {
51
+ const roomHistory = store.get(roomId);
52
+ if (!roomHistory) {
53
+ return [];
54
+ }
55
+ const allEvents = [];
56
+ for (const events of roomHistory.values()) {
57
+ allEvents.push(...events);
58
+ }
59
+ allEvents.sort((a, b) => b.timestamp - a.timestamp);
60
+ if (limit !== void 0 && limit > 0) {
61
+ return allEvents.slice(0, limit);
62
+ }
63
+ return allEvents;
64
+ },
65
+ count(roomId, eventName) {
66
+ const roomHistory = store.get(roomId);
67
+ if (!roomHistory) {
68
+ return 0;
69
+ }
70
+ const events = roomHistory.get(eventName);
71
+ return events?.length ?? 0;
72
+ },
73
+ clearRoom(roomId) {
74
+ const roomHistory = store.get(roomId);
75
+ if (!roomHistory) {
76
+ return;
77
+ }
78
+ if (config.onCleanup) {
79
+ for (const [eventName, events] of roomHistory.entries()) {
80
+ if (events.length > 0) {
81
+ config.onCleanup(roomId, eventName, events);
82
+ }
83
+ }
84
+ }
85
+ store.delete(roomId);
86
+ },
87
+ getEventNames(roomId) {
88
+ const roomHistory = store.get(roomId);
89
+ if (!roomHistory) {
90
+ return [];
91
+ }
92
+ return Array.from(roomHistory.keys());
93
+ }
94
+ };
95
+ }
96
+
97
+ // src/logger.ts
98
+ function createDefaultLogger() {
99
+ return {
100
+ debug(entry) {
101
+ if (process.env.NODE_ENV === "development" || process.env.DEBUG) {
102
+ console.debug("[Dialogue] [DEBUG]", entry);
103
+ }
104
+ },
105
+ info(entry) {
106
+ console.info("[Dialogue] [INFO]", entry);
107
+ },
108
+ warn(entry) {
109
+ console.warn("[Dialogue] [WARN]", entry);
110
+ },
111
+ error(entry) {
112
+ console.error("[Dialogue] [ERROR]", entry);
113
+ }
114
+ };
115
+ }
116
+ function createSilentLogger() {
117
+ const noop = () => {
118
+ };
119
+ return {
120
+ debug: noop,
121
+ info: noop,
122
+ warn: noop,
123
+ error: noop
124
+ };
125
+ }
126
+
127
+ // src/server.ts
128
+ import { Server as BunEngine } from "@socket.io/bun-engine";
129
+ import { cors as honoCors } from "hono/cors";
130
+ import { Server } from "socket.io";
131
+
132
+ // src/client-handler.ts
133
+ import { nanoid } from "nanoid";
134
+ var WILDCARD = "*";
135
+ function createConnectedClient(socket, userId, meta, roomManager, logger) {
136
+ const id = nanoid();
137
+ const joinedRooms = /* @__PURE__ */ new Set();
138
+ const subscriptions = /* @__PURE__ */ new Map();
139
+ const client = {
140
+ id,
141
+ userId,
142
+ socket,
143
+ meta,
144
+ join(roomId) {
145
+ const room = roomManager.get(roomId);
146
+ if (!room) {
147
+ logger.warn({
148
+ message: `Room '${roomId}' does not exist`,
149
+ atFunction: "client.join",
150
+ data: { roomId, clientId: id }
151
+ });
152
+ return;
153
+ }
154
+ if (joinedRooms.has(roomId)) {
155
+ socket.emit("dialogue:joined", { roomId, roomName: room.name });
156
+ return;
157
+ }
158
+ const added = roomManager.addParticipant(roomId, client);
159
+ if (!added) {
160
+ logger.warn({
161
+ message: `Room '${roomId}' is full`,
162
+ atFunction: "client.join",
163
+ data: { roomId, clientId: id }
164
+ });
165
+ socket.emit("dialogue:error", {
166
+ code: "ROOM_FULL",
167
+ message: `Room '${roomId}' is at capacity`
168
+ });
169
+ return;
170
+ }
171
+ joinedRooms.add(roomId);
172
+ subscriptions.set(roomId, /* @__PURE__ */ new Set());
173
+ for (const eventName of room.defaultSubscriptions) {
174
+ this.subscribe(roomId, eventName);
175
+ }
176
+ socket.emit("dialogue:joined", { roomId, roomName: room.name });
177
+ },
178
+ leave(roomId) {
179
+ if (!joinedRooms.has(roomId)) {
180
+ return;
181
+ }
182
+ roomManager.removeParticipant(roomId, id);
183
+ joinedRooms.delete(roomId);
184
+ subscriptions.delete(roomId);
185
+ socket.emit("dialogue:left", { roomId });
186
+ },
187
+ subscribe(roomId, eventName) {
188
+ if (!joinedRooms.has(roomId)) {
189
+ logger.warn({
190
+ message: `Cannot subscribe to '${eventName}' - not in room '${roomId}'`,
191
+ atFunction: "client.subscribe",
192
+ data: { roomId, eventName, clientId: id }
193
+ });
194
+ return;
195
+ }
196
+ const roomSubs = subscriptions.get(roomId);
197
+ if (roomSubs) {
198
+ roomSubs.add(eventName);
199
+ }
200
+ },
201
+ subscribeAll(roomId) {
202
+ this.subscribe(roomId, WILDCARD);
203
+ },
204
+ unsubscribe(roomId, eventName) {
205
+ const roomSubs = subscriptions.get(roomId);
206
+ if (roomSubs) {
207
+ roomSubs.delete(eventName);
208
+ }
209
+ },
210
+ rooms() {
211
+ return Array.from(joinedRooms);
212
+ },
213
+ subscriptions(roomId) {
214
+ const roomSubs = subscriptions.get(roomId);
215
+ return roomSubs ? Array.from(roomSubs) : [];
216
+ },
217
+ send(event, data) {
218
+ socket.emit(event, data);
219
+ },
220
+ disconnect() {
221
+ for (const roomId of joinedRooms) {
222
+ roomManager.removeParticipant(roomId, id);
223
+ }
224
+ joinedRooms.clear();
225
+ subscriptions.clear();
226
+ socket.disconnect(true);
227
+ }
228
+ };
229
+ return client;
230
+ }
231
+ function extractUserFromSocket(socket) {
232
+ const auth = socket.handshake.auth;
233
+ let userId = socket.id;
234
+ if (typeof auth.userId === "string") {
235
+ userId = auth.userId;
236
+ } else if (typeof auth.token === "string") {
237
+ userId = auth.token;
238
+ }
239
+ const meta = {};
240
+ if (typeof auth.role === "string") {
241
+ meta.role = auth.role;
242
+ }
243
+ for (const [key, value] of Object.entries(auth)) {
244
+ if (key !== "userId" && key !== "token") {
245
+ meta[key] = value;
246
+ }
247
+ }
248
+ return { userId, meta };
249
+ }
250
+
251
+ // src/rate-limiter.ts
252
+ function createRateLimiter(config) {
253
+ const entries = /* @__PURE__ */ new Map();
254
+ const cleanupInterval = setInterval(() => {
255
+ const now = Date.now();
256
+ for (const [key, entry] of entries) {
257
+ if (now >= entry.resetAt) {
258
+ entries.delete(key);
259
+ }
260
+ }
261
+ }, config.windowMs);
262
+ cleanupInterval.unref?.();
263
+ return {
264
+ isAllowed(key) {
265
+ const now = Date.now();
266
+ const entry = entries.get(key);
267
+ if (!entry || now >= entry.resetAt) {
268
+ entries.set(key, {
269
+ count: 1,
270
+ resetAt: now + config.windowMs
271
+ });
272
+ return true;
273
+ }
274
+ if (entry.count >= config.maxRequests) {
275
+ return false;
276
+ }
277
+ entry.count++;
278
+ return true;
279
+ },
280
+ remaining(key) {
281
+ const now = Date.now();
282
+ const entry = entries.get(key);
283
+ if (!entry || now >= entry.resetAt) {
284
+ return config.maxRequests;
285
+ }
286
+ return Math.max(0, config.maxRequests - entry.count);
287
+ },
288
+ clear() {
289
+ entries.clear();
290
+ }
291
+ };
292
+ }
293
+
294
+ // src/room.ts
295
+ import { Err as Err2, Ok as Ok2 } from "slang-ts";
296
+
297
+ // src/define-event.ts
298
+ import { Err, Ok } from "slang-ts";
299
+ function defineEvent(name, options) {
300
+ const definition = {
301
+ name,
302
+ description: options?.description,
303
+ schema: options?.schema,
304
+ history: options?.history
305
+ };
306
+ return Object.freeze(definition);
307
+ }
308
+ function validateEventData(event, data) {
309
+ if (!event.schema) {
310
+ return Ok(data);
311
+ }
312
+ const result = event.schema.safeParse(data);
313
+ if (result.success) {
314
+ return Ok(result.data);
315
+ }
316
+ const errorMessages = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(", ");
317
+ return Err(`Event '${event.name}' validation failed: ${errorMessages}`);
318
+ }
319
+ function isEventAllowed(eventName, allowedEvents) {
320
+ if (allowedEvents.length === 0) {
321
+ return false;
322
+ }
323
+ return allowedEvents.some((e) => e.name === "*" || e.name === eventName);
324
+ }
325
+ function getEventByName(eventName, events) {
326
+ return events.find((e) => e.name === eventName);
327
+ }
328
+
329
+ // src/room.ts
330
+ var WILDCARD2 = "*";
331
+ function isParticipantSubscribed(participant, roomId, eventName) {
332
+ const subs = participant.subscriptions(roomId);
333
+ return subs.includes(eventName) || subs.includes(WILDCARD2);
334
+ }
335
+ function createRoom(id, config, _io, logger, historyManager, hooks, externalParticipants, getContext) {
336
+ const participants = externalParticipants ?? /* @__PURE__ */ new Map();
337
+ const eventHandlers = /* @__PURE__ */ new Map();
338
+ function handlePostBroadcast(eventName, message) {
339
+ const eventDef = getEventByName(eventName, config.events);
340
+ if (eventDef?.history?.enabled && historyManager) {
341
+ historyManager.push(id, eventName, message, eventDef.history.limit);
342
+ }
343
+ if (hooks?.events?.onTriggered) {
344
+ Promise.resolve(hooks.events.onTriggered(id, message)).catch((err) => {
345
+ logger.error({
346
+ message: `Error in onTriggered hook for '${eventName}'`,
347
+ atFunction: "room.trigger.onTriggered",
348
+ data: err
349
+ });
350
+ });
351
+ }
352
+ const handlers = eventHandlers.get(eventName);
353
+ if (handlers) {
354
+ for (const handler of handlers) {
355
+ Promise.resolve(handler(message)).catch((err) => {
356
+ logger.error({
357
+ message: `Error in event handler for '${eventName}'`,
358
+ atFunction: "room.trigger.handler",
359
+ data: err
360
+ });
361
+ });
362
+ }
363
+ }
364
+ }
365
+ const room = {
366
+ id,
367
+ name: config.name,
368
+ description: config.description,
369
+ maxSize: config.maxSize,
370
+ events: config.events,
371
+ defaultSubscriptions: config.defaultSubscriptions ?? [],
372
+ createdById: config.createdById,
373
+ trigger(event, data, from, meta) {
374
+ if (!isEventAllowed(event.name, config.events)) {
375
+ const errorMsg = `Event '${event.name}' is not allowed in room '${id}'`;
376
+ logger.warn({
377
+ message: errorMsg,
378
+ atFunction: "room.trigger",
379
+ data: { eventName: event.name, roomId: id }
380
+ });
381
+ return Err2(errorMsg);
382
+ }
383
+ const eventDef = getEventByName(event.name, config.events) ?? event;
384
+ const validation = validateEventData(eventDef, data);
385
+ if (validation.isErr) {
386
+ logger.warn({
387
+ message: validation.error,
388
+ atFunction: "room.trigger",
389
+ data: {
390
+ eventName: event.name,
391
+ roomId: id,
392
+ validationError: validation.error
393
+ }
394
+ });
395
+ return Err2(validation.error);
396
+ }
397
+ const message = {
398
+ event: event.name,
399
+ roomId: id,
400
+ data: validation.value,
401
+ from: from ?? "system",
402
+ timestamp: Date.now(),
403
+ ...meta && { meta }
404
+ };
405
+ let finalMessage = message;
406
+ if (hooks?.events?.beforeEach && getContext) {
407
+ const context = getContext();
408
+ const hookResult = hooks.events.beforeEach({
409
+ context,
410
+ roomId: id,
411
+ message,
412
+ from: message.from
413
+ });
414
+ if (hookResult.isErr) {
415
+ logger.debug({
416
+ message: `Event '${event.name}' blocked by beforeEach: ${hookResult.error}`,
417
+ atFunction: "room.trigger.beforeEach",
418
+ data: {
419
+ eventName: event.name,
420
+ roomId: id,
421
+ reason: hookResult.error
422
+ }
423
+ });
424
+ return Err2(hookResult.error);
425
+ }
426
+ finalMessage = hookResult.value;
427
+ }
428
+ let recipientCount = 0;
429
+ for (const [, participant] of participants) {
430
+ if (isParticipantSubscribed(participant, id, event.name)) {
431
+ participant.socket.emit("dialogue:event", finalMessage);
432
+ recipientCount++;
433
+ }
434
+ }
435
+ handlePostBroadcast(event.name, finalMessage);
436
+ if (hooks?.events?.afterEach && getContext) {
437
+ const context = getContext();
438
+ hooks.events.afterEach({
439
+ context,
440
+ roomId: id,
441
+ message: finalMessage,
442
+ recipientCount
443
+ });
444
+ }
445
+ return Ok2(void 0);
446
+ },
447
+ on(event, handler) {
448
+ let handlers = eventHandlers.get(event.name);
449
+ if (!handlers) {
450
+ handlers = /* @__PURE__ */ new Set();
451
+ eventHandlers.set(event.name, handlers);
452
+ }
453
+ handlers.add(handler);
454
+ return () => {
455
+ handlers?.delete(handler);
456
+ if (handlers?.size === 0) {
457
+ eventHandlers.delete(event.name);
458
+ }
459
+ };
460
+ },
461
+ size() {
462
+ return participants.size;
463
+ },
464
+ isFull() {
465
+ if (config.maxSize === void 0) {
466
+ return false;
467
+ }
468
+ return participants.size >= config.maxSize;
469
+ },
470
+ participants() {
471
+ return Array.from(participants.values());
472
+ },
473
+ async history(eventName, start, end) {
474
+ if (!historyManager) {
475
+ return [];
476
+ }
477
+ const inMemoryEvents = historyManager.get(id, eventName, start, end);
478
+ const inMemoryCount = historyManager.count(id, eventName);
479
+ if (inMemoryEvents.length >= end - start || !hooks?.events?.onLoad) {
480
+ return inMemoryEvents;
481
+ }
482
+ const missingStart = Math.max(start, inMemoryCount);
483
+ const missingEnd = end;
484
+ if (missingStart >= missingEnd) {
485
+ return inMemoryEvents;
486
+ }
487
+ try {
488
+ const externalEvents = await hooks.events.onLoad(
489
+ id,
490
+ eventName,
491
+ missingStart - inMemoryCount,
492
+ missingEnd - inMemoryCount
493
+ );
494
+ return [...inMemoryEvents, ...externalEvents];
495
+ } catch (err) {
496
+ logger.error({
497
+ message: `Error loading history for '${eventName}'`,
498
+ atFunction: "room.history.onLoad",
499
+ data: err
500
+ });
501
+ return inMemoryEvents;
502
+ }
503
+ }
504
+ };
505
+ return room;
506
+ }
507
+ function addParticipantToRoom(room, client, participants) {
508
+ if (room.maxSize !== void 0 && participants.size >= room.maxSize) {
509
+ return false;
510
+ }
511
+ participants.set(client.id, client);
512
+ client.socket.join(room.id);
513
+ return true;
514
+ }
515
+ function removeParticipantFromRoom(room, clientId, participants) {
516
+ const client = participants.get(clientId);
517
+ if (client) {
518
+ client.socket.leave(room.id);
519
+ participants.delete(clientId);
520
+ }
521
+ }
522
+ function createRoomManager(io, logger, historyManager, hooks, getContext) {
523
+ const rooms = /* @__PURE__ */ new Map();
524
+ const roomParticipants = /* @__PURE__ */ new Map();
525
+ return {
526
+ /**
527
+ * Registers a room from config
528
+ */
529
+ register(id, config) {
530
+ const participantsMap = /* @__PURE__ */ new Map();
531
+ roomParticipants.set(id, participantsMap);
532
+ const room = createRoom(
533
+ id,
534
+ config,
535
+ io,
536
+ logger,
537
+ historyManager,
538
+ hooks,
539
+ participantsMap,
540
+ getContext
541
+ );
542
+ rooms.set(id, room);
543
+ if (hooks?.rooms?.onCreated) {
544
+ Promise.resolve(hooks.rooms.onCreated(room)).catch((err) => {
545
+ logger.error({
546
+ message: `Error in onCreated hook for room '${id}'`,
547
+ atFunction: "roomManager.register.onCreated",
548
+ data: err
549
+ });
550
+ });
551
+ }
552
+ return room;
553
+ },
554
+ /**
555
+ * Gets a room by ID
556
+ */
557
+ get(id) {
558
+ return rooms.get(id) ?? null;
559
+ },
560
+ /**
561
+ * Gets all rooms
562
+ */
563
+ all() {
564
+ return Array.from(rooms.values());
565
+ },
566
+ /**
567
+ * Adds a participant to a room
568
+ */
569
+ addParticipant(roomId, client) {
570
+ const room = rooms.get(roomId);
571
+ const participants = roomParticipants.get(roomId);
572
+ if (!(room && participants)) {
573
+ return false;
574
+ }
575
+ return addParticipantToRoom(room, client, participants);
576
+ },
577
+ /**
578
+ * Removes a participant from a room
579
+ */
580
+ removeParticipant(roomId, clientId) {
581
+ const room = rooms.get(roomId);
582
+ const participants = roomParticipants.get(roomId);
583
+ if (room && participants) {
584
+ removeParticipantFromRoom(room, clientId, participants);
585
+ }
586
+ },
587
+ /**
588
+ * Removes a client from all rooms
589
+ */
590
+ removeFromAllRooms(clientId) {
591
+ for (const [roomId] of rooms) {
592
+ this.removeParticipant(roomId, clientId);
593
+ }
594
+ },
595
+ /**
596
+ * Gets participants for a room
597
+ */
598
+ getParticipants(roomId) {
599
+ const participants = roomParticipants.get(roomId);
600
+ return participants ? Array.from(participants.values()) : [];
601
+ },
602
+ /**
603
+ * Gets room size
604
+ */
605
+ getRoomSize(roomId) {
606
+ return roomParticipants.get(roomId)?.size ?? 0;
607
+ },
608
+ /**
609
+ * Unregisters (deletes) a room by ID.
610
+ * Removes all participants and cleans up resources.
611
+ * @returns true if room was deleted, false if it didn't exist
612
+ */
613
+ unregister(id) {
614
+ const room = rooms.get(id);
615
+ if (!room) {
616
+ return false;
617
+ }
618
+ const participants = roomParticipants.get(id);
619
+ if (participants) {
620
+ for (const client of participants.values()) {
621
+ client.socket.leave(id);
622
+ }
623
+ participants.clear();
624
+ }
625
+ if (historyManager) {
626
+ historyManager.clearRoom(id);
627
+ }
628
+ io.to(id).emit("dialogue:roomDeleted", { roomId: id });
629
+ rooms.delete(id);
630
+ roomParticipants.delete(id);
631
+ logger.info({
632
+ message: `Room '${id}' deleted`,
633
+ atFunction: "roomManager.unregister",
634
+ data: { roomId: id }
635
+ });
636
+ if (hooks?.rooms?.onDeleted) {
637
+ Promise.resolve(hooks.rooms.onDeleted(id)).catch((err) => {
638
+ logger.error({
639
+ message: `Error in onDeleted hook for room '${id}'`,
640
+ atFunction: "roomManager.unregister.onDeleted",
641
+ data: err
642
+ });
643
+ });
644
+ }
645
+ return true;
646
+ }
647
+ };
648
+ }
649
+
650
+ // src/server.ts
651
+ function buildCorsOptions(cors) {
652
+ if (cors === void 0 || cors === true) {
653
+ return {
654
+ origin: true,
655
+ methods: ["GET", "POST"],
656
+ credentials: true
657
+ };
658
+ }
659
+ if (cors === false) {
660
+ return { origin: false };
661
+ }
662
+ return {
663
+ origin: cors.origin,
664
+ methods: cors.methods ?? ["GET", "POST"],
665
+ credentials: cors.credentials ?? true
666
+ };
667
+ }
668
+ function buildHonoCorsOptions(cors) {
669
+ if (cors === void 0 || cors === true) {
670
+ return {
671
+ origin: "*",
672
+ allowMethods: ["GET", "POST", "OPTIONS"],
673
+ credentials: true
674
+ };
675
+ }
676
+ if (cors === false) {
677
+ return {
678
+ origin: () => void 0,
679
+ allowMethods: ["GET", "POST"],
680
+ credentials: false
681
+ };
682
+ }
683
+ const originValue = cors.origin;
684
+ let resolvedOrigin;
685
+ if (originValue === true) {
686
+ resolvedOrigin = "*";
687
+ } else if (originValue === false) {
688
+ resolvedOrigin = () => void 0;
689
+ } else {
690
+ resolvedOrigin = originValue;
691
+ }
692
+ return {
693
+ origin: resolvedOrigin,
694
+ allowMethods: cors.methods ?? ["GET", "POST", "OPTIONS"],
695
+ credentials: cors.credentials ?? true
696
+ };
697
+ }
698
+ function addCorsHeaders(response, request, corsConfig) {
699
+ const origin = request.headers.get("Origin");
700
+ if (!origin) {
701
+ return response;
702
+ }
703
+ const headers = new Headers(response.headers);
704
+ if (corsConfig === void 0 || corsConfig === true) {
705
+ headers.set("Access-Control-Allow-Origin", origin);
706
+ headers.set("Access-Control-Allow-Credentials", "true");
707
+ } else if (corsConfig === false) {
708
+ return response;
709
+ } else {
710
+ const allowedOrigin = corsConfig.origin;
711
+ if (allowedOrigin === true) {
712
+ headers.set("Access-Control-Allow-Origin", origin);
713
+ } else if (typeof allowedOrigin === "string" && allowedOrigin === origin) {
714
+ headers.set("Access-Control-Allow-Origin", origin);
715
+ } else if (Array.isArray(allowedOrigin) && allowedOrigin.includes(origin)) {
716
+ headers.set("Access-Control-Allow-Origin", origin);
717
+ }
718
+ if (corsConfig.credentials !== false) {
719
+ headers.set("Access-Control-Allow-Credentials", "true");
720
+ }
721
+ }
722
+ headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
723
+ headers.set("Access-Control-Allow-Headers", "Content-Type");
724
+ return new Response(response.body, {
725
+ status: response.status,
726
+ statusText: response.statusText,
727
+ headers
728
+ });
729
+ }
730
+ function sendHistoryOnJoin(socket, roomId, roomConfig, historyManager) {
731
+ if (!(roomConfig?.syncHistoryOnJoin && historyManager)) {
732
+ return;
733
+ }
734
+ const limit = typeof roomConfig.syncHistoryOnJoin === "number" ? roomConfig.syncHistoryOnJoin : void 0;
735
+ const historyEvents = historyManager.getAll(roomId, limit);
736
+ if (historyEvents.length > 0) {
737
+ socket.emit("dialogue:history", {
738
+ roomId,
739
+ events: historyEvents
740
+ });
741
+ }
742
+ }
743
+ function createDialogueContext(io, connectedClients, roomManager) {
744
+ const clientsRecord = {};
745
+ for (const [id, client] of connectedClients.entries()) {
746
+ clientsRecord[id] = client;
747
+ }
748
+ const roomsRecord = {};
749
+ for (const room of roomManager.all()) {
750
+ roomsRecord[room.id] = room;
751
+ }
752
+ return {
753
+ io,
754
+ clients: clientsRecord,
755
+ rooms: roomsRecord
756
+ };
757
+ }
758
+ function setupServer(app, config, historyManager) {
759
+ const logger = config.logger ?? createDefaultLogger();
760
+ const hooks = config.hooks;
761
+ const connectedClients = /* @__PURE__ */ new Map();
762
+ const userIdToSocketIds = /* @__PURE__ */ new Map();
763
+ const corsOptions = buildCorsOptions(config.cors);
764
+ const honoCorsOptions = buildHonoCorsOptions(config.cors);
765
+ app.use("*", honoCors(honoCorsOptions));
766
+ const io = new Server({ cors: corsOptions });
767
+ const engine = new BunEngine();
768
+ io.bind(engine);
769
+ const getContextForHooks = () => {
770
+ return createDialogueContext(io, connectedClients, roomManager);
771
+ };
772
+ const roomManager = createRoomManager(
773
+ io,
774
+ logger,
775
+ historyManager,
776
+ hooks,
777
+ getContextForHooks
778
+ );
779
+ const historyRateLimiter = createRateLimiter({
780
+ maxRequests: 20,
781
+ windowMs: 6e4
782
+ });
783
+ for (const [roomId, roomConfig] of Object.entries(config.rooms)) {
784
+ roomManager.register(roomId, roomConfig);
785
+ }
786
+ if (hooks?.socket?.authenticate) {
787
+ const authenticateHook = hooks.socket.authenticate;
788
+ io.use(async (socket, next) => {
789
+ const authData = socket.handshake.auth;
790
+ const context = createDialogueContext(io, connectedClients, roomManager);
791
+ try {
792
+ const result = await Promise.resolve(
793
+ authenticateHook({
794
+ context,
795
+ clientSocket: socket,
796
+ authData
797
+ })
798
+ );
799
+ if (result.isErr) {
800
+ logger.warn({
801
+ message: `Authentication rejected: ${result.error}`,
802
+ atFunction: "setupServer.authenticate",
803
+ data: { socketId: socket.id }
804
+ });
805
+ return next(new Error(result.error));
806
+ }
807
+ socket.data = {
808
+ ...socket.data,
809
+ authenticatedAuthData: result.value
810
+ };
811
+ return next();
812
+ } catch (err) {
813
+ logger.error({
814
+ message: "Error in authenticate hook",
815
+ atFunction: "setupServer.authenticate",
816
+ data: err
817
+ });
818
+ return next(new Error("Authentication failed"));
819
+ }
820
+ });
821
+ }
822
+ io.on("connection", (socket) => {
823
+ let userId;
824
+ let meta;
825
+ let authData;
826
+ if (socket.data?.authenticatedAuthData) {
827
+ authData = socket.data.authenticatedAuthData;
828
+ userId = authData.jwt.sub;
829
+ meta = {};
830
+ } else {
831
+ const extracted = extractUserFromSocket(socket);
832
+ userId = extracted.userId;
833
+ meta = extracted.meta;
834
+ }
835
+ const client = createConnectedClient(
836
+ socket,
837
+ userId,
838
+ meta,
839
+ roomManager,
840
+ logger
841
+ );
842
+ if (authData) {
843
+ client.auth = authData;
844
+ }
845
+ connectedClients.set(socket.id, client);
846
+ const userSockets = userIdToSocketIds.get(client.userId) ?? /* @__PURE__ */ new Set();
847
+ userSockets.add(socket.id);
848
+ userIdToSocketIds.set(client.userId, userSockets);
849
+ socket.emit("dialogue:connected", {
850
+ clientId: client.id,
851
+ userId: client.userId
852
+ });
853
+ if (hooks?.clients?.onConnected) {
854
+ Promise.resolve(hooks.clients.onConnected(client)).catch((err) => {
855
+ logger.error({
856
+ message: "Error in onConnected hook",
857
+ atFunction: "setupServer.onConnected",
858
+ data: err
859
+ });
860
+ });
861
+ }
862
+ socket.on("dialogue:join", (data) => {
863
+ if (typeof data?.roomId !== "string") {
864
+ return;
865
+ }
866
+ const room = roomManager.get(data.roomId);
867
+ if (!room) {
868
+ socket.emit("dialogue:error", {
869
+ code: "ROOM_NOT_FOUND",
870
+ message: `Room '${data.roomId}' does not exist`
871
+ });
872
+ return;
873
+ }
874
+ if (hooks?.clients?.beforeJoin) {
875
+ const context = createDialogueContext(
876
+ io,
877
+ connectedClients,
878
+ roomManager
879
+ );
880
+ const joinResult = hooks.clients.beforeJoin({
881
+ context,
882
+ client,
883
+ roomId: data.roomId,
884
+ room
885
+ });
886
+ if (joinResult.isErr) {
887
+ logger.warn({
888
+ message: `Join denied for room '${data.roomId}': ${joinResult.error}`,
889
+ atFunction: "setupServer.beforeJoin",
890
+ data: {
891
+ roomId: data.roomId,
892
+ clientId: client.id,
893
+ reason: joinResult.error
894
+ }
895
+ });
896
+ socket.emit("dialogue:error", {
897
+ code: "JOIN_DENIED",
898
+ message: joinResult.error
899
+ });
900
+ return;
901
+ }
902
+ }
903
+ client.join(data.roomId);
904
+ if (hooks?.clients?.onJoined) {
905
+ Promise.resolve(hooks.clients.onJoined(client, data.roomId)).catch(
906
+ (err) => {
907
+ logger.error({
908
+ message: "Error in onJoined hook",
909
+ atFunction: "setupServer.onJoined",
910
+ data: err
911
+ });
912
+ }
913
+ );
914
+ }
915
+ sendHistoryOnJoin(
916
+ socket,
917
+ data.roomId,
918
+ config.rooms[data.roomId],
919
+ historyManager
920
+ );
921
+ });
922
+ socket.on("dialogue:leave", (data) => {
923
+ if (typeof data?.roomId === "string") {
924
+ client.leave(data.roomId);
925
+ if (hooks?.clients?.onLeft) {
926
+ Promise.resolve(hooks.clients.onLeft(client, data.roomId)).catch(
927
+ (err) => {
928
+ logger.error({
929
+ message: "Error in onLeft hook",
930
+ atFunction: "setupServer.onLeft",
931
+ data: err
932
+ });
933
+ }
934
+ );
935
+ }
936
+ }
937
+ });
938
+ socket.on(
939
+ "dialogue:subscribe",
940
+ (data) => {
941
+ if (typeof data?.roomId === "string" && typeof data?.eventName === "string") {
942
+ client.subscribe(data.roomId, data.eventName);
943
+ }
944
+ }
945
+ );
946
+ socket.on("dialogue:subscribeAll", (data) => {
947
+ if (typeof data?.roomId === "string") {
948
+ client.subscribeAll(data.roomId);
949
+ }
950
+ });
951
+ socket.on(
952
+ "dialogue:unsubscribe",
953
+ (data) => {
954
+ if (typeof data?.roomId === "string" && typeof data?.eventName === "string") {
955
+ client.unsubscribe(data.roomId, data.eventName);
956
+ }
957
+ }
958
+ );
959
+ socket.on(
960
+ "dialogue:getHistory",
961
+ async (data) => {
962
+ if (!historyRateLimiter.isAllowed(socket.id)) {
963
+ socket.emit("dialogue:error", {
964
+ code: "RATE_LIMITED",
965
+ message: "Too many history requests. Please wait before trying again."
966
+ });
967
+ return;
968
+ }
969
+ if (typeof data?.roomId !== "string") {
970
+ socket.emit("dialogue:error", {
971
+ code: "INVALID_REQUEST",
972
+ message: "roomId is required"
973
+ });
974
+ return;
975
+ }
976
+ const room = roomManager.get(data.roomId);
977
+ if (!room) {
978
+ socket.emit("dialogue:error", {
979
+ code: "ROOM_NOT_FOUND",
980
+ message: `Room '${data.roomId}' does not exist`
981
+ });
982
+ return;
983
+ }
984
+ const start = data.start ?? 0;
985
+ const end = data.end ?? 50;
986
+ if (!data.eventName) {
987
+ socket.emit("dialogue:historyResponse", {
988
+ roomId: data.roomId,
989
+ eventName: null,
990
+ events: [],
991
+ start,
992
+ end
993
+ });
994
+ return;
995
+ }
996
+ const events = await room.history(data.eventName, start, end);
997
+ socket.emit("dialogue:historyResponse", {
998
+ roomId: data.roomId,
999
+ eventName: data.eventName,
1000
+ events,
1001
+ start,
1002
+ end
1003
+ });
1004
+ }
1005
+ );
1006
+ socket.on(
1007
+ "dialogue:trigger",
1008
+ (data) => {
1009
+ if (typeof data?.roomId !== "string" || typeof data?.event !== "string") {
1010
+ return;
1011
+ }
1012
+ const room = roomManager.get(data.roomId);
1013
+ if (!room) {
1014
+ socket.emit("dialogue:error", {
1015
+ code: "ROOM_NOT_FOUND",
1016
+ message: `Room '${data.roomId}' does not exist`
1017
+ });
1018
+ return;
1019
+ }
1020
+ const eventDef = room.events.find((e) => e.name === data.event);
1021
+ const hasWildcard = room.events.some((e) => e.name === "*");
1022
+ if (!(eventDef || hasWildcard)) {
1023
+ socket.emit("dialogue:error", {
1024
+ code: "EVENT_NOT_ALLOWED",
1025
+ message: `Event '${data.event}' is not allowed in room '${data.roomId}'`
1026
+ });
1027
+ return;
1028
+ }
1029
+ const triggerEvent = eventDef ?? { name: data.event };
1030
+ const result = room.trigger(
1031
+ triggerEvent,
1032
+ data.data,
1033
+ client.userId,
1034
+ data.meta
1035
+ );
1036
+ if (result.isErr) {
1037
+ socket.emit("dialogue:error", {
1038
+ code: "VALIDATION_FAILED",
1039
+ message: result.error
1040
+ });
1041
+ }
1042
+ }
1043
+ );
1044
+ socket.on("dialogue:listRooms", () => {
1045
+ const rooms = roomManager.all().map((room) => ({
1046
+ id: room.id,
1047
+ name: room.name,
1048
+ description: room.description,
1049
+ size: roomManager.getRoomSize(room.id),
1050
+ maxSize: room.maxSize
1051
+ }));
1052
+ socket.emit("dialogue:rooms", rooms);
1053
+ });
1054
+ socket.on(
1055
+ "dialogue:createRoom",
1056
+ (data) => {
1057
+ if (typeof data?.id !== "string" || typeof data?.name !== "string") {
1058
+ socket.emit("dialogue:error", {
1059
+ code: "INVALID_REQUEST",
1060
+ message: "Room id and name are required"
1061
+ });
1062
+ return;
1063
+ }
1064
+ if (roomManager.get(data.id)) {
1065
+ socket.emit("dialogue:error", {
1066
+ code: "ROOM_EXISTS",
1067
+ message: `Room '${data.id}' already exists`
1068
+ });
1069
+ return;
1070
+ }
1071
+ const room = roomManager.register(data.id, {
1072
+ name: data.name,
1073
+ description: data.description,
1074
+ maxSize: data.maxSize,
1075
+ events: [],
1076
+ createdById: client.userId
1077
+ });
1078
+ socket.emit("dialogue:roomCreated", {
1079
+ id: room.id,
1080
+ name: room.name,
1081
+ description: room.description,
1082
+ maxSize: room.maxSize,
1083
+ createdById: room.createdById
1084
+ });
1085
+ socket.broadcast.emit("dialogue:roomCreated", {
1086
+ id: room.id,
1087
+ name: room.name,
1088
+ description: room.description,
1089
+ maxSize: room.maxSize,
1090
+ createdById: room.createdById
1091
+ });
1092
+ logger.info({
1093
+ message: `Room '${data.id}' created by ${client.userId}`,
1094
+ atFunction: "setupServer.createRoom",
1095
+ data: { roomId: data.id, createdBy: client.userId }
1096
+ });
1097
+ }
1098
+ );
1099
+ socket.on("dialogue:deleteRoom", (data) => {
1100
+ if (typeof data?.roomId !== "string") {
1101
+ return;
1102
+ }
1103
+ const room = roomManager.get(data.roomId);
1104
+ if (!room) {
1105
+ socket.emit("dialogue:error", {
1106
+ code: "ROOM_NOT_FOUND",
1107
+ message: `Room '${data.roomId}' does not exist`
1108
+ });
1109
+ return;
1110
+ }
1111
+ if (room.createdById && room.createdById !== client.userId) {
1112
+ socket.emit("dialogue:error", {
1113
+ code: "PERMISSION_DENIED",
1114
+ message: "Only the room creator can delete this room"
1115
+ });
1116
+ return;
1117
+ }
1118
+ const deleted = roomManager.unregister(data.roomId);
1119
+ if (deleted) {
1120
+ io.emit("dialogue:roomDeleted", { roomId: data.roomId });
1121
+ logger.info({
1122
+ message: `Room '${data.roomId}' deleted by ${client.userId}`,
1123
+ atFunction: "setupServer.deleteRoom",
1124
+ data: { roomId: data.roomId, deletedBy: client.userId }
1125
+ });
1126
+ }
1127
+ });
1128
+ socket.on("disconnect", () => {
1129
+ if (hooks?.clients?.onDisconnected) {
1130
+ Promise.resolve(hooks.clients.onDisconnected(client)).catch((err) => {
1131
+ logger.error({
1132
+ message: "Error in onDisconnected hook",
1133
+ atFunction: "setupServer.onDisconnected",
1134
+ data: err
1135
+ });
1136
+ });
1137
+ }
1138
+ roomManager.removeFromAllRooms(client.id);
1139
+ connectedClients.delete(socket.id);
1140
+ const userSockets2 = userIdToSocketIds.get(client.userId);
1141
+ if (userSockets2) {
1142
+ userSockets2.delete(socket.id);
1143
+ if (userSockets2.size === 0) {
1144
+ userIdToSocketIds.delete(client.userId);
1145
+ }
1146
+ }
1147
+ });
1148
+ });
1149
+ const { websocket } = engine.handler();
1150
+ const port = config.port ?? 3e3;
1151
+ let bunServer = null;
1152
+ return {
1153
+ io,
1154
+ engine,
1155
+ roomManager,
1156
+ start() {
1157
+ bunServer = Bun.serve({
1158
+ port,
1159
+ idleTimeout: 30,
1160
+ async fetch(req, server) {
1161
+ const url = new URL(req.url);
1162
+ if (url.pathname.startsWith("/socket.io")) {
1163
+ if (req.method === "OPTIONS") {
1164
+ return addCorsHeaders(
1165
+ new Response(null, { status: 204 }),
1166
+ req,
1167
+ config.cors
1168
+ );
1169
+ }
1170
+ const response = await engine.handleRequest(req, server);
1171
+ return addCorsHeaders(response, req, config.cors);
1172
+ }
1173
+ return app.fetch(req);
1174
+ },
1175
+ websocket
1176
+ });
1177
+ logger.info({
1178
+ message: `Server running on http://localhost:${port}`,
1179
+ atFunction: "setupServer.start",
1180
+ data: { port }
1181
+ });
1182
+ return Promise.resolve();
1183
+ },
1184
+ stop() {
1185
+ if (bunServer) {
1186
+ bunServer.stop();
1187
+ bunServer = null;
1188
+ }
1189
+ for (const client of connectedClients.values()) {
1190
+ client.disconnect();
1191
+ }
1192
+ connectedClients.clear();
1193
+ userIdToSocketIds.clear();
1194
+ io.close();
1195
+ logger.info({
1196
+ message: "Server stopped",
1197
+ atFunction: "setupServer.stop",
1198
+ data: null
1199
+ });
1200
+ return Promise.resolve();
1201
+ },
1202
+ /**
1203
+ * Gets a connected client by socket ID
1204
+ */
1205
+ getConnectedClient(socketId) {
1206
+ return connectedClients.get(socketId);
1207
+ },
1208
+ /**
1209
+ * Gets all connected clients
1210
+ */
1211
+ getAllConnectedClients() {
1212
+ return Array.from(connectedClients.values());
1213
+ },
1214
+ /**
1215
+ * Gets all connected clients for a specific user ID.
1216
+ * Returns array since a user may have multiple connections.
1217
+ */
1218
+ getClientsByUserId(userId) {
1219
+ const socketIds = userIdToSocketIds.get(userId);
1220
+ if (!socketIds) {
1221
+ return [];
1222
+ }
1223
+ const clients = [];
1224
+ for (const socketId of socketIds) {
1225
+ const client = connectedClients.get(socketId);
1226
+ if (client) {
1227
+ clients.push(client);
1228
+ }
1229
+ }
1230
+ return clients;
1231
+ },
1232
+ /**
1233
+ * Gets all room IDs that a user is currently in.
1234
+ * Aggregates rooms across all connections for this user.
1235
+ */
1236
+ getClientRooms(userId) {
1237
+ const clients = this.getClientsByUserId(userId);
1238
+ const roomSet = /* @__PURE__ */ new Set();
1239
+ for (const client of clients) {
1240
+ for (const roomId of client.rooms()) {
1241
+ roomSet.add(roomId);
1242
+ }
1243
+ }
1244
+ return Array.from(roomSet);
1245
+ },
1246
+ /**
1247
+ * Checks if a user is in a specific room (any of their connections)
1248
+ */
1249
+ isUserInRoom(userId, roomId) {
1250
+ const clients = this.getClientsByUserId(userId);
1251
+ return clients.some((client) => client.rooms().includes(roomId));
1252
+ }
1253
+ };
1254
+ }
1255
+
1256
+ // src/create-dialogue.ts
1257
+ function createDialogue(config) {
1258
+ const app = config.app ?? new Hono();
1259
+ const logger = config.logger ?? createDefaultLogger();
1260
+ const historyManager = createHistoryManager({
1261
+ onCleanup: config.hooks?.events?.onCleanup,
1262
+ onLoad: config.hooks?.events?.onLoad
1263
+ });
1264
+ const {
1265
+ io,
1266
+ roomManager,
1267
+ start,
1268
+ stop,
1269
+ getConnectedClient: _getConnectedClient,
1270
+ getAllConnectedClients,
1271
+ getClientsByUserId,
1272
+ getClientRooms: getClientRoomIds,
1273
+ isUserInRoom
1274
+ } = setupServer(app, config, historyManager);
1275
+ const dialogue = {
1276
+ app,
1277
+ io,
1278
+ trigger(roomId, event, data, from, meta) {
1279
+ const room = roomManager.get(roomId);
1280
+ if (!room) {
1281
+ const errorMsg = `Room '${roomId}' does not exist`;
1282
+ logger.warn({
1283
+ message: errorMsg,
1284
+ atFunction: "dialogue.trigger",
1285
+ data: { roomId, eventName: event.name }
1286
+ });
1287
+ return Err3(errorMsg);
1288
+ }
1289
+ return room.trigger(event, data, from, meta);
1290
+ },
1291
+ on(roomId, event, handler) {
1292
+ const room = roomManager.get(roomId);
1293
+ if (!room) {
1294
+ logger.warn({
1295
+ message: `Room '${roomId}' does not exist`,
1296
+ atFunction: "dialogue.on",
1297
+ data: { roomId, eventName: event.name }
1298
+ });
1299
+ return () => {
1300
+ };
1301
+ }
1302
+ return room.on(event, handler);
1303
+ },
1304
+ room(id) {
1305
+ return roomManager.get(id);
1306
+ },
1307
+ rooms() {
1308
+ return roomManager.all();
1309
+ },
1310
+ createRoom(id, config2) {
1311
+ if (roomManager.get(id)) {
1312
+ logger.warn({
1313
+ message: `Room '${id}' already exists`,
1314
+ atFunction: "dialogue.createRoom",
1315
+ data: { roomId: id }
1316
+ });
1317
+ return null;
1318
+ }
1319
+ const room = roomManager.register(id, config2);
1320
+ io.emit("dialogue:roomCreated", {
1321
+ id: room.id,
1322
+ name: room.name,
1323
+ description: room.description,
1324
+ maxSize: room.maxSize
1325
+ });
1326
+ logger.info({
1327
+ message: `Room '${id}' created`,
1328
+ atFunction: "dialogue.createRoom",
1329
+ data: { roomId: id, roomName: config2.name }
1330
+ });
1331
+ return room;
1332
+ },
1333
+ deleteRoom(id) {
1334
+ const deleted = roomManager.unregister(id);
1335
+ if (deleted) {
1336
+ io.emit("dialogue:roomDeleted", { roomId: id });
1337
+ }
1338
+ return deleted;
1339
+ },
1340
+ getClients(userId) {
1341
+ return getClientsByUserId(userId);
1342
+ },
1343
+ getAllClients() {
1344
+ return getAllConnectedClients();
1345
+ },
1346
+ getClientRooms(userId) {
1347
+ const roomIds = getClientRoomIds(userId);
1348
+ const clients = getClientsByUserId(userId);
1349
+ return {
1350
+ ids: roomIds,
1351
+ forAll(callback) {
1352
+ for (const roomId of roomIds) {
1353
+ callback(roomId);
1354
+ }
1355
+ },
1356
+ leaveAll(callback) {
1357
+ if (callback) {
1358
+ for (const roomId of roomIds) {
1359
+ callback(roomId);
1360
+ }
1361
+ }
1362
+ for (const client of clients) {
1363
+ for (const roomId of client.rooms()) {
1364
+ client.leave(roomId);
1365
+ }
1366
+ }
1367
+ }
1368
+ };
1369
+ },
1370
+ isInRoom(userId, roomId) {
1371
+ return isUserInRoom(userId, roomId);
1372
+ },
1373
+ start,
1374
+ stop
1375
+ };
1376
+ return dialogue;
1377
+ }
1378
+ export {
1379
+ createDefaultLogger,
1380
+ createDialogue,
1381
+ createHistoryManager,
1382
+ createRateLimiter,
1383
+ createSilentLogger,
1384
+ defineEvent,
1385
+ getEventByName,
1386
+ isEventAllowed,
1387
+ validateEventData
1388
+ };
1389
+ //# sourceMappingURL=index.js.map