@gakr-gakr/qqbot 0.1.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.
Files changed (149) hide show
  1. package/api.ts +56 -0
  2. package/autobot.plugin.json +167 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/index.ts +33 -0
  5. package/package.json +64 -0
  6. package/runtime-api.ts +9 -0
  7. package/secret-contract-api.ts +5 -0
  8. package/setup-entry.ts +13 -0
  9. package/setup-plugin-api.ts +3 -0
  10. package/skills/qqbot-channel/SKILL.md +262 -0
  11. package/skills/qqbot-channel/references/api_references.md +521 -0
  12. package/skills/qqbot-media/SKILL.md +37 -0
  13. package/skills/qqbot-remind/SKILL.md +153 -0
  14. package/src/bridge/approval/capability.ts +225 -0
  15. package/src/bridge/approval/handler-runtime.ts +204 -0
  16. package/src/bridge/bootstrap.ts +135 -0
  17. package/src/bridge/channel-entry.ts +18 -0
  18. package/src/bridge/commands/framework-context-adapter.ts +60 -0
  19. package/src/bridge/commands/framework-registration.ts +66 -0
  20. package/src/bridge/commands/from-parser.ts +60 -0
  21. package/src/bridge/commands/result-dispatcher.ts +76 -0
  22. package/src/bridge/config-shared.ts +132 -0
  23. package/src/bridge/config.ts +176 -0
  24. package/src/bridge/gateway.ts +178 -0
  25. package/src/bridge/logger.ts +31 -0
  26. package/src/bridge/narrowing.ts +31 -0
  27. package/src/bridge/plugin-version.ts +102 -0
  28. package/src/bridge/runtime.ts +25 -0
  29. package/src/bridge/sdk-adapter.ts +164 -0
  30. package/src/bridge/setup/finalize.ts +144 -0
  31. package/src/bridge/setup/surface.ts +34 -0
  32. package/src/bridge/tools/channel.ts +58 -0
  33. package/src/bridge/tools/index.ts +15 -0
  34. package/src/bridge/tools/remind.ts +91 -0
  35. package/src/channel.setup.ts +33 -0
  36. package/src/channel.ts +399 -0
  37. package/src/config-schema.ts +84 -0
  38. package/src/engine/access/index.ts +2 -0
  39. package/src/engine/access/resolve-policy.ts +30 -0
  40. package/src/engine/access/sender-match.ts +55 -0
  41. package/src/engine/access/types.ts +2 -0
  42. package/src/engine/adapter/audio.port.ts +27 -0
  43. package/src/engine/adapter/commands.port.ts +22 -0
  44. package/src/engine/adapter/history.port.ts +52 -0
  45. package/src/engine/adapter/index.ts +76 -0
  46. package/src/engine/adapter/mention-gate.port.ts +50 -0
  47. package/src/engine/adapter/types.ts +38 -0
  48. package/src/engine/api/api-client.ts +212 -0
  49. package/src/engine/api/media-chunked.ts +644 -0
  50. package/src/engine/api/media.ts +218 -0
  51. package/src/engine/api/messages.ts +293 -0
  52. package/src/engine/api/retry.ts +217 -0
  53. package/src/engine/api/routes.ts +95 -0
  54. package/src/engine/api/token.ts +277 -0
  55. package/src/engine/approval/index.ts +224 -0
  56. package/src/engine/commands/builtin/log-helpers.ts +341 -0
  57. package/src/engine/commands/builtin/register-all.ts +17 -0
  58. package/src/engine/commands/builtin/register-approve.ts +201 -0
  59. package/src/engine/commands/builtin/register-basic.ts +95 -0
  60. package/src/engine/commands/builtin/register-clear-storage.ts +187 -0
  61. package/src/engine/commands/builtin/register-logs.ts +20 -0
  62. package/src/engine/commands/builtin/register-streaming.ts +138 -0
  63. package/src/engine/commands/builtin/state.ts +31 -0
  64. package/src/engine/commands/slash-command-auth.ts +88 -0
  65. package/src/engine/commands/slash-command-handler.ts +168 -0
  66. package/src/engine/commands/slash-command-test-support.ts +39 -0
  67. package/src/engine/commands/slash-commands-impl.ts +61 -0
  68. package/src/engine/commands/slash-commands.ts +202 -0
  69. package/src/engine/config/credential-backup.ts +108 -0
  70. package/src/engine/config/credentials.ts +76 -0
  71. package/src/engine/config/group.ts +227 -0
  72. package/src/engine/config/resolve.ts +283 -0
  73. package/src/engine/config/setup-logic.ts +84 -0
  74. package/src/engine/gateway/active-cfg.ts +52 -0
  75. package/src/engine/gateway/codec.ts +47 -0
  76. package/src/engine/gateway/constants.ts +117 -0
  77. package/src/engine/gateway/event-dispatcher.ts +177 -0
  78. package/src/engine/gateway/gateway-connection.ts +356 -0
  79. package/src/engine/gateway/gateway.ts +267 -0
  80. package/src/engine/gateway/inbound-attachments.ts +360 -0
  81. package/src/engine/gateway/inbound-context.ts +82 -0
  82. package/src/engine/gateway/inbound-pipeline.ts +171 -0
  83. package/src/engine/gateway/interaction-handler.ts +345 -0
  84. package/src/engine/gateway/message-queue.ts +404 -0
  85. package/src/engine/gateway/outbound-dispatch.ts +590 -0
  86. package/src/engine/gateway/reconnect.ts +199 -0
  87. package/src/engine/gateway/stages/access-stage.ts +99 -0
  88. package/src/engine/gateway/stages/assembly-stage.ts +156 -0
  89. package/src/engine/gateway/stages/content-stage.ts +77 -0
  90. package/src/engine/gateway/stages/envelope-stage.ts +144 -0
  91. package/src/engine/gateway/stages/group-gate-stage.ts +223 -0
  92. package/src/engine/gateway/stages/index.ts +18 -0
  93. package/src/engine/gateway/stages/quote-stage.ts +113 -0
  94. package/src/engine/gateway/stages/refidx-stage.ts +62 -0
  95. package/src/engine/gateway/stages/stub-contexts.ts +77 -0
  96. package/src/engine/gateway/types.ts +230 -0
  97. package/src/engine/gateway/typing-keepalive.ts +102 -0
  98. package/src/engine/gateway/ws-client.ts +16 -0
  99. package/src/engine/group/activation.ts +88 -0
  100. package/src/engine/group/history.ts +321 -0
  101. package/src/engine/group/mention.ts +114 -0
  102. package/src/engine/group/message-gating.ts +108 -0
  103. package/src/engine/messaging/decode-media-path.ts +82 -0
  104. package/src/engine/messaging/media-source.ts +210 -0
  105. package/src/engine/messaging/media-type-detect.ts +27 -0
  106. package/src/engine/messaging/outbound-audio-port.ts +38 -0
  107. package/src/engine/messaging/outbound-deliver.ts +810 -0
  108. package/src/engine/messaging/outbound-media-send.ts +658 -0
  109. package/src/engine/messaging/outbound-reply.ts +27 -0
  110. package/src/engine/messaging/outbound-result-helpers.ts +54 -0
  111. package/src/engine/messaging/outbound-types.ts +47 -0
  112. package/src/engine/messaging/outbound.ts +485 -0
  113. package/src/engine/messaging/reply-dispatcher.ts +597 -0
  114. package/src/engine/messaging/reply-limiter.ts +164 -0
  115. package/src/engine/messaging/sender.ts +741 -0
  116. package/src/engine/messaging/streaming-c2c.ts +1192 -0
  117. package/src/engine/messaging/streaming-media-send.ts +544 -0
  118. package/src/engine/messaging/target-parser.ts +104 -0
  119. package/src/engine/ref/format-message-ref.ts +142 -0
  120. package/src/engine/ref/format-ref-entry.ts +27 -0
  121. package/src/engine/ref/store.ts +211 -0
  122. package/src/engine/ref/types.ts +27 -0
  123. package/src/engine/session/known-users.ts +138 -0
  124. package/src/engine/session/session-store.ts +207 -0
  125. package/src/engine/tools/channel-api.ts +244 -0
  126. package/src/engine/tools/remind-logic.ts +377 -0
  127. package/src/engine/types.ts +313 -0
  128. package/src/engine/utils/attachment-tags.ts +174 -0
  129. package/src/engine/utils/audio.ts +525 -0
  130. package/src/engine/utils/data-paths.ts +38 -0
  131. package/src/engine/utils/diagnostics.ts +93 -0
  132. package/src/engine/utils/file-utils.ts +215 -0
  133. package/src/engine/utils/format.ts +70 -0
  134. package/src/engine/utils/image-size.ts +249 -0
  135. package/src/engine/utils/log.ts +77 -0
  136. package/src/engine/utils/media-tags.ts +177 -0
  137. package/src/engine/utils/payload.ts +157 -0
  138. package/src/engine/utils/platform.ts +265 -0
  139. package/src/engine/utils/request-context.ts +60 -0
  140. package/src/engine/utils/string-normalize.ts +91 -0
  141. package/src/engine/utils/stt.ts +103 -0
  142. package/src/engine/utils/text-parsing.ts +155 -0
  143. package/src/engine/utils/upload-cache.ts +96 -0
  144. package/src/engine/utils/voice-text.ts +15 -0
  145. package/src/exec-approvals.ts +237 -0
  146. package/src/qqbot-test-support.ts +29 -0
  147. package/src/secret-contract.ts +82 -0
  148. package/src/types.ts +210 -0
  149. package/tsconfig.json +16 -0
