@sonoransoftware/sonoran.js 1.0.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.
Files changed (43) hide show
  1. package/.eslintrc.js +11 -0
  2. package/.prettierrc.js +7 -0
  3. package/docs/CAD-Methods-and-Usage.md +59 -0
  4. package/docs/CMS-Methods-and-Usage.md +212 -0
  5. package/docs/REST-Methods-and-Usage.md +47 -0
  6. package/package.json +42 -0
  7. package/readme.md +54 -0
  8. package/src/builders/cad/DispatchCall.ts +159 -0
  9. package/src/builders/cad/index.ts +1 -0
  10. package/src/builders/index.ts +3 -0
  11. package/src/constants.ts +174 -0
  12. package/src/errors/LibraryErrors.ts +43 -0
  13. package/src/errors/Messages.ts +7 -0
  14. package/src/errors/index.ts +2 -0
  15. package/src/index.ts +12 -0
  16. package/src/instance/Instance.ts +118 -0
  17. package/src/instance/instance.types.ts +17 -0
  18. package/src/libs/rest/src/index.ts +6 -0
  19. package/src/libs/rest/src/lib/REST.ts +201 -0
  20. package/src/libs/rest/src/lib/RequestManager.ts +256 -0
  21. package/src/libs/rest/src/lib/errors/APIError.ts +15 -0
  22. package/src/libs/rest/src/lib/errors/HTTPError.ts +22 -0
  23. package/src/libs/rest/src/lib/errors/RateLimitError.ts +21 -0
  24. package/src/libs/rest/src/lib/errors/index.ts +4 -0
  25. package/src/libs/rest/src/lib/handlers/IHandler.ts +13 -0
  26. package/src/libs/rest/src/lib/handlers/SequentialHandler.ts +157 -0
  27. package/src/libs/rest/src/lib/utils/constants.ts +919 -0
  28. package/src/libs/rest/src/lib/utils/utils.ts +18 -0
  29. package/src/managers/BaseManager.ts +15 -0
  30. package/src/managers/CADActiveUnitsManager.ts +50 -0
  31. package/src/managers/CADManager.ts +58 -0
  32. package/src/managers/CADServerManager.ts +27 -0
  33. package/src/managers/CMSManager.ts +133 -0
  34. package/src/managers/CMSServerManager.ts +27 -0
  35. package/src/managers/CacheManager.ts +38 -0
  36. package/src/managers/DataManager.ts +64 -0
  37. package/src/structures/Base.ts +28 -0
  38. package/src/structures/CADActiveUnit.ts +85 -0
  39. package/src/structures/CADServer.ts +37 -0
  40. package/src/structures/CMSServer.ts +26 -0
  41. package/src/utils/index.ts +1 -0
  42. package/src/utils/utils.ts +75 -0
  43. package/tsconfig.json +71 -0
