@fluxerjs/rest 1.0.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/dist/index.d.mts +134 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.js +303 -0
- package/dist/index.mjs +269 -0
- package/package.json +33 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { Routes, APIErrorBody, RateLimitErrorBody } from '@fluxerjs/types';
|
|
3
|
+
export { Routes } from '@fluxerjs/types';
|
|
4
|
+
|
|
5
|
+
interface RESTOptions {
|
|
6
|
+
api?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
authPrefix?: 'Bot' | 'Bearer';
|
|
9
|
+
timeout?: number;
|
|
10
|
+
retries?: number;
|
|
11
|
+
userAgent?: string;
|
|
12
|
+
}
|
|
13
|
+
declare class REST extends EventEmitter {
|
|
14
|
+
private readonly requestManager;
|
|
15
|
+
private _token;
|
|
16
|
+
constructor(options?: RESTOptions);
|
|
17
|
+
setToken(token: string | null): this;
|
|
18
|
+
get token(): string | null;
|
|
19
|
+
get<T>(route: string, options?: {
|
|
20
|
+
auth?: boolean;
|
|
21
|
+
}): Promise<T>;
|
|
22
|
+
post<T>(route: string, options?: {
|
|
23
|
+
body?: unknown;
|
|
24
|
+
auth?: boolean;
|
|
25
|
+
files?: Array<{
|
|
26
|
+
name: string;
|
|
27
|
+
data: Blob | ArrayBuffer | Uint8Array;
|
|
28
|
+
filename?: string;
|
|
29
|
+
}>;
|
|
30
|
+
}): Promise<T>;
|
|
31
|
+
patch<T>(route: string, options?: {
|
|
32
|
+
body?: unknown;
|
|
33
|
+
auth?: boolean;
|
|
34
|
+
}): Promise<T>;
|
|
35
|
+
put<T>(route: string, options?: {
|
|
36
|
+
body?: unknown;
|
|
37
|
+
auth?: boolean;
|
|
38
|
+
}): Promise<T>;
|
|
39
|
+
delete<T>(route: string, options?: {
|
|
40
|
+
auth?: boolean;
|
|
41
|
+
}): Promise<T>;
|
|
42
|
+
/** Route helpers (from @fluxerjs/types) for building paths. */
|
|
43
|
+
static get Routes(): typeof Routes;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface RequestOptions {
|
|
47
|
+
body?: unknown | FormData;
|
|
48
|
+
headers?: Record<string, string>;
|
|
49
|
+
files?: Array<{
|
|
50
|
+
name: string;
|
|
51
|
+
data: Blob | ArrayBuffer | Uint8Array;
|
|
52
|
+
filename?: string;
|
|
53
|
+
}>;
|
|
54
|
+
auth?: boolean;
|
|
55
|
+
}
|
|
56
|
+
interface RestOptions {
|
|
57
|
+
api: string;
|
|
58
|
+
version: string;
|
|
59
|
+
authPrefix: 'Bot' | 'Bearer';
|
|
60
|
+
timeout: number;
|
|
61
|
+
retries: number;
|
|
62
|
+
userAgent: string;
|
|
63
|
+
}
|
|
64
|
+
declare class RequestManager {
|
|
65
|
+
private token;
|
|
66
|
+
private readonly options;
|
|
67
|
+
private readonly rateLimiter;
|
|
68
|
+
constructor(options: Partial<RestOptions>);
|
|
69
|
+
setToken(token: string | null): void;
|
|
70
|
+
get baseUrl(): string;
|
|
71
|
+
/** Hash route for rate limit bucket (use path without ids for grouping). */
|
|
72
|
+
private getRouteHash;
|
|
73
|
+
private waitForRateLimit;
|
|
74
|
+
private buildHeaders;
|
|
75
|
+
request<T>(method: string, route: string, options?: RequestOptions): Promise<T>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Tracks rate limit state per bucket (route hash).
|
|
80
|
+
* Delays requests when limit is exceeded.
|
|
81
|
+
*/
|
|
82
|
+
interface RateLimitState {
|
|
83
|
+
limit: number;
|
|
84
|
+
remaining: number;
|
|
85
|
+
resetAt: number;
|
|
86
|
+
}
|
|
87
|
+
declare class RateLimitManager {
|
|
88
|
+
private buckets;
|
|
89
|
+
private globalResetAt;
|
|
90
|
+
getBucket(route: string): RateLimitState | undefined;
|
|
91
|
+
setBucket(route: string, limit: number, remaining: number, resetAt: number): void;
|
|
92
|
+
setGlobalReset(resetAt: number): void;
|
|
93
|
+
getGlobalReset(): number;
|
|
94
|
+
/** Returns ms to wait before we can send again (0 if no wait). */
|
|
95
|
+
getWaitTime(route: string): number;
|
|
96
|
+
/** Parse rate limit headers and update state. */
|
|
97
|
+
updateFromHeaders(route: string, headers: Headers): void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
declare class FluxerAPIError extends Error {
|
|
101
|
+
readonly code: string;
|
|
102
|
+
readonly statusCode: number;
|
|
103
|
+
readonly errors?: APIErrorBody['errors'];
|
|
104
|
+
constructor(body: APIErrorBody, statusCode: number);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
declare class RateLimitError extends FluxerAPIError {
|
|
108
|
+
readonly retryAfter: number;
|
|
109
|
+
readonly global: boolean;
|
|
110
|
+
constructor(body: RateLimitErrorBody, statusCode: number);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
declare class HTTPError extends Error {
|
|
114
|
+
readonly statusCode: number;
|
|
115
|
+
readonly body: string | null;
|
|
116
|
+
constructor(statusCode: number, body: string | null);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Attachment data for multipart requests.
|
|
121
|
+
*/
|
|
122
|
+
type AttachmentData = Blob | ArrayBuffer | Uint8Array;
|
|
123
|
+
interface AttachmentPayload {
|
|
124
|
+
name: string;
|
|
125
|
+
data: AttachmentData;
|
|
126
|
+
filename?: string;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Build FormData for message with attachments.
|
|
130
|
+
* payload_json is the JSON body; files are attached as files[0], files[1], etc.
|
|
131
|
+
*/
|
|
132
|
+
declare function buildFormData(payloadJson: Record<string, unknown>, files?: AttachmentPayload[]): FormData;
|
|
133
|
+
|
|
134
|
+
export { type AttachmentData, type AttachmentPayload, FluxerAPIError, HTTPError, REST, type RESTOptions, RateLimitError, RateLimitManager, type RateLimitState, RequestManager, type RequestOptions, type RestOptions, buildFormData };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { Routes, APIErrorBody, RateLimitErrorBody } from '@fluxerjs/types';
|
|
3
|
+
export { Routes } from '@fluxerjs/types';
|
|
4
|
+
|
|
5
|
+
interface RESTOptions {
|
|
6
|
+
api?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
authPrefix?: 'Bot' | 'Bearer';
|
|
9
|
+
timeout?: number;
|
|
10
|
+
retries?: number;
|
|
11
|
+
userAgent?: string;
|
|
12
|
+
}
|
|
13
|
+
declare class REST extends EventEmitter {
|
|
14
|
+
private readonly requestManager;
|
|
15
|
+
private _token;
|
|
16
|
+
constructor(options?: RESTOptions);
|
|
17
|
+
setToken(token: string | null): this;
|
|
18
|
+
get token(): string | null;
|
|
19
|
+
get<T>(route: string, options?: {
|
|
20
|
+
auth?: boolean;
|
|
21
|
+
}): Promise<T>;
|
|
22
|
+
post<T>(route: string, options?: {
|
|
23
|
+
body?: unknown;
|
|
24
|
+
auth?: boolean;
|
|
25
|
+
files?: Array<{
|
|
26
|
+
name: string;
|
|
27
|
+
data: Blob | ArrayBuffer | Uint8Array;
|
|
28
|
+
filename?: string;
|
|
29
|
+
}>;
|
|
30
|
+
}): Promise<T>;
|
|
31
|
+
patch<T>(route: string, options?: {
|
|
32
|
+
body?: unknown;
|
|
33
|
+
auth?: boolean;
|
|
34
|
+
}): Promise<T>;
|
|
35
|
+
put<T>(route: string, options?: {
|
|
36
|
+
body?: unknown;
|
|
37
|
+
auth?: boolean;
|
|
38
|
+
}): Promise<T>;
|
|
39
|
+
delete<T>(route: string, options?: {
|
|
40
|
+
auth?: boolean;
|
|
41
|
+
}): Promise<T>;
|
|
42
|
+
/** Route helpers (from @fluxerjs/types) for building paths. */
|
|
43
|
+
static get Routes(): typeof Routes;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface RequestOptions {
|
|
47
|
+
body?: unknown | FormData;
|
|
48
|
+
headers?: Record<string, string>;
|
|
49
|
+
files?: Array<{
|
|
50
|
+
name: string;
|
|
51
|
+
data: Blob | ArrayBuffer | Uint8Array;
|
|
52
|
+
filename?: string;
|
|
53
|
+
}>;
|
|
54
|
+
auth?: boolean;
|
|
55
|
+
}
|
|
56
|
+
interface RestOptions {
|
|
57
|
+
api: string;
|
|
58
|
+
version: string;
|
|
59
|
+
authPrefix: 'Bot' | 'Bearer';
|
|
60
|
+
timeout: number;
|
|
61
|
+
retries: number;
|
|
62
|
+
userAgent: string;
|
|
63
|
+
}
|
|
64
|
+
declare class RequestManager {
|
|
65
|
+
private token;
|
|
66
|
+
private readonly options;
|
|
67
|
+
private readonly rateLimiter;
|
|
68
|
+
constructor(options: Partial<RestOptions>);
|
|
69
|
+
setToken(token: string | null): void;
|
|
70
|
+
get baseUrl(): string;
|
|
71
|
+
/** Hash route for rate limit bucket (use path without ids for grouping). */
|
|
72
|
+
private getRouteHash;
|
|
73
|
+
private waitForRateLimit;
|
|
74
|
+
private buildHeaders;
|
|
75
|
+
request<T>(method: string, route: string, options?: RequestOptions): Promise<T>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Tracks rate limit state per bucket (route hash).
|
|
80
|
+
* Delays requests when limit is exceeded.
|
|
81
|
+
*/
|
|
82
|
+
interface RateLimitState {
|
|
83
|
+
limit: number;
|
|
84
|
+
remaining: number;
|
|
85
|
+
resetAt: number;
|
|
86
|
+
}
|
|
87
|
+
declare class RateLimitManager {
|
|
88
|
+
private buckets;
|
|
89
|
+
private globalResetAt;
|
|
90
|
+
getBucket(route: string): RateLimitState | undefined;
|
|
91
|
+
setBucket(route: string, limit: number, remaining: number, resetAt: number): void;
|
|
92
|
+
setGlobalReset(resetAt: number): void;
|
|
93
|
+
getGlobalReset(): number;
|
|
94
|
+
/** Returns ms to wait before we can send again (0 if no wait). */
|
|
95
|
+
getWaitTime(route: string): number;
|
|
96
|
+
/** Parse rate limit headers and update state. */
|
|
97
|
+
updateFromHeaders(route: string, headers: Headers): void;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
declare class FluxerAPIError extends Error {
|
|
101
|
+
readonly code: string;
|
|
102
|
+
readonly statusCode: number;
|
|
103
|
+
readonly errors?: APIErrorBody['errors'];
|
|
104
|
+
constructor(body: APIErrorBody, statusCode: number);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
declare class RateLimitError extends FluxerAPIError {
|
|
108
|
+
readonly retryAfter: number;
|
|
109
|
+
readonly global: boolean;
|
|
110
|
+
constructor(body: RateLimitErrorBody, statusCode: number);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
declare class HTTPError extends Error {
|
|
114
|
+
readonly statusCode: number;
|
|
115
|
+
readonly body: string | null;
|
|
116
|
+
constructor(statusCode: number, body: string | null);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Attachment data for multipart requests.
|
|
121
|
+
*/
|
|
122
|
+
type AttachmentData = Blob | ArrayBuffer | Uint8Array;
|
|
123
|
+
interface AttachmentPayload {
|
|
124
|
+
name: string;
|
|
125
|
+
data: AttachmentData;
|
|
126
|
+
filename?: string;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Build FormData for message with attachments.
|
|
130
|
+
* payload_json is the JSON body; files are attached as files[0], files[1], etc.
|
|
131
|
+
*/
|
|
132
|
+
declare function buildFormData(payloadJson: Record<string, unknown>, files?: AttachmentPayload[]): FormData;
|
|
133
|
+
|
|
134
|
+
export { type AttachmentData, type AttachmentPayload, FluxerAPIError, HTTPError, REST, type RESTOptions, RateLimitError, RateLimitManager, type RateLimitState, RequestManager, type RequestOptions, type RestOptions, buildFormData };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
FluxerAPIError: () => FluxerAPIError,
|
|
24
|
+
HTTPError: () => HTTPError,
|
|
25
|
+
REST: () => REST,
|
|
26
|
+
RateLimitError: () => RateLimitError,
|
|
27
|
+
RateLimitManager: () => RateLimitManager,
|
|
28
|
+
RequestManager: () => RequestManager,
|
|
29
|
+
Routes: () => import_types2.Routes,
|
|
30
|
+
buildFormData: () => buildFormData
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(index_exports);
|
|
33
|
+
|
|
34
|
+
// src/REST.ts
|
|
35
|
+
var import_events = require("events");
|
|
36
|
+
|
|
37
|
+
// src/RateLimitManager.ts
|
|
38
|
+
var RateLimitManager = class {
|
|
39
|
+
buckets = /* @__PURE__ */ new Map();
|
|
40
|
+
globalResetAt = 0;
|
|
41
|
+
getBucket(route) {
|
|
42
|
+
return this.buckets.get(route);
|
|
43
|
+
}
|
|
44
|
+
setBucket(route, limit, remaining, resetAt) {
|
|
45
|
+
this.buckets.set(route, { limit, remaining, resetAt });
|
|
46
|
+
}
|
|
47
|
+
setGlobalReset(resetAt) {
|
|
48
|
+
this.globalResetAt = resetAt;
|
|
49
|
+
}
|
|
50
|
+
getGlobalReset() {
|
|
51
|
+
return this.globalResetAt;
|
|
52
|
+
}
|
|
53
|
+
/** Returns ms to wait before we can send again (0 if no wait). */
|
|
54
|
+
getWaitTime(route) {
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
const globalWait = this.globalResetAt > now ? this.globalResetAt - now : 0;
|
|
57
|
+
const bucket = this.buckets.get(route);
|
|
58
|
+
const bucketWait = bucket && bucket.remaining <= 0 && bucket.resetAt > now ? bucket.resetAt - now : 0;
|
|
59
|
+
return Math.max(globalWait, bucketWait);
|
|
60
|
+
}
|
|
61
|
+
/** Parse rate limit headers and update state. */
|
|
62
|
+
updateFromHeaders(route, headers) {
|
|
63
|
+
const limit = headers.get("X-RateLimit-Limit");
|
|
64
|
+
const remaining = headers.get("X-RateLimit-Remaining");
|
|
65
|
+
const reset = headers.get("X-RateLimit-Reset");
|
|
66
|
+
if (limit !== null && remaining !== null && reset !== null) {
|
|
67
|
+
const resetAt = parseInt(reset, 10) * 1e3;
|
|
68
|
+
this.setBucket(route, parseInt(limit, 10), parseInt(remaining, 10), resetAt);
|
|
69
|
+
}
|
|
70
|
+
const retryAfter = headers.get("Retry-After");
|
|
71
|
+
if (retryAfter !== null) {
|
|
72
|
+
const sec = parseInt(retryAfter, 10);
|
|
73
|
+
const resetAt = Date.now() + (isNaN(sec) ? 0 : sec * 1e3);
|
|
74
|
+
this.setBucket(route, 1, 0, resetAt);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/errors/FluxerAPIError.ts
|
|
80
|
+
var FluxerAPIError = class _FluxerAPIError extends Error {
|
|
81
|
+
code;
|
|
82
|
+
statusCode;
|
|
83
|
+
errors;
|
|
84
|
+
constructor(body, statusCode) {
|
|
85
|
+
super(body.message);
|
|
86
|
+
this.name = "FluxerAPIError";
|
|
87
|
+
this.code = body.code;
|
|
88
|
+
this.statusCode = statusCode;
|
|
89
|
+
this.errors = body.errors;
|
|
90
|
+
Object.setPrototypeOf(this, _FluxerAPIError.prototype);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// src/errors/RateLimitError.ts
|
|
95
|
+
var RateLimitError = class _RateLimitError extends FluxerAPIError {
|
|
96
|
+
retryAfter;
|
|
97
|
+
global;
|
|
98
|
+
constructor(body, statusCode) {
|
|
99
|
+
super(body, statusCode);
|
|
100
|
+
this.retryAfter = body.retry_after;
|
|
101
|
+
this.global = body.global ?? false;
|
|
102
|
+
this.name = "RateLimitError";
|
|
103
|
+
Object.setPrototypeOf(this, _RateLimitError.prototype);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// src/errors/HTTPError.ts
|
|
108
|
+
var HTTPError = class _HTTPError extends Error {
|
|
109
|
+
statusCode;
|
|
110
|
+
body;
|
|
111
|
+
constructor(statusCode, body) {
|
|
112
|
+
super(`HTTP ${statusCode}: ${body ?? "No body"}`);
|
|
113
|
+
this.name = "HTTPError";
|
|
114
|
+
this.statusCode = statusCode;
|
|
115
|
+
this.body = body;
|
|
116
|
+
Object.setPrototypeOf(this, _HTTPError.prototype);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// src/RequestManager.ts
|
|
121
|
+
var RequestManager = class {
|
|
122
|
+
token = null;
|
|
123
|
+
options;
|
|
124
|
+
rateLimiter = new RateLimitManager();
|
|
125
|
+
constructor(options) {
|
|
126
|
+
this.options = {
|
|
127
|
+
api: options.api ?? "https://api.fluxer.app",
|
|
128
|
+
version: options.version ?? "1",
|
|
129
|
+
authPrefix: options.authPrefix ?? "Bot",
|
|
130
|
+
timeout: options.timeout ?? 15e3,
|
|
131
|
+
retries: options.retries ?? 3,
|
|
132
|
+
userAgent: options.userAgent ?? "fluxer-core.js"
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
setToken(token) {
|
|
136
|
+
this.token = token;
|
|
137
|
+
}
|
|
138
|
+
get baseUrl() {
|
|
139
|
+
return `${this.options.api}/v${this.options.version}`;
|
|
140
|
+
}
|
|
141
|
+
/** Hash route for rate limit bucket (use path without ids for grouping). */
|
|
142
|
+
getRouteHash(route) {
|
|
143
|
+
return route.replace(/\d{17,19}/g, ":id");
|
|
144
|
+
}
|
|
145
|
+
async waitForRateLimit(routeHash) {
|
|
146
|
+
const wait = this.rateLimiter.getWaitTime(routeHash);
|
|
147
|
+
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
148
|
+
}
|
|
149
|
+
buildHeaders(route, options) {
|
|
150
|
+
const headers = {
|
|
151
|
+
"User-Agent": this.options.userAgent,
|
|
152
|
+
...options.headers
|
|
153
|
+
};
|
|
154
|
+
if (options.auth !== false && this.token) {
|
|
155
|
+
headers["Authorization"] = `${this.options.authPrefix} ${this.token}`;
|
|
156
|
+
}
|
|
157
|
+
if (options.body !== void 0 && !(options.body instanceof FormData)) {
|
|
158
|
+
headers["Content-Type"] = "application/json";
|
|
159
|
+
}
|
|
160
|
+
return headers;
|
|
161
|
+
}
|
|
162
|
+
async request(method, route, options = {}) {
|
|
163
|
+
const routeHash = this.getRouteHash(route);
|
|
164
|
+
const url = route.startsWith("http") ? route : `${this.baseUrl}${route}`;
|
|
165
|
+
await this.waitForRateLimit(routeHash);
|
|
166
|
+
let body;
|
|
167
|
+
if (options.body !== void 0) {
|
|
168
|
+
if (options.body instanceof FormData) {
|
|
169
|
+
body = options.body;
|
|
170
|
+
} else {
|
|
171
|
+
body = JSON.stringify(options.body);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const headers = this.buildHeaders(route, options);
|
|
175
|
+
let lastError = null;
|
|
176
|
+
for (let attempt = 0; attempt <= this.options.retries; attempt++) {
|
|
177
|
+
try {
|
|
178
|
+
const controller = new AbortController();
|
|
179
|
+
const timeoutId = setTimeout(() => controller.abort(), this.options.timeout);
|
|
180
|
+
const response = await fetch(url, {
|
|
181
|
+
method,
|
|
182
|
+
headers,
|
|
183
|
+
body,
|
|
184
|
+
signal: controller.signal
|
|
185
|
+
});
|
|
186
|
+
clearTimeout(timeoutId);
|
|
187
|
+
this.rateLimiter.updateFromHeaders(routeHash, response.headers);
|
|
188
|
+
if (response.status === 429) {
|
|
189
|
+
const data = await response.json().catch(() => ({}));
|
|
190
|
+
const retryAfter = (data.retry_after ?? parseInt(response.headers.get("Retry-After") ?? "0", 10)) * 1e3;
|
|
191
|
+
this.rateLimiter.setBucket(routeHash, 1, 0, Date.now() + retryAfter);
|
|
192
|
+
if (data.global) this.rateLimiter.setGlobalReset(Date.now() + retryAfter);
|
|
193
|
+
throw new RateLimitError(
|
|
194
|
+
{ ...data, code: "RATE_LIMITED", message: data.message ?? "Rate limited", retry_after: data.retry_after ?? 0 },
|
|
195
|
+
response.status
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
const text = await response.text();
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
let parsed;
|
|
201
|
+
try {
|
|
202
|
+
parsed = JSON.parse(text);
|
|
203
|
+
} catch {
|
|
204
|
+
throw new HTTPError(response.status, text);
|
|
205
|
+
}
|
|
206
|
+
throw new FluxerAPIError(parsed, response.status);
|
|
207
|
+
}
|
|
208
|
+
if (response.status === 204 || text.length === 0) return void 0;
|
|
209
|
+
return JSON.parse(text);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
212
|
+
if (err instanceof RateLimitError && attempt < this.options.retries) {
|
|
213
|
+
await new Promise((r) => setTimeout(r, err.retryAfter * 1e3));
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (err instanceof FluxerAPIError || err instanceof HTTPError) throw err;
|
|
217
|
+
if (attempt < this.options.retries) {
|
|
218
|
+
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
throw lastError;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
throw lastError ?? new Error("Request failed");
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// src/REST.ts
|
|
229
|
+
var import_types = require("@fluxerjs/types");
|
|
230
|
+
var REST = class extends import_events.EventEmitter {
|
|
231
|
+
requestManager;
|
|
232
|
+
_token = null;
|
|
233
|
+
constructor(options = {}) {
|
|
234
|
+
super();
|
|
235
|
+
this.requestManager = new RequestManager({
|
|
236
|
+
api: options.api ?? "https://api.fluxer.app",
|
|
237
|
+
version: options.version ?? "1",
|
|
238
|
+
authPrefix: options.authPrefix ?? "Bot",
|
|
239
|
+
timeout: options.timeout ?? 15e3,
|
|
240
|
+
retries: options.retries ?? 3,
|
|
241
|
+
userAgent: options.userAgent ?? "fluxer-core.js"
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
setToken(token) {
|
|
245
|
+
this._token = token;
|
|
246
|
+
this.requestManager.setToken(token);
|
|
247
|
+
return this;
|
|
248
|
+
}
|
|
249
|
+
get token() {
|
|
250
|
+
return this._token;
|
|
251
|
+
}
|
|
252
|
+
async get(route, options) {
|
|
253
|
+
return this.requestManager.request("GET", route, { auth: options?.auth });
|
|
254
|
+
}
|
|
255
|
+
async post(route, options) {
|
|
256
|
+
return this.requestManager.request("POST", route, {
|
|
257
|
+
body: options?.body,
|
|
258
|
+
auth: options?.auth,
|
|
259
|
+
files: options?.files
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async patch(route, options) {
|
|
263
|
+
return this.requestManager.request("PATCH", route, { body: options?.body, auth: options?.auth });
|
|
264
|
+
}
|
|
265
|
+
async put(route, options) {
|
|
266
|
+
return this.requestManager.request("PUT", route, { body: options?.body, auth: options?.auth });
|
|
267
|
+
}
|
|
268
|
+
async delete(route, options) {
|
|
269
|
+
return this.requestManager.request("DELETE", route, { auth: options?.auth });
|
|
270
|
+
}
|
|
271
|
+
/** Route helpers (from @fluxerjs/types) for building paths. */
|
|
272
|
+
static get Routes() {
|
|
273
|
+
return import_types.Routes;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// src/utils/files.ts
|
|
278
|
+
function buildFormData(payloadJson, files) {
|
|
279
|
+
const form = new FormData();
|
|
280
|
+
form.append("payload_json", JSON.stringify(payloadJson));
|
|
281
|
+
if (files?.length) {
|
|
282
|
+
for (let i = 0; i < files.length; i++) {
|
|
283
|
+
const file = files[i];
|
|
284
|
+
const blob = file.data instanceof Blob ? file.data : new Blob([file.data]);
|
|
285
|
+
form.append(`files[${i}]`, blob, file.filename ?? file.name);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return form;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/index.ts
|
|
292
|
+
var import_types2 = require("@fluxerjs/types");
|
|
293
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
294
|
+
0 && (module.exports = {
|
|
295
|
+
FluxerAPIError,
|
|
296
|
+
HTTPError,
|
|
297
|
+
REST,
|
|
298
|
+
RateLimitError,
|
|
299
|
+
RateLimitManager,
|
|
300
|
+
RequestManager,
|
|
301
|
+
Routes,
|
|
302
|
+
buildFormData
|
|
303
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// src/REST.ts
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
|
|
4
|
+
// src/RateLimitManager.ts
|
|
5
|
+
var RateLimitManager = class {
|
|
6
|
+
buckets = /* @__PURE__ */ new Map();
|
|
7
|
+
globalResetAt = 0;
|
|
8
|
+
getBucket(route) {
|
|
9
|
+
return this.buckets.get(route);
|
|
10
|
+
}
|
|
11
|
+
setBucket(route, limit, remaining, resetAt) {
|
|
12
|
+
this.buckets.set(route, { limit, remaining, resetAt });
|
|
13
|
+
}
|
|
14
|
+
setGlobalReset(resetAt) {
|
|
15
|
+
this.globalResetAt = resetAt;
|
|
16
|
+
}
|
|
17
|
+
getGlobalReset() {
|
|
18
|
+
return this.globalResetAt;
|
|
19
|
+
}
|
|
20
|
+
/** Returns ms to wait before we can send again (0 if no wait). */
|
|
21
|
+
getWaitTime(route) {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const globalWait = this.globalResetAt > now ? this.globalResetAt - now : 0;
|
|
24
|
+
const bucket = this.buckets.get(route);
|
|
25
|
+
const bucketWait = bucket && bucket.remaining <= 0 && bucket.resetAt > now ? bucket.resetAt - now : 0;
|
|
26
|
+
return Math.max(globalWait, bucketWait);
|
|
27
|
+
}
|
|
28
|
+
/** Parse rate limit headers and update state. */
|
|
29
|
+
updateFromHeaders(route, headers) {
|
|
30
|
+
const limit = headers.get("X-RateLimit-Limit");
|
|
31
|
+
const remaining = headers.get("X-RateLimit-Remaining");
|
|
32
|
+
const reset = headers.get("X-RateLimit-Reset");
|
|
33
|
+
if (limit !== null && remaining !== null && reset !== null) {
|
|
34
|
+
const resetAt = parseInt(reset, 10) * 1e3;
|
|
35
|
+
this.setBucket(route, parseInt(limit, 10), parseInt(remaining, 10), resetAt);
|
|
36
|
+
}
|
|
37
|
+
const retryAfter = headers.get("Retry-After");
|
|
38
|
+
if (retryAfter !== null) {
|
|
39
|
+
const sec = parseInt(retryAfter, 10);
|
|
40
|
+
const resetAt = Date.now() + (isNaN(sec) ? 0 : sec * 1e3);
|
|
41
|
+
this.setBucket(route, 1, 0, resetAt);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/errors/FluxerAPIError.ts
|
|
47
|
+
var FluxerAPIError = class _FluxerAPIError extends Error {
|
|
48
|
+
code;
|
|
49
|
+
statusCode;
|
|
50
|
+
errors;
|
|
51
|
+
constructor(body, statusCode) {
|
|
52
|
+
super(body.message);
|
|
53
|
+
this.name = "FluxerAPIError";
|
|
54
|
+
this.code = body.code;
|
|
55
|
+
this.statusCode = statusCode;
|
|
56
|
+
this.errors = body.errors;
|
|
57
|
+
Object.setPrototypeOf(this, _FluxerAPIError.prototype);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/errors/RateLimitError.ts
|
|
62
|
+
var RateLimitError = class _RateLimitError extends FluxerAPIError {
|
|
63
|
+
retryAfter;
|
|
64
|
+
global;
|
|
65
|
+
constructor(body, statusCode) {
|
|
66
|
+
super(body, statusCode);
|
|
67
|
+
this.retryAfter = body.retry_after;
|
|
68
|
+
this.global = body.global ?? false;
|
|
69
|
+
this.name = "RateLimitError";
|
|
70
|
+
Object.setPrototypeOf(this, _RateLimitError.prototype);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// src/errors/HTTPError.ts
|
|
75
|
+
var HTTPError = class _HTTPError extends Error {
|
|
76
|
+
statusCode;
|
|
77
|
+
body;
|
|
78
|
+
constructor(statusCode, body) {
|
|
79
|
+
super(`HTTP ${statusCode}: ${body ?? "No body"}`);
|
|
80
|
+
this.name = "HTTPError";
|
|
81
|
+
this.statusCode = statusCode;
|
|
82
|
+
this.body = body;
|
|
83
|
+
Object.setPrototypeOf(this, _HTTPError.prototype);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// src/RequestManager.ts
|
|
88
|
+
var RequestManager = class {
|
|
89
|
+
token = null;
|
|
90
|
+
options;
|
|
91
|
+
rateLimiter = new RateLimitManager();
|
|
92
|
+
constructor(options) {
|
|
93
|
+
this.options = {
|
|
94
|
+
api: options.api ?? "https://api.fluxer.app",
|
|
95
|
+
version: options.version ?? "1",
|
|
96
|
+
authPrefix: options.authPrefix ?? "Bot",
|
|
97
|
+
timeout: options.timeout ?? 15e3,
|
|
98
|
+
retries: options.retries ?? 3,
|
|
99
|
+
userAgent: options.userAgent ?? "fluxer-core.js"
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
setToken(token) {
|
|
103
|
+
this.token = token;
|
|
104
|
+
}
|
|
105
|
+
get baseUrl() {
|
|
106
|
+
return `${this.options.api}/v${this.options.version}`;
|
|
107
|
+
}
|
|
108
|
+
/** Hash route for rate limit bucket (use path without ids for grouping). */
|
|
109
|
+
getRouteHash(route) {
|
|
110
|
+
return route.replace(/\d{17,19}/g, ":id");
|
|
111
|
+
}
|
|
112
|
+
async waitForRateLimit(routeHash) {
|
|
113
|
+
const wait = this.rateLimiter.getWaitTime(routeHash);
|
|
114
|
+
if (wait > 0) await new Promise((r) => setTimeout(r, wait));
|
|
115
|
+
}
|
|
116
|
+
buildHeaders(route, options) {
|
|
117
|
+
const headers = {
|
|
118
|
+
"User-Agent": this.options.userAgent,
|
|
119
|
+
...options.headers
|
|
120
|
+
};
|
|
121
|
+
if (options.auth !== false && this.token) {
|
|
122
|
+
headers["Authorization"] = `${this.options.authPrefix} ${this.token}`;
|
|
123
|
+
}
|
|
124
|
+
if (options.body !== void 0 && !(options.body instanceof FormData)) {
|
|
125
|
+
headers["Content-Type"] = "application/json";
|
|
126
|
+
}
|
|
127
|
+
return headers;
|
|
128
|
+
}
|
|
129
|
+
async request(method, route, options = {}) {
|
|
130
|
+
const routeHash = this.getRouteHash(route);
|
|
131
|
+
const url = route.startsWith("http") ? route : `${this.baseUrl}${route}`;
|
|
132
|
+
await this.waitForRateLimit(routeHash);
|
|
133
|
+
let body;
|
|
134
|
+
if (options.body !== void 0) {
|
|
135
|
+
if (options.body instanceof FormData) {
|
|
136
|
+
body = options.body;
|
|
137
|
+
} else {
|
|
138
|
+
body = JSON.stringify(options.body);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const headers = this.buildHeaders(route, options);
|
|
142
|
+
let lastError = null;
|
|
143
|
+
for (let attempt = 0; attempt <= this.options.retries; attempt++) {
|
|
144
|
+
try {
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
const timeoutId = setTimeout(() => controller.abort(), this.options.timeout);
|
|
147
|
+
const response = await fetch(url, {
|
|
148
|
+
method,
|
|
149
|
+
headers,
|
|
150
|
+
body,
|
|
151
|
+
signal: controller.signal
|
|
152
|
+
});
|
|
153
|
+
clearTimeout(timeoutId);
|
|
154
|
+
this.rateLimiter.updateFromHeaders(routeHash, response.headers);
|
|
155
|
+
if (response.status === 429) {
|
|
156
|
+
const data = await response.json().catch(() => ({}));
|
|
157
|
+
const retryAfter = (data.retry_after ?? parseInt(response.headers.get("Retry-After") ?? "0", 10)) * 1e3;
|
|
158
|
+
this.rateLimiter.setBucket(routeHash, 1, 0, Date.now() + retryAfter);
|
|
159
|
+
if (data.global) this.rateLimiter.setGlobalReset(Date.now() + retryAfter);
|
|
160
|
+
throw new RateLimitError(
|
|
161
|
+
{ ...data, code: "RATE_LIMITED", message: data.message ?? "Rate limited", retry_after: data.retry_after ?? 0 },
|
|
162
|
+
response.status
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const text = await response.text();
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
let parsed;
|
|
168
|
+
try {
|
|
169
|
+
parsed = JSON.parse(text);
|
|
170
|
+
} catch {
|
|
171
|
+
throw new HTTPError(response.status, text);
|
|
172
|
+
}
|
|
173
|
+
throw new FluxerAPIError(parsed, response.status);
|
|
174
|
+
}
|
|
175
|
+
if (response.status === 204 || text.length === 0) return void 0;
|
|
176
|
+
return JSON.parse(text);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
179
|
+
if (err instanceof RateLimitError && attempt < this.options.retries) {
|
|
180
|
+
await new Promise((r) => setTimeout(r, err.retryAfter * 1e3));
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (err instanceof FluxerAPIError || err instanceof HTTPError) throw err;
|
|
184
|
+
if (attempt < this.options.retries) {
|
|
185
|
+
await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
throw lastError;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
throw lastError ?? new Error("Request failed");
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// src/REST.ts
|
|
196
|
+
import { Routes } from "@fluxerjs/types";
|
|
197
|
+
var REST = class extends EventEmitter {
|
|
198
|
+
requestManager;
|
|
199
|
+
_token = null;
|
|
200
|
+
constructor(options = {}) {
|
|
201
|
+
super();
|
|
202
|
+
this.requestManager = new RequestManager({
|
|
203
|
+
api: options.api ?? "https://api.fluxer.app",
|
|
204
|
+
version: options.version ?? "1",
|
|
205
|
+
authPrefix: options.authPrefix ?? "Bot",
|
|
206
|
+
timeout: options.timeout ?? 15e3,
|
|
207
|
+
retries: options.retries ?? 3,
|
|
208
|
+
userAgent: options.userAgent ?? "fluxer-core.js"
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
setToken(token) {
|
|
212
|
+
this._token = token;
|
|
213
|
+
this.requestManager.setToken(token);
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
get token() {
|
|
217
|
+
return this._token;
|
|
218
|
+
}
|
|
219
|
+
async get(route, options) {
|
|
220
|
+
return this.requestManager.request("GET", route, { auth: options?.auth });
|
|
221
|
+
}
|
|
222
|
+
async post(route, options) {
|
|
223
|
+
return this.requestManager.request("POST", route, {
|
|
224
|
+
body: options?.body,
|
|
225
|
+
auth: options?.auth,
|
|
226
|
+
files: options?.files
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
async patch(route, options) {
|
|
230
|
+
return this.requestManager.request("PATCH", route, { body: options?.body, auth: options?.auth });
|
|
231
|
+
}
|
|
232
|
+
async put(route, options) {
|
|
233
|
+
return this.requestManager.request("PUT", route, { body: options?.body, auth: options?.auth });
|
|
234
|
+
}
|
|
235
|
+
async delete(route, options) {
|
|
236
|
+
return this.requestManager.request("DELETE", route, { auth: options?.auth });
|
|
237
|
+
}
|
|
238
|
+
/** Route helpers (from @fluxerjs/types) for building paths. */
|
|
239
|
+
static get Routes() {
|
|
240
|
+
return Routes;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// src/utils/files.ts
|
|
245
|
+
function buildFormData(payloadJson, files) {
|
|
246
|
+
const form = new FormData();
|
|
247
|
+
form.append("payload_json", JSON.stringify(payloadJson));
|
|
248
|
+
if (files?.length) {
|
|
249
|
+
for (let i = 0; i < files.length; i++) {
|
|
250
|
+
const file = files[i];
|
|
251
|
+
const blob = file.data instanceof Blob ? file.data : new Blob([file.data]);
|
|
252
|
+
form.append(`files[${i}]`, blob, file.filename ?? file.name);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return form;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/index.ts
|
|
259
|
+
import { Routes as Routes2 } from "@fluxerjs/types";
|
|
260
|
+
export {
|
|
261
|
+
FluxerAPIError,
|
|
262
|
+
HTTPError,
|
|
263
|
+
REST,
|
|
264
|
+
RateLimitError,
|
|
265
|
+
RateLimitManager,
|
|
266
|
+
RequestManager,
|
|
267
|
+
Routes2 as Routes,
|
|
268
|
+
buildFormData
|
|
269
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fluxerjs/rest",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "1.0.2",
|
|
7
|
+
"description": "REST client for the Fluxer API",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.mjs",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.mjs",
|
|
15
|
+
"require": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@fluxerjs/types": "1.0.2"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^20.0.0",
|
|
26
|
+
"tsup": "^8.3.0",
|
|
27
|
+
"typescript": "^5.6.0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
31
|
+
"clean": "rm -rf dist"
|
|
32
|
+
}
|
|
33
|
+
}
|