erlc-v2 1.0.0-beta.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.
package/src/Client.js ADDED
@@ -0,0 +1,353 @@
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 { DEFAULT_OPTIONS, QUERY_FLAG_MAP } = require("./util/constants");
6
+ const { mergeOptions } = require("./util/options");
7
+ const { createLogger } = require("./util/logger");
8
+ const { normalizeServerResponse } = require("./util/normalize");
9
+ const { ERLCError } = require("./errors");
10
+
11
+ const BASE_URL = "https://api.policeroleplay.community";
12
+ const BLOCKED_COMMANDS = new Set([
13
+ ":view",
14
+ ":to",
15
+ ":tocar",
16
+ ":toatv",
17
+ ":logs",
18
+ ":mods",
19
+ ":admins",
20
+ "helpers",
21
+ ":helpers",
22
+ ":administrators",
23
+ ":moderators",
24
+ ":killlogs",
25
+ ":kl",
26
+ ":cmds",
27
+ ":commands",
28
+ ]);
29
+ const EVENT_ALIASES = {
30
+ onReady: "ready",
31
+ onJoin: "playerJoin",
32
+ onLeave: "playerLeave",
33
+ onKill: "kill",
34
+ onVehicleSpawn: "vehicleSpawn",
35
+ onVehicleDespawn: "vehicleDespawn",
36
+ onQueueUpdate: "queueUpdate",
37
+ onStaffUpdate: "staffUpdate",
38
+ onModCall: "modCall",
39
+ onCommandLog: "commandLog",
40
+ onLogCommand: "logCommand",
41
+ onServerUpdate: "serverUpdate",
42
+ onError: "error",
43
+ onDisconnect: "disconnect",
44
+ };
45
+
46
+ function resolveEventName(eventName) {
47
+ if (typeof eventName !== "string") return eventName;
48
+ return EVENT_ALIASES[eventName] || eventName;
49
+ }
50
+
51
+ function getCommandKeyword(command) {
52
+ const raw = String(command ?? "").trim();
53
+ if (!raw) return "";
54
+ return raw.split(/\s+/)[0].toLowerCase();
55
+ }
56
+
57
+ class Client extends EventEmitter {
58
+ constructor(options = {}) {
59
+ super();
60
+
61
+ this.options = mergeOptions(DEFAULT_OPTIONS, options);
62
+ if (!this.options.serverKey || typeof this.options.serverKey !== "string") {
63
+ throw new ERLCError("Client requires a valid serverKey");
64
+ }
65
+
66
+ this.logger = createLogger(this.options.logging, this.options.logger);
67
+ this.cache = new Cache(this.options.cache);
68
+
69
+ this.state = {
70
+ disconnected: false,
71
+ destroyed: false,
72
+ disconnectReason: null,
73
+ disconnectError: null,
74
+ };
75
+ this.commandQueue = Promise.resolve();
76
+
77
+ this.requestManager = new RequestManager({
78
+ baseURL: BASE_URL,
79
+ serverKey: this.options.serverKey,
80
+ globalKey: this.options.globalKey,
81
+ cache: this.cache,
82
+ rateLimit: this.options.rateLimit,
83
+ logger: this.logger,
84
+ onDisconnect: (reason, error) => this._handleDisconnect(reason, error),
85
+ });
86
+
87
+ this.server = {
88
+ fetch: (flags = {}, requestOptions = {}) =>
89
+ this._fetchServer(flags, requestOptions),
90
+ };
91
+
92
+ this.players = {
93
+ list: (requestOptions = {}) =>
94
+ this.server
95
+ .fetch({ players: true }, requestOptions)
96
+ .then((d) => d.players),
97
+ };
98
+
99
+ this.staff = {
100
+ list: (requestOptions = {}) =>
101
+ this.server.fetch({ staff: true }, requestOptions).then((d) => d.staff),
102
+ };
103
+
104
+ this.logs = {
105
+ kills: (requestOptions = {}) =>
106
+ this.server
107
+ .fetch({ killLogs: true }, requestOptions)
108
+ .then((d) => d.killLogs),
109
+ joins: (requestOptions = {}) =>
110
+ this.server
111
+ .fetch({ joinLogs: true }, requestOptions)
112
+ .then((d) => d.joinLogs),
113
+ commands: (requestOptions = {}) =>
114
+ this.server
115
+ .fetch({ commandLogs: true }, requestOptions)
116
+ .then((d) => d.commandLogs),
117
+ modCalls: (requestOptions = {}) =>
118
+ this.server
119
+ .fetch({ modCalls: true }, requestOptions)
120
+ .then((d) => d.modCalls),
121
+ };
122
+
123
+ this.commands = {
124
+ execute: (command, requestOptions = {}) =>
125
+ this._executeCommand(command, requestOptions),
126
+ };
127
+
128
+ this.vehicles = {
129
+ list: (requestOptions = {}) =>
130
+ this.server
131
+ .fetch({ vehicles: true }, requestOptions)
132
+ .then((d) => d.vehicles),
133
+ };
134
+
135
+ this.queue = {
136
+ get: (requestOptions = {}) =>
137
+ this.server.fetch({ queue: true }, requestOptions).then((d) => d.queue),
138
+ };
139
+
140
+ this.poller = new Poller(this, this.options.polling);
141
+ if (this.options.polling?.enabled !== false) {
142
+ this.poller.start();
143
+ }
144
+ }
145
+
146
+ on(eventName, listener) {
147
+ return super.on(resolveEventName(eventName), listener);
148
+ }
149
+
150
+ once(eventName, listener) {
151
+ return super.once(resolveEventName(eventName), listener);
152
+ }
153
+
154
+ addListener(eventName, listener) {
155
+ return super.addListener(resolveEventName(eventName), listener);
156
+ }
157
+
158
+ prependListener(eventName, listener) {
159
+ return super.prependListener(resolveEventName(eventName), listener);
160
+ }
161
+
162
+ prependOnceListener(eventName, listener) {
163
+ return super.prependOnceListener(resolveEventName(eventName), listener);
164
+ }
165
+
166
+ off(eventName, listener) {
167
+ return super.off(resolveEventName(eventName), listener);
168
+ }
169
+
170
+ removeListener(eventName, listener) {
171
+ return super.removeListener(resolveEventName(eventName), listener);
172
+ }
173
+
174
+ onReady(listener) {
175
+ return this.on("ready", listener);
176
+ }
177
+
178
+ onJoin(listener) {
179
+ return this.on("playerJoin", listener);
180
+ }
181
+
182
+ onLeave(listener) {
183
+ return this.on("playerLeave", listener);
184
+ }
185
+
186
+ onKill(listener) {
187
+ return this.on("kill", listener);
188
+ }
189
+
190
+ onVehicleSpawn(listener) {
191
+ return this.on("vehicleSpawn", listener);
192
+ }
193
+
194
+ onVehicleDespawn(listener) {
195
+ return this.on("vehicleDespawn", listener);
196
+ }
197
+
198
+ onQueueUpdate(listener) {
199
+ return this.on("queueUpdate", listener);
200
+ }
201
+
202
+ onStaffUpdate(listener) {
203
+ return this.on("staffUpdate", listener);
204
+ }
205
+
206
+ onModCall(listener) {
207
+ return this.on("modCall", listener);
208
+ }
209
+
210
+ onCommandLog(listener) {
211
+ return this.on("commandLog", listener);
212
+ }
213
+
214
+ onLogCommand(listener) {
215
+ return this.on("logCommand", listener);
216
+ }
217
+
218
+ onServerUpdate(listener) {
219
+ return this.on("serverUpdate", listener);
220
+ }
221
+
222
+ onError(listener) {
223
+ return this.on("error", listener);
224
+ }
225
+
226
+ onDisconnect(listener) {
227
+ return this.on("disconnect", listener);
228
+ }
229
+
230
+ _handleDisconnect(reason, error) {
231
+ if (this.state.disconnected) return;
232
+ this.state.disconnected = true;
233
+ this.state.disconnectReason = reason;
234
+ this.state.disconnectError = error;
235
+
236
+ this.logger.warn({
237
+ msg: "client_disconnected",
238
+ reason,
239
+ error: error?.message,
240
+ });
241
+
242
+ this.poller.stop();
243
+ this.emit("disconnect", { reason, error });
244
+ }
245
+
246
+ _buildQueryFlags(flags) {
247
+ const query = {};
248
+ if (!flags || typeof flags !== "object") return query;
249
+
250
+ for (const [key, apiKey] of Object.entries(QUERY_FLAG_MAP)) {
251
+ if (flags[key] === true) {
252
+ query[apiKey] = true;
253
+ }
254
+ }
255
+
256
+ for (const apiKey of Object.values(QUERY_FLAG_MAP)) {
257
+ if (flags[apiKey] === true) {
258
+ query[apiKey] = true;
259
+ }
260
+ }
261
+
262
+ return query;
263
+ }
264
+
265
+ async _fetchServer(flags = {}, requestOptions = {}) {
266
+ if (this.state.disconnected && this.state.disconnectError) {
267
+ throw this.state.disconnectError;
268
+ }
269
+ if (this.state.destroyed) {
270
+ throw new ERLCError("Client has been destroyed");
271
+ }
272
+
273
+ const query = this._buildQueryFlags(flags);
274
+ const response = await this.requestManager.request({
275
+ method: "GET",
276
+ path: "/v2/server",
277
+ query,
278
+ useCache: requestOptions.bypassCache !== true,
279
+ cacheTtlMs: requestOptions.cacheTtlMs,
280
+ dedupe: requestOptions.dedupe !== false,
281
+ });
282
+
283
+ const normalized = normalizeServerResponse(response.data);
284
+ normalized.meta = {
285
+ status: response.status,
286
+ endpoint: response.endpoint,
287
+ bucket: response.bucket,
288
+ rateLimit: response.rateLimit,
289
+ };
290
+ return normalized;
291
+ }
292
+
293
+ async _executeCommand(command, requestOptions = {}) {
294
+ if (this.state.disconnected && this.state.disconnectError) {
295
+ throw this.state.disconnectError;
296
+ }
297
+ if (this.state.destroyed) {
298
+ throw new ERLCError("Client has been destroyed");
299
+ }
300
+ if (typeof command !== "string" || !command.trim()) {
301
+ throw new ERLCError("Command must be a non-empty string");
302
+ }
303
+
304
+ const normalizedCommand = command.trim();
305
+ const keyword = getCommandKeyword(normalizedCommand);
306
+ if (BLOCKED_COMMANDS.has(keyword)) {
307
+ throw new ERLCError(`Command "${keyword}" is blocked by client policy`);
308
+ }
309
+
310
+ return this._queueCommand(async () => {
311
+ if (this.state.disconnected && this.state.disconnectError) {
312
+ throw this.state.disconnectError;
313
+ }
314
+ if (this.state.destroyed) {
315
+ throw new ERLCError("Client has been destroyed");
316
+ }
317
+
318
+ const response = await this.requestManager.request({
319
+ method: "POST",
320
+ path: "/v1/server/command",
321
+ body: { command: normalizedCommand },
322
+ useCache: false,
323
+ dedupe: requestOptions.dedupe === true,
324
+ });
325
+
326
+ return {
327
+ ok: response.status >= 200 && response.status < 300,
328
+ status: response.status,
329
+ endpoint: response.endpoint,
330
+ bucket: response.bucket,
331
+ rateLimit: response.rateLimit,
332
+ };
333
+ });
334
+ }
335
+
336
+ _queueCommand(task) {
337
+ const run = this.commandQueue.then(task, task);
338
+ this.commandQueue = run.catch(() => {});
339
+ return run;
340
+ }
341
+
342
+ destroy() {
343
+ if (this.state.destroyed) return;
344
+ this.state.destroyed = true;
345
+ this.poller.destroy();
346
+ this.requestManager.destroy(
347
+ this.state.disconnectError || new ERLCError("Client destroyed"),
348
+ );
349
+ this.cache.clear();
350
+ }
351
+ }
352
+
353
+ module.exports = Client;
@@ -0,0 +1,55 @@
1
+ class Cache {
2
+ constructor(options = {}) {
3
+ this.enabled = options.enabled !== false;
4
+ this.ttlMs = typeof options.ttlMs === "number" ? options.ttlMs : 1500;
5
+ this.maxSize = typeof options.maxSize === "number" ? options.maxSize : 500;
6
+ this.store = new Map();
7
+ }
8
+
9
+ _isExpired(entry) {
10
+ return entry.expiresAt > 0 && entry.expiresAt <= Date.now();
11
+ }
12
+
13
+ _touch(key, entry) {
14
+ this.store.delete(key);
15
+ this.store.set(key, entry);
16
+ }
17
+
18
+ get(key) {
19
+ if (!this.enabled) return null;
20
+ const entry = this.store.get(key);
21
+ if (!entry) return null;
22
+ if (this._isExpired(entry)) {
23
+ this.store.delete(key);
24
+ return null;
25
+ }
26
+ this._touch(key, entry);
27
+ return entry.value;
28
+ }
29
+
30
+ set(key, value, ttlMs) {
31
+ if (!this.enabled) return;
32
+ const effectiveTtl = typeof ttlMs === "number" ? ttlMs : this.ttlMs;
33
+ const entry = {
34
+ value,
35
+ expiresAt: effectiveTtl > 0 ? Date.now() + effectiveTtl : 0,
36
+ };
37
+ this.store.set(key, entry);
38
+ if (this.store.size > this.maxSize) {
39
+ const oldest = this.store.keys().next().value;
40
+ if (oldest !== undefined) {
41
+ this.store.delete(oldest);
42
+ }
43
+ }
44
+ }
45
+
46
+ delete(key) {
47
+ return this.store.delete(key);
48
+ }
49
+
50
+ clear() {
51
+ this.store.clear();
52
+ }
53
+ }
54
+
55
+ module.exports = Cache;
@@ -0,0 +1,11 @@
1
+ const ERLCHttpError = require("./ERLCHttpError");
2
+
3
+ class ERLCAPIError extends ERLCHttpError {
4
+ constructor(message, options = {}) {
5
+ super(message, options);
6
+ this.errorCode = options.errorCode ?? 0;
7
+ this.apiMessage = options.apiMessage ?? message;
8
+ }
9
+ }
10
+
11
+ module.exports = ERLCAPIError;
@@ -0,0 +1,9 @@
1
+ class ERLCError extends Error {
2
+ constructor(message, details = {}) {
3
+ super(message);
4
+ this.name = this.constructor.name;
5
+ this.details = details;
6
+ }
7
+ }
8
+
9
+ module.exports = ERLCError;
@@ -0,0 +1,13 @@
1
+ const ERLCError = require("./ERLCError");
2
+
3
+ class ERLCHttpError extends ERLCError {
4
+ constructor(message, options = {}) {
5
+ super(message, options);
6
+ this.status = options.status ?? null;
7
+ this.endpoint = options.endpoint ?? null;
8
+ this.bucket = options.bucket ?? null;
9
+ this.body = options.body ?? null;
10
+ }
11
+ }
12
+
13
+ module.exports = ERLCHttpError;
@@ -0,0 +1,5 @@
1
+ const ERLCAPIError = require("./ERLCAPIError");
2
+
3
+ class InvalidGlobalKeyError extends ERLCAPIError {}
4
+
5
+ module.exports = InvalidGlobalKeyError;
@@ -0,0 +1,5 @@
1
+ const ERLCAPIError = require("./ERLCAPIError");
2
+
3
+ class KeyBannedError extends ERLCAPIError {}
4
+
5
+ module.exports = KeyBannedError;
@@ -0,0 +1,5 @@
1
+ const ERLCAPIError = require("./ERLCAPIError");
2
+
3
+ class KeyExpiredError extends ERLCAPIError {}
4
+
5
+ module.exports = KeyExpiredError;
@@ -0,0 +1,5 @@
1
+ const ERLCAPIError = require("./ERLCAPIError");
2
+
3
+ class ModuleOutOfDateError extends ERLCAPIError {}
4
+
5
+ module.exports = ModuleOutOfDateError;
@@ -0,0 +1,11 @@
1
+ const ERLCAPIError = require("./ERLCAPIError");
2
+
3
+ class RateLimitError extends ERLCAPIError {
4
+ constructor(message, options = {}) {
5
+ super(message, options);
6
+ this.retryAfterMs = options.retryAfterMs ?? 0;
7
+ this.resetEpochMs = options.resetEpochMs ?? null;
8
+ }
9
+ }
10
+
11
+ module.exports = RateLimitError;
@@ -0,0 +1,5 @@
1
+ const ERLCAPIError = require("./ERLCAPIError");
2
+
3
+ class RestrictedError extends ERLCAPIError {}
4
+
5
+ module.exports = RestrictedError;
@@ -0,0 +1,5 @@
1
+ const ERLCAPIError = require("./ERLCAPIError");
2
+
3
+ class ServerOfflineError extends ERLCAPIError {}
4
+
5
+ module.exports = ServerOfflineError;
@@ -0,0 +1,12 @@
1
+ module.exports = {
2
+ ERLCError: require("./ERLCError"),
3
+ ERLCHttpError: require("./ERLCHttpError"),
4
+ ERLCAPIError: require("./ERLCAPIError"),
5
+ RateLimitError: require("./RateLimitError"),
6
+ KeyExpiredError: require("./KeyExpiredError"),
7
+ KeyBannedError: require("./KeyBannedError"),
8
+ InvalidGlobalKeyError: require("./InvalidGlobalKeyError"),
9
+ ServerOfflineError: require("./ServerOfflineError"),
10
+ RestrictedError: require("./RestrictedError"),
11
+ ModuleOutOfDateError: require("./ModuleOutOfDateError"),
12
+ };