@@ -0,0 +1,256 @@
1
+ import Collection from '@discordjs/collection';
2
+ // import { DiscordSnowflake } from '@sapphire/snowflake';
3
+ import { EventEmitter } from 'node:events';
4
+ // import type { RequestInit, BodyInit } from 'node-fetch';
5
+
6
+ import type Instance from '../../../../instance/Instance';
7
+ import { RESTOptions, RateLimitData, RestEvents } from './REST';
8
+ import { DefaultCADRestOptions, DefaultCMSRestOptions, AllAPITypes/**, RESTTypedAPIDataStructs, PossibleRequestData*/ } from './utils/constants';
9
+ import { productEnums } from '../../../../constants';
10
+ // import { APIError, HTTPError } from './errors';
11
+ import { IHandler } from './handlers/IHandler';
12
+ import { SequentialHandler } from './handlers/SequentialHandler';
13
+ import { cloneObject } from '../../../../utils/utils';
14
+
15
+ export type RouteLike = `/${string}`;
16
+
17
+ export const enum RequestMethod {
18
+ Delete = 'delete',
19
+ Get = 'get',
20
+ Patch = 'patch',
21
+ Post = 'post',
22
+ Put = 'put',
23
+ }
24
+
25
+ export type ReqDataType = Array<unknown> | unknown;
26
+
27
+ export interface RequestData {
28
+ id: string;
29
+ key: string;
30
+ type: string;
31
+ data: any;
32
+ }
33
+
34
+ export interface InternalRequestData extends RequestData {
35
+ product: productEnums;
36
+ }
37
+
38
+ export interface RequestHeaders {
39
+ 'User-Agent': string;
40
+ }
41
+
42
+ export interface APIData {
43
+ requestTypeId: string;
44
+ typePath: string;
45
+ fullUrl: string;
46
+ method: string;
47
+ fetchOptions: RequestInit;
48
+ data: RequestData;
49
+ product: productEnums;
50
+ type: string;
51
+ }
52
+
53
+ export interface RequestManager {
54
+ on: (<K extends keyof RestEvents>(event: K, listener: (...args: RestEvents[K]) => void) => this) &
55
+ (<S extends string | symbol>(event: Exclude<S, keyof RestEvents>, listener: (...args: any[]) => void) => this);
56
+
57
+ once: (<K extends keyof RestEvents>(event: K, listener: (...args: RestEvents[K]) => void) => this) &
58
+ (<S extends string | symbol>(event: Exclude<S, keyof RestEvents>, listener: (...args: any[]) => void) => this);
59
+
60
+ emit: (<K extends keyof RestEvents>(event: K, ...args: RestEvents[K]) => boolean) &
61
+ (<S extends string | symbol>(event: Exclude<S, keyof RestEvents>, ...args: any[]) => boolean);
62
+
63
+ off: (<K extends keyof RestEvents>(event: K, listener: (...args: RestEvents[K]) => void) => this) &
64
+ (<S extends string | symbol>(event: Exclude<S, keyof RestEvents>, listener: (...args: any[]) => void) => this);
65
+
66
+ removeAllListeners: (<K extends keyof RestEvents>(event?: K) => this) &
67
+ (<S extends string | symbol>(event?: Exclude<S, keyof RestEvents>) => this);
68
+ }
69
+
70
+ export class RequestManager extends EventEmitter {
71
+ public readonly ratelimitedTypes = new Collection<string, RateLimitData>();
72
+ public readonly handlers = new Collection<string, IHandler>();
73
+ public readonly product: productEnums;
74
+ public readonly options: RESTOptions;
75
+ private instance: Instance;
76
+
77
+ constructor(_instance: Instance, _product: productEnums, options: RESTOptions) {
78
+ super();
79
+ this.product = _product;
80
+ this.instance = _instance;
81
+ switch (_product) {
82
+ case productEnums.CAD: {
83
+ this.options = { ...DefaultCADRestOptions, ...options };
84
+ break;
85
+ }
86
+ case productEnums.CMS: {
87
+ this.options = { ...DefaultCMSRestOptions, ...options };
88
+ break;
89
+ }
90
+ default: {
91
+ throw new Error('No Product provided for RequestManager initialization');
92
+ }
93
+ }
94
+ }
95
+
96
+ public async queueRequest(request: InternalRequestData): Promise<unknown> {
97
+ let requestData = request as RequestData;
98
+ const resolvedData: APIData = RequestManager.resolveRequestData(this.instance, request.type, request.product, requestData);
99
+ const handler = this.handlers.get(`${resolvedData.typePath}:${String(request.product)}`) ?? this.createHandler(resolvedData);
100
+ return handler.queueRequest(resolvedData.fullUrl, resolvedData.fetchOptions as any, resolvedData);
101
+ }
102
+
103
+ public onRateLimit(id: string, rateLimitData: RateLimitData): void {
104
+ this.ratelimitedTypes.set(id, rateLimitData);
105
+ }
106
+
107
+ public removeRateLimit(id: string): void {
108
+ this.ratelimitedTypes.delete(id);
109
+ }
110
+
111
+ private createHandler(data: APIData) {
112
+ const queue = new SequentialHandler(this, data);
113
+ this.handlers.set(queue.id, queue);
114
+ return queue;
115
+ }
116
+
117
+ private static resolveRequestData(instance: Instance, type: string, product: productEnums, data: RequestData): APIData {
118
+ let apiURL: string | boolean = false;
119
+ let apiData: APIData = {
120
+ requestTypeId: `${type}:${String(product)}`,
121
+ typePath: '',
122
+ fullUrl: '',
123
+ method: '',
124
+ fetchOptions: {},
125
+ data,
126
+ product,
127
+ type
128
+ };
129
+
130
+ switch (product) {
131
+ case productEnums.CAD:
132
+ apiURL = instance.cadApiUrl;
133
+ break;
134
+ case productEnums.CMS:
135
+ apiURL = instance.cmsApiUrl;
136
+ break;
137
+ }
138
+
139
+ const findType = AllAPITypes.find((_type) => _type.type === type);
140
+ if (findType) {
141
+ apiData.fullUrl = `${apiURL}/${findType.path}`;
142
+ apiData.method = findType.method;
143
+ apiData.fetchOptions.method = findType.method;
144
+ apiData.typePath = findType.path;
145
+
146
+ const clonedData = cloneObject(data.data);
147
+
148
+ switch (findType.type) {
149
+ case 'SET_SERVERS': {
150
+ apiData.data.data = clonedData;
151
+ break;
152
+ }
153
+ case 'SET_PENAL_CODES': {
154
+ apiData.data.data = clonedData.data;
155
+ break;
156
+ }
157
+ case 'SET_API_ID': {
158
+ apiData.data.data = clonedData.data;
159
+ break;
160
+ }
161
+ case 'NEW_RECORD': {
162
+ apiData.data.data = clonedData.data;
163
+ break;
164
+ }
165
+ case 'EDIT_RECORD': {
166
+ apiData.data.data = clonedData.data;
167
+ break;
168
+ }
169
+ case 'LOOKUP_INT': {
170
+ apiData.data.data = clonedData.data;
171
+ break;
172
+ }
173
+ case 'LOOKUP': {
174
+ apiData.data.data = clonedData.data;
175
+ break;
176
+ }
177
+ case 'SET_ACCOUNT_PERMISSIONS': {
178
+ apiData.data.data = clonedData.data;
179
+ break;
180
+ }
181
+ case 'BAN_USER': {
182
+ apiData.data.data = clonedData.data;
183
+ break;
184
+ }
185
+ case 'AUTH_STREETSIGNS': {
186
+ apiData.data.data = clonedData;
187
+ break;
188
+ }
189
+ case 'SET_POSTALS': {
190
+ apiData.data.data = clonedData.data;
191
+ break;
192
+ }
193
+ case 'NEW_CHARACTER': {
194
+ apiData.data.data = clonedData.data;
195
+ break;
196
+ }
197
+ case 'EDIT_CHARACTER': {
198
+ apiData.data.data = clonedData.data;
199
+ break;
200
+ }
201
+ case 'MODIFY_IDENTIFIER': {
202
+ apiData.data.data = clonedData.data;
203
+ break;
204
+ }
205
+ case 'ADD_BLIP': {
206
+ apiData.data.data = clonedData.data;
207
+ break;
208
+ }
209
+ case 'MODIFY_BLIP': {
210
+ apiData.data.data = clonedData.data;
211
+ break;
212
+ }
213
+ case 'GET_CALLS': {
214
+ apiData.data.data = clonedData.data;
215
+ break;
216
+ }
217
+ case 'GET_ACTIVE_UNITS': {
218
+ apiData.data.data = clonedData.data;
219
+ break;
220
+ }
221
+ case 'NEW_DISPATCH': {
222
+ apiData.data.data = clonedData.data;
223
+ break;
224
+ }
225
+ case 'UNIT_LOCATION': {
226
+ apiData.data.data = clonedData.data;
227
+ break;
228
+ }
229
+ default: {
230
+ if (data.data) {
231
+ if (Array.isArray(data.data)) {
232
+ if (data.data.length > 0) {
233
+ apiData.data.data = [ clonedData ];
234
+ } else {
235
+ apiData.data.data = [];
236
+ }
237
+ } else {
238
+ apiData.data.data = [ clonedData ];
239
+ }
240
+ } else {
241
+ apiData.data.data = [];
242
+ }
243
+ break;
244
+ }
245
+ }
246
+ }
247
+
248
+ apiData.fetchOptions.body = JSON.stringify(apiData.data);
249
+ apiData.fetchOptions.headers = {
250
+ 'Accept': 'application/json',
251
+ 'Content-Type': 'application/json'
252
+ };
253
+
254
+ return apiData;
255
+ }
256
+ }
@@ -0,0 +1,15 @@
1
+ export class APIError extends Error {
2
+ public constructor(
3
+ public response: string,
4
+ public requestType: string,
5
+ public requestUrl: string,
6
+ public responseCode: number,
7
+ public requestData: any
8
+ ) {
9
+ super(response);
10
+ }
11
+
12
+ public override get name(): string {
13
+ return `Sonoran.js API Error - ${this.requestType} [${this.responseCode}]`;
14
+ }
15
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Represents a HTTP error
3
+ */
4
+ export class HTTPError extends Error {
5
+
6
+ /**
7
+ * @param message The error message
8
+ * @param name The name of the error
9
+ * @param status The status code of the response
10
+ * @param method The method of the request that erred
11
+ * @param url The url of the request that erred
12
+ */
13
+ public constructor(
14
+ message: string,
15
+ public override name: string,
16
+ public status: number,
17
+ public method: string,
18
+ public url: string
19
+ ) {
20
+ super(message);
21
+ }
22
+ }
@@ -0,0 +1,21 @@
1
+ import type { RateLimitData } from '../REST';
2
+ import { productEnums } from '../../../../../constants';
3
+
4
+ export class RateLimitError extends Error implements RateLimitData {
5
+ public product: productEnums;
6
+ public type: string;
7
+ public timeTill: NodeJS.Timer;
8
+ public constructor({ product, type, timeTill }: RateLimitData) {
9
+ super();
10
+ this.product = product;
11
+ this.type = type;
12
+ this.timeTill = timeTill;
13
+ }
14
+
15
+ /**
16
+ * The name of the error
17
+ */
18
+ public override get name(): string {
19
+ return `Ratelimit Hit - [${this.product === productEnums.CAD ? 'Sonoran CAD' : this.product === productEnums.CMS ? 'Sonoran CMS' : 'Invalid Product' } '${this.type}']`;
20
+ }
21
+ }
@@ -0,0 +1,4 @@
1
+ export * from './APIError';
2
+ export * from './APIError';
3
+ export * from './HTTPError';
4
+ export * from './RateLimitError';
@@ -0,0 +1,13 @@
1
+ import type { RequestInit } from 'node-fetch';
2
+ import type { APIData } from '../RequestManager';
3
+
4
+ export interface IHandler {
5
+ queueRequest: (
6
+ url: string,
7
+ options: RequestInit,
8
+ data: APIData,
9
+ ) => Promise<unknown>;
10
+ // eslint-disable-next-line @typescript-eslint/method-signature-style -- This is meant to be a getter returning a bool
11
+ get inactive(): boolean;
12
+ readonly id: string;
13
+ }
@@ -0,0 +1,157 @@
1
+ // import { setTimeout as sleep } from 'node:timers/promises';
2
+ import { AsyncQueue } from '@sapphire/async-queue';
3
+ import fetch, { RequestInit, Response } from 'node-fetch';
4
+ // import { DiscordAPIError, DiscordErrorData, OAuthErrorData } from '../errors/DiscordAPIError';
5
+ import { APIError } from '../errors';
6
+ import { HTTPError } from '../errors/HTTPError';
7
+ // import { RateLimitError } from '../errors/RateLimitError';
8
+ import type { RequestManager, APIData, /**RequestData*/ } from '../RequestManager';
9
+ import { RateLimitData } from '../REST';
10
+ // import { RESTEvents } from '../utils/constants';
11
+ // import type { RateLimitData } from '../REST';
12
+ import type { IHandler } from './IHandler';
13
+
14
+ export class SequentialHandler implements IHandler {
15
+ /**
16
+ * The unique id of the handler
17
+ */
18
+ public readonly id: string;
19
+
20
+ /**
21
+ * The total number of requests that can be made before we are rate limited
22
+ */
23
+ // private limit = Infinity;
24
+
25
+ /**
26
+ * The interface used to sequence async requests sequentially
27
+ */
28
+ // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility
29
+ #asyncQueue = new AsyncQueue();
30
+
31
+ /**
32
+ * @param manager The request manager
33
+ * @param hash The hash that this RequestHandler handles
34
+ * @param majorParameter The major parameter for this handler
35
+ */
36
+ public constructor(
37
+ private readonly manager: RequestManager,
38
+ private readonly data: APIData,
39
+ ) {
40
+ this.id = `${this.data.typePath}:${String(this.data.product)}`;
41
+ }
42
+
43
+ /**
44
+ * If the bucket is currently inactive (no pending requests)
45
+ */
46
+ public get inactive(): boolean {
47
+ return (
48
+ this.#asyncQueue.remaining === 0
49
+ );
50
+ }
51
+
52
+ public getMang(): RequestManager {
53
+ return this.manager;
54
+ }
55
+
56
+ /**
57
+ * Emits a debug message
58
+ * @param message The message to debug
59
+ */
60
+ // private debug(message: string) {
61
+ // this.manager.emit(RESTEvents.Debug, `[REST ${this.id}] ${message}`);
62
+ // }
63
+
64
+ /*
65
+ * Determines whether the request should be queued or whether a RateLimitError should be thrown
66
+ */
67
+ // private async onRateLimit(rateLimitData: RateLimitData) {
68
+ // const { options } = this.manager;
69
+ // if (options.rejectOnRateLimit) {
70
+ // throw new RateLimitError(rateLimitData);
71
+ // }
72
+ // }
73
+
74
+ /**
75
+ * Queues a request to be sent
76
+ * @param routeId The generalized api route with literal ids for major parameters
77
+ * @param url The url to do the request on
78
+ * @param options All the information needed to make a request
79
+ * @param requestData Extra data from the user's request needed for errors and additional processing
80
+ */
81
+ public async queueRequest(
82
+ url: string,
83
+ options: RequestInit,
84
+ data: APIData
85
+ ): Promise<unknown> {
86
+ let queue = this.#asyncQueue;
87
+ // Wait for any previous requests to be completed before this one is run
88
+ await queue.wait();
89
+ try {
90
+ // Make the request, and return the results
91
+ return await this.runRequest(url, options, data);
92
+ } finally {
93
+ // Allow the next request to fire
94
+ queue.shift();
95
+ }
96
+ }
97
+
98
+ /**
99
+ * The method that actually makes the request to the api, and updates info about the bucket accordingly
100
+ * @param routeId The generalized api route with literal ids for major parameters
101
+ * @param url The fully resolved url to make the request to
102
+ * @param options The node-fetch options needed to make the request
103
+ * @param requestData Extra data from the user's request needed for errors and additional processing
104
+ * @param retries The number of retries this request has already attempted (recursion)
105
+ */
106
+ private async runRequest(
107
+ url: string,
108
+ options: RequestInit,
109
+ data: APIData,
110
+ // retries = 0,
111
+ ): Promise<unknown> {
112
+ const controller = new AbortController();
113
+ const timeout = setTimeout(() => controller.abort(), 30000).unref();
114
+ let res: Response;
115
+
116
+ try {
117
+ // node-fetch typings are a bit weird, so we have to cast to any to get the correct signature
118
+ // Type 'AbortSignal' is not assignable to type 'import('discord.js-modules/node_modules/@types/node-fetch/externals').AbortSignal'
119
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
120
+ res = await fetch(url, { ...options, signal: controller.signal as any });
121
+ } catch (error: unknown) {
122
+ throw error;
123
+ } finally {
124
+ clearTimeout(timeout);
125
+ }
126
+
127
+ if (res.ok) {
128
+ const parsedRes = await SequentialHandler.parseResponse(res);
129
+ return parsedRes;
130
+ } else if (res.status === 400 || res.status === 401 || res.status === 404) {
131
+ const parsedRes = await SequentialHandler.parseResponse(res);
132
+ // throw new HTTPError(String(parsedRes), res.constructor.name, res.status, data.method, url);
133
+ throw new APIError(parsedRes as string, data.type, data.fullUrl, res.status, data);
134
+ } else if (res.status === 429) {
135
+ const timeout = setTimeout(() => {
136
+ this.manager.removeRateLimit(data.requestTypeId);
137
+ }, 60 * 1000);
138
+ const ratelimitData: RateLimitData = {
139
+ product: data.product,
140
+ type: data.type,
141
+ timeTill: timeout
142
+ };
143
+ this.manager.onRateLimit(data.requestTypeId, ratelimitData);
144
+ } else if (res.status >= 500 && res.status < 600) {
145
+ throw new HTTPError(res.statusText, res.constructor.name, res.status, data.method, url);
146
+ }
147
+ return null;
148
+ }
149
+
150
+ private static parseResponse(res: Response): Promise<unknown> {
151
+ if (res.headers.get('Content-Type')?.startsWith('application/json')) {
152
+ return res.json();
153
+ }
154
+
155
+ return res.text();
156
+ }
157
+ }