@yxcinebendjebbar/asdc 0.0.4
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/LICENSE +21 -0
- package/README.md +45 -0
- package/dist/core/InterceptorManager.d.ts +10 -0
- package/dist/core/InterceptorManager.js +30 -0
- package/dist/core/RequestConfig.d.ts +17 -0
- package/dist/core/RequestConfig.js +1 -0
- package/dist/core/Response.d.ts +10 -0
- package/dist/core/Response.js +17 -0
- package/dist/core/errors.d.ts +17 -0
- package/dist/core/errors.js +34 -0
- package/dist/http/HttpClient.d.ts +21 -0
- package/dist/http/HttpClient.js +51 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/rpc/JsonRpcClient.d.ts +17 -0
- package/dist/rpc/JsonRpcClient.js +67 -0
- package/dist/transport/FetchTransport.d.ts +7 -0
- package/dist/transport/FetchTransport.js +56 -0
- package/dist/transport/Transport.d.ts +5 -0
- package/dist/transport/Transport.js +1 -0
- package/dist/utils/QueryString.d.ts +2 -0
- package/dist/utils/QueryString.js +17 -0
- package/dist/ws/WebSocketClient.d.ts +23 -0
- package/dist/ws/WebSocketClient.js +52 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# http-toolkit
|
|
2
|
+
|
|
3
|
+
Zero-dependency, OOP HTTP / WebSocket / JSON-RPC client for TypeScript, built with native `fetch` and `WebSocket` only.
|
|
4
|
+
Architecture is deliberately language-agnostic (Strategy, Chain of Responsibility, Adapter) so the same design ports to Go / Python later.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { HttpClient } from './src';
|
|
10
|
+
|
|
11
|
+
const api = new HttpClient({ baseURL: 'https://api.example.com', timeout: 5000 });
|
|
12
|
+
|
|
13
|
+
api.interceptors.request.use((config) => {
|
|
14
|
+
config.headers = { ...config.headers, Authorization: 'Bearer token' };
|
|
15
|
+
return config;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const { data } = await api.get('/users/1');
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { WebSocketClient, JsonRpcClient } from './src';
|
|
23
|
+
|
|
24
|
+
const ws = new WebSocketClient('wss://api.example.com/ws');
|
|
25
|
+
ws.connect();
|
|
26
|
+
const rpc = new JsonRpcClient(ws);
|
|
27
|
+
const result = await rpc.call('getUser', { id: 1 });
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Structure
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
src/
|
|
34
|
+
core/ RequestConfig, Response, errors, InterceptorManager (Chain of Responsibility)
|
|
35
|
+
transport/ Transport interface (Strategy) + FetchTransport
|
|
36
|
+
http/ HttpClient (axios-style API)
|
|
37
|
+
ws/ WebSocketClient (native WS + backoff reconnect)
|
|
38
|
+
rpc/ JsonRpcClient (JSON-RPC 2.0, Adapter over HTTP or WS)
|
|
39
|
+
utils/ QueryString helpers
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Roadmap
|
|
43
|
+
- Node `http`/`https` transport (no `fetch` dependency for older runtimes)
|
|
44
|
+
- GraphQL client (Adapter over `HttpClient`, same interceptor pipeline)
|
|
45
|
+
- Retry policy plugin (Strategy, composes with `Transport`)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface InterceptorHandlers<T> {
|
|
2
|
+
fulfilled: (value: T) => T | Promise<T>;
|
|
3
|
+
rejected?: (error: unknown) => unknown;
|
|
4
|
+
}
|
|
5
|
+
export declare class InterceptorManager<T> {
|
|
6
|
+
private handlers;
|
|
7
|
+
use(fulfilled: InterceptorHandlers<T>['fulfilled'], rejected?: InterceptorHandlers<T>['rejected']): number;
|
|
8
|
+
eject(id: number): void;
|
|
9
|
+
run(value: T): Promise<T>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export class InterceptorManager {
|
|
2
|
+
handlers = [];
|
|
3
|
+
use(fulfilled, rejected) {
|
|
4
|
+
this.handlers.push({ fulfilled, rejected });
|
|
5
|
+
return this.handlers.length - 1;
|
|
6
|
+
}
|
|
7
|
+
eject(id) {
|
|
8
|
+
if (this.handlers[id])
|
|
9
|
+
this.handlers[id] = null;
|
|
10
|
+
}
|
|
11
|
+
async run(value) {
|
|
12
|
+
let result = value;
|
|
13
|
+
for (const handler of this.handlers) {
|
|
14
|
+
if (!handler)
|
|
15
|
+
continue;
|
|
16
|
+
try {
|
|
17
|
+
result = await handler.fulfilled(result);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
if (handler.rejected) {
|
|
21
|
+
result = (await handler.rejected(err));
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
2
|
+
export interface RequestConfig {
|
|
3
|
+
url: string;
|
|
4
|
+
method?: HttpMethod;
|
|
5
|
+
baseURL?: string;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
params?: Record<string, string | number | boolean | undefined>;
|
|
8
|
+
data?: unknown;
|
|
9
|
+
timeout?: number;
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
responseType?: 'json' | 'text' | 'blob' | 'arraybuffer';
|
|
12
|
+
}
|
|
13
|
+
export interface ClientConfig {
|
|
14
|
+
baseURL?: string;
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RequestConfig } from './RequestConfig.ts';
|
|
2
|
+
export declare class HttpResponse<T = unknown> {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly statusText: string;
|
|
5
|
+
readonly headers: Record<string, string>;
|
|
6
|
+
readonly data: T;
|
|
7
|
+
readonly config: RequestConfig;
|
|
8
|
+
constructor(status: number, statusText: string, headers: Record<string, string>, data: T, config: RequestConfig);
|
|
9
|
+
get ok(): boolean;
|
|
10
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export class HttpResponse {
|
|
2
|
+
status;
|
|
3
|
+
statusText;
|
|
4
|
+
headers;
|
|
5
|
+
data;
|
|
6
|
+
config;
|
|
7
|
+
constructor(status, statusText, headers, data, config) {
|
|
8
|
+
this.status = status;
|
|
9
|
+
this.statusText = statusText;
|
|
10
|
+
this.headers = headers;
|
|
11
|
+
this.data = data;
|
|
12
|
+
this.config = config;
|
|
13
|
+
}
|
|
14
|
+
get ok() {
|
|
15
|
+
return this.status >= 200 && this.status < 300;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare class HttpToolkitError extends Error {
|
|
2
|
+
config?: unknown | undefined;
|
|
3
|
+
constructor(message: string, config?: unknown | undefined);
|
|
4
|
+
}
|
|
5
|
+
export declare class NetworkError extends HttpToolkitError {
|
|
6
|
+
cause?: unknown | undefined;
|
|
7
|
+
constructor(message: string, cause?: unknown | undefined, config?: unknown);
|
|
8
|
+
}
|
|
9
|
+
export declare class TimeoutError extends HttpToolkitError {
|
|
10
|
+
constructor(message?: string, config?: unknown);
|
|
11
|
+
}
|
|
12
|
+
export declare class ResponseError<T = unknown> extends HttpToolkitError {
|
|
13
|
+
status: number;
|
|
14
|
+
statusText: string;
|
|
15
|
+
data: T;
|
|
16
|
+
constructor(message: string, status: number, statusText: string, data: T, config?: unknown);
|
|
17
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export class HttpToolkitError extends Error {
|
|
2
|
+
config;
|
|
3
|
+
constructor(message, config) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.config = config;
|
|
6
|
+
this.name = 'HttpToolkitError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class NetworkError extends HttpToolkitError {
|
|
10
|
+
cause;
|
|
11
|
+
constructor(message, cause, config) {
|
|
12
|
+
super(message, config);
|
|
13
|
+
this.cause = cause;
|
|
14
|
+
this.name = 'NetworkError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class TimeoutError extends HttpToolkitError {
|
|
18
|
+
constructor(message = 'Request timed out', config) {
|
|
19
|
+
super(message, config);
|
|
20
|
+
this.name = 'TimeoutError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class ResponseError extends HttpToolkitError {
|
|
24
|
+
status;
|
|
25
|
+
statusText;
|
|
26
|
+
data;
|
|
27
|
+
constructor(message, status, statusText, data, config) {
|
|
28
|
+
super(message, config);
|
|
29
|
+
this.status = status;
|
|
30
|
+
this.statusText = statusText;
|
|
31
|
+
this.data = data;
|
|
32
|
+
this.name = 'ResponseError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ClientConfig, RequestConfig } from '../core/RequestConfig.ts';
|
|
2
|
+
import { HttpResponse } from '../core/Response.ts';
|
|
3
|
+
import { InterceptorManager } from '../core/InterceptorManager.ts';
|
|
4
|
+
import type { Transport } from '../transport/Transport.ts';
|
|
5
|
+
export declare class HttpClient {
|
|
6
|
+
private readonly config;
|
|
7
|
+
private readonly transport;
|
|
8
|
+
readonly interceptors: {
|
|
9
|
+
request: InterceptorManager<RequestConfig>;
|
|
10
|
+
response: InterceptorManager<HttpResponse<unknown>>;
|
|
11
|
+
};
|
|
12
|
+
constructor(config?: ClientConfig, transport?: Transport);
|
|
13
|
+
request<T = unknown>(config: RequestConfig): Promise<HttpResponse<T>>;
|
|
14
|
+
private method;
|
|
15
|
+
get<T = unknown>(url: string, config?: Partial<RequestConfig>): Promise<HttpResponse<T>>;
|
|
16
|
+
delete<T = unknown>(url: string, config?: Partial<RequestConfig>): Promise<HttpResponse<T>>;
|
|
17
|
+
head<T = unknown>(url: string, config?: Partial<RequestConfig>): Promise<HttpResponse<T>>;
|
|
18
|
+
post<T = unknown>(url: string, data?: unknown, config?: Partial<RequestConfig>): Promise<HttpResponse<T>>;
|
|
19
|
+
put<T = unknown>(url: string, data?: unknown, config?: Partial<RequestConfig>): Promise<HttpResponse<T>>;
|
|
20
|
+
patch<T = unknown>(url: string, data?: unknown, config?: Partial<RequestConfig>): Promise<HttpResponse<T>>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { InterceptorManager } from "../core/InterceptorManager.js";
|
|
2
|
+
import { ResponseError } from "../core/errors.js";
|
|
3
|
+
import { FetchTransport } from "../transport/FetchTransport.js";
|
|
4
|
+
export class HttpClient {
|
|
5
|
+
config;
|
|
6
|
+
transport;
|
|
7
|
+
interceptors = {
|
|
8
|
+
request: new InterceptorManager(),
|
|
9
|
+
response: new InterceptorManager(),
|
|
10
|
+
};
|
|
11
|
+
constructor(config = {}, transport = new FetchTransport()) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
this.transport = transport;
|
|
14
|
+
}
|
|
15
|
+
async request(config) {
|
|
16
|
+
const merged = {
|
|
17
|
+
...config,
|
|
18
|
+
baseURL: config.baseURL ?? this.config.baseURL,
|
|
19
|
+
headers: { ...this.config.headers, ...config.headers },
|
|
20
|
+
timeout: config.timeout ?? this.config.timeout,
|
|
21
|
+
};
|
|
22
|
+
const finalConfig = await this.interceptors.request.run(merged);
|
|
23
|
+
const response = await this.transport.send(finalConfig);
|
|
24
|
+
const finalResponse = await this.interceptors.response.run(response);
|
|
25
|
+
if (!finalResponse.ok) {
|
|
26
|
+
throw new ResponseError(`Request failed with status ${finalResponse.status}`, finalResponse.status, finalResponse.statusText, finalResponse.data, finalConfig);
|
|
27
|
+
}
|
|
28
|
+
return finalResponse;
|
|
29
|
+
}
|
|
30
|
+
method(method, url, config) {
|
|
31
|
+
return this.request({ ...config, url, method });
|
|
32
|
+
}
|
|
33
|
+
get(url, config) {
|
|
34
|
+
return this.method('GET', url, config);
|
|
35
|
+
}
|
|
36
|
+
delete(url, config) {
|
|
37
|
+
return this.method('DELETE', url, config);
|
|
38
|
+
}
|
|
39
|
+
head(url, config) {
|
|
40
|
+
return this.method('HEAD', url, config);
|
|
41
|
+
}
|
|
42
|
+
post(url, data, config) {
|
|
43
|
+
return this.method('POST', url, { ...config, data });
|
|
44
|
+
}
|
|
45
|
+
put(url, data, config) {
|
|
46
|
+
return this.method('PUT', url, { ...config, data });
|
|
47
|
+
}
|
|
48
|
+
patch(url, data, config) {
|
|
49
|
+
return this.method('PATCH', url, { ...config, data });
|
|
50
|
+
}
|
|
51
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './core/RequestConfig.ts';
|
|
2
|
+
export * from './core/Response.ts';
|
|
3
|
+
export * from './core/errors.ts';
|
|
4
|
+
export * from './core/InterceptorManager.ts';
|
|
5
|
+
export * from './transport/Transport.ts';
|
|
6
|
+
export * from './transport/FetchTransport.ts';
|
|
7
|
+
export * from './http/HttpClient.ts';
|
|
8
|
+
export * from './ws/WebSocketClient.ts';
|
|
9
|
+
export * from './rpc/JsonRpcClient.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./core/RequestConfig.js";
|
|
2
|
+
export * from "./core/Response.js";
|
|
3
|
+
export * from "./core/errors.js";
|
|
4
|
+
export * from "./core/InterceptorManager.js";
|
|
5
|
+
export * from "./transport/Transport.js";
|
|
6
|
+
export * from "./transport/FetchTransport.js";
|
|
7
|
+
export * from "./http/HttpClient.js";
|
|
8
|
+
export * from "./ws/WebSocketClient.js";
|
|
9
|
+
export * from "./rpc/JsonRpcClient.js";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { HttpClient } from '../http/HttpClient.ts';
|
|
2
|
+
import { WebSocketClient } from '../ws/WebSocketClient.ts';
|
|
3
|
+
export declare class RpcError extends Error {
|
|
4
|
+
code: number;
|
|
5
|
+
data?: unknown | undefined;
|
|
6
|
+
constructor(message: string, code: number, data?: unknown | undefined);
|
|
7
|
+
}
|
|
8
|
+
export declare class JsonRpcClient {
|
|
9
|
+
private readonly transport;
|
|
10
|
+
private readonly endpoint;
|
|
11
|
+
private nextId;
|
|
12
|
+
private pending;
|
|
13
|
+
constructor(transport: HttpClient | WebSocketClient, endpoint?: string);
|
|
14
|
+
call<T = unknown>(method: string, params?: unknown): Promise<T>;
|
|
15
|
+
private handleMessage;
|
|
16
|
+
private rejectAllPending;
|
|
17
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { WebSocketClient } from "../ws/WebSocketClient.js";
|
|
2
|
+
export class RpcError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
data;
|
|
5
|
+
constructor(message, code, data) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.data = data;
|
|
9
|
+
this.name = 'RpcError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class JsonRpcClient {
|
|
13
|
+
transport;
|
|
14
|
+
endpoint;
|
|
15
|
+
nextId = 1;
|
|
16
|
+
pending = new Map();
|
|
17
|
+
constructor(transport, endpoint = '/') {
|
|
18
|
+
this.transport = transport;
|
|
19
|
+
this.endpoint = endpoint;
|
|
20
|
+
if (transport instanceof WebSocketClient) {
|
|
21
|
+
transport.on('message', (raw) => this.handleMessage(raw));
|
|
22
|
+
transport.on('close', () => this.rejectAllPending(new RpcError('WebSocket closed', -32000)));
|
|
23
|
+
transport.on('error', () => this.rejectAllPending(new RpcError('WebSocket error', -32000)));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async call(method, params) {
|
|
27
|
+
const id = this.nextId++;
|
|
28
|
+
const payload = { jsonrpc: '2.0', id, method, params };
|
|
29
|
+
const transport = this.transport;
|
|
30
|
+
if (transport instanceof WebSocketClient) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
this.pending.set(id, { resolve: resolve, reject });
|
|
33
|
+
transport.send(JSON.stringify(payload));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const res = await transport.post(this.endpoint, payload);
|
|
37
|
+
if (res.data.error) {
|
|
38
|
+
throw new RpcError(res.data.error.message, res.data.error.code, res.data.error.data);
|
|
39
|
+
}
|
|
40
|
+
return res.data.result;
|
|
41
|
+
}
|
|
42
|
+
handleMessage(raw) {
|
|
43
|
+
let msg;
|
|
44
|
+
try {
|
|
45
|
+
msg = JSON.parse(raw);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const pending = this.pending.get(msg.id);
|
|
51
|
+
if (!pending)
|
|
52
|
+
return;
|
|
53
|
+
this.pending.delete(msg.id);
|
|
54
|
+
if (msg.error) {
|
|
55
|
+
pending.reject(new RpcError(msg.error.message, msg.error.code, msg.error.data));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
pending.resolve(msg.result);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
rejectAllPending(error) {
|
|
62
|
+
for (const pending of this.pending.values()) {
|
|
63
|
+
pending.reject(error);
|
|
64
|
+
}
|
|
65
|
+
this.pending.clear();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Transport } from './Transport.ts';
|
|
2
|
+
import type { RequestConfig } from '../core/RequestConfig.ts';
|
|
3
|
+
import { HttpResponse } from '../core/Response.ts';
|
|
4
|
+
export declare class FetchTransport implements Transport {
|
|
5
|
+
send<T = unknown>(config: RequestConfig): Promise<HttpResponse<T>>;
|
|
6
|
+
private parseBody;
|
|
7
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { HttpResponse } from "../core/Response.js";
|
|
2
|
+
import { NetworkError, TimeoutError } from "../core/errors.js";
|
|
3
|
+
import { buildURL } from "../utils/QueryString.js";
|
|
4
|
+
export class FetchTransport {
|
|
5
|
+
async send(config) {
|
|
6
|
+
const url = buildURL(config.baseURL, config.url, config.params);
|
|
7
|
+
const controller = new AbortController();
|
|
8
|
+
const timer = config.timeout ? setTimeout(() => controller.abort(), config.timeout) : undefined;
|
|
9
|
+
const signals = [controller.signal];
|
|
10
|
+
if (config.signal)
|
|
11
|
+
signals.push(config.signal);
|
|
12
|
+
const signal = AbortSignal.any(signals);
|
|
13
|
+
const reqHeaders = { ...config.headers };
|
|
14
|
+
if (config.data !== undefined && !reqHeaders['Content-Type'] && !reqHeaders['content-type']) {
|
|
15
|
+
reqHeaders['Content-Type'] = 'application/json';
|
|
16
|
+
}
|
|
17
|
+
let res;
|
|
18
|
+
try {
|
|
19
|
+
res = await fetch(url, {
|
|
20
|
+
method: config.method ?? 'GET',
|
|
21
|
+
headers: reqHeaders,
|
|
22
|
+
body: config.data !== undefined ? JSON.stringify(config.data) : undefined,
|
|
23
|
+
signal,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
if (err.name === 'AbortError') {
|
|
28
|
+
throw new TimeoutError(undefined, config);
|
|
29
|
+
}
|
|
30
|
+
throw new NetworkError(err.message, err, config);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
if (timer)
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
}
|
|
36
|
+
const headers = {};
|
|
37
|
+
res.headers.forEach((value, key) => (headers[key] = value));
|
|
38
|
+
const data = await this.parseBody(res, config.responseType);
|
|
39
|
+
return new HttpResponse(res.status, res.statusText, headers, data, config);
|
|
40
|
+
}
|
|
41
|
+
async parseBody(res, type) {
|
|
42
|
+
if (type === 'text')
|
|
43
|
+
return (await res.text());
|
|
44
|
+
if (type === 'blob')
|
|
45
|
+
return (await res.blob());
|
|
46
|
+
if (type === 'arraybuffer')
|
|
47
|
+
return (await res.arrayBuffer());
|
|
48
|
+
const text = await res.text();
|
|
49
|
+
try {
|
|
50
|
+
return text ? JSON.parse(text) : undefined;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return text;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function buildQueryString(params) {
|
|
2
|
+
if (!params)
|
|
3
|
+
return '';
|
|
4
|
+
const search = new URLSearchParams();
|
|
5
|
+
for (const [key, value] of Object.entries(params)) {
|
|
6
|
+
if (value !== undefined)
|
|
7
|
+
search.append(key, String(value));
|
|
8
|
+
}
|
|
9
|
+
const qs = search.toString();
|
|
10
|
+
return qs ? `?${qs}` : '';
|
|
11
|
+
}
|
|
12
|
+
export function buildURL(baseURL, url, params) {
|
|
13
|
+
const isAbsolute = /^https?:\/\//i.test(url);
|
|
14
|
+
const base = isAbsolute ? '' : (baseURL ?? '');
|
|
15
|
+
const full = `${base}${url}`;
|
|
16
|
+
return `${full}${buildQueryString(params)}`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type WSEvent = 'open' | 'message' | 'close' | 'error';
|
|
2
|
+
type Listener = (...args: unknown[]) => void;
|
|
3
|
+
export interface ReconnectPolicy {
|
|
4
|
+
maxAttempts?: number;
|
|
5
|
+
delayMs?: (attempt: number) => number;
|
|
6
|
+
}
|
|
7
|
+
export declare class WebSocketClient {
|
|
8
|
+
private readonly url;
|
|
9
|
+
private readonly reconnect;
|
|
10
|
+
private ws?;
|
|
11
|
+
private listeners;
|
|
12
|
+
private attempt;
|
|
13
|
+
private shouldReconnect;
|
|
14
|
+
constructor(url: string, reconnect?: ReconnectPolicy);
|
|
15
|
+
connect(): void;
|
|
16
|
+
private tryReconnect;
|
|
17
|
+
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void;
|
|
18
|
+
close(code?: number, reason?: string): void;
|
|
19
|
+
on(event: WSEvent, listener: Listener): void;
|
|
20
|
+
off(event: WSEvent, listener: Listener): void;
|
|
21
|
+
private emit;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export class WebSocketClient {
|
|
2
|
+
url;
|
|
3
|
+
reconnect;
|
|
4
|
+
ws;
|
|
5
|
+
listeners = new Map();
|
|
6
|
+
attempt = 0;
|
|
7
|
+
shouldReconnect = true;
|
|
8
|
+
constructor(url, reconnect = { maxAttempts: 5, delayMs: (a) => Math.min(1000 * 2 ** a, 30000) }) {
|
|
9
|
+
this.url = url;
|
|
10
|
+
this.reconnect = reconnect;
|
|
11
|
+
}
|
|
12
|
+
connect() {
|
|
13
|
+
this.shouldReconnect = true;
|
|
14
|
+
this.ws = new WebSocket(this.url);
|
|
15
|
+
this.ws.addEventListener('open', (e) => this.emit('open', e));
|
|
16
|
+
this.ws.addEventListener('message', (e) => this.emit('message', e.data));
|
|
17
|
+
this.ws.addEventListener('close', (e) => {
|
|
18
|
+
this.emit('close', e);
|
|
19
|
+
if (this.shouldReconnect)
|
|
20
|
+
this.tryReconnect();
|
|
21
|
+
});
|
|
22
|
+
this.ws.addEventListener('error', (e) => this.emit('error', e));
|
|
23
|
+
}
|
|
24
|
+
tryReconnect() {
|
|
25
|
+
const max = this.reconnect.maxAttempts ?? 5;
|
|
26
|
+
if (this.attempt >= max)
|
|
27
|
+
return;
|
|
28
|
+
const delay = this.reconnect.delayMs?.(this.attempt) ?? 1000;
|
|
29
|
+
this.attempt++;
|
|
30
|
+
setTimeout(() => this.connect(), delay);
|
|
31
|
+
}
|
|
32
|
+
send(data) {
|
|
33
|
+
this.ws?.send(data);
|
|
34
|
+
}
|
|
35
|
+
close(code, reason) {
|
|
36
|
+
this.shouldReconnect = false;
|
|
37
|
+
this.ws?.close(code, reason);
|
|
38
|
+
}
|
|
39
|
+
on(event, listener) {
|
|
40
|
+
if (!this.listeners.has(event))
|
|
41
|
+
this.listeners.set(event, new Set());
|
|
42
|
+
this.listeners.get(event).add(listener);
|
|
43
|
+
}
|
|
44
|
+
off(event, listener) {
|
|
45
|
+
this.listeners.get(event)?.delete(listener);
|
|
46
|
+
}
|
|
47
|
+
emit(event, ...args) {
|
|
48
|
+
this.listeners.get(event)?.forEach((l) => l(...args));
|
|
49
|
+
if (event === 'open')
|
|
50
|
+
this.attempt = 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yxcinebendjebbar/asdc",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"description": "A zero-dependency, OOP HTTP/WebSocket/RPC client designed to be ported cleanly to other languages",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18.0.0"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"http",
|
|
22
|
+
"fetch",
|
|
23
|
+
"websocket",
|
|
24
|
+
"json-rpc",
|
|
25
|
+
"api",
|
|
26
|
+
"client",
|
|
27
|
+
"oop",
|
|
28
|
+
"zero-dependency"
|
|
29
|
+
],
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/yxcinebendjebbar/asdc"
|
|
33
|
+
},
|
|
34
|
+
"author": "yxcinebendjebbar",
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsc",
|
|
37
|
+
"prepublishOnly": "npm run build",
|
|
38
|
+
"test": "node --experimental-transform-types --test test/**/*.test.ts"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^26.4.0",
|
|
43
|
+
"typescript": "^5.5.0"
|
|
44
|
+
}
|
|
45
|
+
}
|