@pilaf/backends 1.0.2 → 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,267 @@
1
+ /**
2
+ * EventObserver - Observe and subscribe to Minecraft server events
3
+ *
4
+ * Wraps LogMonitor and MinecraftLogParser to provide a clean API for
5
+ * subscribing to Minecraft server events without manually parsing logs.
6
+ *
7
+ * Responsibilities (MECE):
8
+ * - ONLY: Provide clean API for event subscriptions
9
+ * - NOT: Collect logs (LogCollector's responsibility)
10
+ * - NOT: Parse logs (LogParser's responsibility)
11
+ * - NOT: Monitor logs (LogMonitor's responsibility)
12
+ *
13
+ * Usage Example:
14
+ * const observer = new EventObserver(logMonitor, parser);
15
+ * observer.onPlayerJoin((event) => {
16
+ * console.log('Player joined:', event.data.player);
17
+ * });
18
+ * await observer.start();
19
+ */
20
+
21
+ const { EventEmitter } = require('events');
22
+
23
+ /**
24
+ * EventObserver class for event subscriptions
25
+ * @extends EventEmitter
26
+ */
27
+ class EventObserver extends EventEmitter {
28
+ /**
29
+ * Create an EventObserver
30
+ *
31
+ * @param {Object} options - Observer options
32
+ * @param {Object} options.logMonitor - LogMonitor instance
33
+ * @param {Object} options.parser - LogParser instance
34
+ * @throws {Error} If required dependencies are not provided
35
+ */
36
+ constructor(options = {}) {
37
+ super();
38
+
39
+ if (!options.logMonitor) {
40
+ throw new Error('logMonitor is required');
41
+ }
42
+ if (!options.parser) {
43
+ throw new Error('parser is required');
44
+ }
45
+
46
+ /**
47
+ * LogMonitor instance for log collection
48
+ * @private
49
+ * @type {Object}
50
+ */
51
+ this._logMonitor = options.logMonitor;
52
+
53
+ /**
54
+ * LogParser instance for parsing logs
55
+ * @private
56
+ * @type {Object}
57
+ */
58
+ this._parser = options.parser;
59
+
60
+ /**
61
+ * Event subscriptions: pattern -> Set<callback>
62
+ * @private
63
+ * @type {Map<string, Set<Function>>}
64
+ */
65
+ this._subscriptions = new Map();
66
+
67
+ /**
68
+ * Whether observer is currently running
69
+ * @private
70
+ * @type {boolean}
71
+ */
72
+ this._isObserving = false;
73
+
74
+ // Set up event listener for log monitor
75
+ this._logMonitor.on('event', (event) => {
76
+ this._handleEvent(event);
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Subscribe to events matching a pattern
82
+ *
83
+ * @param {string} pattern - Event pattern (supports wildcards: "entity.*")
84
+ * @param {Function} callback - Callback function(event)
85
+ * @returns {Function} - Unsubscribe function
86
+ *
87
+ * @example
88
+ * const unsubscribe = observer.onEvent('entity.*', (event) => {
89
+ * console.log('Entity event:', event.type);
90
+ * });
91
+ */
92
+ onEvent(pattern, callback) {
93
+ if (!this._subscriptions.has(pattern)) {
94
+ this._subscriptions.set(pattern, new Set());
95
+ }
96
+ this._subscriptions.get(pattern).add(callback);
97
+
98
+ // Return unsubscribe function
99
+ return () => {
100
+ this._subscriptions.get(pattern)?.delete(callback);
101
+ if (this._subscriptions.get(pattern)?.size === 0) {
102
+ this._subscriptions.delete(pattern);
103
+ }
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Subscribe to player join events
109
+ *
110
+ * @param {Function} callback - Callback function(event)
111
+ * @returns {Function} - Unsubscribe function
112
+ */
113
+ onPlayerJoin(callback) {
114
+ return this.onEvent('entity.join', callback);
115
+ }
116
+
117
+ /**
118
+ * Subscribe to player leave events
119
+ *
120
+ * @param {Function} callback - Callback function(event)
121
+ * @returns {Function} - Unsubscribe function
122
+ */
123
+ onPlayerLeave(callback) {
124
+ return this.onEvent('entity.leave', callback);
125
+ }
126
+
127
+ /**
128
+ * Subscribe to player death events
129
+ *
130
+ * @param {Function} callback - Callback function(event)
131
+ * @returns {Function} - Unsubscribe function
132
+ */
133
+ onPlayerDeath(callback) {
134
+ return this.onEvent('entity.death.*', callback);
135
+ }
136
+
137
+ /**
138
+ * Subscribe to command events
139
+ *
140
+ * @param {Function} callback - Callback function(event)
141
+ * @returns {Function} - Unsubscribe function
142
+ */
143
+ onCommand(callback) {
144
+ return this.onEvent('command.*', callback);
145
+ }
146
+
147
+ /**
148
+ * Subscribe to world events (time, weather, save)
149
+ *
150
+ * @param {Function} callback - Callback function(event)
151
+ * @returns {Function} - Unsubscribe function
152
+ */
153
+ onWorldEvent(callback) {
154
+ return this.onEvent('world.*', callback);
155
+ }
156
+
157
+ /**
158
+ * Start observing events
159
+ *
160
+ * @returns {Promise<void>}
161
+ * @throws {Error} If already observing
162
+ */
163
+ async start() {
164
+ if (this._isObserving) {
165
+ throw new Error('EventObserver is already observing');
166
+ }
167
+
168
+ this._isObserving = true;
169
+ await this._logMonitor.start();
170
+ this.emit('start');
171
+ }
172
+
173
+ /**
174
+ * Stop observing events
175
+ *
176
+ * @returns {void}
177
+ */
178
+ stop() {
179
+ if (!this._isObserving) {
180
+ return;
181
+ }
182
+
183
+ this._isObserving = false;
184
+ this._logMonitor.stop();
185
+ this.emit('stop');
186
+ }
187
+
188
+ /**
189
+ * Check if observer is currently running
190
+ *
191
+ * @type {boolean}
192
+ */
193
+ get isObserving() {
194
+ return this._isObserving;
195
+ }
196
+
197
+ /**
198
+ * Get all active subscriptions
199
+ *
200
+ * @returns {Array<Object>} - Array of {pattern, callbackCount}
201
+ */
202
+ getSubscriptions() {
203
+ return Array.from(this._subscriptions.entries()).map(([pattern, callbacks]) => ({
204
+ pattern,
205
+ callbackCount: callbacks.size
206
+ }));
207
+ }
208
+
209
+ /**
210
+ * Clear all event subscriptions
211
+ *
212
+ * @returns {void}
213
+ */
214
+ clearSubscriptions() {
215
+ this._subscriptions.clear();
216
+ }
217
+
218
+ /**
219
+ * Handle incoming event from log monitor
220
+ *
221
+ * @private
222
+ * @param {Object} event - Parsed event
223
+ * @returns {void}
224
+ */
225
+ _handleEvent(event) {
226
+ // Find matching subscriptions and notify
227
+ for (const [pattern, callbacks] of this._subscriptions) {
228
+ if (this._matchesPattern(event.type, pattern)) {
229
+ callbacks.forEach(cb => {
230
+ try {
231
+ cb(event);
232
+ } catch (error) {
233
+ // Emit error event but don't crash
234
+ this.emit('error', { error, event });
235
+ }
236
+ });
237
+ }
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Check if event type matches pattern
243
+ *
244
+ * @private
245
+ * @param {string} eventType - Event type (e.g., "entity.join")
246
+ * @param {string} pattern - Pattern (e.g., "entity.*", "command.issued")
247
+ * @returns {boolean} - True if pattern matches
248
+ */
249
+ _matchesPattern(eventType, pattern) {
250
+ // Wildcard match
251
+ if (pattern === '*') {
252
+ return true;
253
+ }
254
+
255
+ // Glob-style pattern matching
256
+ if (pattern.includes('*')) {
257
+ const regexPattern = '^' + pattern.replace(/\*/g, '.*') + '$';
258
+ const regex = new RegExp(regexPattern);
259
+ return regex.test(eventType);
260
+ }
261
+
262
+ // Exact match
263
+ return eventType === pattern;
264
+ }
265
+ }
266
+
267
+ module.exports = { EventObserver };
@@ -0,0 +1,279 @@
1
+ /**
2
+ * QueryHelper - Convenience methods for common Minecraft server queries
3
+ *
4
+ * Wraps RconBackend to provide structured data from common RCON commands.
5
+ * Parses raw RCON responses into useful JavaScript objects.
6
+ *
7
+ * Responsibilities (MECE):
8
+ * - ONLY: Provide convenience methods for RCON queries
9
+ * - NOT: Connect to RCON (RconBackend's responsibility)
10
+ * - NOT: Handle errors (RconBackend's responsibility)
11
+ * - NOT: Cache results (caller's responsibility)
12
+ *
13
+ * Usage Example:
14
+ * const helper = new QueryHelper(rconBackend);
15
+ * const players = await helper.listPlayers();
16
+ * // Returns: { online: 2, players: ['Steve', 'Alex'] }
17
+ */
18
+
19
+ /**
20
+ * QueryHelper class for structured RCON queries
21
+ */
22
+ class QueryHelper {
23
+ /**
24
+ * Create a QueryHelper
25
+ *
26
+ * @param {Object} rconBackend - RconBackend instance
27
+ * @throws {Error} If rconBackend is not provided
28
+ */
29
+ constructor(rconBackend) {
30
+ if (!rconBackend) {
31
+ throw new Error('RconBackend is required');
32
+ }
33
+
34
+ /**
35
+ * RconBackend instance
36
+ * @private
37
+ * @type {Object}
38
+ */
39
+ this._rcon = rconBackend;
40
+ }
41
+
42
+ /**
43
+ * Get information about a specific player
44
+ *
45
+ * @param {string} username - Player username
46
+ * @returns {Promise<Object>} - Player information
47
+ * @property {string} username - Player name
48
+ * @property {Object} position - Player position {x, y, z}
49
+ * @property {number} health - Player health
50
+ * @property {number} food - Player food level
51
+ * @property {number} exp - Player experience
52
+ * @property {number} level - Player level
53
+ */
54
+ async getPlayerInfo(username) {
55
+ const response = await this._rcon.sendCommand(`data get entity ${username}`);
56
+
57
+ // If RCON returned parsed NBT data, use it
58
+ if (response.parsed) {
59
+ return this._parsePlayerInfo(response.parsed, username);
60
+ }
61
+
62
+ // Otherwise parse from raw text or return minimal info
63
+ return {
64
+ username,
65
+ position: null,
66
+ health: null,
67
+ food: null,
68
+ exp: null,
69
+ level: null,
70
+ raw: response.raw
71
+ };
72
+ }
73
+
74
+ /**
75
+ * List all online players
76
+ *
77
+ * @returns {Promise<Object>} - List of online players
78
+ * @property {number} online - Number of online players
79
+ * @property {Array<string>} players - Array of player names
80
+ */
81
+ async listPlayers() {
82
+ const response = await this._rcon.sendCommand('/list');
83
+
84
+ // Parse response format: "There are 2 players online: Steve, Alex"
85
+ const onlineMatch = response.raw.match(/There are (\d+) players online:? (.*)/);
86
+ if (onlineMatch) {
87
+ const online = parseInt(onlineMatch[1], 10);
88
+ const playersStr = onlineMatch[2].trim();
89
+ const players = playersStr === '' ? [] : playersStr.split(', ').map(p => p.trim());
90
+ return { online, players };
91
+ }
92
+
93
+ // Alternative format: "Steve, Alex"
94
+ const playersMatch = response.raw.match(/^([a-zA-Z0-9_]+(?:, [a-zA-Z0-9_]+)*)$/);
95
+ if (playersMatch) {
96
+ const players = response.raw.split(', ').map(p => p.trim());
97
+ return { online: players.length, players };
98
+ }
99
+
100
+ // Fallback
101
+ return { online: 0, players: [], raw: response.raw };
102
+ }
103
+
104
+ /**
105
+ * Get current world time
106
+ *
107
+ * @returns {Promise<Object>} - World time information
108
+ * @property {number} time - Game time (0-24000)
109
+ * @property {boolean} daytime - Whether it's daytime
110
+ */
111
+ async getWorldTime() {
112
+ const response = await this._rcon.sendCommand('/time query daytime');
113
+
114
+ // Parse: "The time is 1500"
115
+ const timeMatch = response.raw.match(/The time is (\d+)/);
116
+ if (timeMatch) {
117
+ const time = parseInt(timeMatch[1], 10);
118
+ return {
119
+ time,
120
+ daytime: time >= 0 && time < 13000
121
+ };
122
+ }
123
+
124
+ // Parse: "Time: 1500" (alternative format)
125
+ const altMatch = response.raw.match(/Time: (\d+)/);
126
+ if (altMatch) {
127
+ const time = parseInt(altMatch[1], 10);
128
+ return {
129
+ time,
130
+ daytime: time >= 0 && time < 13000
131
+ };
132
+ }
133
+
134
+ return { time: null, daytime: null, raw: response.raw };
135
+ }
136
+
137
+ /**
138
+ * Get current weather state
139
+ *
140
+ * @returns {Promise<Object>} - Weather information
141
+ * @property {string} weather - Weather type (clear, rain, thunder)
142
+ * @property {number} duration - Remaining duration (if available)
143
+ */
144
+ async getWeather() {
145
+ const response = await this._rcon.sendCommand('/weather query');
146
+
147
+ // Parse: "The weather is clear"
148
+ const weatherMatch = response.raw.match(/The weather is (\w+)/);
149
+ if (weatherMatch) {
150
+ return { weather: weatherMatch[1].toLowerCase() };
151
+ }
152
+
153
+ // Parse: "Weather: clear" (alternative format)
154
+ const altMatch = response.raw.match(/Weather: (\w+)/);
155
+ if (altMatch) {
156
+ return { weather: altMatch[1].toLowerCase() };
157
+ }
158
+
159
+ return { weather: null, raw: response.raw };
160
+ }
161
+
162
+ /**
163
+ * Get difficulty level
164
+ *
165
+ * @returns {Promise<Object>} - Difficulty information
166
+ * @property {string} difficulty - Difficulty (peaceful, easy, normal, hard)
167
+ */
168
+ async getDifficulty() {
169
+ const response = await this._rcon.sendCommand('/difficulty');
170
+
171
+ // Parse: "The difficulty is set to: Normal"
172
+ const match = response.raw.match(/The difficulty is set to: (\w+)/);
173
+ if (match) {
174
+ return { difficulty: match[1].toLowerCase() };
175
+ }
176
+
177
+ // Parse: "Difficulty: Normal" (alternative format)
178
+ const altMatch = response.raw.match(/Difficulty: (\w+)/);
179
+ if (altMatch) {
180
+ return { difficulty: altMatch[1].toLowerCase() };
181
+ }
182
+
183
+ return { difficulty: null, raw: response.raw };
184
+ }
185
+
186
+ /**
187
+ * Get game mode
188
+ *
189
+ * @param {string} [player='@s'] - Target player selector
190
+ * @returns {Promise<Object>} - Game mode information
191
+ * @property {string} gameMode - Game mode (survival, creative, adventure, spectator)
192
+ */
193
+ async getGameMode(player = '@s') {
194
+ const response = await this._rcon.sendCommand(`/gamemode query ${player}`);
195
+
196
+ // Parse: "Game mode: Creative (Player: Steve)"
197
+ const match = response.raw.match(/Game mode: (\w+)/);
198
+ if (match) {
199
+ return { gameMode: match[1].toLowerCase() };
200
+ }
201
+
202
+ return { gameMode: null, raw: response.raw };
203
+ }
204
+
205
+ /**
206
+ * Get server TPS (ticks per second)
207
+ *
208
+ * @returns {Promise<Object>} - TPS information
209
+ * @property {number} tps - Current TPS
210
+ */
211
+ async getTPS() {
212
+ const response = await this._rcon.sendCommand('/tps');
213
+
214
+ // Parse: "TPS from last 1m, 1m, 5m, 15m: 20.0, 20.0, 20.0, 20.0"
215
+ const match = response.raw.match(/TPS from last [\w, ]+: ([\d., ]+)/);
216
+ if (match) {
217
+ const tpsValues = match[1].split(', ').map(s => parseFloat(s.trim()));
218
+ return {
219
+ tps: tpsValues[0] || null,
220
+ allTPS: tpsValues
221
+ };
222
+ }
223
+
224
+ // Parse: "Current TPS: 20.0"
225
+ const simpleMatch = response.raw.match(/Current TPS: ([\d.]+)/);
226
+ if (simpleMatch) {
227
+ return { tps: parseFloat(simpleMatch[1]) };
228
+ }
229
+
230
+ return { tps: null, raw: response.raw };
231
+ }
232
+
233
+ /**
234
+ * Get world seed
235
+ *
236
+ * @returns {Promise<Object>} - Seed information
237
+ * @property {string} seed - World seed
238
+ */
239
+ async getSeed() {
240
+ const response = await this._rcon.sendCommand('/seed');
241
+
242
+ // Parse: "Seed: [123456789]"
243
+ const match = response.raw.match(/Seed: \[([-\d]+)\]/);
244
+ if (match) {
245
+ return { seed: match[1] };
246
+ }
247
+
248
+ return { seed: null, raw: response.raw };
249
+ }
250
+
251
+ /**
252
+ * Parse player info from NBT data
253
+ *
254
+ * @private
255
+ * @param {Object} nbtData - NBT data from RCON
256
+ * @param {string} username - Player username
257
+ * @returns {Object} - Parsed player info
258
+ */
259
+ _parsePlayerInfo(nbtData, username) {
260
+ // NBT data structure varies by Minecraft version
261
+ // Common structure: { Pos: [I; x, y, z], Health, FoodLevel, XpLevel, ... }
262
+
263
+ return {
264
+ username,
265
+ position: nbtData.Pos ? {
266
+ x: nbtData.Pos[0],
267
+ y: nbtData.Pos[1],
268
+ z: nbtData.Pos[2]
269
+ } : null,
270
+ health: nbtData.Health || null,
271
+ food: nbtData.FoodLevel || null,
272
+ exp: nbtData.XpTotal || nbtData.XpLevel ? (nbtData.XpTotal || 0) : null,
273
+ level: nbtData.XpLevel || null,
274
+ raw: nbtData
275
+ };
276
+ }
277
+ }
278
+
279
+ module.exports = { QueryHelper };
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Helper Classes for Pilaf Backends
3
+ *
4
+ * Utility classes that enhance backend functionality.
5
+ */
6
+
7
+ const { QueryHelper } = require('./QueryHelper.js');
8
+ const { EventObserver } = require('./EventObserver.js');
9
+
10
+ module.exports = {
11
+ QueryHelper,
12
+ EventObserver
13
+ };
package/lib/index.js CHANGED
@@ -11,6 +11,42 @@ const { BotLifecycleManager } = require('./BotLifecycleManager.js');
11
11
  const { ServerHealthChecker } = require('./ServerHealthChecker.js');
12
12
  const { BotPool } = require('./BotPool.js');
13
13
 
14
+ // Core abstractions
15
+ const { LogCollector, LogParser, CommandRouter, CorrelationStrategy } = require('./core/index.js');
16
+
17
+ // Collectors
18
+ const { DockerLogCollector } = require('./collectors/index.js');
19
+
20
+ // Parsers
21
+ const { MinecraftLogParser } = require('./parsers/index.js');
22
+
23
+ // Monitoring
24
+ const { CircularBuffer, LogMonitor, TagCorrelationStrategy, UsernameCorrelationStrategy } = require('./monitoring/index.js');
25
+
26
+ // Helpers
27
+ const { QueryHelper, EventObserver } = require('./helpers/index.js');
28
+
29
+ // Errors
30
+ const {
31
+ PilafError,
32
+ ConnectionError,
33
+ RconConnectionError,
34
+ DockerConnectionError,
35
+ FileAccessError,
36
+ CommandExecutionError,
37
+ CommandTimeoutError,
38
+ CommandRejectedError,
39
+ ParseError,
40
+ MalformedLogError,
41
+ UnknownPatternError,
42
+ CorrelationError,
43
+ ResponseTimeoutError,
44
+ AmbiguousMatchError,
45
+ ResourceError,
46
+ BufferOverflowError,
47
+ HandleExhaustedError
48
+ } = require('./errors/index.js');
49
+
14
50
  module.exports = {
15
51
  PilafBackend,
16
52
  RconBackend,
@@ -19,5 +55,40 @@ module.exports = {
19
55
  ConnectionState,
20
56
  BotLifecycleManager,
21
57
  ServerHealthChecker,
22
- BotPool
58
+ BotPool,
59
+ // Core abstractions
60
+ LogCollector,
61
+ LogParser,
62
+ CommandRouter,
63
+ CorrelationStrategy,
64
+ // Collectors
65
+ DockerLogCollector,
66
+ // Parsers
67
+ MinecraftLogParser,
68
+ // Monitoring
69
+ CircularBuffer,
70
+ LogMonitor,
71
+ TagCorrelationStrategy,
72
+ UsernameCorrelationStrategy,
73
+ // Helpers
74
+ QueryHelper,
75
+ EventObserver,
76
+ // Errors
77
+ PilafError,
78
+ ConnectionError,
79
+ RconConnectionError,
80
+ DockerConnectionError,
81
+ FileAccessError,
82
+ CommandExecutionError,
83
+ CommandTimeoutError,
84
+ CommandRejectedError,
85
+ ParseError,
86
+ MalformedLogError,
87
+ UnknownPatternError,
88
+ CorrelationError,
89
+ ResponseTimeoutError,
90
+ AmbiguousMatchError,
91
+ ResourceError,
92
+ BufferOverflowError,
93
+ HandleExhaustedError
23
94
  };