@poolzin/pool-bot 2026.3.21 → 2026.3.23

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 (124) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/acp/bindings-store.js +209 -0
  3. package/dist/acp/control-plane/runtime-cache.js +54 -0
  4. package/dist/acp/control-plane/runtime-options.js +215 -0
  5. package/dist/acp/control-plane/session-actor-queue.js +36 -0
  6. package/dist/acp/runtime/errors.js +47 -0
  7. package/dist/acp/runtime/registry.js +86 -0
  8. package/dist/acp/runtime/types.js +1 -0
  9. package/dist/acp/translator.js +97 -0
  10. package/dist/agents/failover-error.js +145 -47
  11. package/dist/browser/browser-profile-manager.js +319 -0
  12. package/dist/browser/cdp-proxy-bypass.js +129 -0
  13. package/dist/browser/cdp-timeouts.js +41 -0
  14. package/dist/browser/chrome-extension-validator.js +406 -0
  15. package/dist/browser/chrome-mcp-snapshot.js +222 -0
  16. package/dist/browser/chrome-mcp.js +421 -0
  17. package/dist/browser/chrome-mcp.snapshot.js +133 -0
  18. package/dist/browser/errors.js +67 -0
  19. package/dist/browser/form-fields.js +22 -0
  20. package/dist/browser/output-atomic.js +44 -0
  21. package/dist/browser/profile-capabilities.js +47 -0
  22. package/dist/browser/safe-filename.js +25 -0
  23. package/dist/browser/snapshot-roles.js +60 -0
  24. package/dist/build-info.json +3 -3
  25. package/dist/commands/security-owner-only.js +86 -0
  26. package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
  27. package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
  28. package/dist/control-ui/index.html +1 -1
  29. package/dist/cron/cron-filters.js +150 -0
  30. package/dist/gateway/device-pairing-security.js +197 -0
  31. package/dist/gateway/event-deduplication.js +167 -0
  32. package/dist/gateway/run-tracker.js +253 -0
  33. package/dist/gateway/server-methods/nodes.js +14 -0
  34. package/dist/gateway/websocket-preauth-security.js +188 -0
  35. package/dist/infra/errors.js +53 -13
  36. package/dist/infra/exec-approvals-security.js +217 -0
  37. package/dist/infra/security/command-analyzer.js +257 -0
  38. package/dist/plugins/loader.js +16 -8
  39. package/dist/security/external-content.js +51 -1
  40. package/dist/sessions/session-costs.js +228 -0
  41. package/dist/shared/param-key.js +16 -0
  42. package/dist/shared/poll-params.js +58 -0
  43. package/dist/shared/polls.js +55 -0
  44. package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
  45. package/docs/FEATURES.md +523 -0
  46. package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
  47. package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
  48. package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
  49. package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
  50. package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
  51. package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
  52. package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
  53. package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
  54. package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
  55. package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
  56. package/docs/MIKRODASH-ANALYSIS.md +412 -0
  57. package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
  58. package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
  59. package/docs/PHASE-7-SUMMARY.md +144 -0
  60. package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
  61. package/docs/PROJECT-FINAL-STATUS.md +237 -0
  62. package/docs/README.md +116 -0
  63. package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
  64. package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
  65. package/docs/channels/googlechat.md +235 -206
  66. package/docs/channels/irc.md +332 -0
  67. package/docs/channels/nostr.md +255 -168
  68. package/docs/components/command-palette.md +166 -0
  69. package/docs/components/login-gate.md +219 -0
  70. package/docs/getting-started/installation.md +191 -0
  71. package/docs/getting-started/introduction.md +120 -0
  72. package/docs/improvements/USAGE-GUIDE.md +359 -0
  73. package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
  74. package/docs/reference/deadcode-detection.md +72 -0
  75. package/extensions/acpx/node_modules/.bin/acpx +21 -0
  76. package/extensions/agency-agents/node_modules/.bin/vite +4 -4
  77. package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
  78. package/extensions/googlechat/node_modules/.bin/tsc +21 -0
  79. package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
  80. package/extensions/googlechat/node_modules/.bin/vitest +21 -0
  81. package/extensions/googlechat/package.json +11 -28
  82. package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
  83. package/extensions/googlechat/src/googlechat-channel.ts +120 -0
  84. package/extensions/googlechat/src/index.ts +14 -0
  85. package/extensions/irc/node_modules/.bin/tsc +21 -0
  86. package/extensions/irc/node_modules/.bin/tsserver +21 -0
  87. package/extensions/irc/node_modules/.bin/vitest +21 -0
  88. package/extensions/irc/package.json +16 -8
  89. package/extensions/irc/src/index.ts +14 -0
  90. package/extensions/irc/src/irc-channel.test.ts +43 -0
  91. package/extensions/irc/src/irc-channel.ts +191 -0
  92. package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
  93. package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
  94. package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
  95. package/extensions/keyed-async-queue/package.json +20 -0
  96. package/extensions/keyed-async-queue/src/index.ts +14 -0
  97. package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
  98. package/extensions/keyed-async-queue/src/queue.ts +200 -0
  99. package/extensions/memory-core/node_modules/.bin/tsc +21 -0
  100. package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
  101. package/extensions/memory-core/node_modules/.bin/vitest +21 -0
  102. package/extensions/memory-core/package.json +11 -8
  103. package/extensions/memory-core/src/index.ts +14 -0
  104. package/extensions/memory-core/src/memory-manager.test.ts +124 -0
  105. package/extensions/memory-core/src/memory-manager.ts +186 -0
  106. package/extensions/nostr/node_modules/.bin/tsc +2 -2
  107. package/extensions/nostr/node_modules/.bin/tsserver +2 -2
  108. package/extensions/nostr/node_modules/.bin/vitest +21 -0
  109. package/extensions/nostr/package.json +15 -24
  110. package/extensions/nostr/src/index.ts +14 -0
  111. package/extensions/nostr/src/nostr-channel.test.ts +55 -0
  112. package/extensions/nostr/src/nostr-channel.ts +228 -0
  113. package/extensions/page-agent/node_modules/.bin/vitest +2 -2
  114. package/extensions/test-utils/node_modules/.bin/jiti +21 -0
  115. package/extensions/test-utils/node_modules/.bin/playwright +21 -0
  116. package/extensions/test-utils/node_modules/.bin/tsx +21 -0
  117. package/extensions/test-utils/node_modules/.bin/vite +21 -0
  118. package/extensions/test-utils/node_modules/.bin/vitest +21 -0
  119. package/extensions/test-utils/node_modules/.bin/yaml +21 -0
  120. package/extensions/xyops/node_modules/.bin/vitest +2 -2
  121. package/package.json +2 -1
  122. package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
  123. package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
  124. package/extensions/memory-core/node_modules/.bin/poolbot +0 -21
