blockmine 1.25.0 → 1.27.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/CHANGELOG.md +46 -1
- package/backend/cli.js +1 -1
- package/backend/package.json +2 -2
- package/backend/prisma/migrations/20260328173000_add_plugin_source_ref/migration.sql +2 -0
- package/backend/prisma/migrations/migration_lock.toml +2 -2
- package/backend/prisma/schema.prisma +2 -0
- package/backend/src/api/routes/apiKeys.js +8 -0
- package/backend/src/api/routes/bots.js +258 -9
- package/backend/src/api/routes/eventGraphs.js +151 -1
- package/backend/src/api/routes/health.js +38 -0
- package/backend/src/api/routes/nodeRegistry.js +63 -0
- package/backend/src/api/routes/plugins.js +254 -29
- package/backend/src/container.js +11 -8
- package/backend/src/core/BotCommandLoader.js +161 -0
- package/backend/src/core/BotConnection.js +125 -0
- package/backend/src/core/BotEventHandlers.js +234 -0
- package/backend/src/core/BotIPCHandler.js +445 -0
- package/backend/src/core/BotManager.js +15 -7
- package/backend/src/core/BotProcess.js +75 -142
- package/backend/src/core/EventGraphManager.js +7 -3
- package/backend/src/core/GraphDebugHandler.js +229 -0
- package/backend/src/core/GraphDebugIPC.js +117 -0
- package/backend/src/core/GraphExecutionEngine.js +545 -978
- package/backend/src/core/GraphTraversal.js +80 -0
- package/backend/src/core/GraphValidation.js +73 -0
- package/backend/src/core/NodeDefinition.js +138 -0
- package/backend/src/core/NodeRegistry.js +153 -141
- package/backend/src/core/PluginManager.js +272 -31
- package/backend/src/core/RewindSignal.js +9 -0
- package/backend/src/core/config/ConfigValidator.js +72 -0
- package/backend/src/core/config/FeatureFlags.js +52 -0
- package/backend/src/core/config/__tests__/ConfigValidator.test.js +232 -0
- package/backend/src/core/domain/entities/Bot.js +39 -0
- package/backend/src/core/domain/entities/Command.js +41 -0
- package/backend/src/core/domain/entities/EventGraph.js +39 -0
- package/backend/src/core/domain/entities/Plugin.js +45 -0
- package/backend/src/core/domain/entities/User.js +40 -0
- package/backend/src/core/domain/services/DependencyResolver.js +168 -0
- package/backend/src/core/domain/services/GraphValidator.js +117 -0
- package/backend/src/core/domain/services/PermissionChecker.js +34 -0
- package/backend/src/core/domain/services/__tests__/DependencyResolver.test.js +126 -0
- package/backend/src/core/domain/valueObjects/BotConfig.js +27 -0
- package/backend/src/core/domain/valueObjects/DependencyGraph.js +86 -0
- package/backend/src/core/domain/valueObjects/PluginManifest.js +36 -0
- package/backend/src/core/errors/BaseError.js +29 -0
- package/backend/src/core/errors/ErrorHandler.js +81 -0
- package/backend/src/core/errors/__tests__/ErrorHandler.test.js +188 -0
- package/backend/src/core/errors/index.js +68 -0
- package/backend/src/core/infrastructure/BatchingUtility.js +66 -0
- package/backend/src/core/infrastructure/CircuitBreaker.js +103 -0
- package/backend/src/core/infrastructure/ConnectionPool.js +81 -0
- package/backend/src/core/infrastructure/RateLimiter.js +64 -0
- package/backend/src/core/infrastructure/__tests__/BatchingUtility.test.js +86 -0
- package/backend/src/core/infrastructure/__tests__/CircuitBreaker.test.js +156 -0
- package/backend/src/core/infrastructure/__tests__/ConnectionPool.test.js +146 -0
- package/backend/src/core/infrastructure/__tests__/RateLimiter.test.js +171 -0
- package/backend/src/core/ipc/botApiFactory.js +72 -0
- package/backend/src/core/ipc/ipcMessageTypes.js +115 -0
- package/backend/src/core/logging/AuditLogger.js +61 -0
- package/backend/src/core/logging/StructuredLogger.js +80 -0
- package/backend/src/core/logging/__tests__/StructuredLogger.test.js +213 -0
- package/backend/src/core/logging/index.js +7 -0
- package/backend/src/core/metrics/MetricsCollector.js +104 -0
- package/backend/src/core/metrics/__tests__/MetricsCollector.test.js +131 -0
- package/backend/src/core/node-registries/actionsNodes.js +191 -0
- package/backend/src/core/node-registries/arraysNodes.js +152 -0
- package/backend/src/core/node-registries/botNodes.js +48 -0
- package/backend/src/core/node-registries/containerNodes.js +141 -0
- package/backend/src/core/node-registries/dataNodes.js +284 -0
- package/backend/src/core/node-registries/debugNodes.js +23 -0
- package/backend/src/core/node-registries/eventsNodes.js +223 -0
- package/backend/src/core/node-registries/flowNodes.js +151 -0
- package/backend/src/core/node-registries/furnaceNodes.js +123 -0
- package/backend/src/core/node-registries/index.js +108 -0
- package/backend/src/core/node-registries/inventory.js +102 -106
- package/backend/src/core/node-registries/logicNodes.js +54 -0
- package/backend/src/core/node-registries/mathNodes.js +38 -0
- package/backend/src/core/node-registries/navigationNodes.js +109 -0
- package/backend/src/core/node-registries/objectsNodes.js +90 -0
- package/backend/src/core/node-registries/stringsNodes.js +165 -0
- package/backend/src/core/node-registries/timeNodes.js +105 -0
- package/backend/src/core/node-registries/typeNodes.js +22 -0
- package/backend/src/core/node-registries/usersNodes.js +126 -0
- package/backend/src/core/nodes/arrays/shuffle.js +14 -0
- package/backend/src/core/nodes/bot/get_name.js +8 -0
- package/backend/src/core/nodes/bot/stop_bot.js +5 -0
- package/backend/src/core/nodes/container/open.js +101 -111
- package/backend/src/core/nodes/data/store_read.js +26 -0
- package/backend/src/core/nodes/data/store_write.js +23 -0
- package/backend/src/core/nodes/event/call_event.js +31 -0
- package/backend/src/core/nodes/event/custom_event.js +8 -0
- package/backend/src/core/nodes/flow/timer.js +35 -0
- package/backend/src/core/nodes/inventory/drop.js +73 -65
- package/backend/src/core/nodes/inventory/equip.js +54 -45
- package/backend/src/core/nodes/inventory/select_slot.js +48 -46
- package/backend/src/core/nodes/navigation/follow.js +54 -51
- package/backend/src/core/nodes/navigation/go_to.js +41 -53
- package/backend/src/core/nodes/navigation/go_to_entity.js +65 -69
- package/backend/src/core/nodes/navigation/go_to_player.js +65 -70
- package/backend/src/core/nodes/navigation/stop.js +17 -26
- package/backend/src/core/nodes/users/add_to_group.js +24 -0
- package/backend/src/core/nodes/users/check_permission.js +26 -0
- package/backend/src/core/nodes/users/remove_from_group.js +24 -0
- package/backend/src/core/services/BotIPCMessageRouter.js +337 -0
- package/backend/src/core/services/BotLifecycleService.js +41 -632
- package/backend/src/core/services/CacheManager.js +83 -23
- package/backend/src/core/services/CrashRestartManager.js +42 -0
- package/backend/src/core/services/DebugSessionManager.js +114 -12
- package/backend/src/core/services/EventGraphService.js +69 -0
- package/backend/src/core/services/MinecraftBotManager.js +9 -1
- package/backend/src/core/services/PluginManagementService.js +84 -0
- package/backend/src/core/services/TestModeContext.js +65 -0
- package/backend/src/core/services/__tests__/CacheManager.test.js +168 -0
- package/backend/src/core/services.js +1 -11
- package/backend/src/core/validation/InputValidator.js +167 -0
- package/backend/src/core/validation/__tests__/InputValidator.test.js +296 -0
- package/backend/src/real-time/botApi/index.js +1 -1
- package/backend/src/real-time/socketHandler.js +26 -0
- package/backend/src/server.js +10 -5
- package/frontend/dist/assets/{browser-ponyfill-DN7pwmHT.js → browser-ponyfill-D8y0Ty7C.js} +1 -1
- package/frontend/dist/assets/index-CFJLS0dk.css +32 -0
- package/frontend/dist/assets/{index-LSy71uwm.js → index-D91UGNMG.js} +1880 -1881
- package/frontend/dist/index.html +2 -2
- package/frontend/dist/locales/en/bots.json +4 -1
- package/frontend/dist/locales/en/common.json +7 -1
- package/frontend/dist/locales/en/login.json +2 -0
- package/frontend/dist/locales/en/management.json +79 -1
- package/frontend/dist/locales/en/nodes.json +59 -4
- package/frontend/dist/locales/en/plugin-detail.json +24 -4
- package/frontend/dist/locales/en/plugins.json +226 -7
- package/frontend/dist/locales/en/setup.json +2 -0
- package/frontend/dist/locales/en/sidebar.json +171 -3
- package/frontend/dist/locales/en/visual-editor.json +230 -31
- package/frontend/dist/locales/ru/bots.json +4 -1
- package/frontend/dist/locales/ru/login.json +2 -0
- package/frontend/dist/locales/ru/management.json +79 -1
- package/frontend/dist/locales/ru/minecraft-viewer.json +3 -0
- package/frontend/dist/locales/ru/nodes.json +105 -51
- package/frontend/dist/locales/ru/plugins.json +103 -4
- package/frontend/dist/locales/ru/setup.json +2 -0
- package/frontend/dist/locales/ru/sidebar.json +171 -3
- package/frontend/dist/locales/ru/visual-editor.json +232 -33
- package/frontend/package.json +2 -0
- package/nul +12 -0
- package/package.json +3 -3
- package/scripts/postinstall.js +38 -0
- package/backend/package-lock.json +0 -6801
- package/backend/src/core/node-registries/actions.js +0 -202
- package/backend/src/core/node-registries/arrays.js +0 -155
- package/backend/src/core/node-registries/bot.js +0 -23
- package/backend/src/core/node-registries/container.js +0 -162
- package/backend/src/core/node-registries/data.js +0 -290
- package/backend/src/core/node-registries/debug.js +0 -26
- package/backend/src/core/node-registries/events.js +0 -201
- package/backend/src/core/node-registries/flow.js +0 -139
- package/backend/src/core/node-registries/furnace.js +0 -143
- package/backend/src/core/node-registries/logic.js +0 -62
- package/backend/src/core/node-registries/math.js +0 -42
- package/backend/src/core/node-registries/navigation.js +0 -111
- package/backend/src/core/node-registries/objects.js +0 -98
- package/backend/src/core/node-registries/strings.js +0 -187
- package/backend/src/core/node-registries/time.js +0 -113
- package/backend/src/core/node-registries/type.js +0 -25
- package/backend/src/core/node-registries/users.js +0 -79
- package/frontend/dist/assets/index-SfhKxI4-.css +0 -32
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const CircuitBreaker = require('../CircuitBreaker');
|
|
2
|
+
const { ExternalServiceError } = require('../../errors/index');
|
|
3
|
+
|
|
4
|
+
const { CLOSED, OPEN, HALF_OPEN } = CircuitBreaker.STATE;
|
|
5
|
+
|
|
6
|
+
function makeBreaker(opts = {}) {
|
|
7
|
+
return new CircuitBreaker({ failureThreshold: 3, successThreshold: 2, timeout: 1000, ...opts });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('CircuitBreaker', () => {
|
|
11
|
+
describe('initial state', () => {
|
|
12
|
+
it('starts in CLOSED state', () => {
|
|
13
|
+
expect(makeBreaker().getState()).toBe(CLOSED);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('CLOSED state', () => {
|
|
18
|
+
it('calls fn and returns result', async () => {
|
|
19
|
+
const cb = makeBreaker();
|
|
20
|
+
const result = await cb.call(async () => 42);
|
|
21
|
+
expect(result).toBe(42);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('opens after failureThreshold failures', async () => {
|
|
25
|
+
const cb = makeBreaker({ failureThreshold: 3 });
|
|
26
|
+
const fail = () => { throw new Error('fail'); };
|
|
27
|
+
for (let i = 0; i < 3; i++) {
|
|
28
|
+
await cb.call(fail).catch(() => {});
|
|
29
|
+
}
|
|
30
|
+
expect(cb.getState()).toBe(OPEN);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('resets failure count on success', async () => {
|
|
34
|
+
const cb = makeBreaker({ failureThreshold: 3 });
|
|
35
|
+
const fail = () => { throw new Error('fail'); };
|
|
36
|
+
await cb.call(fail).catch(() => {});
|
|
37
|
+
await cb.call(fail).catch(() => {});
|
|
38
|
+
await cb.call(async () => 'ok');
|
|
39
|
+
expect(cb.getStats().failureCount).toBe(0);
|
|
40
|
+
expect(cb.getState()).toBe(CLOSED);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('OPEN state', () => {
|
|
45
|
+
it('throws ExternalServiceError without calling fn', async () => {
|
|
46
|
+
const cb = makeBreaker({ failureThreshold: 1 });
|
|
47
|
+
await cb.call(() => { throw new Error('fail'); }).catch(() => {});
|
|
48
|
+
expect(cb.getState()).toBe(OPEN);
|
|
49
|
+
|
|
50
|
+
const fn = jest.fn();
|
|
51
|
+
await expect(cb.call(fn)).rejects.toBeInstanceOf(ExternalServiceError);
|
|
52
|
+
expect(fn).not.toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('returns fallback value when open', async () => {
|
|
56
|
+
const cb = makeBreaker({ failureThreshold: 1 });
|
|
57
|
+
await cb.call(() => { throw new Error('fail'); }).catch(() => {});
|
|
58
|
+
const result = await cb.call(jest.fn(), 'fallback');
|
|
59
|
+
expect(result).toBe('fallback');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('returns fallback function result when open', async () => {
|
|
63
|
+
const cb = makeBreaker({ failureThreshold: 1 });
|
|
64
|
+
await cb.call(() => { throw new Error('fail'); }).catch(() => {});
|
|
65
|
+
const result = await cb.call(jest.fn(), () => 'computed');
|
|
66
|
+
expect(result).toBe('computed');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('transitions to HALF_OPEN after timeout', async () => {
|
|
70
|
+
jest.useFakeTimers();
|
|
71
|
+
const cb = makeBreaker({ failureThreshold: 1, timeout: 1000 });
|
|
72
|
+
await cb.call(() => { throw new Error('fail'); }).catch(() => {});
|
|
73
|
+
expect(cb.getState()).toBe(OPEN);
|
|
74
|
+
jest.advanceTimersByTime(1001);
|
|
75
|
+
await cb.call(async () => 'ok');
|
|
76
|
+
jest.useRealTimers();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('HALF_OPEN state', () => {
|
|
81
|
+
async function openThenHalfOpen(cb) {
|
|
82
|
+
await cb.call(() => { throw new Error('fail'); }).catch(() => {});
|
|
83
|
+
cb.nextAttempt = Date.now() - 1;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
it('closes after successThreshold successes', async () => {
|
|
87
|
+
const cb = makeBreaker({ failureThreshold: 1, successThreshold: 2 });
|
|
88
|
+
await openThenHalfOpen(cb);
|
|
89
|
+
await cb.call(async () => 'ok');
|
|
90
|
+
expect(cb.getState()).toBe(HALF_OPEN);
|
|
91
|
+
await cb.call(async () => 'ok');
|
|
92
|
+
expect(cb.getState()).toBe(CLOSED);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('reopens on failure in HALF_OPEN', async () => {
|
|
96
|
+
const cb = makeBreaker({ failureThreshold: 1, successThreshold: 2 });
|
|
97
|
+
await openThenHalfOpen(cb);
|
|
98
|
+
await cb.call(() => { throw new Error('fail'); }).catch(() => {});
|
|
99
|
+
expect(cb.getState()).toBe(OPEN);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('fallback', () => {
|
|
104
|
+
it('returns fallback on failure in CLOSED state', async () => {
|
|
105
|
+
const cb = makeBreaker({ failureThreshold: 10 });
|
|
106
|
+
const result = await cb.call(() => { throw new Error('fail'); }, 'default');
|
|
107
|
+
expect(result).toBe('default');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('calls fallback function on failure', async () => {
|
|
111
|
+
const cb = makeBreaker({ failureThreshold: 10 });
|
|
112
|
+
const result = await cb.call(() => { throw new Error('fail'); }, () => 'computed');
|
|
113
|
+
expect(result).toBe('computed');
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('events', () => {
|
|
118
|
+
it('emits stateChange when transitioning to OPEN', async () => {
|
|
119
|
+
const cb = makeBreaker({ failureThreshold: 1 });
|
|
120
|
+
const listener = jest.fn();
|
|
121
|
+
cb.on('stateChange', listener);
|
|
122
|
+
await cb.call(() => { throw new Error('fail'); }).catch(() => {});
|
|
123
|
+
expect(listener).toHaveBeenCalledWith({ from: CLOSED, to: OPEN });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('emits stateChange when transitioning to CLOSED', async () => {
|
|
127
|
+
const cb = makeBreaker({ failureThreshold: 1, successThreshold: 1 });
|
|
128
|
+
const listener = jest.fn();
|
|
129
|
+
await cb.call(() => { throw new Error('fail'); }).catch(() => {});
|
|
130
|
+
cb.nextAttempt = Date.now() - 1;
|
|
131
|
+
cb.on('stateChange', listener);
|
|
132
|
+
await cb.call(async () => 'ok');
|
|
133
|
+
expect(listener).toHaveBeenCalledWith(expect.objectContaining({ to: CLOSED }));
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('logging', () => {
|
|
138
|
+
it('logs state transitions when logger provided', async () => {
|
|
139
|
+
const logger = { warn: jest.fn() };
|
|
140
|
+
const cb = makeBreaker({ failureThreshold: 1, logger });
|
|
141
|
+
await cb.call(() => { throw new Error('fail'); }).catch(() => {});
|
|
142
|
+
expect(logger.warn).toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('getStats', () => {
|
|
147
|
+
it('returns current stats', async () => {
|
|
148
|
+
const cb = makeBreaker();
|
|
149
|
+
const stats = cb.getStats();
|
|
150
|
+
expect(stats.state).toBe(CLOSED);
|
|
151
|
+
expect(stats.failureCount).toBe(0);
|
|
152
|
+
expect(stats.successCount).toBe(0);
|
|
153
|
+
expect(stats.nextAttempt).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
const ConnectionPool = require('../ConnectionPool');
|
|
2
|
+
const { ExternalServiceError } = require('../../errors/index');
|
|
3
|
+
|
|
4
|
+
function makeFactory(failCreate = false) {
|
|
5
|
+
let id = 0;
|
|
6
|
+
return {
|
|
7
|
+
create: jest.fn(async () => {
|
|
8
|
+
if (failCreate) throw new Error('create failed');
|
|
9
|
+
return { id: ++id };
|
|
10
|
+
}),
|
|
11
|
+
destroy: jest.fn(async () => {}),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('ConnectionPool', () => {
|
|
16
|
+
describe('acquire', () => {
|
|
17
|
+
it('creates a new connection when pool is empty', async () => {
|
|
18
|
+
const factory = makeFactory();
|
|
19
|
+
const pool = new ConnectionPool({ factory, min: 0, max: 5 });
|
|
20
|
+
const conn = await pool.acquire();
|
|
21
|
+
expect(conn).toBeDefined();
|
|
22
|
+
expect(factory.create).toHaveBeenCalledTimes(1);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns idle connection without creating a new one', async () => {
|
|
26
|
+
const factory = makeFactory();
|
|
27
|
+
const pool = new ConnectionPool({ factory, min: 0, max: 5 });
|
|
28
|
+
const conn = await pool.acquire();
|
|
29
|
+
pool.release(conn);
|
|
30
|
+
const conn2 = await pool.acquire();
|
|
31
|
+
expect(conn2).toBe(conn);
|
|
32
|
+
expect(factory.create).toHaveBeenCalledTimes(1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('queues request when pool is at max capacity', async () => {
|
|
36
|
+
const factory = makeFactory();
|
|
37
|
+
const pool = new ConnectionPool({ factory, min: 0, max: 1 });
|
|
38
|
+
const conn1 = await pool.acquire();
|
|
39
|
+
const pending = pool.acquire();
|
|
40
|
+
expect(pool.getStats().pending).toBe(1);
|
|
41
|
+
pool.release(conn1);
|
|
42
|
+
const conn2 = await pending;
|
|
43
|
+
expect(conn2).toBe(conn1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('throws ExternalServiceError on timeout', async () => {
|
|
47
|
+
const factory = makeFactory();
|
|
48
|
+
const pool = new ConnectionPool({ factory, min: 0, max: 1, acquireTimeout: 50 });
|
|
49
|
+
await pool.acquire();
|
|
50
|
+
await expect(pool.acquire()).rejects.toBeInstanceOf(ExternalServiceError);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('throws ExternalServiceError when factory.create fails', async () => {
|
|
54
|
+
const factory = makeFactory(true);
|
|
55
|
+
const pool = new ConnectionPool({ factory, min: 0, max: 5 });
|
|
56
|
+
await expect(pool.acquire()).rejects.toBeInstanceOf(ExternalServiceError);
|
|
57
|
+
expect(pool.getStats().total).toBe(0);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('release', () => {
|
|
62
|
+
it('returns connection to idle pool when no queue', async () => {
|
|
63
|
+
const factory = makeFactory();
|
|
64
|
+
const pool = new ConnectionPool({ factory, min: 0, max: 5 });
|
|
65
|
+
const conn = await pool.acquire();
|
|
66
|
+
pool.release(conn);
|
|
67
|
+
expect(pool.getStats().idle).toBe(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('passes connection directly to next queued waiter', async () => {
|
|
71
|
+
const factory = makeFactory();
|
|
72
|
+
const pool = new ConnectionPool({ factory, min: 0, max: 1 });
|
|
73
|
+
const conn = await pool.acquire();
|
|
74
|
+
const waiterPromise = pool.acquire();
|
|
75
|
+
pool.release(conn);
|
|
76
|
+
const waiterConn = await waiterPromise;
|
|
77
|
+
expect(waiterConn).toBe(conn);
|
|
78
|
+
expect(pool.getStats().idle).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('destroy', () => {
|
|
83
|
+
it('destroys all idle connections', async () => {
|
|
84
|
+
const factory = makeFactory();
|
|
85
|
+
const pool = new ConnectionPool({ factory, min: 0, max: 5 });
|
|
86
|
+
const conn = await pool.acquire();
|
|
87
|
+
pool.release(conn);
|
|
88
|
+
await pool.destroy();
|
|
89
|
+
expect(factory.destroy).toHaveBeenCalledWith(conn);
|
|
90
|
+
expect(pool.getStats().total).toBe(0);
|
|
91
|
+
expect(pool.getStats().idle).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('rejects pending queue entries on destroy', async () => {
|
|
95
|
+
const factory = makeFactory();
|
|
96
|
+
const pool = new ConnectionPool({ factory, min: 0, max: 1 });
|
|
97
|
+
await pool.acquire();
|
|
98
|
+
const pending = pool.acquire();
|
|
99
|
+
await pool.destroy();
|
|
100
|
+
await expect(pending).rejects.toBeInstanceOf(ExternalServiceError);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('getStats', () => {
|
|
105
|
+
it('returns correct stats', async () => {
|
|
106
|
+
const factory = makeFactory();
|
|
107
|
+
const pool = new ConnectionPool({ factory, min: 0, max: 5 });
|
|
108
|
+
const conn = await pool.acquire();
|
|
109
|
+
const stats = pool.getStats();
|
|
110
|
+
expect(stats.total).toBe(1);
|
|
111
|
+
expect(stats.idle).toBe(0);
|
|
112
|
+
expect(stats.pending).toBe(0);
|
|
113
|
+
expect(stats.utilization).toBe(1);
|
|
114
|
+
pool.release(conn);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('logs warning when utilization exceeds 0.8', async () => {
|
|
118
|
+
const factory = makeFactory();
|
|
119
|
+
const logger = { warn: jest.fn() };
|
|
120
|
+
const pool = new ConnectionPool({ factory, min: 0, max: 5, logger });
|
|
121
|
+
const conns = await Promise.all([
|
|
122
|
+
pool.acquire(), pool.acquire(), pool.acquire(), pool.acquire(), pool.acquire(),
|
|
123
|
+
]);
|
|
124
|
+
pool.getStats();
|
|
125
|
+
expect(logger.warn).toHaveBeenCalled();
|
|
126
|
+
conns.forEach(c => pool.release(c));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('does not log warning when utilization is low', async () => {
|
|
130
|
+
const factory = makeFactory();
|
|
131
|
+
const logger = { warn: jest.fn() };
|
|
132
|
+
const pool = new ConnectionPool({ factory, min: 0, max: 10, logger });
|
|
133
|
+
const conn = await pool.acquire();
|
|
134
|
+
pool.release(conn);
|
|
135
|
+
pool.getStats();
|
|
136
|
+
expect(logger.warn).not.toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('returns zero utilization when total is 0', () => {
|
|
140
|
+
const factory = makeFactory();
|
|
141
|
+
const pool = new ConnectionPool({ factory, min: 0, max: 5 });
|
|
142
|
+
const stats = pool.getStats();
|
|
143
|
+
expect(stats.utilization).toBe(0);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const RateLimiter = require('../RateLimiter');
|
|
2
|
+
|
|
3
|
+
describe('RateLimiter', () => {
|
|
4
|
+
describe('constructor defaults', () => {
|
|
5
|
+
it('uses default options when none provided', () => {
|
|
6
|
+
const limiter = new RateLimiter();
|
|
7
|
+
expect(limiter.windowMs).toBe(60000);
|
|
8
|
+
expect(limiter.max).toBe(100);
|
|
9
|
+
expect(limiter.keyPrefix).toBe('');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('accepts custom options', () => {
|
|
13
|
+
const limiter = new RateLimiter({ windowMs: 1000, max: 5, keyPrefix: 'api' });
|
|
14
|
+
expect(limiter.windowMs).toBe(1000);
|
|
15
|
+
expect(limiter.max).toBe(5);
|
|
16
|
+
expect(limiter.keyPrefix).toBe('api');
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('check', () => {
|
|
21
|
+
it('allows requests under the limit', () => {
|
|
22
|
+
const limiter = new RateLimiter({ windowMs: 60000, max: 3 });
|
|
23
|
+
const result = limiter.check('user1');
|
|
24
|
+
expect(result.allowed).toBe(true);
|
|
25
|
+
expect(result.remaining).toBe(2);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('tracks remaining count correctly across multiple requests', () => {
|
|
29
|
+
const limiter = new RateLimiter({ windowMs: 60000, max: 3 });
|
|
30
|
+
limiter.check('user1');
|
|
31
|
+
limiter.check('user1');
|
|
32
|
+
const result = limiter.check('user1');
|
|
33
|
+
expect(result.allowed).toBe(true);
|
|
34
|
+
expect(result.remaining).toBe(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('blocks request when limit is exceeded', () => {
|
|
38
|
+
const limiter = new RateLimiter({ windowMs: 60000, max: 2 });
|
|
39
|
+
limiter.check('user1');
|
|
40
|
+
limiter.check('user1');
|
|
41
|
+
const result = limiter.check('user1');
|
|
42
|
+
expect(result.allowed).toBe(false);
|
|
43
|
+
expect(result.remaining).toBe(0);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('returns retryAfter when blocked', () => {
|
|
47
|
+
const limiter = new RateLimiter({ windowMs: 60000, max: 1 });
|
|
48
|
+
limiter.check('user1');
|
|
49
|
+
const result = limiter.check('user1');
|
|
50
|
+
expect(result.allowed).toBe(false);
|
|
51
|
+
expect(result.retryAfter).toBeGreaterThan(0);
|
|
52
|
+
expect(result.retryAfter).toBeLessThanOrEqual(60);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('does not include retryAfter when allowed', () => {
|
|
56
|
+
const limiter = new RateLimiter({ windowMs: 60000, max: 5 });
|
|
57
|
+
const result = limiter.check('user1');
|
|
58
|
+
expect(result.retryAfter).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns resetAt as a future timestamp', () => {
|
|
62
|
+
const limiter = new RateLimiter({ windowMs: 60000, max: 5 });
|
|
63
|
+
const before = Date.now();
|
|
64
|
+
const result = limiter.check('user1');
|
|
65
|
+
expect(result.resetAt).toBeGreaterThan(before);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('isolates counts per key', () => {
|
|
69
|
+
const limiter = new RateLimiter({ windowMs: 60000, max: 2 });
|
|
70
|
+
limiter.check('user1');
|
|
71
|
+
limiter.check('user1');
|
|
72
|
+
const result = limiter.check('user2');
|
|
73
|
+
expect(result.allowed).toBe(true);
|
|
74
|
+
expect(result.remaining).toBe(1);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('slides the window and allows requests after old ones expire', () => {
|
|
78
|
+
jest.useFakeTimers();
|
|
79
|
+
const limiter = new RateLimiter({ windowMs: 1000, max: 2 });
|
|
80
|
+
limiter.check('user1');
|
|
81
|
+
limiter.check('user1');
|
|
82
|
+
expect(limiter.check('user1').allowed).toBe(false);
|
|
83
|
+
jest.advanceTimersByTime(1001);
|
|
84
|
+
expect(limiter.check('user1').allowed).toBe(true);
|
|
85
|
+
jest.useRealTimers();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('reset', () => {
|
|
90
|
+
it('clears the counter for a key', () => {
|
|
91
|
+
const limiter = new RateLimiter({ windowMs: 60000, max: 2 });
|
|
92
|
+
limiter.check('user1');
|
|
93
|
+
limiter.check('user1');
|
|
94
|
+
limiter.reset('user1');
|
|
95
|
+
const result = limiter.check('user1');
|
|
96
|
+
expect(result.allowed).toBe(true);
|
|
97
|
+
expect(result.remaining).toBe(1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('handles reset on non-existent key without error', () => {
|
|
101
|
+
const limiter = new RateLimiter({ windowMs: 60000, max: 5 });
|
|
102
|
+
expect(() => limiter.reset('nonexistent')).not.toThrow();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('middleware', () => {
|
|
107
|
+
function makeReqRes(ip = '127.0.0.1', user = null) {
|
|
108
|
+
const headers = {};
|
|
109
|
+
const res = {
|
|
110
|
+
set: jest.fn((k, v) => { headers[k] = v; }),
|
|
111
|
+
status: jest.fn().mockReturnThis(),
|
|
112
|
+
json: jest.fn(),
|
|
113
|
+
_headers: headers,
|
|
114
|
+
};
|
|
115
|
+
const req = { ip, user };
|
|
116
|
+
return { req, res };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
it('calls next() when request is allowed', () => {
|
|
120
|
+
const limiter = new RateLimiter({ windowMs: 60000, max: 5 });
|
|
121
|
+
const { req, res } = makeReqRes();
|
|
122
|
+
const next = jest.fn();
|
|
123
|
+
limiter.middleware()(req, res, next);
|
|
124
|
+
expect(next).toHaveBeenCalled();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('sets rate limit headers on allowed request', () => {
|
|
128
|
+
const limiter = new RateLimiter({ windowMs: 60000, max: 5 });
|
|
129
|
+
const { req, res } = makeReqRes();
|
|
130
|
+
const next = jest.fn();
|
|
131
|
+
limiter.middleware()(req, res, next);
|
|
132
|
+
expect(res.set).toHaveBeenCalledWith('X-RateLimit-Limit', '5');
|
|
133
|
+
expect(res.set).toHaveBeenCalledWith('X-RateLimit-Remaining', '4');
|
|
134
|
+
expect(res.set).toHaveBeenCalledWith('X-RateLimit-Reset', expect.any(String));
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('returns 429 when limit exceeded', () => {
|
|
138
|
+
const limiter = new RateLimiter({ windowMs: 60000, max: 1 });
|
|
139
|
+
const { req, res } = makeReqRes();
|
|
140
|
+
const next = jest.fn();
|
|
141
|
+
limiter.middleware()(req, res, next);
|
|
142
|
+
limiter.middleware()(req, res, next);
|
|
143
|
+
expect(res.status).toHaveBeenCalledWith(429);
|
|
144
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
145
|
+
expect.objectContaining({ error: 'errors.rateLimit.exceeded', retryAfter: expect.any(Number) })
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('uses req.user.id as key when user is present', () => {
|
|
150
|
+
const limiter = new RateLimiter({ windowMs: 60000, max: 1 });
|
|
151
|
+
const { req: req1, res: res1 } = makeReqRes('1.1.1.1', { id: 42 });
|
|
152
|
+
const { req: req2, res: res2 } = makeReqRes('2.2.2.2', { id: 42 });
|
|
153
|
+
const next = jest.fn();
|
|
154
|
+
limiter.middleware()(req1, res1, next);
|
|
155
|
+
next.mockClear();
|
|
156
|
+
limiter.middleware()(req2, res2, next);
|
|
157
|
+
expect(next).not.toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('uses req.ip as key when no user', () => {
|
|
161
|
+
const limiter = new RateLimiter({ windowMs: 60000, max: 1 });
|
|
162
|
+
const { req: req1, res: res1 } = makeReqRes('1.1.1.1');
|
|
163
|
+
const { req: req2, res: res2 } = makeReqRes('2.2.2.2');
|
|
164
|
+
const next = jest.fn();
|
|
165
|
+
limiter.middleware()(req1, res1, next);
|
|
166
|
+
next.mockClear();
|
|
167
|
+
limiter.middleware()(req2, res2, next);
|
|
168
|
+
expect(next).toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const { Vec3 } = require('vec3');
|
|
2
|
+
|
|
3
|
+
function createBotApi(bot, options = {}) {
|
|
4
|
+
const { enableLogging = false } = options;
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
sendMessage(chatType, messageText, recipient) {
|
|
8
|
+
if (!bot || !bot.messageQueue) {
|
|
9
|
+
if (enableLogging) {
|
|
10
|
+
log('[BotApi] Bot not ready for sendMessage');
|
|
11
|
+
}
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
bot.messageQueue.enqueue(chatType, messageText, recipient);
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
executeCommand(command) {
|
|
18
|
+
if (!bot || !bot.messageQueue) {
|
|
19
|
+
if (enableLogging) {
|
|
20
|
+
log('[BotApi] Bot not ready for executeCommand');
|
|
21
|
+
}
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
bot.messageQueue.enqueue('command', command);
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async lookAt(x, y, z) {
|
|
28
|
+
if (!bot) return;
|
|
29
|
+
const target = new Vec3(x, y, z);
|
|
30
|
+
await bot.lookAt(target);
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
async navigate(x, y, z) {
|
|
34
|
+
if (!bot || !bot.pathfinder) return;
|
|
35
|
+
const GoalBlock = require('mineflayer-pathfinder').goals.GoalBlock;
|
|
36
|
+
const goal = new GoalBlock(x, y, z);
|
|
37
|
+
await bot.pathfinder.goto(goal);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
attack(entityId) {
|
|
41
|
+
if (!bot) return;
|
|
42
|
+
const entity = bot.entities[entityId];
|
|
43
|
+
if (entity) bot.attack(entity);
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
follow(username) {
|
|
47
|
+
if (!bot || !bot.pathfinder) return;
|
|
48
|
+
const player = bot.players[username];
|
|
49
|
+
if (player && player.entity) {
|
|
50
|
+
const GoalFollow = require('mineflayer-pathfinder').goals.GoalFollow;
|
|
51
|
+
const goal = new GoalFollow(player.entity, 3);
|
|
52
|
+
bot.pathfinder.setGoal(goal, true);
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
stopFollow() {
|
|
57
|
+
if (bot && bot.pathfinder) {
|
|
58
|
+
bot.pathfinder.setGoal(null);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function log(message) {
|
|
65
|
+
if (process.send) {
|
|
66
|
+
process.send({ type: 'log', content: message });
|
|
67
|
+
} else {
|
|
68
|
+
console.log(message);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { createBotApi };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const MessageTypes = {
|
|
2
|
+
PLUGIN: {
|
|
3
|
+
UI_START_UPDATES: 'plugin:ui:start-updates',
|
|
4
|
+
UI_DATA: 'plugin:data',
|
|
5
|
+
},
|
|
6
|
+
|
|
7
|
+
SYSTEM: {
|
|
8
|
+
GET_PLAYER_LIST: 'system:get_player_list',
|
|
9
|
+
GET_PLAYER_LIST_RESPONSE: 'get_player_list_response',
|
|
10
|
+
GET_NEARBY_ENTITIES: 'system:get_nearby_entities',
|
|
11
|
+
GET_NEARBY_ENTITIES_RESPONSE: 'get_nearby_entities_response',
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
VIEWER: {
|
|
15
|
+
GET_STATE: 'viewer:get_state',
|
|
16
|
+
STATE_RESPONSE: 'viewer:state_response',
|
|
17
|
+
CONTROL: 'viewer:control',
|
|
18
|
+
CHAT: 'viewer:chat',
|
|
19
|
+
SPAWN: 'viewer:spawn',
|
|
20
|
+
HEALTH: 'viewer:health',
|
|
21
|
+
MOVE: 'viewer:move',
|
|
22
|
+
BLOCK_UPDATE: 'viewer:blockUpdate',
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
BOT: {
|
|
26
|
+
READY: 'bot_ready',
|
|
27
|
+
STATUS: 'status',
|
|
28
|
+
LOG: 'log',
|
|
29
|
+
STOP: 'stop',
|
|
30
|
+
START: 'start',
|
|
31
|
+
RESTART: 'restart_bot',
|
|
32
|
+
CHANGE_CREDENTIALS: 'change_credentials',
|
|
33
|
+
UPDATE_CREDENTIALS: 'update_credentials',
|
|
34
|
+
EVENT: 'event',
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
GRAPH: {
|
|
38
|
+
EXECUTE_EVENT_GRAPH: 'execute_event_graph',
|
|
39
|
+
EXECUTE_HANDLER: 'execute_handler',
|
|
40
|
+
EXECUTE_COMMAND_REQUEST: 'execute_command_request',
|
|
41
|
+
EXECUTE_COMMAND_RESPONSE: 'execute_command_response',
|
|
42
|
+
TRACE_COMPLETED: 'trace:completed',
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
DEBUG: {
|
|
46
|
+
CHECK_BREAKPOINT: 'debug:check_breakpoint',
|
|
47
|
+
BREAKPOINT_RESPONSE: 'debug:breakpoint_response',
|
|
48
|
+
CHECK_STEP_MODE: 'debug:check_step_mode',
|
|
49
|
+
STEP_RESPONSE: 'debug:step_response',
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
COMMAND: {
|
|
53
|
+
REGISTER: 'register_command',
|
|
54
|
+
REGISTER_TEMP: 'register_temp_command',
|
|
55
|
+
UNREGISTER_TEMP: 'unregister_temp_command',
|
|
56
|
+
VALIDATE_AND_RUN: 'validate_and_run_command',
|
|
57
|
+
HANDLE_PERMISSION_ERROR: 'handle_permission_error',
|
|
58
|
+
HANDLE_WRONG_CHAT: 'handle_wrong_chat',
|
|
59
|
+
HANDLE_COOLDOWN: 'handle_cooldown',
|
|
60
|
+
HANDLE_BLACKLIST: 'handle_blacklist',
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
CONFIG: {
|
|
64
|
+
RELOAD: 'config:reload',
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
CHAT: {
|
|
68
|
+
SEND_MESSAGE: 'send_message',
|
|
69
|
+
CHAT: 'chat',
|
|
70
|
+
ACTION: 'action',
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
SERVER: {
|
|
74
|
+
COMMAND: 'server_command',
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
PLUGINS: {
|
|
78
|
+
RELOAD: 'plugins:reload',
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
USER: {
|
|
82
|
+
ACTION_RESPONSE: 'user_action_response',
|
|
83
|
+
REQUEST_ACTION: 'request_user_action',
|
|
84
|
+
INVALIDATE_USER_CACHE: 'invalidate_user_cache',
|
|
85
|
+
INVALIDATE_ALL_USER_CACHE: 'invalidate_all_user_cache',
|
|
86
|
+
CREDENTIALS_OPERATION_RESPONSE: 'credentials_operation_response',
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
WEBSOCKET: {
|
|
90
|
+
SEND_MESSAGE: 'send_websocket_message',
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const EventTypes = {
|
|
95
|
+
LOG: 'log',
|
|
96
|
+
EVENT: 'event',
|
|
97
|
+
BOT_READY: 'bot_ready',
|
|
98
|
+
STATUS: 'status',
|
|
99
|
+
CHAT: 'chat',
|
|
100
|
+
WHISPER: 'whisper',
|
|
101
|
+
RAW_MESSAGE: 'raw_message',
|
|
102
|
+
PLAYER_JOINED: 'playerJoined',
|
|
103
|
+
PLAYER_LEFT: 'playerLeft',
|
|
104
|
+
ENTITY_SPAWN: 'entitySpawn',
|
|
105
|
+
ENTITY_MOVED: 'entityMoved',
|
|
106
|
+
ENTITY_GONE: 'entityGone',
|
|
107
|
+
BOT_DIED: 'botDied',
|
|
108
|
+
HEALTH: 'health',
|
|
109
|
+
KICKED: 'kicked',
|
|
110
|
+
ERROR: 'error',
|
|
111
|
+
END: 'end',
|
|
112
|
+
SPAWN: 'spawn',
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
module.exports = { MessageTypes, EventTypes };
|