@pilaf/backends 1.0.1 → 1.2.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.
@@ -0,0 +1,366 @@
1
+ /**
2
+ * PatternRegistry - Centralized pattern management for log parsing
3
+ *
4
+ * Responsibilities (MECE):
5
+ * - ONLY: Store, compile, and match regex patterns
6
+ * - NOT: Parse log lines (LogParser's responsibility - uses this registry)
7
+ * - NOT: Emit events (LogMonitor's responsibility)
8
+ *
9
+ * Design Principles:
10
+ * - Lazy compilation: Regex compiled on first use (performance)
11
+ * - Priority ordering: Patterns tested in order of addition
12
+ * - Named capture groups: Support for extracting named data
13
+ * - Thread-safe: Safe for concurrent access
14
+ *
15
+ * Usage:
16
+ * const registry = new PatternRegistry();
17
+ * registry.addPattern('teleport', /Teleported (\w+) to (.+)/, (match) => ({
18
+ * player: match[1],
19
+ * destination: match[2]
20
+ * }));
21
+ * const result = registry.match(logLine);
22
+ */
23
+
24
+ class PatternRegistry {
25
+ /**
26
+ * Create a PatternRegistry
27
+ * @param {Object} options - Registry options
28
+ * @param {boolean} [options.caseInsensitive=false] - Make all patterns case-insensitive
29
+ */
30
+ constructor(options = {}) {
31
+ /**
32
+ * Map of pattern name to pattern definition
33
+ * @private
34
+ * @type {Map<string, Object>}
35
+ */
36
+ this._patterns = new Map();
37
+
38
+ /**
39
+ * Ordered array of pattern names (for priority ordering)
40
+ * @private
41
+ * @type {Array<string>}
42
+ */
43
+ this._patternOrder = [];
44
+
45
+ /**
46
+ * Compiled regex cache
47
+ * @private
48
+ * @type {Map<string, RegExp>}
49
+ */
50
+ this._compiled = new Map();
51
+
52
+ /**
53
+ * Case-insensitive flag
54
+ * @private
55
+ * @type {boolean}
56
+ */
57
+ this._caseInsensitive = options?.caseInsensitive || false;
58
+ }
59
+
60
+ /**
61
+ * Add a pattern to the registry
62
+ *
63
+ * @param {string} name - Unique pattern name/identifier
64
+ * @param {RegExp|string} pattern - Regex pattern or string (will be converted to RegExp)
65
+ * @param {Function} handler - Handler function: (match: RegExpMatchArray) => Object
66
+ * @param {number} [priority] - Optional priority (lower = higher priority, default: append to end)
67
+ * @returns {void}
68
+ * @throws {Error} If pattern name already exists
69
+ * @throws {Error} If pattern is invalid
70
+ * @throws {Error} If handler is not a function
71
+ *
72
+ * @example
73
+ * // Add with regex
74
+ * registry.addPattern('teleport', /Teleported (\w+) to (.+)/, (match) => ({
75
+ * player: match[1],
76
+ * position: match[2]
77
+ * }));
78
+ *
79
+ * // Add with string (converted to regex)
80
+ * registry.addPattern('join', 'UUID of player .* is (.+)', (match) => ({
81
+ * username: match[1]
82
+ * }));
83
+ */
84
+ addPattern(name, pattern, handler, priority) {
85
+ // Validate name
86
+ if (typeof name !== 'string' || name.trim() === '') {
87
+ throw new Error('Pattern name must be a non-empty string');
88
+ }
89
+
90
+ // Check for duplicate
91
+ if (this._patterns.has(name)) {
92
+ throw new Error(`Pattern "${name}" already exists`);
93
+ }
94
+
95
+ // Validate pattern
96
+ if (!(pattern instanceof RegExp) && typeof pattern !== 'string') {
97
+ throw new Error('Pattern must be a RegExp or string');
98
+ }
99
+
100
+ // Validate handler
101
+ if (typeof handler !== 'function') {
102
+ throw new Error('Handler must be a function');
103
+ }
104
+
105
+ // Convert string to RegExp if needed
106
+ let regexPattern = pattern;
107
+ if (typeof pattern === 'string') {
108
+ const flags = this._caseInsensitive ? 'i' : '';
109
+ regexPattern = new RegExp(pattern, flags);
110
+ }
111
+
112
+ // Store pattern definition
113
+ this._patterns.set(name, {
114
+ name,
115
+ pattern: regexPattern,
116
+ handler,
117
+ priority: priority ?? this._patternOrder.length
118
+ });
119
+
120
+ // Update order
121
+ if (priority !== undefined) {
122
+ this._patternOrder.push(name);
123
+ this._patternOrder.sort((a, b) => {
124
+ const pA = this._patterns.get(a).priority;
125
+ const pB = this._patterns.get(b).priority;
126
+ return pA - pB;
127
+ });
128
+ } else {
129
+ this._patternOrder.push(name);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Remove a pattern from the registry
135
+ *
136
+ * @param {string} name - Pattern name to remove
137
+ * @returns {boolean} - True if pattern was removed, false if not found
138
+ */
139
+ removePattern(name) {
140
+ if (!this._patterns.has(name)) {
141
+ return false;
142
+ }
143
+
144
+ this._patterns.delete(name);
145
+ this._compiled.delete(name);
146
+ this._patternOrder = this._patternOrder.filter(n => n !== name);
147
+
148
+ return true;
149
+ }
150
+
151
+ /**
152
+ * Get a pattern by name
153
+ *
154
+ * @param {string} name - Pattern name
155
+ * @returns {Object|null} - Pattern definition or null if not found
156
+ * @property {RegExp} pattern - The regex pattern
157
+ * @property {Function} handler - The handler function
158
+ */
159
+ getPattern(name) {
160
+ return this._patterns.get(name) || null;
161
+ }
162
+
163
+ /**
164
+ * Get all pattern names
165
+ *
166
+ * @returns {Array<string>} - Array of pattern names in priority order
167
+ */
168
+ getPatterns() {
169
+ return [...this._patternOrder];
170
+ }
171
+
172
+ /**
173
+ * Get the count of registered patterns
174
+ *
175
+ * @returns {number} - Number of patterns
176
+ */
177
+ get size() {
178
+ return this._patterns.size;
179
+ }
180
+
181
+ /**
182
+ * Clear all patterns
183
+ *
184
+ * @returns {void}
185
+ */
186
+ clear() {
187
+ this._patterns.clear();
188
+ this._compiled.clear();
189
+ this._patternOrder = [];
190
+ }
191
+
192
+ /**
193
+ * Match a line against all registered patterns
194
+ *
195
+ * Tests patterns in priority order and returns the first match.
196
+ * Returns null if no patterns match.
197
+ *
198
+ * @param {string} line - Log line to match
199
+ * @returns {Object|null} - Match result or null if no match
200
+ * @property {string} name - Name of the pattern that matched
201
+ * @property {*} data - Data returned by the pattern's handler
202
+ * @property {RegExpMatchArray} match - The regex match result
203
+ * @property {string} raw - Original input line
204
+ *
205
+ * @example
206
+ * const result = registry.match('[12:34:56] Teleported TestPlayer to 100.0, 64.0, 100.0');
207
+ * // Returns:
208
+ * // {
209
+ * // name: 'teleport',
210
+ * // data: { player: 'TestPlayer', position: '100.0, 64.0, 100.0' },
211
+ * // match: RegExpMatchArray,
212
+ * // raw: '[12:34:56] Teleported TestPlayer to 100.0, 64.0, 100.0'
213
+ * // }
214
+ */
215
+ match(line) {
216
+ if (typeof line !== 'string') {
217
+ return null;
218
+ }
219
+
220
+ // Test each pattern in priority order
221
+ for (const name of this._patternOrder) {
222
+ const patternDef = this._patterns.get(name);
223
+ const regex = this._getCompiledPattern(name, patternDef.pattern);
224
+
225
+ const match = line.match(regex);
226
+ if (match) {
227
+ try {
228
+ const data = patternDef.handler(match);
229
+ return {
230
+ name,
231
+ data,
232
+ match,
233
+ raw: line
234
+ };
235
+ } catch (error) {
236
+ // Handler failed - continue to next pattern
237
+ // (Or we could emit an error event)
238
+ continue;
239
+ }
240
+ }
241
+ }
242
+
243
+ return null;
244
+ }
245
+
246
+ /**
247
+ * Match a line against a specific pattern
248
+ *
249
+ * @param {string} line - Log line to match
250
+ * @param {string} patternName - Name of pattern to match against
251
+ * @returns {Object|null} - Match result or null if no match
252
+ */
253
+ matchPattern(line, patternName) {
254
+ const patternDef = this._patterns.get(patternName);
255
+ if (!patternDef) {
256
+ return null;
257
+ }
258
+
259
+ const regex = this._getCompiledPattern(patternName, patternDef.pattern);
260
+ const match = line.match(regex);
261
+
262
+ if (!match) {
263
+ return null;
264
+ }
265
+
266
+ try {
267
+ const data = patternDef.handler(match);
268
+ return {
269
+ name: patternName,
270
+ data,
271
+ match,
272
+ raw: line
273
+ };
274
+ } catch (error) {
275
+ return null;
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Test if a pattern matches (without executing handler)
281
+ *
282
+ * @param {string} line - Log line to test
283
+ * @param {string} patternName - Name of pattern to test
284
+ * @returns {boolean} - True if pattern matches
285
+ */
286
+ test(line, patternName) {
287
+ const patternDef = this._patterns.get(patternName);
288
+ if (!patternDef) {
289
+ return false;
290
+ }
291
+
292
+ const regex = this._getCompiledPattern(patternName, patternDef.pattern);
293
+ return regex.test(line);
294
+ }
295
+
296
+ /**
297
+ * Get compiled regex for a pattern (lazy compilation)
298
+ *
299
+ * @private
300
+ * @param {string} name - Pattern name
301
+ * @param {RegExp} pattern - The regex pattern
302
+ * @returns {RegExp} - Compiled regex (cached or newly compiled)
303
+ */
304
+ _getCompiledPattern(name, pattern) {
305
+ // Check cache first
306
+ if (this._compiled.has(name)) {
307
+ return this._compiled.get(name);
308
+ }
309
+
310
+ // For string patterns, we've already converted to RegExp in addPattern
311
+ // For RegExp patterns, use as-is
312
+ this._compiled.set(name, pattern);
313
+ return pattern;
314
+ }
315
+
316
+ /**
317
+ * Clone the registry (creates a new instance with same patterns)
318
+ *
319
+ * @returns {PatternRegistry} - New registry with copied patterns
320
+ */
321
+ clone() {
322
+ const cloned = new PatternRegistry({
323
+ caseInsensitive: this._caseInsensitive
324
+ });
325
+
326
+ // Copy all patterns
327
+ for (const [name, patternDef] of this._patterns) {
328
+ cloned.addPattern(
329
+ name,
330
+ patternDef.pattern,
331
+ patternDef.handler,
332
+ patternDef.priority
333
+ );
334
+ }
335
+
336
+ return cloned;
337
+ }
338
+
339
+ /**
340
+ * Export patterns as JSON (for serialization)
341
+ *
342
+ * Note: Handlers cannot be serialized (functions are lost)
343
+ *
344
+ * @returns {Object} - JSON-serializable representation
345
+ */
346
+ toJSON() {
347
+ const patterns = {};
348
+
349
+ for (const [name, patternDef] of this._patterns) {
350
+ patterns[name] = {
351
+ name,
352
+ pattern: patternDef.pattern.toString(),
353
+ priority: patternDef.priority
354
+ // Note: handler function is lost
355
+ };
356
+ }
357
+
358
+ return {
359
+ caseInsensitive: this._caseInsensitive,
360
+ patternCount: this._patterns.size,
361
+ patterns
362
+ };
363
+ }
364
+ }
365
+
366
+ module.exports = { PatternRegistry };
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Minecraft Log Fixtures
3
+ *
4
+ * Real log lines from Minecraft servers 1.19, 1.20, 1.21+
5
+ * Used for testing MinecraftLogParser
6
+ */
7
+
8
+ module.exports = {
9
+ // ============================================================================
10
+ // STATUS EVENTS (Server Lifecycle)
11
+ // ============================================================================
12
+
13
+ status: {
14
+ starting: [
15
+ '[12:34:56] [Server thread/INFO]: Starting minecraft server version 1.20.1',
16
+ '[12:34:56] [Server thread/INFO]: Starting minecraft server on *:25565',
17
+ '[12:34:56] [Server thread/INFO]: Loading properties',
18
+ '[12:34:56] [Server thread/INFO]: Default game type: SURVIVAL',
19
+ '[12:34:56] [Server thread/INFO]: Generating keypair',
20
+ '[12:34:56] [Worker-Main-1/INFO]: Starting minecraft server version 1.19.4',
21
+ '[12:34:56] [Worker-Main-2/INFO]: Starting minecraft server version 1.21'
22
+ ],
23
+ preparing: [
24
+ '[12:34:56] [Server thread/INFO]: Preparing level "world"',
25
+ '[12:34:56] [Server thread/INFO]: Preparing start region for dimension minecraft:overworld',
26
+ '[12:34:56] [Server thread/INFO]: Preparing start region for level 0'
27
+ ],
28
+ done: [
29
+ '[12:34:56] [Server thread/INFO]: Done (3.452s)! For help, type "help"',
30
+ '[12:34:56] [Server thread/INFO]: Done (5.123s)! For help, type "help"',
31
+ '[12:34:56] [Worker-Main-3/INFO]: Time elapsed: 3422 ms'
32
+ ]
33
+ },
34
+
35
+ // ============================================================================
36
+ // ENTITY EVENTS (Player join/leave/death/spawn)
37
+ // ============================================================================
38
+
39
+ entity: {
40
+ join: [
41
+ '[12:34:56] [Server thread/INFO]: TestPlayer joined the game',
42
+ '[12:34:56] [Server thread/INFO]: Steve joined the game',
43
+ '[12:34:56] [Server thread/INFO]: Alex joined the game'
44
+ ],
45
+ leave: [
46
+ '[12:34:56] [Server thread/INFO]: TestPlayer lost connection: Disconnected',
47
+ '[12:34:56] [Server thread/INFO]: Steve lost connection: Timed out',
48
+ '[12:34:56] [Server thread/INFO]: Alex lost connection: Internal Exception: java.io.IOException'
49
+ ],
50
+ spawn: [
51
+ '[12:34:56] [Server thread/INFO]: UUID of player TestPlayer is abc123-def456-7890-abcd-ef1234567890',
52
+ '[12:34:56] [Server thread/INFO]: UUID of player Steve is 12345678-1234-5678-1234-5678123456789'
53
+ ],
54
+ death: {
55
+ slain: [
56
+ '[12:34:56] [Server thread/INFO]: TestPlayer was slain by Zombie',
57
+ '[12:34:56] [Server thread/INFO]: Steve was slain by Skeleton',
58
+ '[12:34:56] [Server thread/INFO]: Alex was slain by Spider',
59
+ '[12:34:56] [Server thread/INFO]: TestPlayer was slain by Creeper',
60
+ '[12:34:56] [Server thread/INFO]: Player was slain by Husk',
61
+ '[12:34:56] [Server thread/INFO]: TestPlayer was slain by Phantom'
62
+ ],
63
+ fall: [
64
+ '[12:34:56] [Server thread/INFO]: TestPlayer fell from a high place',
65
+ '[12:34:56] [Server thread/INFO]: Steve fell from a high place'
66
+ ],
67
+ fire: [
68
+ '[12:34:56] [Server thread/INFO]: TestPlayer burned to death',
69
+ '[12:34:56] [Server thread/INFO]: Alex was burnt to a crisp whilst fighting Skeleton'
70
+ ],
71
+ lava: [
72
+ '[12:34:56] [Server thread/INFO]: TestPlayer tried to swim in lava',
73
+ '[12:34:56] [Server thread/INFO]: Steve was killed by Magma Block',
74
+ '[12:34:56] [Server thread/INFO]: Alex was killed by Lava'
75
+ ],
76
+ drown: [
77
+ '[12:34:56] [Server thread/INFO]: TestPlayer drowned',
78
+ '[12:34:56] [Server thread/INFO]: Steve drowned'
79
+ ],
80
+ sprint: [
81
+ '[12:34:56] [Server thread/INFO]: TestPlayer splatted against a wall'
82
+ ],
83
+ generic: [
84
+ '[12:34:56] [Server thread/INFO]: TestPlayer died',
85
+ '[12:34:56] [Server thread/INFO]: Steve died'
86
+ ]
87
+ }
88
+ },
89
+
90
+ // ============================================================================
91
+ // MOVEMENT EVENTS (Teleport)
92
+ // ============================================================================
93
+
94
+ movement: {
95
+ teleport: [
96
+ '[12:34:56] [Server thread/INFO]: Teleported TestPlayer from 100.5, 64.0, 200.3 to 150.2, 70.0, -300.5',
97
+ '[12:34:56] [Server thread/INFO]: Teleported Steve from -50.0, 80.0, 100.0 to 50.0, 64.0, -50.0',
98
+ '[12:34:56] [Server thread/INFO]: Teleported Alex from 0.5, 100.0, 0.5 to 100.0, 64.0, 200.0',
99
+ '[12:34:56] [Server thread/INFO]: Teleported TestPlayer1 from 256.5, 64.0, -128.9 to -256.5, 70.0, 128.9',
100
+ '[12:34:56] [Server thread/INFO]: Teleported NPC from 10, 70, 20 to 100, 65, -100'
101
+ ]
102
+ },
103
+
104
+ // ============================================================================
105
+ // COMMAND EVENTS (Server commands)
106
+ // ============================================================================
107
+
108
+ command: {
109
+ issued: [
110
+ '[12:34:56] [Server thread/INFO]: TestPlayer issued server command: /gamemode creative',
111
+ '[12:34:56] [Server thread/INFO]: Steve issued server command: /time set 1000',
112
+ '[12:34:56] [Server thread/INFO]: Alex issued server command: /weather rain',
113
+ '[12:34:56] [Server thread/INFO]: TestPlayer issued server command: /tp Steve 100 64 100',
114
+ '[12:34:56] [Server thread/INFO]: OP issued server command: /stop',
115
+ '[12:34:56] [Server thread/INFO]: Server issued server command: /save-all'
116
+ ]
117
+ },
118
+
119
+ // ============================================================================
120
+ // WORLD EVENTS (Time/weather/save)
121
+ // ============================================================================
122
+
123
+ world: {
124
+ time: [
125
+ '[12:34:56] [Server thread/INFO]: Changing the time to 1000',
126
+ '[12:34:56] [Server thread/INFO]: Changing the time to 13000',
127
+ '[12:34:56] [Server thread/INFO]: Changing the time to 18000'
128
+ ],
129
+ weather: [
130
+ '[12:34:56] [Server thread/INFO]: Changing the weather to rain',
131
+ '[12:34:56] [Server thread/INFO]: Changing the weather to thunder',
132
+ '[12:34:56] [Server thread/INFO]: Changing the weather to clear'
133
+ ],
134
+ difficulty: [
135
+ '[12:34:56] [Server thread/INFO]: Changing the difficulty to hard',
136
+ '[12:34:56] [Server thread/INFO]: Changing the difficulty to easy',
137
+ '[12:34:56] [Server thread/INFO]: Changing the difficulty to normal'
138
+ ],
139
+ gamemode: [
140
+ '[12:34:56] [Server thread/INFO]: The game mode has been updated to Creative',
141
+ '[12:34:56] [Server thread/INFO]: Gamemode has been updated to Survival Mode',
142
+ '[12:34:56] [Server thread/INFO]: The game mode has been updated to Spectator'
143
+ ],
144
+ save: [
145
+ '[12:34:56] [Server thread/INFO]: Saving the game',
146
+ '[12:34:56] [Server thread/INFO]: Saving the game in level 0',
147
+ '[12:34:56] [Server thread/INFO]: Saved the game',
148
+ '[12:34:56] [Server thread/INFO]: Saving chunks for level \'world\'/minecraft:overworld'
149
+ ]
150
+ },
151
+
152
+ // ============================================================================
153
+ // MIXED EVENTS (for testing priority ordering)
154
+ // ============================================================================
155
+
156
+ mixed: [
157
+ // Teleport should match over other entity events (priority test)
158
+ '[12:34:56] [Server thread/INFO]: Teleported TestPlayer from 100.0, 64.0, 200.0 to 150.0, 70.0, -300.0',
159
+ // Death should match even if contains command-like text
160
+ '[12:34:56] [Server thread/INFO]: TestPlayer was slain by Zombie using /kill'
161
+ ],
162
+
163
+ // ============================================================================
164
+ // EDGE CASES AND VARIATIONS
165
+ // ============================================================================
166
+
167
+ edge: [
168
+ // Different thread names
169
+ '[12:34:56] [Worker-Main-1/INFO]: TestPlayer joined the game',
170
+ '[12:34:56] [Worker-Main-2/INFO]: Steve issued server command: /gamemode creative',
171
+ '[12:34:56] [Server-Add-Worker-1/INFO]: Saving the game',
172
+
173
+ // Unicode player names
174
+ '[12:34:56] [Server thread/INFO]: 张三 joined the game',
175
+ '[12:34:56] [Server thread/INFO]: игрок joined the game',
176
+
177
+ // Long coordinates (negative values)
178
+ '[12:34:56] [Server thread/INFO]: Teleported TestPlayer from -1024.5, 128.0, 2048.9 to 512.0, -60.0, -512.0',
179
+
180
+ // Multi-word death messages
181
+ '[12:34:56] [Server thread/INFO]: TestPlayer was shot by a Skeleton',
182
+ '[12:34:56] [Server thread/INFO]: TestPlayer was blown up by a Creeper',
183
+
184
+ // Unknown/unmatched patterns
185
+ '[12:34:56] [Server thread/INFO]: Some unknown log message that does not match any pattern',
186
+ '[12:34:56] [Server thread/WARN]: Can\'t keep up! Did the system time change, or is the server overloaded?'
187
+ ]
188
+ };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Log Parsers
3
+ *
4
+ * Concrete implementations of LogParser for various log formats.
5
+ */
6
+
7
+ const { PatternRegistry } = require('./PatternRegistry.js');
8
+ const { MinecraftLogParser } = require('./MinecraftLogParser.js');
9
+
10
+ module.exports = {
11
+ PatternRegistry,
12
+ MinecraftLogParser
13
+ };
@@ -20,6 +20,7 @@ class RconBackend extends PilafBackend {
20
20
  * @param {number} config.port - RCON port
21
21
  * @param {string} config.password - RCON password
22
22
  * @param {number} [config.timeout=30000] - Connection timeout in milliseconds
23
+ * @param {number} [config.maxRetries=5] - Max connection retry attempts
23
24
  * @returns {Promise<void>}
24
25
  */
25
26
  async connect(config) {
@@ -27,38 +28,53 @@ class RconBackend extends PilafBackend {
27
28
  const port = config?.port || 25575;
28
29
  const password = config?.password || '';
29
30
  const timeout = config?.timeout || 30000; // 30 second default timeout
30
-
31
- try {
32
- // Add timeout to prevent hanging on slow/unresponsive RCON servers
33
- // Store timeout ID so we can clear it on success
34
- const timeoutPromise = new Promise((_, reject) => {
35
- this._connectTimeout = setTimeout(() => {
31
+ const maxRetries = config?.maxRetries || 5; // Retry up to 5 times
32
+
33
+ let lastError = null;
34
+
35
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
36
+ try {
37
+ // Add timeout to prevent hanging on slow/unresponsive RCON servers
38
+ // Store timeout ID so we can clear it on success
39
+ const timeoutPromise = new Promise((_, reject) => {
40
+ this._connectTimeout = setTimeout(() => {
41
+ this._connectTimeout = null;
42
+ reject(new Error(`RCON connection timeout after ${timeout}ms`));
43
+ }, timeout);
44
+ });
45
+
46
+ this.client = await Promise.race([
47
+ Rcon.connect({ host, port, password }),
48
+ timeoutPromise
49
+ ]);
50
+
51
+ // Clear timeout if connection succeeded
52
+ if (this._connectTimeout) {
53
+ clearTimeout(this._connectTimeout);
36
54
  this._connectTimeout = null;
37
- reject(new Error(`RCON connection timeout after ${timeout}ms`));
38
- }, timeout);
39
- });
55
+ }
40
56
 
41
- this.client = await Promise.race([
42
- Rcon.connect({ host, port, password }),
43
- timeoutPromise
44
- ]);
57
+ this.connected = true;
58
+ return this;
59
+ } catch (error) {
60
+ // Clean up timeout on error
61
+ if (this._connectTimeout) {
62
+ clearTimeout(this._connectTimeout);
63
+ this._connectTimeout = null;
64
+ }
45
65
 
46
- // Clear timeout if connection succeeded
47
- if (this._connectTimeout) {
48
- clearTimeout(this._connectTimeout);
49
- this._connectTimeout = null;
50
- }
66
+ lastError = error;
51
67
 
52
- this.connected = true;
53
- return this;
54
- } catch (error) {
55
- // Clean up timeout on error
56
- if (this._connectTimeout) {
57
- clearTimeout(this._connectTimeout);
58
- this._connectTimeout = null;
68
+ // If not the last attempt, wait before retrying with exponential backoff
69
+ if (attempt < maxRetries) {
70
+ const waitTime = Math.min(1000 * Math.pow(2, attempt - 1), 5000); // Max 5 seconds
71
+ await new Promise(resolve => setTimeout(resolve, waitTime));
72
+ }
59
73
  }
60
- throw new Error(`Failed to connect to RCON: ${error.message}`);
61
74
  }
75
+
76
+ // All retries exhausted
77
+ throw new Error(`Failed to connect to RCON after ${maxRetries} attempts: ${lastError.message}`);
62
78
  }
63
79
 
64
80
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pilaf/backends",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "main": "lib/index.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,6 +18,7 @@
18
18
  "access": "public"
19
19
  },
20
20
  "dependencies": {
21
+ "dockerode": "^4.0.0",
21
22
  "mineflayer": "^4.20.1",
22
23
  "rcon-client": "^4.2.5"
23
24
  }