@@ -0,0 +1,191 @@
1
+ /**
2
+ * IRC Channel Implementation
3
+ * Supports connecting to IRC servers, joining channels, and sending/receiving messages
4
+ */
5
+
6
+ import IRC from "irc-framework";
7
+ import type { Channel, ChannelMessage, ChannelPresence } from "../../../src/channels/types.js";
8
+
9
+ export type IRCConfig = {
10
+ host: string;
11
+ port: number;
12
+ nick: string;
13
+ channels: string[];
14
+ password?: string;
15
+ sasl?: {
16
+ account: string;
17
+ password: string;
18
+ };
19
+ tls?: boolean;
20
+ };
21
+
22
+ export class IRCChannel implements Channel {
23
+ public readonly id = "irc";
24
+ public readonly name = "IRC";
25
+
26
+ private config: IRCConfig;
27
+ private client: IRC.Client;
28
+ private connected = false;
29
+ private messageCallback?: (message: ChannelMessage) => void;
30
+
31
+ constructor(config: IRCConfig) {
32
+ this.config = config;
33
+ this.client = new IRC.Client({
34
+ host: config.host,
35
+ port: config.port,
36
+ nick: config.nick,
37
+ password: config.password,
38
+ sasl: config.sasl ? {
39
+ account: config.sasl.account,
40
+ password: config.sasl.password,
41
+ } : undefined,
42
+ tls: config.tls,
43
+ auto_reconnect: true,
44
+ auto_reconnect_wait: 10,
45
+ max_reconnect_attempts: 5,
46
+ });
47
+
48
+ this.setupEventHandlers();
49
+ }
50
+
51
+ private setupEventHandlers(): void {
52
+ this.client.on("connect", () => {
53
+ this.connected = true;
54
+ console.log("[irc] Connected to server");
55
+
56
+ // Join configured channels
57
+ for (const channel of this.config.channels) {
58
+ this.client.join(channel);
59
+ }
60
+ });
61
+
62
+ this.client.on("disconnect", () => {
63
+ this.connected = false;
64
+ console.log("[irc] Disconnected from server");
65
+ });
66
+
67
+ this.client.on("message", (context) => {
68
+ if (this.messageCallback && context.type === "message") {
69
+ this.messageCallback({
70
+ id: `${context.target}-${Date.now()}`,
71
+ text: context.message,
72
+ from: context.nick,
73
+ to: context.target,
74
+ timestamp: Date.now(),
75
+ threadId: undefined,
76
+ });
77
+ }
78
+ });
79
+
80
+ this.client.on("notice", (context) => {
81
+ console.log("[irc] Notice:", context);
82
+ });
83
+
84
+ this.client.on("error", (error) => {
85
+ console.error("[irc] Error:", error);
86
+ });
87
+ }
88
+
89
+ async connect(): Promise<void> {
90
+ return new Promise((resolve, reject) => {
91
+ try {
92
+ this.client.connect();
93
+
94
+ const timeout = setTimeout(() => {
95
+ reject(new Error("IRC connection timeout"));
96
+ }, 30000);
97
+
98
+ this.client.once("connect", () => {
99
+ clearTimeout(timeout);
100
+ resolve();
101
+ });
102
+
103
+ this.client.once("error", (error) => {
104
+ clearTimeout(timeout);
105
+ reject(new Error(`IRC connection error: ${error.error}`));
106
+ });
107
+ } catch (error) {
108
+ reject(error);
109
+ }
110
+ });
111
+ }
112
+
113
+ async disconnect(): Promise<void> {
114
+ this.client.quit("Disconnecting");
115
+ this.connected = false;
116
+ }
117
+
118
+ async sendMessage(message: ChannelMessage): Promise<void> {
119
+ if (!this.connected) {
120
+ throw new Error("IRC channel not connected");
121
+ }
122
+
123
+ const target = message.to || this.config.channels[0];
124
+ if (!target) {
125
+ throw new Error("No target channel specified");
126
+ }
127
+
128
+ this.client.say(target, message.text);
129
+ }
130
+
131
+ async receiveMessages(callback: (message: ChannelMessage) => void): Promise<void> {
132
+ this.messageCallback = callback;
133
+ }
134
+
135
+ async getPresences(): Promise<ChannelPresence[]> {
136
+ const presences: ChannelPresence[] = [];
137
+
138
+ for (const channel of this.config.channels) {
139
+ try {
140
+ const users = await this.getChannelUsers(channel);
141
+ for (const user of users) {
142
+ presences.push({
143
+ channelId: channel,
144
+ userId: user,
145
+ status: "online",
146
+ lastSeen: Date.now(),
147
+ });
148
+ }
149
+ } catch {
150
+ // Ignore errors getting users
151
+ }
152
+ }
153
+
154
+ return presences;
155
+ }
156
+
157
+ private getChannelUsers(channel: string): Promise<string[]> {
158
+ return new Promise((resolve) => {
159
+ this.client.who(channel, (response) => {
160
+ if (response.users) {
161
+ resolve(response.users.map((u: any) => u.nick));
162
+ } else {
163
+ resolve([]);
164
+ }
165
+ });
166
+ });
167
+ }
168
+
169
+ getStatus(): "connected" | "disconnected" | "error" {
170
+ return this.connected ? "connected" : "disconnected";
171
+ }
172
+
173
+ async joinChannel(channel: string): Promise<void> {
174
+ this.client.join(channel);
175
+ if (!this.config.channels.includes(channel)) {
176
+ this.config.channels.push(channel);
177
+ }
178
+ }
179
+
180
+ async leaveChannel(channel: string): Promise<void> {
181
+ this.client.part(channel);
182
+ this.config.channels = this.config.channels.filter((c) => c !== channel);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Create IRC channel from config
188
+ */
189
+ export function createIRCChannel(config: IRCConfig): IRCChannel {
190
+ return new IRCChannel(config);
191
+ }
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
19
+ else
20
+ exec node "$basedir/../typescript/bin/tsc" "$@"
21
+ fi
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
19
+ else
20
+ exec node "$basedir/../typescript/bin/tsserver" "$@"
21
+ fi
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/vitest@1.6.1_@types+node@20.19.30_@vitest+browser@4.0.18_vite@7.3.1_@types+node@25.2.3__a102e25684722dd7e16c8e55a5848dc8/node_modules/vitest/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/vitest@1.6.1_@types+node@20.19.30_@vitest+browser@4.0.18_vite@7.3.1_@types+node@25.2.3__a102e25684722dd7e16c8e55a5848dc8/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/vitest@1.6.1_@types+node@20.19.30_@vitest+browser@4.0.18_vite@7.3.1_@types+node@25.2.3__a102e25684722dd7e16c8e55a5848dc8/node_modules/vitest/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/vitest@1.6.1_@types+node@20.19.30_@vitest+browser@4.0.18_vite@7.3.1_@types+node@25.2.3__a102e25684722dd7e16c8e55a5848dc8/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../vitest/vitest.mjs" "$@"
19
+ else
20
+ exec node "$basedir/../vitest/vitest.mjs" "$@"
21
+ fi
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@poolbot/keyed-async-queue",
3
+ "version": "1.0.0",
4
+ "description": "Keyed async queue for concurrent task management",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "scripts": {
8
+ "test": "vitest run",
9
+ "build": "tsc"
10
+ },
11
+ "dependencies": {},
12
+ "devDependencies": {
13
+ "@types/node": "^20.0.0",
14
+ "typescript": "^5.0.0",
15
+ "vitest": "^1.0.0"
16
+ },
17
+ "peerDependencies": {
18
+ "poolbot": "workspace:*"
19
+ }
20
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Keyed Async Queue Plugin Entry Point
3
+ */
4
+
5
+ export { KeyedAsyncQueue, createKeyedAsyncQueue, type QueueTask, type QueueStats } from "./queue.js";
6
+
7
+ // Export plugin metadata
8
+ export const pluginInfo = {
9
+ id: "keyed-async-queue",
10
+ name: "Keyed Async Queue",
11
+ version: "1.0.0",
12
+ description: "Key-based async queue for concurrent task management",
13
+ author: "Pool Bot Team",
14
+ };
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { KeyedAsyncQueue, createKeyedAsyncQueue } from "./queue";
3
+
4
+ describe("KeyedAsyncQueue", () => {
5
+ let queue: KeyedAsyncQueue;
6
+
7
+ beforeEach(() => {
8
+ queue = createKeyedAsyncQueue();
9
+ });
10
+
11
+ it("enqueues and processes tasks", async () => {
12
+ const result = await queue.enqueue("key1", async () => "result");
13
+ expect(result).toBe("result");
14
+ });
15
+
16
+ it("processes tasks in order for same key", async () => {
17
+ const results: string[] = [];
18
+
19
+ const task1 = queue.enqueue("key1", async () => {
20
+ await new Promise((resolve) => setTimeout(resolve, 50));
21
+ results.push("task1");
22
+ return "result1";
23
+ });
24
+
25
+ const task2 = queue.enqueue("key1", async () => {
26
+ results.push("task2");
27
+ return "result2";
28
+ });
29
+
30
+ await Promise.all([task1, task2]);
31
+ expect(results).toEqual(["task1", "task2"]);
32
+ });
33
+
34
+ it("processes tasks in parallel for different keys", async () => {
35
+ const results: string[] = [];
36
+
37
+ const task1 = queue.enqueue("key1", async () => {
38
+ await new Promise((resolve) => setTimeout(resolve, 50));
39
+ results.push("key1");
40
+ return "result1";
41
+ });
42
+
43
+ const task2 = queue.enqueue("key2", async () => {
44
+ results.push("key2");
45
+ return "result2";
46
+ });
47
+
48
+ await Promise.all([task1, task2]);
49
+ expect(results).toEqual(["key2", "key1"]); // key2 finishes first
50
+ });
51
+
52
+ it("respects priority", async () => {
53
+ const results: string[] = [];
54
+
55
+ queue.enqueue("key1", async () => {
56
+ await new Promise((resolve) => setTimeout(resolve, 50));
57
+ results.push("low");
58
+ return "low";
59
+ }, 0);
60
+
61
+ queue.enqueue("key1", async () => {
62
+ results.push("high");
63
+ return "high";
64
+ }, 10);
65
+
66
+ await new Promise((resolve) => setTimeout(resolve, 100));
67
+ expect(results).toEqual(["high", "low"]);
68
+ });
69
+
70
+ it("gets queue length", async () => {
71
+ queue.enqueue("key1", async () => {
72
+ await new Promise((resolve) => setTimeout(resolve, 100));
73
+ });
74
+
75
+ queue.enqueue("key1", async () => {});
76
+ queue.enqueue("key1", async () => {});
77
+
78
+ expect(queue.getQueueLength("key1")).toBeGreaterThanOrEqual(1);
79
+ });
80
+
81
+ it("clears queue for key", () => {
82
+ queue.enqueue("key1", async () => {});
83
+ queue.enqueue("key1", async () => {});
84
+ queue.enqueue("key2", async () => {});
85
+
86
+ const cleared = queue.clearQueue("key1");
87
+ expect(cleared).toBeGreaterThanOrEqual(1);
88
+ expect(queue.getQueueLength("key2")).toBe(1);
89
+ });
90
+
91
+ it("clears all queues", () => {
92
+ queue.enqueue("key1", async () => {});
93
+ queue.enqueue("key2", async () => {});
94
+
95
+ const cleared = queue.clearAll();
96
+ expect(cleared).toBeGreaterThanOrEqual(2);
97
+ expect(queue.getTotalQueueLength()).toBe(0);
98
+ });
99
+
100
+ it("gets statistics", async () => {
101
+ queue.enqueue("key1", async () => {
102
+ await new Promise((resolve) => setTimeout(resolve, 100));
103
+ });
104
+ queue.enqueue("key1", async () => {});
105
+ queue.enqueue("key2", async () => {});
106
+
107
+ const stats = queue.getStats();
108
+ expect(stats.total).toBeGreaterThanOrEqual(2);
109
+ expect(stats.byKey.key1).toBeGreaterThanOrEqual(1);
110
+ expect(stats.byKey.key2).toBeGreaterThanOrEqual(1);
111
+ });
112
+
113
+ it("handles task errors", async () => {
114
+ const errorTask = queue.enqueue("key1", async () => {
115
+ throw new Error("Task failed");
116
+ });
117
+
118
+ await expect(errorTask).rejects.toThrow("Task failed");
119
+ });
120
+
121
+ it("continues processing after error", async () => {
122
+ const results: string[] = [];
123
+
124
+ queue.enqueue("key1", async () => {
125
+ throw new Error("First failed");
126
+ });
127
+
128
+ await queue.enqueue("key1", async () => {
129
+ results.push("second");
130
+ return "success";
131
+ });
132
+
133
+ expect(results).toEqual(["second"]);
134
+ });
135
+ });
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Keyed Async Queue
3
+ * Manages concurrent tasks with key-based ordering
4
+ */
5
+
6
+ export type QueueTask<T = unknown> = {
7
+ id: string;
8
+ key: string;
9
+ fn: () => Promise<T>;
10
+ priority?: number;
11
+ createdAt: number;
12
+ };
13
+
14
+ export type QueueStats = {
15
+ total: number;
16
+ byKey: Record<string, number>;
17
+ processing: number;
18
+ queued: number;
19
+ };
20
+
21
+ export class KeyedAsyncQueue {
22
+ private queues: Map<string, QueueTask[]> = new Map();
23
+ private processing: Map<string, boolean> = new Map();
24
+ private concurrency: number;
25
+
26
+ constructor(concurrency = 5) {
27
+ this.concurrency = concurrency;
28
+ }
29
+
30
+ /**
31
+ * Add a task to the queue
32
+ */
33
+ async enqueue<T>(key: string, fn: () => Promise<T>, priority = 0): Promise<T> {
34
+ const task: QueueTask<T> = {
35
+ id: `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
36
+ key,
37
+ fn,
38
+ priority,
39
+ createdAt: Date.now(),
40
+ };
41
+
42
+ // Get or create queue for this key
43
+ let queue = this.queues.get(key);
44
+ if (!queue) {
45
+ queue = [];
46
+ this.queues.set(key, queue);
47
+ }
48
+
49
+ // Insert by priority (higher priority first)
50
+ const insertIndex = queue.findIndex((t) => t.priority < priority);
51
+ if (insertIndex === -1) {
52
+ queue.push(task);
53
+ } else {
54
+ queue.splice(insertIndex, 0, task);
55
+ }
56
+
57
+ // Process queue
58
+ return this.processQueue(key);
59
+ }
60
+
61
+ /**
62
+ * Process queue for a specific key
63
+ */
64
+ private async processQueue<T>(key: string): Promise<T> {
65
+ // Check if already processing for this key
66
+ if (this.processing.get(key)) {
67
+ // Wait for current processing to finish
68
+ return new Promise((resolve, reject) => {
69
+ const checkQueue = async () => {
70
+ const queue = this.queues.get(key);
71
+ if (!queue || queue.length === 0) {
72
+ reject(new Error("Task not found in queue"));
73
+ return;
74
+ }
75
+
76
+ const task = queue[0];
77
+ if (!task) {
78
+ reject(new Error("Task not found"));
79
+ return;
80
+ }
81
+
82
+ try {
83
+ const result = await task.fn();
84
+ queue.shift();
85
+ resolve(result);
86
+ } catch (error) {
87
+ queue.shift();
88
+ reject(error);
89
+ }
90
+ };
91
+
92
+ setTimeout(checkQueue, 10);
93
+ });
94
+ }
95
+
96
+ // Mark as processing
97
+ this.processing.set(key, true);
98
+
99
+ // Get next task
100
+ const queue = this.queues.get(key);
101
+ if (!queue || queue.length === 0) {
102
+ this.processing.set(key, false);
103
+ throw new Error("No tasks in queue");
104
+ }
105
+
106
+ const task = queue[0];
107
+ if (!task) {
108
+ this.processing.set(key, false);
109
+ throw new Error("Task not found");
110
+ }
111
+
112
+ try {
113
+ const result = await task.fn();
114
+ queue.shift();
115
+ return result;
116
+ } finally {
117
+ this.processing.set(key, false);
118
+
119
+ // Process next task if available
120
+ if (queue.length > 0) {
121
+ this.processQueue(key).catch(console.error);
122
+ }
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Get queue length for a key
128
+ */
129
+ getQueueLength(key: string): number {
130
+ const queue = this.queues.get(key);
131
+ return queue ? queue.length : 0;
132
+ }
133
+
134
+ /**
135
+ * Get total queue length
136
+ */
137
+ getTotalQueueLength(): number {
138
+ let total = 0;
139
+ for (const queue of this.queues.values()) {
140
+ total += queue.length;
141
+ }
142
+ return total;
143
+ }
144
+
145
+ /**
146
+ * Clear queue for a key
147
+ */
148
+ clearQueue(key: string): number {
149
+ const queue = this.queues.get(key);
150
+ if (!queue) {
151
+ return 0;
152
+ }
153
+
154
+ const count = queue.length;
155
+ queue.length = 0;
156
+ return count;
157
+ }
158
+
159
+ /**
160
+ * Clear all queues
161
+ */
162
+ clearAll(): number {
163
+ let count = 0;
164
+ for (const queue of this.queues.values()) {
165
+ count += queue.length;
166
+ }
167
+ this.queues.clear();
168
+ this.processing.clear();
169
+ return count;
170
+ }
171
+
172
+ /**
173
+ * Get statistics
174
+ */
175
+ getStats(): QueueStats {
176
+ const stats: QueueStats = {
177
+ total: 0,
178
+ byKey: {},
179
+ processing: 0,
180
+ queued: 0,
181
+ };
182
+
183
+ for (const [key, queue] of this.queues.entries()) {
184
+ stats.byKey[key] = queue.length;
185
+ stats.total += queue.length;
186
+ }
187
+
188
+ stats.queued = stats.total;
189
+ stats.processing = Array.from(this.processing.values()).filter(Boolean).length;
190
+
191
+ return stats;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Create keyed async queue
197
+ */
198
+ export function createKeyedAsyncQueue(concurrency?: number): KeyedAsyncQueue {
199
+ return new KeyedAsyncQueue(concurrency);
200
+ }
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
19
+ else
20
+ exec node "$basedir/../typescript/bin/tsc" "$@"
21
+ fi
@@ -0,0 +1,21 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+
4
+ case `uname` in
5
+ *CYGWIN*|*MINGW*|*MSYS*)
6
+ if command -v cygpath > /dev/null 2>&1; then
7
+ basedir=`cygpath -w "$basedir"`
8
+ fi
9
+ ;;
10
+ esac
11
+
12
+ if [ -z "$NODE_PATH" ]; then
13
+ export NODE_PATH="/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/node_modules"
14
+ else
15
+ export NODE_PATH="/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/typescript@5.9.3/node_modules:/Users/pool/Documents/GitHub/pool-bot/node_modules/.pnpm/node_modules:$NODE_PATH"
16
+ fi
17
+ if [ -x "$basedir/node" ]; then
18
+ exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
19
+ else
20
+ exec node "$basedir/../typescript/bin/tsserver" "$@"
21
+ fi