vantiv.io 1.0.0
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/LICENSE +21 -0
- package/README.md +864 -0
- package/index.js +13 -0
- package/package.json +28 -0
- package/src/classes/Actions/Awaiter.js +202 -0
- package/src/classes/Actions/Channel.js +73 -0
- package/src/classes/Actions/Direct.js +263 -0
- package/src/classes/Actions/Inventory.js +156 -0
- package/src/classes/Actions/Music.js +278 -0
- package/src/classes/Actions/Player.js +377 -0
- package/src/classes/Actions/Public.js +66 -0
- package/src/classes/Actions/Room.js +333 -0
- package/src/classes/Actions/Utils.js +29 -0
- package/src/classes/Actions/lib/AudioStreaming.js +447 -0
- package/src/classes/Caches/MovementCache.js +357 -0
- package/src/classes/Handlers/AxiosErrorHandler.js +68 -0
- package/src/classes/Handlers/ErrorHandler.js +65 -0
- package/src/classes/Handlers/EventHandlers.js +259 -0
- package/src/classes/Handlers/WebSocketHandlers.js +54 -0
- package/src/classes/Managers/ChannelManager.js +303 -0
- package/src/classes/Managers/DanceFloorManagers.js +509 -0
- package/src/classes/Managers/Helpers/CleanupManager.js +130 -0
- package/src/classes/Managers/Helpers/LoggerManager.js +171 -0
- package/src/classes/Managers/Helpers/MetricsManager.js +83 -0
- package/src/classes/Managers/Networking/ConnectionManager.js +259 -0
- package/src/classes/Managers/Networking/CooldownManager.js +516 -0
- package/src/classes/Managers/Networking/EventsManager.js +64 -0
- package/src/classes/Managers/Networking/KeepAliveManager.js +109 -0
- package/src/classes/Managers/Networking/MessageHandler.js +110 -0
- package/src/classes/Managers/Networking/Request.js +329 -0
- package/src/classes/Managers/PermissionManager.js +288 -0
- package/src/classes/WebApi/Category/Grab.js +98 -0
- package/src/classes/WebApi/Category/Item.js +347 -0
- package/src/classes/WebApi/Category/Post.js +154 -0
- package/src/classes/WebApi/Category/Room.js +137 -0
- package/src/classes/WebApi/Category/User.js +88 -0
- package/src/classes/WebApi/webapi.js +52 -0
- package/src/constants/TypesConstants.js +89 -0
- package/src/constants/WebSocketConstants.js +80 -0
- package/src/core/Highrise.js +123 -0
- package/src/core/HighriseWebsocket.js +228 -0
- package/src/utils/ConvertSvgToPng.js +51 -0
- package/src/utils/ModelPool.js +160 -0
- package/src/utils/Models.js +128 -0
- package/src/utils/versionCheck.js +27 -0
- package/src/validators/ConfigValidator.js +205 -0
- package/src/validators/ConnectionValidator.js +65 -0
- package/typings/index.d.ts +3820 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
const ModelPool = require("highrise-core/src/utils/ModelPool");
|
|
2
|
+
const { User, Position, Message, AnchorPosition, SessionMetadata, Voice, Direct, HiddenChannel, Tip, RoomModerate } = require('highrise-core/src/utils/Models')
|
|
3
|
+
|
|
4
|
+
const modelPool = new ModelPool()
|
|
5
|
+
|
|
6
|
+
modelPool.registerModel('Tip', (data) => new Tip(data))
|
|
7
|
+
modelPool.registerModel('User', (data) => new User(data));
|
|
8
|
+
modelPool.registerModel('Voice', (data) => new Voice(data))
|
|
9
|
+
modelPool.registerModel('Direct', (data) => new Direct(data));
|
|
10
|
+
modelPool.registerModel('Message', (data) => new Message(data));
|
|
11
|
+
modelPool.registerModel('Position', (data) => new Position(data));
|
|
12
|
+
modelPool.registerModel('RoomModerate', (data) => new RoomModerate(data))
|
|
13
|
+
modelPool.registerModel('HiddenChannel', (data) => new HiddenChannel(data))
|
|
14
|
+
modelPool.registerModel('AnchorPosition', (data) => new AnchorPosition(data));
|
|
15
|
+
modelPool.registerModel('SessionMetadata', (data) => new SessionMetadata(data));
|
|
16
|
+
|
|
17
|
+
function safeEmit(server, eventName, ...args) {
|
|
18
|
+
try {
|
|
19
|
+
server.emit(eventName, ...args);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error(`[PlayerHandlers] Error emitting ${eventName}:`, error);
|
|
22
|
+
server.emit('handlerError', error, eventName);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function releaseModels(modelMap) {
|
|
27
|
+
if (!modelMap) return;
|
|
28
|
+
|
|
29
|
+
for (const [type, models] of Object.entries(modelMap)) {
|
|
30
|
+
if (Array.isArray(models)) {
|
|
31
|
+
modelPool.releaseBatch(type, models);
|
|
32
|
+
} else if (models) {
|
|
33
|
+
modelPool.release(type, models);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function HiddenChannelHandler(server, data) {
|
|
39
|
+
let hiddenChannel = null
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
hiddenChannel = modelPool.get('HiddenChannel', data)
|
|
43
|
+
|
|
44
|
+
if (server._channelManager) {
|
|
45
|
+
server._channelManager.storeIncomingMessage(
|
|
46
|
+
hiddenChannel.sender_id,
|
|
47
|
+
hiddenChannel.message,
|
|
48
|
+
hiddenChannel.tags
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
safeEmit(server, 'Channel', hiddenChannel.sender_id, hiddenChannel.message, hiddenChannel.tags)
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('[HiddenChannelHandler] Error:', error);
|
|
56
|
+
server.emit('error', error);
|
|
57
|
+
} finally {
|
|
58
|
+
if (hiddenChannel) {
|
|
59
|
+
modelPool.release('HiddenChannel', hiddenChannel);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function SessionMetadataHandler(server, data) {
|
|
65
|
+
let sessionMetadata = null;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
server._info.user.id = data.user_id
|
|
69
|
+
server._info.room.owner_id = data.room_info.owner_id
|
|
70
|
+
server._info.room.name = data.room_info.room_name
|
|
71
|
+
|
|
72
|
+
server.logger.info('Highrise', `Bot is now Online in ${data.room_info.room_name}!\n`);
|
|
73
|
+
|
|
74
|
+
sessionMetadata = modelPool.get('SessionMetadata', data);
|
|
75
|
+
safeEmit(server, 'Ready', sessionMetadata);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('[SessionMetadataHandler] Error:', error);
|
|
78
|
+
server.emit('error', error);
|
|
79
|
+
} finally {
|
|
80
|
+
if (sessionMetadata) {
|
|
81
|
+
modelPool.release('SessionMetadata', sessionMetadata);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function UserJoinedHandler(server, data) {
|
|
87
|
+
let user = null, position = null;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
user = modelPool.get('User', data);
|
|
91
|
+
position = modelPool.get('Position', data);
|
|
92
|
+
|
|
93
|
+
server._movementCache._update(user.id, user.username, position);
|
|
94
|
+
safeEmit(server, 'UserJoined', user, position);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('[UserJoinHandler] Error:', error);
|
|
97
|
+
server.emit('error', error);
|
|
98
|
+
} finally {
|
|
99
|
+
releaseModels({
|
|
100
|
+
'User': [user],
|
|
101
|
+
'Position': [position]
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function UserLeftHandler(server, data) {
|
|
107
|
+
let user = null;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
user = modelPool.get('User', data);
|
|
111
|
+
server._movementCache._remove(user.id);
|
|
112
|
+
safeEmit(server, 'UserLeft', user);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error('[UserLeftHandler] Error:', error);
|
|
115
|
+
server.emit('error', error);
|
|
116
|
+
} finally {
|
|
117
|
+
if (user) {
|
|
118
|
+
modelPool.release('User', user);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function MessageEventHandler(server, data) {
|
|
124
|
+
let message = null;
|
|
125
|
+
if (data.user?.id === server._info?.user?.id) return;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
message = modelPool.get('Message', data);
|
|
129
|
+
const eventName = data.whisper ? 'Whisper' : 'Chat';
|
|
130
|
+
safeEmit(server, eventName, message.user, message.message);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error('[MessageEventHandler] Error:', error);
|
|
133
|
+
server.emit('error', error);
|
|
134
|
+
} finally {
|
|
135
|
+
if (message) {
|
|
136
|
+
modelPool.release('Message', message);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function VoiceEventHandler(server, data) {
|
|
142
|
+
let voice = null
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
voice = modelPool.get('Voice', data)
|
|
146
|
+
if (voice.users.length === 0) voice.ended = true
|
|
147
|
+
|
|
148
|
+
safeEmit(server, 'Voice', voice.users, voice.seconds_left, voice.ended)
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error('[VoiceEventHandler] Error:', error);
|
|
151
|
+
server.emit('error', error);
|
|
152
|
+
} finally {
|
|
153
|
+
modelPool.release('Voice', voice);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function DirectEventHandler(server, data) {
|
|
158
|
+
let direct = null
|
|
159
|
+
|
|
160
|
+
if (data.user_id === server._info?.user?.id) return;
|
|
161
|
+
|
|
162
|
+
const response = await server._direct.messages.get(data.conversation_id)
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
if (!response || !Array.isArray(response) || response.length === 0) {
|
|
166
|
+
server.logger.warn('DirectEventHandler', 'No messages in response', {
|
|
167
|
+
conversationId: data.conversation_id
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const message = response[0].content || '';
|
|
173
|
+
data.message = message;
|
|
174
|
+
|
|
175
|
+
direct = modelPool.get('Direct', data)
|
|
176
|
+
server.await._processEvent('direct',
|
|
177
|
+
{ id: data.user_id, username: null },
|
|
178
|
+
data.message,
|
|
179
|
+
{ id: data.conversation_id, is_new_conversation: data.is_new_conversation }
|
|
180
|
+
);
|
|
181
|
+
safeEmit(server, 'Direct', direct.user, direct.message, direct.conversation);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
console.error('[DirectEventHandler] Error:', error);
|
|
184
|
+
server.emit('error', error);
|
|
185
|
+
} finally {
|
|
186
|
+
modelPool.release('Direct', direct);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function UserMovementHandler(server, data) {
|
|
191
|
+
let user = null, position = null, anchor = null;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
user = modelPool.get('User', data);
|
|
195
|
+
position = modelPool.get('Position', data);
|
|
196
|
+
|
|
197
|
+
const isAnchor = data.position?.entity_id;
|
|
198
|
+
if (isAnchor) {
|
|
199
|
+
anchor = modelPool.get('AnchorPosition', data);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const changed = server._movementCache._update(user.id, user.username, position, anchor);
|
|
203
|
+
|
|
204
|
+
if (changed) {
|
|
205
|
+
safeEmit(server, 'Movement', user, position, anchor);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error('[UserMovementHandler] Error:', error);
|
|
210
|
+
server.emit('error', error);
|
|
211
|
+
} finally {
|
|
212
|
+
releaseModels({
|
|
213
|
+
'User': [user],
|
|
214
|
+
'Position': [position],
|
|
215
|
+
'AnchorPosition': [anchor]
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function TipReactionHandler(server, data) {
|
|
221
|
+
let tip = null
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
tip = modelPool.get('Tip', data)
|
|
225
|
+
safeEmit(server, 'Tip', tip.sender, tip.receiver, tip.item)
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error('[TipReactionHandler] Error:', error);
|
|
228
|
+
server.emit('error', error);
|
|
229
|
+
} finally {
|
|
230
|
+
modelPool.release('Tip', tip);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function RoomModerateHandler(server, data) {
|
|
235
|
+
let roomModerate = null
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
roomModerate = modelPool.get('RoomModerate', data)
|
|
239
|
+
safeEmit(server, 'Moderation', roomModerate.moderator, roomModerate.target, roomModerate.action)
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error('[RoomModerateHandler] Error:', error);
|
|
242
|
+
server.emit('error', error);
|
|
243
|
+
} finally {
|
|
244
|
+
modelPool.release('RoomModerate', roomModerate);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
module.exports = {
|
|
249
|
+
UserLeftHandler,
|
|
250
|
+
VoiceEventHandler,
|
|
251
|
+
UserJoinedHandler,
|
|
252
|
+
TipReactionHandler,
|
|
253
|
+
DirectEventHandler,
|
|
254
|
+
MessageEventHandler,
|
|
255
|
+
UserMovementHandler,
|
|
256
|
+
RoomModerateHandler,
|
|
257
|
+
HiddenChannelHandler,
|
|
258
|
+
SessionMetadataHandler,
|
|
259
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const WebSocketConstants = require('highrise-core/src/constants/WebSocketConstants');
|
|
2
|
+
|
|
3
|
+
class WebSocketHandlers {
|
|
4
|
+
constructor(server) {
|
|
5
|
+
this.server = server;
|
|
6
|
+
this.logger = server.logger;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
setupErrorHandlers() {
|
|
10
|
+
const handlers = {
|
|
11
|
+
uncaughtException: this._handleUncaughtException.bind(this),
|
|
12
|
+
unhandledRejection: this._handleUnhandledRejection.bind(this),
|
|
13
|
+
SIGINT: this._handleSIGINT.bind(this),
|
|
14
|
+
SIGTERM: this._handleSIGTERM.bind(this)
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
process.on('uncaughtException', handlers.uncaughtException);
|
|
18
|
+
process.on('unhandledRejection', handlers.unhandledRejection);
|
|
19
|
+
process.on('SIGINT', handlers.SIGINT);
|
|
20
|
+
process.on('SIGTERM', handlers.SIGTERM);
|
|
21
|
+
|
|
22
|
+
return handlers;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_handleUncaughtException(error) {
|
|
26
|
+
this.logger.error('WebSocketHandlers', `Uncaught exception: ${error.message}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_handleUnhandledRejection(reason, promise) {
|
|
30
|
+
this.logger.error('WebSocketHandlers', `Unhandled promise rejection: ${reason}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_handleSIGINT() {
|
|
34
|
+
this.logger.info('WebSocketHandlers', 'Received SIGINT, shutting down gracefully');
|
|
35
|
+
this.server.disconnect();
|
|
36
|
+
setTimeout(() => process.exit(0), 1000);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_handleSIGTERM() {
|
|
40
|
+
this.logger.info('WebSocketHandlers', 'Received SIGTERM, shutting down gracefully');
|
|
41
|
+
this.server.disconnect();
|
|
42
|
+
setTimeout(() => process.exit(0), 1000);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
removeErrorHandlers(handlers) {
|
|
46
|
+
if (!handlers) return;
|
|
47
|
+
|
|
48
|
+
Object.entries(handlers).forEach(([event, handler]) => {
|
|
49
|
+
process.removeListener(event, handler);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = WebSocketHandlers;
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
class ChannelManager {
|
|
4
|
+
constructor(server) {
|
|
5
|
+
this.server = server;
|
|
6
|
+
this.logger = server.logger;
|
|
7
|
+
|
|
8
|
+
this._messageHistory = [];
|
|
9
|
+
this._maxHistory = 500;
|
|
10
|
+
this._listeners = new Map();
|
|
11
|
+
this._tagIndex = new Map();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async send(message, tags = []) {
|
|
15
|
+
const method = 'bot.channel.send';
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
if (typeof message !== 'string' || message.length === 0) {
|
|
19
|
+
throw new TypeError('Message must be a non-empty string');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!Array.isArray(tags)) {
|
|
23
|
+
throw new TypeError('Tags must be an array');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const normalizedTags = this._normalizeTags(tags);
|
|
27
|
+
|
|
28
|
+
const payload = {
|
|
29
|
+
_type: 'ChannelRequest',
|
|
30
|
+
message: message,
|
|
31
|
+
tags: normalizedTags,
|
|
32
|
+
rid: crypto.randomUUID()
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const response = await this.server._fireAndForget.send(payload);
|
|
36
|
+
|
|
37
|
+
if (response.success) {
|
|
38
|
+
this._storeMessage({
|
|
39
|
+
id: crypto.randomUUID(),
|
|
40
|
+
message,
|
|
41
|
+
tags: normalizedTags,
|
|
42
|
+
timestamp: Date.now(),
|
|
43
|
+
senderId: this.server.info.user?.id || 'unknown',
|
|
44
|
+
sent: true
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
this.logger.debug(method, 'Channel message sent', {
|
|
48
|
+
tags: normalizedTags,
|
|
49
|
+
messageLength: message.length
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return false;
|
|
56
|
+
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error instanceof TypeError) {
|
|
59
|
+
this.logger.error(method, `TypeError: ${error.message}`, { message, tags }, error);
|
|
60
|
+
} else {
|
|
61
|
+
this.logger.error(method, `Failed to send channel message: ${error.message}`,
|
|
62
|
+
{ message, tags }, error);
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
on(tags, callback, options = {}) {
|
|
69
|
+
const method = 'bot.channel.on';
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
if (!callback || typeof callback !== 'function') {
|
|
73
|
+
throw new TypeError('Callback must be a function');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const normalizedTags = this._normalizeTags(Array.isArray(tags) ? tags : [tags]);
|
|
77
|
+
|
|
78
|
+
const listenerId = crypto.randomUUID();
|
|
79
|
+
const listener = {
|
|
80
|
+
id: listenerId,
|
|
81
|
+
callback,
|
|
82
|
+
tags: normalizedTags,
|
|
83
|
+
options: {
|
|
84
|
+
once: options.once || false,
|
|
85
|
+
...options
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
this._listeners.set(listenerId, listener);
|
|
90
|
+
|
|
91
|
+
normalizedTags.forEach(tag => {
|
|
92
|
+
if (!this._tagIndex.has(tag)) {
|
|
93
|
+
this._tagIndex.set(tag, new Set());
|
|
94
|
+
}
|
|
95
|
+
this._tagIndex.get(tag).add(listenerId);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
this.logger.debug(method, 'Listener registered', {
|
|
99
|
+
listenerId,
|
|
100
|
+
tags: normalizedTags
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return listenerId;
|
|
104
|
+
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error instanceof TypeError) {
|
|
107
|
+
this.logger.error(method, `TypeError: ${error.message}`, { tags }, error);
|
|
108
|
+
} else {
|
|
109
|
+
this.logger.error(method, `Failed to register listener: ${error.message}`, { tags }, error);
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
once(tags, callback) {
|
|
116
|
+
return this.on(tags, callback, { once: true });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
off(listenerId) {
|
|
120
|
+
const method = 'bot.channel.off';
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const listener = this._listeners.get(listenerId);
|
|
124
|
+
if (!listener) {
|
|
125
|
+
this.logger.warn(method, 'Listener not found', { listenerId });
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
listener.tags.forEach(tag => {
|
|
130
|
+
const set = this._tagIndex.get(tag);
|
|
131
|
+
if (set) {
|
|
132
|
+
set.delete(listenerId);
|
|
133
|
+
if (set.size === 0) {
|
|
134
|
+
this._tagIndex.delete(tag);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
this._listeners.delete(listenerId);
|
|
140
|
+
|
|
141
|
+
this.logger.debug(method, 'Listener removed', { listenerId });
|
|
142
|
+
return true;
|
|
143
|
+
|
|
144
|
+
} catch (error) {
|
|
145
|
+
this.logger.error(method, `Failed to remove listener: ${error.message}`, { listenerId }, error);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
offAll(tags) {
|
|
151
|
+
const normalizedTags = this._normalizeTags(Array.isArray(tags) ? tags : [tags]);
|
|
152
|
+
|
|
153
|
+
const listenersToRemove = new Set();
|
|
154
|
+
|
|
155
|
+
normalizedTags.forEach(tag => {
|
|
156
|
+
const listeners = this._tagIndex.get(tag);
|
|
157
|
+
if (listeners) {
|
|
158
|
+
listeners.forEach(listenerId => listenersToRemove.add(listenerId));
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
let removed = 0;
|
|
163
|
+
listenersToRemove.forEach(listenerId => {
|
|
164
|
+
if (this.off(listenerId)) {
|
|
165
|
+
removed++;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return removed;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
query(filter = {}) {
|
|
173
|
+
const {
|
|
174
|
+
tags = [],
|
|
175
|
+
since = 0,
|
|
176
|
+
until = Date.now(),
|
|
177
|
+
limit = 50
|
|
178
|
+
} = filter;
|
|
179
|
+
|
|
180
|
+
const normalizedTags = this._normalizeTags(Array.isArray(tags) ? tags : [tags]);
|
|
181
|
+
|
|
182
|
+
const results = [];
|
|
183
|
+
|
|
184
|
+
for (const msg of this._messageHistory) {
|
|
185
|
+
if (this._matchesFilter(msg, { tags: normalizedTags, since, until })) {
|
|
186
|
+
results.push(msg);
|
|
187
|
+
if (results.length >= limit) break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return results;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
getStats() {
|
|
195
|
+
return {
|
|
196
|
+
totalMessages: this._messageHistory.length,
|
|
197
|
+
listeners: this._listeners.size
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
storeIncomingMessage(senderId, message, tags) {
|
|
202
|
+
try {
|
|
203
|
+
const normalizedTags = this._normalizeTags(tags);
|
|
204
|
+
|
|
205
|
+
const storedMsg = {
|
|
206
|
+
id: crypto.randomUUID(),
|
|
207
|
+
message,
|
|
208
|
+
tags: normalizedTags,
|
|
209
|
+
timestamp: Date.now(),
|
|
210
|
+
senderId,
|
|
211
|
+
sent: false
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
this._storeMessage(storedMsg);
|
|
215
|
+
|
|
216
|
+
this._triggerListeners(storedMsg);
|
|
217
|
+
|
|
218
|
+
} catch (error) {
|
|
219
|
+
this.logger.error('ChannelManager', 'Failed to store incoming message', { error: error.message });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
_triggerListeners(message) {
|
|
224
|
+
const matchedListeners = new Map();
|
|
225
|
+
|
|
226
|
+
message.tags.forEach(tag => {
|
|
227
|
+
const listeners = this._tagIndex.get(tag);
|
|
228
|
+
if (listeners) {
|
|
229
|
+
listeners.forEach(listenerId => {
|
|
230
|
+
const listener = this._listeners.get(listenerId);
|
|
231
|
+
if (listener && !matchedListeners.has(listenerId)) {
|
|
232
|
+
const hasAllTags = listener.tags.every(t => message.tags.includes(t));
|
|
233
|
+
if (hasAllTags) {
|
|
234
|
+
matchedListeners.set(listenerId, listener);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
matchedListeners.forEach((listener, listenerId) => {
|
|
242
|
+
try {
|
|
243
|
+
listener.callback(message);
|
|
244
|
+
|
|
245
|
+
if (listener.options.once) {
|
|
246
|
+
this.off(listenerId);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
} catch (error) {
|
|
250
|
+
this.logger.error('ChannelManager', 'Listener callback error', { listenerId, error: error.message });
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_normalizeTags(tags) {
|
|
256
|
+
const normalized = tags
|
|
257
|
+
.map(tag => {
|
|
258
|
+
if (typeof tag !== 'string') return '';
|
|
259
|
+
return tag.toLowerCase()
|
|
260
|
+
.trim()
|
|
261
|
+
.replace(/[^a-z0-9_:.-]/g, '-')
|
|
262
|
+
.replace(/-+/g, '-')
|
|
263
|
+
.substring(0, 50);
|
|
264
|
+
})
|
|
265
|
+
.filter(tag => tag.length > 0)
|
|
266
|
+
.filter((tag, index, self) => self.indexOf(tag) === index)
|
|
267
|
+
.sort();
|
|
268
|
+
|
|
269
|
+
return normalized.length > 0 ? normalized : ['global'];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
_storeMessage(message) {
|
|
273
|
+
this._messageHistory.unshift(message);
|
|
274
|
+
|
|
275
|
+
if (this._messageHistory.length > this._maxHistory) {
|
|
276
|
+
this._messageHistory.splice(this._maxHistory);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
_matchesFilter(message, filter) {
|
|
281
|
+
const { tags = [], since = 0, until = Date.now() } = filter;
|
|
282
|
+
|
|
283
|
+
if (message.timestamp < since || message.timestamp > until) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (tags.length > 0) {
|
|
288
|
+
const hasAllTags = tags.every(tag => message.tags.includes(tag));
|
|
289
|
+
if (!hasAllTags) return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
clear() {
|
|
296
|
+
this._messageHistory = [];
|
|
297
|
+
this._listeners.clear();
|
|
298
|
+
this._tagIndex.clear();
|
|
299
|
+
this.logger.info('ChannelManager', 'Cleared channel history and listeners');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
module.exports = ChannelManager;
|