@@ -0,0 +1,404 @@
1
+ import { formatErrorMessage } from "../utils/format.js";
2
+
3
+ const DEFAULT_GLOBAL_QUEUE_SIZE = 1000;
4
+ const DEFAULT_PER_PEER_QUEUE_SIZE = 20;
5
+ const DEFAULT_GROUP_QUEUE_SIZE = 50;
6
+ const DEFAULT_MAX_CONCURRENT_USERS = 10;
7
+
8
+ export interface QueuedMention {
9
+ scope?: "all" | "single";
10
+ id?: string;
11
+ user_openid?: string;
12
+ member_openid?: string;
13
+ username?: string;
14
+ nickname?: string;
15
+ bot?: boolean;
16
+ is_you?: boolean;
17
+ }
18
+
19
+ interface QueuedMergeInfo {
20
+ count: number;
21
+ messages: readonly QueuedMessage[];
22
+ }
23
+
24
+ export interface QueuedMessage {
25
+ type: "c2c" | "guild" | "dm" | "group";
26
+ senderId: string;
27
+ senderName?: string;
28
+ senderIsBot?: boolean;
29
+ content: string;
30
+ messageId: string;
31
+ timestamp: string;
32
+ channelId?: string;
33
+ guildId?: string;
34
+ groupOpenid?: string;
35
+ attachments?: Array<{
36
+ content_type: string;
37
+ url: string;
38
+ filename?: string;
39
+ voice_wav_url?: string;
40
+ asr_refer_text?: string;
41
+ }>;
42
+ refMsgIdx?: string;
43
+ msgIdx?: string;
44
+ msgType?: number;
45
+ msgElements?: Array<{
46
+ msg_idx?: string;
47
+ content?: string;
48
+ attachments?: Array<{
49
+ content_type: string;
50
+ url: string;
51
+ filename?: string;
52
+ height?: number;
53
+ width?: number;
54
+ size?: number;
55
+ voice_wav_url?: string;
56
+ asr_refer_text?: string;
57
+ }>;
58
+ }>;
59
+ eventType?: string;
60
+ mentions?: QueuedMention[];
61
+ messageScene?: { source?: string; ext?: string[] };
62
+ merge?: QueuedMergeInfo;
63
+ }
64
+
65
+ export function isMergedTurn(msg: QueuedMessage): msg is QueuedMessage & {
66
+ merge: QueuedMergeInfo;
67
+ } {
68
+ return (msg.merge?.count ?? 0) > 1;
69
+ }
70
+
71
+ interface MessageQueueContext {
72
+ accountId: string;
73
+ log?: {
74
+ info: (msg: string, meta?: Record<string, unknown>) => void;
75
+ error: (msg: string, meta?: Record<string, unknown>) => void;
76
+ debug?: (msg: string, meta?: Record<string, unknown>) => void;
77
+ };
78
+ isAborted: () => boolean;
79
+ groupQueueSize?: number;
80
+ peerQueueSize?: number;
81
+ globalQueueSize?: number;
82
+ maxConcurrentUsers?: number;
83
+ }
84
+
85
+ interface QueueSnapshot {
86
+ totalPending: number;
87
+ activeUsers: number;
88
+ maxConcurrentUsers: number;
89
+ senderPending: number;
90
+ }
91
+
92
+ interface MessageQueue {
93
+ enqueue: (msg: QueuedMessage) => void;
94
+ startProcessor: (handleMessageFn: (msg: QueuedMessage) => Promise<void>) => void;
95
+ getSnapshot: (senderPeerId: string) => QueueSnapshot;
96
+ getMessagePeerId: (msg: QueuedMessage) => string;
97
+ clearUserQueue: (peerId: string) => number;
98
+ executeImmediate: (msg: QueuedMessage) => void;
99
+ }
100
+
101
+ function isGroupPeer(peerId: string): boolean {
102
+ return peerId.startsWith("group:") || peerId.startsWith("guild:");
103
+ }
104
+
105
+ function isSlashCommand(msg: QueuedMessage): boolean {
106
+ return (msg.content ?? "").trim().startsWith("/");
107
+ }
108
+
109
+ /**
110
+ * Merge several queued group messages into one representative message.
111
+ *
112
+ * Merge semantics:
113
+ * - `content` is joined with newlines; each line prefixed with `[sender]`
114
+ * so the downstream formatter can attribute speakers.
115
+ * - `attachments` is concatenated.
116
+ * - `mentions` is deduplicated by member/user openid; if *any* source
117
+ * message was a `GROUP_AT_MESSAGE_CREATE`, the merged result inherits
118
+ * that eventType (the merged turn effectively @-s the bot).
119
+ * - `messageId`, `msgIdx`, `timestamp` come from the last message — the
120
+ * most recent identity is what the outbound reply should quote.
121
+ * - `refMsgIdx` (the message that the user quoted) comes from the FIRST
122
+ * message in the batch because the first quote anchors the topic.
123
+ * - `senderIsBot` is true only when every source message was authored
124
+ * by a bot. Any human participation flips the flag.
125
+ *
126
+ * A single-message batch is returned unchanged (no merge overhead).
127
+ */
128
+ export function mergeGroupMessages(batch: QueuedMessage[]): QueuedMessage {
129
+ if (batch.length === 0) {
130
+ throw new Error("mergeGroupMessages: empty batch");
131
+ }
132
+ if (batch.length === 1) {
133
+ return batch[0];
134
+ }
135
+
136
+ const first = batch[0];
137
+ const last = batch[batch.length - 1];
138
+
139
+ const mergedContent = batch
140
+ .map((m) => `[${m.senderName ?? m.senderId}]: ${m.content}`)
141
+ .join("\n");
142
+
143
+ const mergedAttachments: QueuedMessage["attachments"] = [];
144
+ for (const m of batch) {
145
+ if (m.attachments?.length) {
146
+ mergedAttachments.push(...m.attachments);
147
+ }
148
+ }
149
+
150
+ const seenMentionIds = new Set<string>();
151
+ const mergedMentions: NonNullable<QueuedMessage["mentions"]> = [];
152
+ let anyAtYouEvent = false;
153
+ for (const m of batch) {
154
+ if (m.eventType === "GROUP_AT_MESSAGE_CREATE") {
155
+ anyAtYouEvent = true;
156
+ }
157
+ if (m.mentions) {
158
+ for (const mt of m.mentions) {
159
+ const key = mt.member_openid ?? mt.id ?? mt.user_openid ?? "";
160
+ if (key && seenMentionIds.has(key)) {
161
+ continue;
162
+ }
163
+ if (key) {
164
+ seenMentionIds.add(key);
165
+ }
166
+ mergedMentions.push(mt);
167
+ }
168
+ }
169
+ }
170
+
171
+ const allFromBot = batch.every((m) => m.senderIsBot);
172
+
173
+ return {
174
+ type: last.type,
175
+ senderId: last.senderId,
176
+ senderName: last.senderName,
177
+ senderIsBot: allFromBot,
178
+ content: mergedContent,
179
+ messageId: last.messageId,
180
+ timestamp: last.timestamp,
181
+ channelId: last.channelId,
182
+ guildId: last.guildId,
183
+ groupOpenid: last.groupOpenid,
184
+ attachments: mergedAttachments.length > 0 ? mergedAttachments : undefined,
185
+ refMsgIdx: first.refMsgIdx,
186
+ msgIdx: last.msgIdx,
187
+ eventType: anyAtYouEvent ? "GROUP_AT_MESSAGE_CREATE" : last.eventType,
188
+ mentions: mergedMentions.length > 0 ? mergedMentions : undefined,
189
+ messageScene: last.messageScene,
190
+ merge: { count: batch.length, messages: batch },
191
+ };
192
+ }
193
+
194
+ export function createMessageQueue(ctx: MessageQueueContext): MessageQueue {
195
+ const { accountId: _accountId, log } = ctx;
196
+ const globalQueueSize = ctx.globalQueueSize ?? DEFAULT_GLOBAL_QUEUE_SIZE;
197
+ const peerQueueSize = ctx.peerQueueSize ?? DEFAULT_PER_PEER_QUEUE_SIZE;
198
+ const groupQueueSize = ctx.groupQueueSize ?? DEFAULT_GROUP_QUEUE_SIZE;
199
+ const maxConcurrentUsers = ctx.maxConcurrentUsers ?? DEFAULT_MAX_CONCURRENT_USERS;
200
+
201
+ const userQueues = new Map<string, QueuedMessage[]>();
202
+ const activeUsers = new Set<string>();
203
+ let handleMessageFnRef: ((msg: QueuedMessage) => Promise<void>) | null = null;
204
+ let totalEnqueued = 0;
205
+
206
+ const getMessagePeerId = (msg: QueuedMessage): string => {
207
+ if (msg.type === "guild") {
208
+ return `guild:${msg.channelId ?? "unknown"}`;
209
+ }
210
+ if (msg.type === "group") {
211
+ return `group:${msg.groupOpenid ?? "unknown"}`;
212
+ }
213
+ return `dm:${msg.senderId}`;
214
+ };
215
+
216
+ const evictOne = (queue: QueuedMessage[], isGroup: boolean): QueuedMessage | undefined => {
217
+ if (isGroup) {
218
+ const botIdx = queue.findIndex((m) => m.senderIsBot);
219
+ if (botIdx >= 0) {
220
+ return queue.splice(botIdx, 1)[0];
221
+ }
222
+ }
223
+ return queue.shift();
224
+ };
225
+
226
+ const processOne = async (msg: QueuedMessage, peerId: string, label: string): Promise<void> => {
227
+ try {
228
+ await handleMessageFnRef!(msg);
229
+ } catch (err) {
230
+ log?.error(`${label} error for ${peerId}: ${formatErrorMessage(err)}`);
231
+ }
232
+ };
233
+
234
+ const drainGroupBatch = async (batch: QueuedMessage[], peerId: string): Promise<void> => {
235
+ const commands: QueuedMessage[] = [];
236
+ const normal: QueuedMessage[] = [];
237
+ for (const m of batch) {
238
+ if (isSlashCommand(m)) {
239
+ commands.push(m);
240
+ } else {
241
+ normal.push(m);
242
+ }
243
+ }
244
+
245
+ for (const cmd of commands) {
246
+ log?.debug?.(
247
+ `Processing command independently for ${peerId}: ${(cmd.content ?? "").trim().slice(0, 50)}`,
248
+ );
249
+ await processOne(cmd, peerId, "Command processor");
250
+ }
251
+
252
+ if (normal.length > 0) {
253
+ const merged = mergeGroupMessages(normal);
254
+ if (normal.length > 1) {
255
+ log?.debug?.(`Merged ${normal.length} queued group messages for ${peerId} into one`);
256
+ }
257
+ await processOne(merged, peerId, `Message processor (merged batch of ${normal.length})`);
258
+ }
259
+ };
260
+
261
+ const drainUserQueue = async (peerId: string): Promise<void> => {
262
+ if (activeUsers.has(peerId)) {
263
+ return;
264
+ }
265
+ if (activeUsers.size >= maxConcurrentUsers) {
266
+ log?.debug?.(`Max concurrent users (${maxConcurrentUsers}) reached, ${peerId} will wait`);
267
+ return;
268
+ }
269
+
270
+ const queue = userQueues.get(peerId);
271
+ if (!queue || queue.length === 0) {
272
+ userQueues.delete(peerId);
273
+ return;
274
+ }
275
+
276
+ activeUsers.add(peerId);
277
+ const isGroup = isGroupPeer(peerId);
278
+
279
+ try {
280
+ while (queue.length > 0 && !ctx.isAborted()) {
281
+ if (isGroup && queue.length > 1 && handleMessageFnRef) {
282
+ const batch = queue.splice(0);
283
+ totalEnqueued = Math.max(0, totalEnqueued - batch.length);
284
+ await drainGroupBatch(batch, peerId);
285
+ continue;
286
+ }
287
+
288
+ const msg = queue.shift()!;
289
+ totalEnqueued = Math.max(0, totalEnqueued - 1);
290
+ if (handleMessageFnRef) {
291
+ await processOne(msg, peerId, "Message processor");
292
+ }
293
+ }
294
+ } finally {
295
+ activeUsers.delete(peerId);
296
+ userQueues.delete(peerId);
297
+
298
+ for (const [waitingPeerId, waitingQueue] of userQueues) {
299
+ if (activeUsers.size >= maxConcurrentUsers) {
300
+ break;
301
+ }
302
+ if (waitingQueue.length > 0 && !activeUsers.has(waitingPeerId)) {
303
+ void drainUserQueue(waitingPeerId);
304
+ }
305
+ }
306
+ }
307
+ };
308
+
309
+ const enqueue = (msg: QueuedMessage): void => {
310
+ const peerId = getMessagePeerId(msg);
311
+ const isGroup = isGroupPeer(peerId);
312
+
313
+ let queue = userQueues.get(peerId);
314
+ if (!queue) {
315
+ queue = [];
316
+ userQueues.set(peerId, queue);
317
+ }
318
+
319
+ const maxSize = isGroup ? groupQueueSize : peerQueueSize;
320
+ if (queue.length >= maxSize) {
321
+ const dropped = evictOne(queue, isGroup);
322
+ totalEnqueued = Math.max(0, totalEnqueued - 1);
323
+ if (isGroup && dropped?.senderIsBot) {
324
+ log?.info(`Queue full for ${peerId}, dropping bot message ${dropped.messageId}`, {
325
+ accountId: ctx.accountId,
326
+ peerId,
327
+ droppedMessageId: dropped.messageId,
328
+ reason: "queue_full_evict_bot",
329
+ });
330
+ } else {
331
+ log?.error(`Queue full for ${peerId}, dropping oldest message ${dropped?.messageId}`, {
332
+ accountId: ctx.accountId,
333
+ peerId,
334
+ droppedMessageId: dropped?.messageId,
335
+ reason: "queue_full_evict_oldest",
336
+ });
337
+ }
338
+ }
339
+
340
+ totalEnqueued++;
341
+ if (totalEnqueued > globalQueueSize) {
342
+ log?.error(
343
+ `Global queue limit reached (${totalEnqueued}), message from ${peerId} may be delayed`,
344
+ { accountId: ctx.accountId, peerId, totalEnqueued, globalQueueSize },
345
+ );
346
+ }
347
+
348
+ queue.push(msg);
349
+ log?.debug?.(
350
+ `Message enqueued for ${peerId}, user queue: ${queue.length}, active users: ${activeUsers.size}`,
351
+ );
352
+
353
+ void drainUserQueue(peerId);
354
+ };
355
+
356
+ const startProcessor = (handleMessageFn: (msg: QueuedMessage) => Promise<void>): void => {
357
+ handleMessageFnRef = handleMessageFn;
358
+ log?.debug?.(
359
+ `Message processor started (per-user concurrency, max ${maxConcurrentUsers} users)`,
360
+ );
361
+ };
362
+
363
+ const getSnapshot = (senderPeerId: string): QueueSnapshot => {
364
+ let totalPending = 0;
365
+ for (const [, q] of userQueues) {
366
+ totalPending += q.length;
367
+ }
368
+ const senderQueue = userQueues.get(senderPeerId);
369
+ return {
370
+ totalPending,
371
+ activeUsers: activeUsers.size,
372
+ maxConcurrentUsers,
373
+ senderPending: senderQueue ? senderQueue.length : 0,
374
+ };
375
+ };
376
+
377
+ const clearUserQueue = (peerId: string): number => {
378
+ const queue = userQueues.get(peerId);
379
+ if (!queue || queue.length === 0) {
380
+ return 0;
381
+ }
382
+ const droppedCount = queue.length;
383
+ queue.length = 0;
384
+ totalEnqueued = Math.max(0, totalEnqueued - droppedCount);
385
+ return droppedCount;
386
+ };
387
+
388
+ const executeImmediate = (msg: QueuedMessage): void => {
389
+ if (handleMessageFnRef) {
390
+ handleMessageFnRef(msg).catch((err) => {
391
+ log?.error(`Immediate execution error: ${err}`);
392
+ });
393
+ }
394
+ };
395
+
396
+ return {
397
+ enqueue,
398
+ startProcessor,
399
+ getSnapshot,
400
+ getMessagePeerId,
401
+ clearUserQueue,
402
+ executeImmediate,
403
+ };
404
+ }