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