erlc-v2 1.1.0 → 1.1.2

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/src/Client.js CHANGED
@@ -1,480 +1,475 @@
1
- const { EventEmitter } = require("events");
2
- const Cache = require("./cache/Cache");
3
- const RequestManager = require("./rest/RequestManager");
4
- const Poller = require("./events/Poller");
5
- const LocalApiServer = require("./api/LocalApiServer");
6
- const { DEFAULT_OPTIONS, QUERY_FLAG_MAP } = require("./util/constants");
7
- const { mergeOptions } = require("./util/options");
8
- const { createLogger } = require("./util/logger");
9
- const { normalizeServerResponse } = require("./util/normalize");
10
- const { renderPlayerMap } = require("./map/renderPlayerMap");
11
- const { searchVehicles, findVehicleByPlate } = require("./util/vehicleSearch");
12
- const { ERLCError } = require("./errors");
13
-
14
- const BASE_URL = "https://api.policeroleplay.community";
15
- const BLOCKED_COMMANDS = new Set([
16
- ":view",
17
- ":to",
18
- ":tocar",
19
- ":toatv",
20
- ":logs",
21
- ":mods",
22
- ":admins",
23
- "helpers",
24
- ":helpers",
25
- ":administrators",
26
- ":moderators",
27
- ":killlogs",
28
- ":kl",
29
- ":cmds",
30
- ":commands",
31
- ]);
32
- const EVENT_ALIASES = {
33
- onReady: "ready",
34
- onJoin: "playerJoin",
35
- onLeave: "playerLeave",
36
- onKill: "kill",
37
- onVehicleSpawn: "vehicleSpawn",
38
- onVehicleDespawn: "vehicleDespawn",
39
- onQueueUpdate: "queueUpdate",
40
- onStaffUpdate: "staffUpdate",
41
- onModCall: "modCall",
42
- onEmergencyCall: "emergencyCall",
43
- onCommandLog: "commandLog",
1
+ const { EventEmitter } = require("events");
2
+ const Cache = require("./cache/Cache");
3
+ const RequestManager = require("./rest/RequestManager");
4
+ const Poller = require("./events/Poller");
5
+ const LocalApiServer = require("./api/LocalApiServer");
6
+ const { DEFAULT_OPTIONS, QUERY_FLAG_MAP } = require("./util/constants");
7
+ const { mergeOptions } = require("./util/options");
8
+ const { createLogger } = require("./util/logger");
9
+ const { normalizeServerResponse } = require("./util/normalize");
10
+ const { renderPlayerMap } = require("./map/renderPlayerMap");
11
+ const { searchVehicles, findVehicleByPlate } = require("./util/vehicleSearch");
12
+ const { ERLCError } = require("./errors");
13
+
14
+ const BASE_URL = "https://api.erlc.gg";
15
+ const BLOCKED_COMMANDS = new Set([
16
+ ":view",
17
+ ":to",
18
+ ":tocar",
19
+ ":toatv",
20
+ ":logs",
21
+ ":mods",
22
+ ":admins",
23
+ "helpers",
24
+ ":helpers",
25
+ ":administrators",
26
+ ":moderators",
27
+ ":killlogs",
28
+ ":kl",
29
+ ":cmds",
30
+ ":commands",
31
+ ]);
32
+ const EVENT_ALIASES = {
33
+ onReady: "ready",
34
+ onJoin: "playerJoin",
35
+ onLeave: "playerLeave",
36
+ onKill: "kill",
37
+ onVehicleSpawn: "vehicleSpawn",
38
+ onVehicleDespawn: "vehicleDespawn",
39
+ onQueueUpdate: "queueUpdate",
40
+ onStaffUpdate: "staffUpdate",
41
+ onModCall: "modCall",
42
+ onEmergencyCall: "emergencyCall",
43
+ onCommandLog: "commandLog",
44
44
  onLogCommand: "logCommand",
45
45
  onServerUpdate: "serverUpdate",
46
46
  onApiRequest: "apiRequest",
47
47
  onWebhook: "webhook",
48
- onWebhookCommand: "webhookCommand",
49
48
  onWebhookEmergencyCall: "webhookEmergencyCall",
50
49
  onError: "error",
51
50
  onDisconnect: "disconnect",
52
51
  };
53
-
54
- function resolveEventName(eventName) {
55
- if (typeof eventName !== "string") return eventName;
56
- return EVENT_ALIASES[eventName] || eventName;
57
- }
58
-
59
- function getCommandKeyword(command) {
60
- const raw = String(command ?? "").trim();
61
- if (!raw) return "";
62
- return raw.split(/\s+/)[0].toLowerCase();
63
- }
64
-
65
- function isPromiseLike(value) {
66
- return (
67
- value !== null &&
68
- typeof value === "object" &&
69
- typeof value.then === "function"
70
- );
71
- }
72
-
73
- class Client extends EventEmitter {
74
- constructor(options = {}) {
75
- super();
76
-
77
- this.options = mergeOptions(DEFAULT_OPTIONS, options);
78
- if (!this.options.serverKey || typeof this.options.serverKey !== "string") {
79
- throw new ERLCError("Client requires a valid serverKey");
80
- }
81
-
82
- this.logger = createLogger(this.options.logging, this.options.logger);
83
- this.cache = new Cache(this.options.cache, this.logger);
84
-
85
- this.state = {
86
- disconnected: false,
87
- destroyed: false,
88
- disconnectReason: null,
89
- disconnectError: null,
90
- };
91
- this.commandQueue = Promise.resolve();
92
-
93
- this.requestManager = new RequestManager({
94
- baseURL: BASE_URL,
95
- serverKey: this.options.serverKey,
96
- globalKey: this.options.globalKey,
97
- cache: this.cache,
98
- rateLimit: this.options.rateLimit,
99
- logger: this.logger,
100
- onDisconnect: (reason, error) => this._handleDisconnect(reason, error),
101
- });
102
-
103
- this.server = {
104
- fetch: (flags = {}, requestOptions = {}) =>
105
- this._fetchServer(flags, requestOptions),
106
- };
107
-
108
- this.players = {
109
- list: (requestOptions = {}) =>
110
- this.server
111
- .fetch({ players: true }, requestOptions)
112
- .then((d) => d.players),
113
- };
114
-
115
- this.map = {
116
- render: (options = {}, requestOptions = {}) =>
117
- this._renderMap(options, requestOptions),
118
- renderUser: (userId, options = {}, requestOptions = {}) =>
119
- this._renderMap({ ...options, userId }, requestOptions),
120
- };
121
-
122
- this.staff = {
123
- list: (requestOptions = {}) =>
124
- this.server.fetch({ staff: true }, requestOptions).then((d) => d.staff),
125
- };
126
-
127
- this.logs = {
128
- kills: (requestOptions = {}) =>
129
- this.server
130
- .fetch({ killLogs: true }, requestOptions)
131
- .then((d) => d.killLogs),
132
- joins: (requestOptions = {}) =>
133
- this.server
134
- .fetch({ joinLogs: true }, requestOptions)
135
- .then((d) => d.joinLogs),
136
- commands: (requestOptions = {}) =>
137
- this.server
138
- .fetch({ commandLogs: true }, requestOptions)
139
- .then((d) => d.commandLogs),
140
- modCalls: (requestOptions = {}) =>
141
- this.server
142
- .fetch({ modCalls: true }, requestOptions)
143
- .then((d) => d.modCalls),
144
- emergencyCalls: (requestOptions = {}) =>
145
- this.server
146
- .fetch({ emergencyCalls: true }, requestOptions)
147
- .then((d) => d.emergencyCalls),
148
- };
149
-
150
- this.commands = {
151
- execute: (command, requestOptions = {}) =>
152
- this._executeCommand(command, requestOptions),
153
- };
154
-
155
- this.vehicles = {
156
- list: (requestOptions = {}) =>
157
- this.server
158
- .fetch({ vehicles: true }, requestOptions)
159
- .then((d) => d.vehicles),
160
- search: (filters = {}, requestOptions = {}) =>
161
- this.server
162
- .fetch({ vehicles: true }, requestOptions)
163
- .then((d) => searchVehicles(d.vehicles, filters)),
164
- findByPlate: (plate, requestOptions = {}) =>
165
- this.server
166
- .fetch({ vehicles: true }, requestOptions)
167
- .then((d) => findVehicleByPlate(d.vehicles, plate)),
168
- findByOwner: (owner, requestOptions = {}) =>
169
- this.server
170
- .fetch({ vehicles: true }, requestOptions)
171
- .then((d) => searchVehicles(d.vehicles, { owner })),
172
- findOne: (filters = {}, requestOptions = {}) =>
173
- this.server
174
- .fetch({ vehicles: true }, requestOptions)
175
- .then((d) =>
176
- searchVehicles(
177
- d.vehicles,
178
- typeof filters === "string"
179
- ? { query: filters, limit: 1 }
180
- : { ...filters, limit: 1 },
181
- )[0] ?? null,
182
- ),
183
- };
184
-
185
- this.queue = {
186
- get: (requestOptions = {}) =>
187
- this.server.fetch({ queue: true }, requestOptions).then((d) => d.queue),
188
- };
189
-
190
- this.api = new LocalApiServer(this, this.options.api);
191
-
192
- this.poller = new Poller(this, this.options.polling);
193
- if (this.options.polling?.enabled !== false) {
194
- this.poller.start();
195
- }
196
- if (this.options.api?.enabled || this.options.api?.port) {
197
- this.api.start().catch((error) => {
198
- this.logger.error({
199
- msg: "local_api_start_failed",
200
- error: error?.message || String(error),
201
- });
202
- if (this.listenerCount("error") > 0) {
203
- this.emit("error", error);
204
- }
205
- });
206
- }
207
- }
208
-
209
- on(eventName, listener) {
210
- return super.on(resolveEventName(eventName), listener);
211
- }
212
-
213
- once(eventName, listener) {
214
- return super.once(resolveEventName(eventName), listener);
215
- }
216
-
217
- addListener(eventName, listener) {
218
- return super.addListener(resolveEventName(eventName), listener);
219
- }
220
-
221
- prependListener(eventName, listener) {
222
- return super.prependListener(resolveEventName(eventName), listener);
223
- }
224
-
225
- prependOnceListener(eventName, listener) {
226
- return super.prependOnceListener(resolveEventName(eventName), listener);
227
- }
228
-
229
- off(eventName, listener) {
230
- return super.off(resolveEventName(eventName), listener);
231
- }
232
-
233
- removeListener(eventName, listener) {
234
- return super.removeListener(resolveEventName(eventName), listener);
235
- }
236
-
237
- onReady(listener) {
238
- return this.on("ready", listener);
239
- }
240
-
241
- onJoin(listener) {
242
- return this.on("playerJoin", listener);
243
- }
244
-
245
- onLeave(listener) {
246
- return this.on("playerLeave", listener);
247
- }
248
-
249
- onKill(listener) {
250
- return this.on("kill", listener);
251
- }
252
-
253
- onVehicleSpawn(listener) {
254
- return this.on("vehicleSpawn", listener);
255
- }
256
-
257
- onVehicleDespawn(listener) {
258
- return this.on("vehicleDespawn", listener);
259
- }
260
-
261
- onQueueUpdate(listener) {
262
- return this.on("queueUpdate", listener);
263
- }
264
-
265
- onStaffUpdate(listener) {
266
- return this.on("staffUpdate", listener);
267
- }
268
-
269
- onModCall(listener) {
270
- return this.on("modCall", listener);
271
- }
272
-
273
- onCommandLog(listener) {
274
- return this.on("commandLog", listener);
275
- }
276
-
277
- onEmergencyCall(listener) {
278
- return this.on("emergencyCall", listener);
279
- }
280
-
281
- onLogCommand(listener) {
282
- return this.on("logCommand", listener);
283
- }
284
-
285
- onServerUpdate(listener) {
286
- return this.on("serverUpdate", listener);
287
- }
288
-
289
- onApiRequest(listener) {
290
- return this.on("apiRequest", listener);
291
- }
292
-
52
+
53
+ function resolveEventName(eventName) {
54
+ if (typeof eventName !== "string") return eventName;
55
+ return EVENT_ALIASES[eventName] || eventName;
56
+ }
57
+
58
+ function getCommandKeyword(command) {
59
+ const raw = String(command ?? "").trim();
60
+ if (!raw) return "";
61
+ return raw.split(/\s+/)[0].toLowerCase();
62
+ }
63
+
64
+ function isPromiseLike(value) {
65
+ return (
66
+ value !== null &&
67
+ typeof value === "object" &&
68
+ typeof value.then === "function"
69
+ );
70
+ }
71
+
72
+ class Client extends EventEmitter {
73
+ constructor(options = {}) {
74
+ super();
75
+
76
+ this.options = mergeOptions(DEFAULT_OPTIONS, options);
77
+ if (!this.options.serverKey || typeof this.options.serverKey !== "string") {
78
+ throw new ERLCError("Client requires a valid serverKey");
79
+ }
80
+
81
+ this.logger = createLogger(this.options.logging, this.options.logger);
82
+ this.cache = new Cache(this.options.cache, this.logger);
83
+
84
+ this.state = {
85
+ disconnected: false,
86
+ destroyed: false,
87
+ disconnectReason: null,
88
+ disconnectError: null,
89
+ };
90
+ this.commandQueue = Promise.resolve();
91
+
92
+ this.requestManager = new RequestManager({
93
+ baseURL: BASE_URL,
94
+ serverKey: this.options.serverKey,
95
+ globalKey: this.options.globalKey,
96
+ cache: this.cache,
97
+ rateLimit: this.options.rateLimit,
98
+ logger: this.logger,
99
+ onDisconnect: (reason, error) => this._handleDisconnect(reason, error),
100
+ });
101
+
102
+ this.server = {
103
+ fetch: (flags = {}, requestOptions = {}) =>
104
+ this._fetchServer(flags, requestOptions),
105
+ };
106
+
107
+ this.players = {
108
+ list: (requestOptions = {}) =>
109
+ this.server
110
+ .fetch({ players: true }, requestOptions)
111
+ .then((d) => d.players),
112
+ };
113
+
114
+ this.map = {
115
+ render: (options = {}, requestOptions = {}) =>
116
+ this._renderMap(options, requestOptions),
117
+ renderUser: (userId, options = {}, requestOptions = {}) =>
118
+ this._renderMap({ ...options, userId }, requestOptions),
119
+ };
120
+
121
+ this.staff = {
122
+ list: (requestOptions = {}) =>
123
+ this.server.fetch({ staff: true }, requestOptions).then((d) => d.staff),
124
+ };
125
+
126
+ this.logs = {
127
+ kills: (requestOptions = {}) =>
128
+ this.server
129
+ .fetch({ killLogs: true }, requestOptions)
130
+ .then((d) => d.killLogs),
131
+ joins: (requestOptions = {}) =>
132
+ this.server
133
+ .fetch({ joinLogs: true }, requestOptions)
134
+ .then((d) => d.joinLogs),
135
+ commands: (requestOptions = {}) =>
136
+ this.server
137
+ .fetch({ commandLogs: true }, requestOptions)
138
+ .then((d) => d.commandLogs),
139
+ modCalls: (requestOptions = {}) =>
140
+ this.server
141
+ .fetch({ modCalls: true }, requestOptions)
142
+ .then((d) => d.modCalls),
143
+ emergencyCalls: (requestOptions = {}) =>
144
+ this.server
145
+ .fetch({ emergencyCalls: true }, requestOptions)
146
+ .then((d) => d.emergencyCalls),
147
+ };
148
+
149
+ this.commands = {
150
+ execute: (command, requestOptions = {}) =>
151
+ this._executeCommand(command, requestOptions),
152
+ };
153
+
154
+ this.vehicles = {
155
+ list: (requestOptions = {}) =>
156
+ this.server
157
+ .fetch({ vehicles: true }, requestOptions)
158
+ .then((d) => d.vehicles),
159
+ search: (filters = {}, requestOptions = {}) =>
160
+ this.server
161
+ .fetch({ vehicles: true }, requestOptions)
162
+ .then((d) => searchVehicles(d.vehicles, filters)),
163
+ findByPlate: (plate, requestOptions = {}) =>
164
+ this.server
165
+ .fetch({ vehicles: true }, requestOptions)
166
+ .then((d) => findVehicleByPlate(d.vehicles, plate)),
167
+ findByOwner: (owner, requestOptions = {}) =>
168
+ this.server
169
+ .fetch({ vehicles: true }, requestOptions)
170
+ .then((d) => searchVehicles(d.vehicles, { owner })),
171
+ findOne: (filters = {}, requestOptions = {}) =>
172
+ this.server
173
+ .fetch({ vehicles: true }, requestOptions)
174
+ .then((d) =>
175
+ searchVehicles(
176
+ d.vehicles,
177
+ typeof filters === "string"
178
+ ? { query: filters, limit: 1 }
179
+ : { ...filters, limit: 1 },
180
+ )[0] ?? null,
181
+ ),
182
+ };
183
+
184
+ this.queue = {
185
+ get: (requestOptions = {}) =>
186
+ this.server.fetch({ queue: true }, requestOptions).then((d) => d.queue),
187
+ };
188
+
189
+ this.api = new LocalApiServer(this, this.options.api);
190
+
191
+ this.poller = new Poller(this, this.options.polling);
192
+ if (this.options.polling?.enabled !== false) {
193
+ this.poller.start();
194
+ }
195
+ if (this.options.api?.enabled || this.options.api?.port) {
196
+ this.api.start().catch((error) => {
197
+ this.logger.error({
198
+ msg: "local_api_start_failed",
199
+ error: error?.message || String(error),
200
+ });
201
+ if (this.listenerCount("error") > 0) {
202
+ this.emit("error", error);
203
+ }
204
+ });
205
+ }
206
+ }
207
+
208
+ on(eventName, listener) {
209
+ return super.on(resolveEventName(eventName), listener);
210
+ }
211
+
212
+ once(eventName, listener) {
213
+ return super.once(resolveEventName(eventName), listener);
214
+ }
215
+
216
+ addListener(eventName, listener) {
217
+ return super.addListener(resolveEventName(eventName), listener);
218
+ }
219
+
220
+ prependListener(eventName, listener) {
221
+ return super.prependListener(resolveEventName(eventName), listener);
222
+ }
223
+
224
+ prependOnceListener(eventName, listener) {
225
+ return super.prependOnceListener(resolveEventName(eventName), listener);
226
+ }
227
+
228
+ off(eventName, listener) {
229
+ return super.off(resolveEventName(eventName), listener);
230
+ }
231
+
232
+ removeListener(eventName, listener) {
233
+ return super.removeListener(resolveEventName(eventName), listener);
234
+ }
235
+
236
+ onReady(listener) {
237
+ return this.on("ready", listener);
238
+ }
239
+
240
+ onJoin(listener) {
241
+ return this.on("playerJoin", listener);
242
+ }
243
+
244
+ onLeave(listener) {
245
+ return this.on("playerLeave", listener);
246
+ }
247
+
248
+ onKill(listener) {
249
+ return this.on("kill", listener);
250
+ }
251
+
252
+ onVehicleSpawn(listener) {
253
+ return this.on("vehicleSpawn", listener);
254
+ }
255
+
256
+ onVehicleDespawn(listener) {
257
+ return this.on("vehicleDespawn", listener);
258
+ }
259
+
260
+ onQueueUpdate(listener) {
261
+ return this.on("queueUpdate", listener);
262
+ }
263
+
264
+ onStaffUpdate(listener) {
265
+ return this.on("staffUpdate", listener);
266
+ }
267
+
268
+ onModCall(listener) {
269
+ return this.on("modCall", listener);
270
+ }
271
+
272
+ onCommandLog(listener) {
273
+ return this.on("commandLog", listener);
274
+ }
275
+
276
+ onEmergencyCall(listener) {
277
+ return this.on("emergencyCall", listener);
278
+ }
279
+
280
+ onLogCommand(listener) {
281
+ return this.on("logCommand", listener);
282
+ }
283
+
284
+ onServerUpdate(listener) {
285
+ return this.on("serverUpdate", listener);
286
+ }
287
+
288
+ onApiRequest(listener) {
289
+ return this.on("apiRequest", listener);
290
+ }
291
+
293
292
  onWebhook(listener) {
294
293
  return this.on("webhook", listener);
295
294
  }
296
295
 
297
- onWebhookCommand(listener) {
298
- return this.on("webhookCommand", listener);
299
- }
300
-
301
296
  onWebhookEmergencyCall(listener) {
302
297
  return this.on("webhookEmergencyCall", listener);
303
298
  }
304
-
305
- onError(listener) {
306
- return this.on("error", listener);
307
- }
308
-
309
- onDisconnect(listener) {
310
- return this.on("disconnect", listener);
311
- }
312
-
313
- _handleDisconnect(reason, error) {
314
- if (this.state.disconnected) return;
315
- this.state.disconnected = true;
316
- this.state.disconnectReason = reason;
317
- this.state.disconnectError = error;
318
-
319
- this.logger.warn({
320
- msg: "client_disconnected",
321
- reason,
322
- error: error?.message,
323
- });
324
-
325
- this.poller.stop();
326
- this.emit("disconnect", { reason, error });
327
- }
328
-
329
- _buildQueryFlags(flags) {
330
- const query = {};
331
- if (!flags || typeof flags !== "object") return query;
332
-
333
- for (const [key, apiKey] of Object.entries(QUERY_FLAG_MAP)) {
334
- if (flags[key] === true) {
335
- query[apiKey] = true;
336
- }
337
- }
338
-
339
- for (const apiKey of Object.values(QUERY_FLAG_MAP)) {
340
- if (flags[apiKey] === true) {
341
- query[apiKey] = true;
342
- }
343
- }
344
-
345
- return query;
346
- }
347
-
348
- async _fetchServer(flags = {}, requestOptions = {}) {
349
- if (this.state.disconnected && this.state.disconnectError) {
350
- throw this.state.disconnectError;
351
- }
352
- if (this.state.destroyed) {
353
- throw new ERLCError("Client has been destroyed");
354
- }
355
-
356
- const query = this._buildQueryFlags(flags);
357
- const response = await this.requestManager.request({
358
- method: "GET",
359
- path: "/v2/server",
360
- query,
361
- useCache: requestOptions.bypassCache !== true,
362
- cacheTtlMs: requestOptions.cacheTtlMs,
363
- dedupe: requestOptions.dedupe !== false,
364
- });
365
-
366
- const normalized = normalizeServerResponse(response.data);
367
- normalized.meta = {
368
- status: response.status,
369
- endpoint: response.endpoint,
370
- bucket: response.bucket,
371
- rateLimit: response.rateLimit,
372
- };
373
- return normalized;
374
- }
375
-
376
- async _executeCommand(command, requestOptions = {}) {
377
- if (this.state.disconnected && this.state.disconnectError) {
378
- throw this.state.disconnectError;
379
- }
380
- if (this.state.destroyed) {
381
- throw new ERLCError("Client has been destroyed");
382
- }
383
- if (typeof command !== "string" || !command.trim()) {
384
- throw new ERLCError("Command must be a non-empty string");
385
- }
386
-
387
- const normalizedCommand = command.trim();
388
- const keyword = getCommandKeyword(normalizedCommand);
389
- if (BLOCKED_COMMANDS.has(keyword)) {
390
- throw new ERLCError(`Command "${keyword}" is blocked by client policy`);
391
- }
392
-
393
- return this._queueCommand(async () => {
394
- if (this.state.disconnected && this.state.disconnectError) {
395
- throw this.state.disconnectError;
396
- }
397
- if (this.state.destroyed) {
398
- throw new ERLCError("Client has been destroyed");
399
- }
400
-
401
- const response = await this.requestManager.request({
402
- method: "POST",
403
- path: "/v2/server/command",
404
- body: { command: normalizedCommand },
405
- useCache: false,
406
- dedupe: requestOptions.dedupe === true,
407
- });
408
-
409
- return {
410
- ok: response.status >= 200 && response.status < 300,
411
- status: response.status,
412
- endpoint: response.endpoint,
413
- bucket: response.bucket,
414
- rateLimit: response.rateLimit,
415
- message: response.data?.message ?? null,
416
- commandId:
417
- response.data?.commandId ??
418
- response.data?.CommandId ??
419
- null,
420
- };
421
- });
422
- }
423
-
424
- async _renderMap(options = {}, requestOptions = {}) {
425
- if (this.state.disconnected && this.state.disconnectError) {
426
- throw this.state.disconnectError;
427
- }
428
- if (this.state.destroyed) {
429
- throw new ERLCError("Client has been destroyed");
430
- }
431
-
432
- return renderPlayerMap(this, options, requestOptions);
433
- }
434
-
435
- _queueCommand(task) {
436
- const run = this.commandQueue.then(task, task);
437
- this.commandQueue = run.catch(() => {});
438
- return run;
439
- }
440
-
441
- destroy() {
442
- if (this.state.destroyed) return;
443
- this.state.destroyed = true;
444
- this.poller.destroy();
445
- this.api.stop();
446
- this.requestManager.destroy(
447
- this.state.disconnectError || new ERLCError("Client destroyed"),
448
- );
449
- const clearResult = this.cache.clear();
450
- const maybeLog = (promiseLike, msg) => {
451
- if (!isPromiseLike(promiseLike)) return;
452
- promiseLike.catch((error) => {
453
- this.logger.warn({
454
- msg,
455
- error: error?.message || String(error),
456
- });
457
- });
458
- };
459
-
460
- if (isPromiseLike(clearResult)) {
461
- maybeLog(
462
- clearResult
463
- .catch((error) => {
464
- this.logger.warn({
465
- msg: "cache_clear_failed",
466
- error: error?.message || String(error),
467
- });
468
- })
469
- .finally(() => this.cache.destroy?.()),
470
- "cache_destroy_failed",
471
- );
472
- return;
473
- }
474
-
475
- const destroyResult = this.cache.destroy?.();
476
- maybeLog(destroyResult, "cache_destroy_failed");
477
- }
478
- }
479
-
480
- module.exports = Client;
299
+
300
+ onError(listener) {
301
+ return this.on("error", listener);
302
+ }
303
+
304
+ onDisconnect(listener) {
305
+ return this.on("disconnect", listener);
306
+ }
307
+
308
+ _handleDisconnect(reason, error) {
309
+ if (this.state.disconnected) return;
310
+ this.state.disconnected = true;
311
+ this.state.disconnectReason = reason;
312
+ this.state.disconnectError = error;
313
+
314
+ this.logger.warn({
315
+ msg: "client_disconnected",
316
+ reason,
317
+ error: error?.message,
318
+ });
319
+
320
+ this.poller.stop();
321
+ this.emit("disconnect", { reason, error });
322
+ }
323
+
324
+ _buildQueryFlags(flags) {
325
+ const query = {};
326
+ if (!flags || typeof flags !== "object") return query;
327
+
328
+ for (const [key, apiKey] of Object.entries(QUERY_FLAG_MAP)) {
329
+ if (flags[key] === true) {
330
+ query[apiKey] = true;
331
+ }
332
+ }
333
+
334
+ for (const apiKey of Object.values(QUERY_FLAG_MAP)) {
335
+ if (flags[apiKey] === true) {
336
+ query[apiKey] = true;
337
+ }
338
+ }
339
+
340
+ return query;
341
+ }
342
+
343
+ async _fetchServer(flags = {}, requestOptions = {}) {
344
+ if (this.state.disconnected && this.state.disconnectError) {
345
+ throw this.state.disconnectError;
346
+ }
347
+ if (this.state.destroyed) {
348
+ throw new ERLCError("Client has been destroyed");
349
+ }
350
+
351
+ const query = this._buildQueryFlags(flags);
352
+ const response = await this.requestManager.request({
353
+ method: "GET",
354
+ path: "/v2/server",
355
+ query,
356
+ useCache: requestOptions.bypassCache !== true,
357
+ cacheTtlMs: requestOptions.cacheTtlMs,
358
+ dedupe: requestOptions.dedupe !== false,
359
+ });
360
+
361
+ const normalized = normalizeServerResponse(response.data);
362
+ normalized.meta = {
363
+ status: response.status,
364
+ endpoint: response.endpoint,
365
+ bucket: response.bucket,
366
+ rateLimit: response.rateLimit,
367
+ };
368
+ return normalized;
369
+ }
370
+
371
+ async _executeCommand(command, requestOptions = {}) {
372
+ if (this.state.disconnected && this.state.disconnectError) {
373
+ throw this.state.disconnectError;
374
+ }
375
+ if (this.state.destroyed) {
376
+ throw new ERLCError("Client has been destroyed");
377
+ }
378
+ if (typeof command !== "string" || !command.trim()) {
379
+ throw new ERLCError("Command must be a non-empty string");
380
+ }
381
+
382
+ const normalizedCommand = command.trim();
383
+ const keyword = getCommandKeyword(normalizedCommand);
384
+ if (BLOCKED_COMMANDS.has(keyword)) {
385
+ throw new ERLCError(`Command "${keyword}" is blocked by client policy`);
386
+ }
387
+
388
+ return this._queueCommand(async () => {
389
+ if (this.state.disconnected && this.state.disconnectError) {
390
+ throw this.state.disconnectError;
391
+ }
392
+ if (this.state.destroyed) {
393
+ throw new ERLCError("Client has been destroyed");
394
+ }
395
+
396
+ const response = await this.requestManager.request({
397
+ method: "POST",
398
+ path: "/v2/server/command",
399
+ body: { command: normalizedCommand },
400
+ useCache: false,
401
+ dedupe: requestOptions.dedupe === true,
402
+ });
403
+
404
+ return {
405
+ ok: response.status >= 200 && response.status < 300,
406
+ status: response.status,
407
+ endpoint: response.endpoint,
408
+ bucket: response.bucket,
409
+ rateLimit: response.rateLimit,
410
+ message: response.data?.message ?? null,
411
+ commandId:
412
+ response.data?.commandId ??
413
+ response.data?.CommandId ??
414
+ null,
415
+ };
416
+ });
417
+ }
418
+
419
+ async _renderMap(options = {}, requestOptions = {}) {
420
+ if (this.state.disconnected && this.state.disconnectError) {
421
+ throw this.state.disconnectError;
422
+ }
423
+ if (this.state.destroyed) {
424
+ throw new ERLCError("Client has been destroyed");
425
+ }
426
+
427
+ return renderPlayerMap(this, options, requestOptions);
428
+ }
429
+
430
+ _queueCommand(task) {
431
+ const run = this.commandQueue.then(task, task);
432
+ this.commandQueue = run.catch(() => {});
433
+ return run;
434
+ }
435
+
436
+ destroy() {
437
+ if (this.state.destroyed) return;
438
+ this.state.destroyed = true;
439
+ this.poller.destroy();
440
+ this.api.stop();
441
+ this.requestManager.destroy(
442
+ this.state.disconnectError || new ERLCError("Client destroyed"),
443
+ );
444
+ const clearResult = this.cache.clear();
445
+ const maybeLog = (promiseLike, msg) => {
446
+ if (!isPromiseLike(promiseLike)) return;
447
+ promiseLike.catch((error) => {
448
+ this.logger.warn({
449
+ msg,
450
+ error: error?.message || String(error),
451
+ });
452
+ });
453
+ };
454
+
455
+ if (isPromiseLike(clearResult)) {
456
+ maybeLog(
457
+ clearResult
458
+ .catch((error) => {
459
+ this.logger.warn({
460
+ msg: "cache_clear_failed",
461
+ error: error?.message || String(error),
462
+ });
463
+ })
464
+ .finally(() => this.cache.destroy?.()),
465
+ "cache_destroy_failed",
466
+ );
467
+ return;
468
+ }
469
+
470
+ const destroyResult = this.cache.destroy?.();
471
+ maybeLog(destroyResult, "cache_destroy_failed");
472
+ }
473
+ }
474
+
475
+ module.exports = Client;